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.servlet.tags; 018 019import java.io.IOException; 020import java.nio.charset.UnsupportedCharsetException; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Set; 026 027import javax.servlet.ServletRequest; 028import javax.servlet.http.HttpServletRequest; 029import javax.servlet.http.HttpServletResponse; 030import javax.servlet.jsp.JspException; 031import javax.servlet.jsp.PageContext; 032 033import org.springframework.lang.Nullable; 034import org.springframework.util.Assert; 035import org.springframework.util.StringUtils; 036import org.springframework.web.servlet.support.RequestDataValueProcessor; 037import org.springframework.web.util.JavaScriptUtils; 038import org.springframework.web.util.TagUtils; 039import org.springframework.web.util.UriUtils; 040 041/** 042 * The {@code <url>} tag creates URLs. Modeled after the JSTL {@code c:url} tag with 043 * backwards compatibility in mind. 044 * 045 * <p>Enhancements to the JSTL functionality include: 046 * <ul> 047 * <li>URL encoded template URI variables</li> 048 * <li>HTML/XML escaping of URLs</li> 049 * <li>JavaScript escaping of URLs</li> 050 * </ul> 051 * 052 * <p>Template URI variables are indicated in the {@link #setValue(String) 'value'} 053 * attribute and marked by braces '{variableName}'. The braces and attribute name are 054 * replaced by the URL encoded value of a parameter defined with the spring:param tag 055 * in the body of the url tag. If no parameter is available the literal value is 056 * passed through. Params matched to template variables will not be added to the query 057 * string. 058 * 059 * <p>Use of the spring:param tag for URI template variables is strongly recommended 060 * over direct EL substitution as the values are URL encoded. Failure to properly 061 * encode URL can leave an application vulnerable to XSS and other injection attacks. 062 * 063 * <p>URLs can be HTML/XML escaped by setting the {@link #setHtmlEscape(boolean) 064 * 'htmlEscape'} attribute to 'true'. Detects an HTML escaping setting, either on 065 * this tag instance, the page level, or the {@code web.xml} level. The default 066 * is 'false'. When setting the URL value into a variable, escaping is not recommended. 067 * 068 * <p>Example usage: 069 * <pre class="code"><spring:url value="/url/path/{variableName}"> 070 * <spring:param name="variableName" value="more than JSTL c:url" /> 071 * </spring:url></pre> 072 * 073 * <p>The above results in: 074 * {@code /currentApplicationContext/url/path/more%20than%20JSTL%20c%3Aurl} 075 * 076 * <table> 077 * <caption>Attribute Summary</caption> 078 * <thead> 079 * <tr> 080 * <th>Attribute</th> 081 * <th>Required?</th> 082 * <th>Runtime Expression?</th> 083 * <th>Description</th> 084 * </tr> 085 * </thead> 086 * <tbody> 087 * <tr> 088 * <td>value</td> 089 * <td>true</td> 090 * <td>true</td> 091 * <td>The URL to build. This value can include template {placeholders} that are 092 * replaced with the URL encoded value of the named parameter. Parameters 093 * must be defined using the param tag inside the body of this tag.</td> 094 * </tr> 095 * <tr> 096 * <td>context</td> 097 * <td>false</td> 098 * <td>true</td> 099 * <td>Specifies a remote application context path. 100 * The default is the current application context path.</td> 101 * </tr> 102 * <tr> 103 * <td>var</td> 104 * <td>false</td> 105 * <td>true</td> 106 * <td>The name of the variable to export the URL value to. 107 * If not specified the URL is written as output.</td> 108 * </tr> 109 * <tr> 110 * <td>scope</td> 111 * <td>false</td> 112 * <td>true</td> 113 * <td>The scope for the var. 'application', 'session', 'request' and 'page' 114 * scopes are supported. Defaults to page scope. This attribute has no 115 * effect unless the var attribute is also defined.</td> 116 * </tr> 117 * <tr> 118 * <td>htmlEscape</td> 119 * <td>false</td> 120 * <td>true</td> 121 * <td>Set HTML escaping for this tag, as a boolean value. Overrides the 122 * default HTML escaping setting for the current page.</td> 123 * </tr> 124 * <tr> 125 * <td>javaScriptEscape</td> 126 * <td>false</td> 127 * <td>true</td> 128 * <td>Set JavaScript escaping for this tag, as a boolean value. 129 * Default is false.</td> 130 * </tr> 131 * </tbody> 132 * </table> 133 * 134 * @author Scott Andrews 135 * @since 3.0 136 * @see ParamTag 137 */ 138@SuppressWarnings("serial") 139public class UrlTag extends HtmlEscapingAwareTag implements ParamAware { 140 141 private static final String URL_TEMPLATE_DELIMITER_PREFIX = "{"; 142 143 private static final String URL_TEMPLATE_DELIMITER_SUFFIX = "}"; 144 145 private static final String URL_TYPE_ABSOLUTE = "://"; 146 147 148 private List<Param> params = Collections.emptyList(); 149 150 private Set<String> templateParams = Collections.emptySet(); 151 152 @Nullable 153 private UrlType type; 154 155 @Nullable 156 private String value; 157 158 @Nullable 159 private String context; 160 161 @Nullable 162 private String var; 163 164 private int scope = PageContext.PAGE_SCOPE; 165 166 private boolean javaScriptEscape = false; 167 168 169 /** 170 * Set the value of the URL. 171 */ 172 public void setValue(String value) { 173 if (value.contains(URL_TYPE_ABSOLUTE)) { 174 this.type = UrlType.ABSOLUTE; 175 this.value = value; 176 } 177 else if (value.startsWith("/")) { 178 this.type = UrlType.CONTEXT_RELATIVE; 179 this.value = value; 180 } 181 else { 182 this.type = UrlType.RELATIVE; 183 this.value = value; 184 } 185 } 186 187 /** 188 * Set the context path for the URL. 189 * Defaults to the current context. 190 */ 191 public void setContext(String context) { 192 if (context.startsWith("/")) { 193 this.context = context; 194 } 195 else { 196 this.context = "/" + context; 197 } 198 } 199 200 /** 201 * Set the variable name to expose the URL under. Defaults to rendering the 202 * URL to the current JspWriter 203 */ 204 public void setVar(String var) { 205 this.var = var; 206 } 207 208 /** 209 * Set the scope to export the URL variable to. This attribute has no 210 * meaning unless var is also defined. 211 */ 212 public void setScope(String scope) { 213 this.scope = TagUtils.getScope(scope); 214 } 215 216 /** 217 * Set JavaScript escaping for this tag, as boolean value. 218 * Default is "false". 219 */ 220 public void setJavaScriptEscape(boolean javaScriptEscape) throws JspException { 221 this.javaScriptEscape = javaScriptEscape; 222 } 223 224 @Override 225 public void addParam(Param param) { 226 this.params.add(param); 227 } 228 229 230 @Override 231 public int doStartTagInternal() throws JspException { 232 this.params = new LinkedList<>(); 233 this.templateParams = new HashSet<>(); 234 return EVAL_BODY_INCLUDE; 235 } 236 237 @Override 238 public int doEndTag() throws JspException { 239 String url = createUrl(); 240 241 RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor(); 242 ServletRequest request = this.pageContext.getRequest(); 243 if ((processor != null) && (request instanceof HttpServletRequest)) { 244 url = processor.processUrl((HttpServletRequest) request, url); 245 } 246 247 if (this.var == null) { 248 // print the url to the writer 249 try { 250 this.pageContext.getOut().print(url); 251 } 252 catch (IOException ex) { 253 throw new JspException(ex); 254 } 255 } 256 else { 257 // store the url as a variable 258 this.pageContext.setAttribute(this.var, url, this.scope); 259 } 260 return EVAL_PAGE; 261 } 262 263 264 /** 265 * Build the URL for the tag from the tag attributes and parameters. 266 * @return the URL value as a String 267 */ 268 String createUrl() throws JspException { 269 Assert.state(this.value != null, "No value set"); 270 HttpServletRequest request = (HttpServletRequest) this.pageContext.getRequest(); 271 HttpServletResponse response = (HttpServletResponse) this.pageContext.getResponse(); 272 273 StringBuilder url = new StringBuilder(); 274 if (this.type == UrlType.CONTEXT_RELATIVE) { 275 // add application context to url 276 if (this.context == null) { 277 url.append(request.getContextPath()); 278 } 279 else { 280 if (this.context.endsWith("/")) { 281 url.append(this.context, 0, this.context.length() - 1); 282 } 283 else { 284 url.append(this.context); 285 } 286 } 287 } 288 if (this.type != UrlType.RELATIVE && this.type != UrlType.ABSOLUTE && !this.value.startsWith("/")) { 289 url.append("/"); 290 } 291 url.append(replaceUriTemplateParams(this.value, this.params, this.templateParams)); 292 url.append(createQueryString(this.params, this.templateParams, (url.indexOf("?") == -1))); 293 294 String urlStr = url.toString(); 295 if (this.type != UrlType.ABSOLUTE) { 296 // Add the session identifier if needed 297 // (Do not embed the session identifier in a remote link!) 298 urlStr = response.encodeURL(urlStr); 299 } 300 301 // HTML and/or JavaScript escape, if demanded. 302 urlStr = htmlEscape(urlStr); 303 urlStr = (this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(urlStr) : urlStr); 304 305 return urlStr; 306 } 307 308 /** 309 * Build the query string from available parameters that have not already 310 * been applied as template params. 311 * <p>The names and values of parameters are URL encoded. 312 * @param params the parameters to build the query string from 313 * @param usedParams set of parameter names that have been applied as 314 * template params 315 * @param includeQueryStringDelimiter true if the query string should start 316 * with a '?' instead of '&' 317 * @return the query string 318 */ 319 protected String createQueryString(List<Param> params, Set<String> usedParams, boolean includeQueryStringDelimiter) 320 throws JspException { 321 322 String encoding = this.pageContext.getResponse().getCharacterEncoding(); 323 StringBuilder qs = new StringBuilder(); 324 for (Param param : params) { 325 if (!usedParams.contains(param.getName()) && StringUtils.hasLength(param.getName())) { 326 if (includeQueryStringDelimiter && qs.length() == 0) { 327 qs.append("?"); 328 } 329 else { 330 qs.append("&"); 331 } 332 try { 333 qs.append(UriUtils.encodeQueryParam(param.getName(), encoding)); 334 if (param.getValue() != null) { 335 qs.append("="); 336 qs.append(UriUtils.encodeQueryParam(param.getValue(), encoding)); 337 } 338 } 339 catch (UnsupportedCharsetException ex) { 340 throw new JspException(ex); 341 } 342 } 343 } 344 return qs.toString(); 345 } 346 347 /** 348 * Replace template markers in the URL matching available parameters. The 349 * name of matched parameters are added to the used parameters set. 350 * <p>Parameter values are URL encoded. 351 * @param uri the URL with template parameters to replace 352 * @param params parameters used to replace template markers 353 * @param usedParams set of template parameter names that have been replaced 354 * @return the URL with template parameters replaced 355 */ 356 protected String replaceUriTemplateParams(String uri, List<Param> params, Set<String> usedParams) 357 throws JspException { 358 359 String encoding = this.pageContext.getResponse().getCharacterEncoding(); 360 for (Param param : params) { 361 String template = URL_TEMPLATE_DELIMITER_PREFIX + param.getName() + URL_TEMPLATE_DELIMITER_SUFFIX; 362 if (uri.contains(template)) { 363 usedParams.add(param.getName()); 364 String value = param.getValue(); 365 try { 366 uri = StringUtils.replace(uri, template, 367 (value != null ? UriUtils.encodePath(value, encoding) : "")); 368 } 369 catch (UnsupportedCharsetException ex) { 370 throw new JspException(ex); 371 } 372 } 373 else { 374 template = URL_TEMPLATE_DELIMITER_PREFIX + '/' + param.getName() + URL_TEMPLATE_DELIMITER_SUFFIX; 375 if (uri.contains(template)) { 376 usedParams.add(param.getName()); 377 String value = param.getValue(); 378 try { 379 uri = StringUtils.replace(uri, template, 380 (value != null ? UriUtils.encodePathSegment(value, encoding) : "")); 381 } 382 catch (UnsupportedCharsetException ex) { 383 throw new JspException(ex); 384 } 385 } 386 } 387 } 388 return uri; 389 } 390 391 392 /** 393 * Internal enum that classifies URLs by type. 394 */ 395 private enum UrlType { 396 397 CONTEXT_RELATIVE, RELATIVE, ABSOLUTE 398 } 399 400}