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.util; 018 019import java.io.File; 020import java.io.FileNotFoundException; 021import java.util.Collection; 022import java.util.Enumeration; 023import java.util.Map; 024import java.util.StringTokenizer; 025import java.util.TreeMap; 026import javax.servlet.ServletContext; 027import javax.servlet.ServletRequest; 028import javax.servlet.ServletRequestWrapper; 029import javax.servlet.ServletResponse; 030import javax.servlet.ServletResponseWrapper; 031import javax.servlet.http.Cookie; 032import javax.servlet.http.HttpServletRequest; 033import javax.servlet.http.HttpServletResponse; 034import javax.servlet.http.HttpSession; 035 036import org.springframework.http.HttpRequest; 037import org.springframework.http.server.ServletServerHttpRequest; 038import org.springframework.util.Assert; 039import org.springframework.util.CollectionUtils; 040import org.springframework.util.LinkedMultiValueMap; 041import org.springframework.util.MultiValueMap; 042import org.springframework.util.ObjectUtils; 043import org.springframework.util.StringUtils; 044 045/** 046 * Miscellaneous utilities for web applications. 047 * Used by various framework classes. 048 * 049 * @author Rod Johnson 050 * @author Juergen Hoeller 051 * @author Sebastien Deleuze 052 */ 053public abstract class WebUtils { 054 055 /** 056 * Standard Servlet 2.3+ spec request attributes for include URI and paths. 057 * <p>If included via a RequestDispatcher, the current resource will see the 058 * originating request. Its own URI and paths are exposed as request attributes. 059 */ 060 public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri"; 061 public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path"; 062 public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path"; 063 public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info"; 064 public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string"; 065 066 /** 067 * Standard Servlet 2.4+ spec request attributes for forward URI and paths. 068 * <p>If forwarded to via a RequestDispatcher, the current resource will see its 069 * own URI and paths. The originating URI and paths are exposed as request attributes. 070 */ 071 public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri"; 072 public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path"; 073 public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path"; 074 public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info"; 075 public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string"; 076 077 /** 078 * Standard Servlet 2.3+ spec request attributes for error pages. 079 * <p>To be exposed to JSPs that are marked as error pages, when forwarding 080 * to them directly rather than through the servlet container's error page 081 * resolution mechanism. 082 */ 083 public static final String ERROR_STATUS_CODE_ATTRIBUTE = "javax.servlet.error.status_code"; 084 public static final String ERROR_EXCEPTION_TYPE_ATTRIBUTE = "javax.servlet.error.exception_type"; 085 public static final String ERROR_MESSAGE_ATTRIBUTE = "javax.servlet.error.message"; 086 public static final String ERROR_EXCEPTION_ATTRIBUTE = "javax.servlet.error.exception"; 087 public static final String ERROR_REQUEST_URI_ATTRIBUTE = "javax.servlet.error.request_uri"; 088 public static final String ERROR_SERVLET_NAME_ATTRIBUTE = "javax.servlet.error.servlet_name"; 089 090 091 /** 092 * Prefix of the charset clause in a content type String: ";charset=" 093 */ 094 public static final String CONTENT_TYPE_CHARSET_PREFIX = ";charset="; 095 096 /** 097 * Default character encoding to use when {@code request.getCharacterEncoding} 098 * returns {@code null}, according to the Servlet spec. 099 * @see ServletRequest#getCharacterEncoding 100 */ 101 public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1"; 102 103 /** 104 * Standard Servlet spec context attribute that specifies a temporary 105 * directory for the current web application, of type {@code java.io.File}. 106 */ 107 public static final String TEMP_DIR_CONTEXT_ATTRIBUTE = "javax.servlet.context.tempdir"; 108 109 /** 110 * HTML escape parameter at the servlet context level 111 * (i.e. a context-param in {@code web.xml}): "defaultHtmlEscape". 112 */ 113 public static final String HTML_ESCAPE_CONTEXT_PARAM = "defaultHtmlEscape"; 114 115 /** 116 * Use of response encoding for HTML escaping parameter at the servlet context level 117 * (i.e. a context-param in {@code web.xml}): "responseEncodedHtmlEscape". 118 * @since 4.1.2 119 */ 120 public static final String RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM = "responseEncodedHtmlEscape"; 121 122 /** 123 * Web app root key parameter at the servlet context level 124 * (i.e. a context-param in {@code web.xml}): "webAppRootKey". 125 */ 126 public static final String WEB_APP_ROOT_KEY_PARAM = "webAppRootKey"; 127 128 /** Default web app root key: "webapp.root" */ 129 public static final String DEFAULT_WEB_APP_ROOT_KEY = "webapp.root"; 130 131 /** Name suffixes in case of image buttons */ 132 public static final String[] SUBMIT_IMAGE_SUFFIXES = {".x", ".y"}; 133 134 /** Key for the mutex session attribute */ 135 public static final String SESSION_MUTEX_ATTRIBUTE = WebUtils.class.getName() + ".MUTEX"; 136 137 138 /** 139 * Set a system property to the web application root directory. 140 * The key of the system property can be defined with the "webAppRootKey" 141 * context-param in {@code web.xml}. Default is "webapp.root". 142 * <p>Can be used for tools that support substitution with {@code System.getProperty} 143 * values, like log4j's "${key}" syntax within log file locations. 144 * @param servletContext the servlet context of the web application 145 * @throws IllegalStateException if the system property is already set, 146 * or if the WAR file is not expanded 147 * @see #WEB_APP_ROOT_KEY_PARAM 148 * @see #DEFAULT_WEB_APP_ROOT_KEY 149 * @see WebAppRootListener 150 * @see Log4jWebConfigurer 151 */ 152 public static void setWebAppRootSystemProperty(ServletContext servletContext) throws IllegalStateException { 153 Assert.notNull(servletContext, "ServletContext must not be null"); 154 String root = servletContext.getRealPath("/"); 155 if (root == null) { 156 throw new IllegalStateException( 157 "Cannot set web app root system property when WAR file is not expanded"); 158 } 159 String param = servletContext.getInitParameter(WEB_APP_ROOT_KEY_PARAM); 160 String key = (param != null ? param : DEFAULT_WEB_APP_ROOT_KEY); 161 String oldValue = System.getProperty(key); 162 if (oldValue != null && !StringUtils.pathEquals(oldValue, root)) { 163 throw new IllegalStateException("Web app root system property already set to different value: '" + 164 key + "' = [" + oldValue + "] instead of [" + root + "] - " + 165 "Choose unique values for the 'webAppRootKey' context-param in your web.xml files!"); 166 } 167 System.setProperty(key, root); 168 servletContext.log("Set web app root system property: '" + key + "' = [" + root + "]"); 169 } 170 171 /** 172 * Remove the system property that points to the web app root directory. 173 * To be called on shutdown of the web application. 174 * @param servletContext the servlet context of the web application 175 * @see #setWebAppRootSystemProperty 176 */ 177 public static void removeWebAppRootSystemProperty(ServletContext servletContext) { 178 Assert.notNull(servletContext, "ServletContext must not be null"); 179 String param = servletContext.getInitParameter(WEB_APP_ROOT_KEY_PARAM); 180 String key = (param != null ? param : DEFAULT_WEB_APP_ROOT_KEY); 181 System.getProperties().remove(key); 182 } 183 184 /** 185 * Return whether default HTML escaping is enabled for the web application, 186 * i.e. the value of the "defaultHtmlEscape" context-param in {@code web.xml} 187 * (if any). Falls back to {@code false} in case of no explicit default given. 188 * @param servletContext the servlet context of the web application 189 * @return whether default HTML escaping is enabled (default is {@code false}) 190 * @deprecated as of Spring 4.1, in favor of {@link #getDefaultHtmlEscape} 191 */ 192 @Deprecated 193 public static boolean isDefaultHtmlEscape(ServletContext servletContext) { 194 if (servletContext == null) { 195 return false; 196 } 197 String param = servletContext.getInitParameter(HTML_ESCAPE_CONTEXT_PARAM); 198 return Boolean.valueOf(param); 199 } 200 201 /** 202 * Return whether default HTML escaping is enabled for the web application, 203 * i.e. the value of the "defaultHtmlEscape" context-param in {@code web.xml} 204 * (if any). 205 * <p>This method differentiates between no param specified at all and 206 * an actual boolean value specified, allowing to have a context-specific 207 * default in case of no setting at the global level. 208 * @param servletContext the servlet context of the web application 209 * @return whether default HTML escaping is enabled for the given application 210 * ({@code null} = no explicit default) 211 */ 212 public static Boolean getDefaultHtmlEscape(ServletContext servletContext) { 213 if (servletContext == null) { 214 return null; 215 } 216 String param = servletContext.getInitParameter(HTML_ESCAPE_CONTEXT_PARAM); 217 return (StringUtils.hasText(param) ? Boolean.valueOf(param) : null); 218 } 219 220 /** 221 * Return whether response encoding should be used when HTML escaping characters, 222 * thus only escaping XML markup significant characters with UTF-* encodings. 223 * This option is enabled for the web application with a ServletContext param, 224 * i.e. the value of the "responseEncodedHtmlEscape" context-param in {@code web.xml} 225 * (if any). 226 * <p>This method differentiates between no param specified at all and 227 * an actual boolean value specified, allowing to have a context-specific 228 * default in case of no setting at the global level. 229 * @param servletContext the servlet context of the web application 230 * @return whether response encoding is to be used for HTML escaping 231 * ({@code null} = no explicit default) 232 * @since 4.1.2 233 */ 234 public static Boolean getResponseEncodedHtmlEscape(ServletContext servletContext) { 235 if (servletContext == null) { 236 return null; 237 } 238 String param = servletContext.getInitParameter(RESPONSE_ENCODED_HTML_ESCAPE_CONTEXT_PARAM); 239 return (StringUtils.hasText(param) ? Boolean.valueOf(param) : null); 240 } 241 242 /** 243 * Return the temporary directory for the current web application, 244 * as provided by the servlet container. 245 * @param servletContext the servlet context of the web application 246 * @return the File representing the temporary directory 247 */ 248 public static File getTempDir(ServletContext servletContext) { 249 Assert.notNull(servletContext, "ServletContext must not be null"); 250 return (File) servletContext.getAttribute(TEMP_DIR_CONTEXT_ATTRIBUTE); 251 } 252 253 /** 254 * Return the real path of the given path within the web application, 255 * as provided by the servlet container. 256 * <p>Prepends a slash if the path does not already start with a slash, 257 * and throws a FileNotFoundException if the path cannot be resolved to 258 * a resource (in contrast to ServletContext's {@code getRealPath}, 259 * which returns null). 260 * @param servletContext the servlet context of the web application 261 * @param path the path within the web application 262 * @return the corresponding real path 263 * @throws FileNotFoundException if the path cannot be resolved to a resource 264 * @see javax.servlet.ServletContext#getRealPath 265 */ 266 public static String getRealPath(ServletContext servletContext, String path) throws FileNotFoundException { 267 Assert.notNull(servletContext, "ServletContext must not be null"); 268 // Interpret location as relative to the web application root directory. 269 if (!path.startsWith("/")) { 270 path = "/" + path; 271 } 272 String realPath = servletContext.getRealPath(path); 273 if (realPath == null) { 274 throw new FileNotFoundException( 275 "ServletContext resource [" + path + "] cannot be resolved to absolute file path - " + 276 "web application archive not expanded?"); 277 } 278 return realPath; 279 } 280 281 /** 282 * Determine the session id of the given request, if any. 283 * @param request current HTTP request 284 * @return the session id, or {@code null} if none 285 */ 286 public static String getSessionId(HttpServletRequest request) { 287 Assert.notNull(request, "Request must not be null"); 288 HttpSession session = request.getSession(false); 289 return (session != null ? session.getId() : null); 290 } 291 292 /** 293 * Check the given request for a session attribute of the given name. 294 * Returns null if there is no session or if the session has no such attribute. 295 * Does not create a new session if none has existed before! 296 * @param request current HTTP request 297 * @param name the name of the session attribute 298 * @return the value of the session attribute, or {@code null} if not found 299 */ 300 public static Object getSessionAttribute(HttpServletRequest request, String name) { 301 Assert.notNull(request, "Request must not be null"); 302 HttpSession session = request.getSession(false); 303 return (session != null ? session.getAttribute(name) : null); 304 } 305 306 /** 307 * Check the given request for a session attribute of the given name. 308 * Throws an exception if there is no session or if the session has no such 309 * attribute. Does not create a new session if none has existed before! 310 * @param request current HTTP request 311 * @param name the name of the session attribute 312 * @return the value of the session attribute, or {@code null} if not found 313 * @throws IllegalStateException if the session attribute could not be found 314 */ 315 public static Object getRequiredSessionAttribute(HttpServletRequest request, String name) 316 throws IllegalStateException { 317 318 Object attr = getSessionAttribute(request, name); 319 if (attr == null) { 320 throw new IllegalStateException("No session attribute '" + name + "' found"); 321 } 322 return attr; 323 } 324 325 /** 326 * Set the session attribute with the given name to the given value. 327 * Removes the session attribute if value is null, if a session existed at all. 328 * Does not create a new session if not necessary! 329 * @param request current HTTP request 330 * @param name the name of the session attribute 331 * @param value the value of the session attribute 332 */ 333 public static void setSessionAttribute(HttpServletRequest request, String name, Object value) { 334 Assert.notNull(request, "Request must not be null"); 335 if (value != null) { 336 request.getSession().setAttribute(name, value); 337 } 338 else { 339 HttpSession session = request.getSession(false); 340 if (session != null) { 341 session.removeAttribute(name); 342 } 343 } 344 } 345 346 /** 347 * Get the specified session attribute, creating and setting a new attribute if 348 * no existing found. The given class needs to have a public no-arg constructor. 349 * Useful for on-demand state objects in a web tier, like shopping carts. 350 * @param session current HTTP session 351 * @param name the name of the session attribute 352 * @param clazz the class to instantiate for a new attribute 353 * @return the value of the session attribute, newly created if not found 354 * @throws IllegalArgumentException if the session attribute could not be instantiated 355 * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes 356 */ 357 @Deprecated 358 public static Object getOrCreateSessionAttribute(HttpSession session, String name, Class<?> clazz) 359 throws IllegalArgumentException { 360 361 Assert.notNull(session, "Session must not be null"); 362 Object sessionObject = session.getAttribute(name); 363 if (sessionObject == null) { 364 try { 365 sessionObject = clazz.newInstance(); 366 } 367 catch (InstantiationException ex) { 368 throw new IllegalArgumentException( 369 "Could not instantiate class [" + clazz.getName() + 370 "] for session attribute '" + name + "': " + ex.getMessage()); 371 } 372 catch (IllegalAccessException ex) { 373 throw new IllegalArgumentException( 374 "Could not access default constructor of class [" + clazz.getName() + 375 "] for session attribute '" + name + "': " + ex.getMessage()); 376 } 377 session.setAttribute(name, sessionObject); 378 } 379 return sessionObject; 380 } 381 382 /** 383 * Return the best available mutex for the given session: 384 * that is, an object to synchronize on for the given session. 385 * <p>Returns the session mutex attribute if available; usually, 386 * this means that the HttpSessionMutexListener needs to be defined 387 * in {@code web.xml}. Falls back to the HttpSession itself 388 * if no mutex attribute found. 389 * <p>The session mutex is guaranteed to be the same object during 390 * the entire lifetime of the session, available under the key defined 391 * by the {@code SESSION_MUTEX_ATTRIBUTE} constant. It serves as a 392 * safe reference to synchronize on for locking on the current session. 393 * <p>In many cases, the HttpSession reference itself is a safe mutex 394 * as well, since it will always be the same object reference for the 395 * same active logical session. However, this is not guaranteed across 396 * different servlet containers; the only 100% safe way is a session mutex. 397 * @param session the HttpSession to find a mutex for 398 * @return the mutex object (never {@code null}) 399 * @see #SESSION_MUTEX_ATTRIBUTE 400 * @see HttpSessionMutexListener 401 */ 402 public static Object getSessionMutex(HttpSession session) { 403 Assert.notNull(session, "Session must not be null"); 404 Object mutex = session.getAttribute(SESSION_MUTEX_ATTRIBUTE); 405 if (mutex == null) { 406 mutex = session; 407 } 408 return mutex; 409 } 410 411 412 /** 413 * Return an appropriate request object of the specified type, if available, 414 * unwrapping the given request as far as necessary. 415 * @param request the servlet request to introspect 416 * @param requiredType the desired type of request object 417 * @return the matching request object, or {@code null} if none 418 * of that type is available 419 */ 420 @SuppressWarnings("unchecked") 421 public static <T> T getNativeRequest(ServletRequest request, Class<T> requiredType) { 422 if (requiredType != null) { 423 if (requiredType.isInstance(request)) { 424 return (T) request; 425 } 426 else if (request instanceof ServletRequestWrapper) { 427 return getNativeRequest(((ServletRequestWrapper) request).getRequest(), requiredType); 428 } 429 } 430 return null; 431 } 432 433 /** 434 * Return an appropriate response object of the specified type, if available, 435 * unwrapping the given response as far as necessary. 436 * @param response the servlet response to introspect 437 * @param requiredType the desired type of response object 438 * @return the matching response object, or {@code null} if none 439 * of that type is available 440 */ 441 @SuppressWarnings("unchecked") 442 public static <T> T getNativeResponse(ServletResponse response, Class<T> requiredType) { 443 if (requiredType != null) { 444 if (requiredType.isInstance(response)) { 445 return (T) response; 446 } 447 else if (response instanceof ServletResponseWrapper) { 448 return getNativeResponse(((ServletResponseWrapper) response).getResponse(), requiredType); 449 } 450 } 451 return null; 452 } 453 454 /** 455 * Determine whether the given request is an include request, 456 * that is, not a top-level HTTP request coming in from the outside. 457 * <p>Checks the presence of the "javax.servlet.include.request_uri" 458 * request attribute. Could check any request attribute that is only 459 * present in an include request. 460 * @param request current servlet request 461 * @return whether the given request is an include request 462 */ 463 public static boolean isIncludeRequest(ServletRequest request) { 464 return (request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE) != null); 465 } 466 467 /** 468 * Expose the Servlet spec's error attributes as {@link javax.servlet.http.HttpServletRequest} 469 * attributes under the keys defined in the Servlet 2.3 specification, for error pages that 470 * are rendered directly rather than through the Servlet container's error page resolution: 471 * {@code javax.servlet.error.status_code}, 472 * {@code javax.servlet.error.exception_type}, 473 * {@code javax.servlet.error.message}, 474 * {@code javax.servlet.error.exception}, 475 * {@code javax.servlet.error.request_uri}, 476 * {@code javax.servlet.error.servlet_name}. 477 * <p>Does not override values if already present, to respect attribute values 478 * that have been exposed explicitly before. 479 * <p>Exposes status code 200 by default. Set the "javax.servlet.error.status_code" 480 * attribute explicitly (before or after) in order to expose a different status code. 481 * @param request current servlet request 482 * @param ex the exception encountered 483 * @param servletName the name of the offending servlet 484 */ 485 public static void exposeErrorRequestAttributes(HttpServletRequest request, Throwable ex, String servletName) { 486 exposeRequestAttributeIfNotPresent(request, ERROR_STATUS_CODE_ATTRIBUTE, HttpServletResponse.SC_OK); 487 exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_TYPE_ATTRIBUTE, ex.getClass()); 488 exposeRequestAttributeIfNotPresent(request, ERROR_MESSAGE_ATTRIBUTE, ex.getMessage()); 489 exposeRequestAttributeIfNotPresent(request, ERROR_EXCEPTION_ATTRIBUTE, ex); 490 exposeRequestAttributeIfNotPresent(request, ERROR_REQUEST_URI_ATTRIBUTE, request.getRequestURI()); 491 exposeRequestAttributeIfNotPresent(request, ERROR_SERVLET_NAME_ATTRIBUTE, servletName); 492 } 493 494 /** 495 * Expose the specified request attribute if not already present. 496 * @param request current servlet request 497 * @param name the name of the attribute 498 * @param value the suggested value of the attribute 499 */ 500 private static void exposeRequestAttributeIfNotPresent(ServletRequest request, String name, Object value) { 501 if (request.getAttribute(name) == null) { 502 request.setAttribute(name, value); 503 } 504 } 505 506 /** 507 * Clear the Servlet spec's error attributes as {@link javax.servlet.http.HttpServletRequest} 508 * attributes under the keys defined in the Servlet 2.3 specification: 509 * {@code javax.servlet.error.status_code}, 510 * {@code javax.servlet.error.exception_type}, 511 * {@code javax.servlet.error.message}, 512 * {@code javax.servlet.error.exception}, 513 * {@code javax.servlet.error.request_uri}, 514 * {@code javax.servlet.error.servlet_name}. 515 * @param request current servlet request 516 */ 517 public static void clearErrorRequestAttributes(HttpServletRequest request) { 518 request.removeAttribute(ERROR_STATUS_CODE_ATTRIBUTE); 519 request.removeAttribute(ERROR_EXCEPTION_TYPE_ATTRIBUTE); 520 request.removeAttribute(ERROR_MESSAGE_ATTRIBUTE); 521 request.removeAttribute(ERROR_EXCEPTION_ATTRIBUTE); 522 request.removeAttribute(ERROR_REQUEST_URI_ATTRIBUTE); 523 request.removeAttribute(ERROR_SERVLET_NAME_ATTRIBUTE); 524 } 525 526 /** 527 * Expose the given Map as request attributes, using the keys as attribute names 528 * and the values as corresponding attribute values. Keys need to be Strings. 529 * @param request current HTTP request 530 * @param attributes the attributes Map 531 * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes 532 */ 533 @Deprecated 534 public static void exposeRequestAttributes(ServletRequest request, Map<String, ?> attributes) { 535 Assert.notNull(request, "Request must not be null"); 536 Assert.notNull(attributes, "Attributes Map must not be null"); 537 for (Map.Entry<String, ?> entry : attributes.entrySet()) { 538 request.setAttribute(entry.getKey(), entry.getValue()); 539 } 540 } 541 542 /** 543 * Retrieve the first cookie with the given name. Note that multiple 544 * cookies can have the same name but different paths or domains. 545 * @param request current servlet request 546 * @param name cookie name 547 * @return the first cookie with the given name, or {@code null} if none is found 548 */ 549 public static Cookie getCookie(HttpServletRequest request, String name) { 550 Assert.notNull(request, "Request must not be null"); 551 Cookie[] cookies = request.getCookies(); 552 if (cookies != null) { 553 for (Cookie cookie : cookies) { 554 if (name.equals(cookie.getName())) { 555 return cookie; 556 } 557 } 558 } 559 return null; 560 } 561 562 /** 563 * Check if a specific input type="submit" parameter was sent in the request, 564 * either via a button (directly with name) or via an image (name + ".x" or 565 * name + ".y"). 566 * @param request current HTTP request 567 * @param name name of the parameter 568 * @return if the parameter was sent 569 * @see #SUBMIT_IMAGE_SUFFIXES 570 */ 571 public static boolean hasSubmitParameter(ServletRequest request, String name) { 572 Assert.notNull(request, "Request must not be null"); 573 if (request.getParameter(name) != null) { 574 return true; 575 } 576 for (String suffix : SUBMIT_IMAGE_SUFFIXES) { 577 if (request.getParameter(name + suffix) != null) { 578 return true; 579 } 580 } 581 return false; 582 } 583 584 /** 585 * Obtain a named parameter from the given request parameters. 586 * <p>See {@link #findParameterValue(java.util.Map, String)} 587 * for a description of the lookup algorithm. 588 * @param request current HTTP request 589 * @param name the <i>logical</i> name of the request parameter 590 * @return the value of the parameter, or {@code null} 591 * if the parameter does not exist in given request 592 */ 593 public static String findParameterValue(ServletRequest request, String name) { 594 return findParameterValue(request.getParameterMap(), name); 595 } 596 597 /** 598 * Obtain a named parameter from the given request parameters. 599 * <p>This method will try to obtain a parameter value using the 600 * following algorithm: 601 * <ol> 602 * <li>Try to get the parameter value using just the given <i>logical</i> name. 603 * This handles parameters of the form <tt>logicalName = value</tt>. For normal 604 * parameters, e.g. submitted using a hidden HTML form field, this will return 605 * the requested value.</li> 606 * <li>Try to obtain the parameter value from the parameter name, where the 607 * parameter name in the request is of the form <tt>logicalName_value = xyz</tt> 608 * with "_" being the configured delimiter. This deals with parameter values 609 * submitted using an HTML form submit button.</li> 610 * <li>If the value obtained in the previous step has a ".x" or ".y" suffix, 611 * remove that. This handles cases where the value was submitted using an 612 * HTML form image button. In this case the parameter in the request would 613 * actually be of the form <tt>logicalName_value.x = 123</tt>. </li> 614 * </ol> 615 * @param parameters the available parameter map 616 * @param name the <i>logical</i> name of the request parameter 617 * @return the value of the parameter, or {@code null} 618 * if the parameter does not exist in given request 619 */ 620 public static String findParameterValue(Map<String, ?> parameters, String name) { 621 // First try to get it as a normal name=value parameter 622 Object value = parameters.get(name); 623 if (value instanceof String[]) { 624 String[] values = (String[]) value; 625 return (values.length > 0 ? values[0] : null); 626 } 627 else if (value != null) { 628 return value.toString(); 629 } 630 // If no value yet, try to get it as a name_value=xyz parameter 631 String prefix = name + "_"; 632 for (String paramName : parameters.keySet()) { 633 if (paramName.startsWith(prefix)) { 634 // Support images buttons, which would submit parameters as name_value.x=123 635 for (String suffix : SUBMIT_IMAGE_SUFFIXES) { 636 if (paramName.endsWith(suffix)) { 637 return paramName.substring(prefix.length(), paramName.length() - suffix.length()); 638 } 639 } 640 return paramName.substring(prefix.length()); 641 } 642 } 643 // We couldn't find the parameter value... 644 return null; 645 } 646 647 /** 648 * Return a map containing all parameters with the given prefix. 649 * Maps single values to String and multiple values to String array. 650 * <p>For example, with a prefix of "spring_", "spring_param1" and 651 * "spring_param2" result in a Map with "param1" and "param2" as keys. 652 * @param request HTTP request in which to look for parameters 653 * @param prefix the beginning of parameter names 654 * (if this is null or the empty string, all parameters will match) 655 * @return map containing request parameters <b>without the prefix</b>, 656 * containing either a String or a String array as values 657 * @see javax.servlet.ServletRequest#getParameterNames 658 * @see javax.servlet.ServletRequest#getParameterValues 659 * @see javax.servlet.ServletRequest#getParameterMap 660 */ 661 public static Map<String, Object> getParametersStartingWith(ServletRequest request, String prefix) { 662 Assert.notNull(request, "Request must not be null"); 663 Enumeration<String> paramNames = request.getParameterNames(); 664 Map<String, Object> params = new TreeMap<String, Object>(); 665 if (prefix == null) { 666 prefix = ""; 667 } 668 while (paramNames != null && paramNames.hasMoreElements()) { 669 String paramName = paramNames.nextElement(); 670 if ("".equals(prefix) || paramName.startsWith(prefix)) { 671 String unprefixed = paramName.substring(prefix.length()); 672 String[] values = request.getParameterValues(paramName); 673 if (values == null || values.length == 0) { 674 // Do nothing, no values found at all. 675 } 676 else if (values.length > 1) { 677 params.put(unprefixed, values); 678 } 679 else { 680 params.put(unprefixed, values[0]); 681 } 682 } 683 } 684 return params; 685 } 686 687 /** 688 * Return the target page specified in the request. 689 * @param request current servlet request 690 * @param paramPrefix the parameter prefix to check for 691 * (e.g. "_target" for parameters like "_target1" or "_target2") 692 * @param currentPage the current page, to be returned as fallback 693 * if no target page specified 694 * @return the page specified in the request, or current page if not found 695 * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes 696 */ 697 @Deprecated 698 public static int getTargetPage(ServletRequest request, String paramPrefix, int currentPage) { 699 Enumeration<String> paramNames = request.getParameterNames(); 700 while (paramNames.hasMoreElements()) { 701 String paramName = paramNames.nextElement(); 702 if (paramName.startsWith(paramPrefix)) { 703 for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) { 704 String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i]; 705 if (paramName.endsWith(suffix)) { 706 paramName = paramName.substring(0, paramName.length() - suffix.length()); 707 } 708 } 709 return Integer.parseInt(paramName.substring(paramPrefix.length())); 710 } 711 } 712 return currentPage; 713 } 714 715 716 /** 717 * Extract the URL filename from the given request URL path. 718 * Correctly resolves nested paths such as "/products/view.html" as well. 719 * @param urlPath the request URL path (e.g. "/index.html") 720 * @return the extracted URI filename (e.g. "index") 721 * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes 722 */ 723 @Deprecated 724 public static String extractFilenameFromUrlPath(String urlPath) { 725 String filename = extractFullFilenameFromUrlPath(urlPath); 726 int dotIndex = filename.lastIndexOf('.'); 727 if (dotIndex != -1) { 728 filename = filename.substring(0, dotIndex); 729 } 730 return filename; 731 } 732 733 /** 734 * Extract the full URL filename (including file extension) from the given 735 * request URL path. Correctly resolve nested paths such as 736 * "/products/view.html" and remove any path and or query parameters. 737 * @param urlPath the request URL path (e.g. "/products/index.html") 738 * @return the extracted URI filename (e.g. "index.html") 739 * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes 740 * (or {@link UriUtils#extractFileExtension} for the file extension use case) 741 */ 742 @Deprecated 743 public static String extractFullFilenameFromUrlPath(String urlPath) { 744 int end = urlPath.indexOf('?'); 745 if (end == -1) { 746 end = urlPath.indexOf('#'); 747 if (end == -1) { 748 end = urlPath.length(); 749 } 750 } 751 int begin = urlPath.lastIndexOf('/', end) + 1; 752 int paramIndex = urlPath.indexOf(';', begin); 753 end = (paramIndex != -1 && paramIndex < end ? paramIndex : end); 754 return urlPath.substring(begin, end); 755 } 756 757 /** 758 * Parse the given string with matrix variables. An example string would look 759 * like this {@code "q1=a;q1=b;q2=a,b,c"}. The resulting map would contain 760 * keys {@code "q1"} and {@code "q2"} with values {@code ["a","b"]} and 761 * {@code ["a","b","c"]} respectively. 762 * @param matrixVariables the unparsed matrix variables string 763 * @return a map with matrix variable names and values (never {@code null}) 764 * @since 3.2 765 */ 766 public static MultiValueMap<String, String> parseMatrixVariables(String matrixVariables) { 767 MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(); 768 if (!StringUtils.hasText(matrixVariables)) { 769 return result; 770 } 771 StringTokenizer pairs = new StringTokenizer(matrixVariables, ";"); 772 while (pairs.hasMoreTokens()) { 773 String pair = pairs.nextToken(); 774 int index = pair.indexOf('='); 775 if (index != -1) { 776 String name = pair.substring(0, index); 777 if (name.equalsIgnoreCase("jsessionid")) { 778 continue; 779 } 780 String rawValue = pair.substring(index + 1); 781 for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) { 782 result.add(name, value); 783 } 784 } 785 else { 786 result.add(pair, ""); 787 } 788 } 789 return result; 790 } 791 792 /** 793 * Check the given request origin against a list of allowed origins. 794 * A list containing "*" means that all origins are allowed. 795 * An empty list means only same origin is allowed. 796 * <p><strong>Note:</strong> this method may use values from "Forwarded" 797 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 798 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, 799 * if present, in order to reflect the client-originated address. 800 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 801 * central place whether to extract and use, or to discard such headers. 802 * See the Spring Framework reference for more on this filter. 803 * @return {@code true} if the request origin is valid, {@code false} otherwise 804 * @since 4.1.5 805 * @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454: The Web Origin Concept</a> 806 */ 807 public static boolean isValidOrigin(HttpRequest request, Collection<String> allowedOrigins) { 808 Assert.notNull(request, "Request must not be null"); 809 Assert.notNull(allowedOrigins, "Allowed origins must not be null"); 810 811 String origin = request.getHeaders().getOrigin(); 812 if (origin == null || allowedOrigins.contains("*")) { 813 return true; 814 } 815 else if (CollectionUtils.isEmpty(allowedOrigins)) { 816 return isSameOrigin(request); 817 } 818 else { 819 return allowedOrigins.contains(origin); 820 } 821 } 822 823 /** 824 * Check if the request is a same-origin one, based on {@code Origin}, {@code Host}, 825 * {@code Forwarded}, {@code X-Forwarded-Proto}, {@code X-Forwarded-Host} and 826 * @code X-Forwarded-Port} headers. 827 * <p><strong>Note:</strong> this method uses values from "Forwarded" 828 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 829 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, 830 * if present, in order to reflect the client-originated address. 831 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 832 * central place whether to extract and use, or to discard such headers. 833 * See the Spring Framework reference for more on this filter. 834 * @return {@code true} if the request is a same-origin one, {@code false} in case 835 * of cross-origin request 836 * @since 4.2 837 */ 838 public static boolean isSameOrigin(HttpRequest request) { 839 String origin = request.getHeaders().getOrigin(); 840 if (origin == null) { 841 return true; 842 } 843 UriComponentsBuilder urlBuilder; 844 if (request instanceof ServletServerHttpRequest) { 845 // Build more efficiently if we can: we only need scheme, host, port for origin comparison 846 HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); 847 urlBuilder = new UriComponentsBuilder(). 848 scheme(servletRequest.getScheme()). 849 host(servletRequest.getServerName()). 850 port(servletRequest.getServerPort()). 851 adaptFromForwardedHeaders(request.getHeaders()); 852 } 853 else { 854 urlBuilder = UriComponentsBuilder.fromHttpRequest(request); 855 } 856 UriComponents actualUrl = urlBuilder.build(); 857 UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build(); 858 return (ObjectUtils.nullSafeEquals(actualUrl.getHost(), originUrl.getHost()) && 859 getPort(actualUrl) == getPort(originUrl)); 860 } 861 862 private static int getPort(UriComponents uri) { 863 int port = uri.getPort(); 864 if (port == -1) { 865 if ("http".equals(uri.getScheme()) || "ws".equals(uri.getScheme())) { 866 port = 80; 867 } 868 else if ("https".equals(uri.getScheme()) || "wss".equals(uri.getScheme())) { 869 port = 443; 870 } 871 } 872 return port; 873 } 874 875}