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