/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.tags;
import java.io.IOException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.springframework.web.util.JavaScriptUtils;
import org.springframework.web.util.TagUtils;
import org.springframework.web.util.UriUtils;
The <url>
tag creates URLs. Modeled after the JSTL c:url
tag with backwards compatibility in mind. Enhancements to the JSTL functionality include:
- URL encoded template URI variables
- HTML/XML escaping of URLs
- JavaScript escaping of URLs
Template URI variables are indicated in the 'value'
attribute and marked by braces '{variableName}'. The braces and attribute name are replaced by the URL encoded value of a parameter defined with the spring:param tag in the body of the url tag. If no parameter is available the literal value is passed through. Params matched to template variables will not be added to the query string.
Use of the spring:param tag for URI template variables is strongly recommended
over direct EL substitution as the values are URL encoded. Failure to properly
encode URL can leave an application vulnerable to XSS and other injection attacks.
URLs can be HTML/XML escaped by setting the
'htmlEscape'
attribute to 'true'. Detects an HTML escaping setting, either on this tag instance, the page level, or the web.xml
level. The default is 'false'. When setting the URL value into a variable, escaping is not recommended.
Example usage:
<spring:url value="/url/path/{variableName}">
<spring:param name="variableName" value="more than JSTL c:url" />
</spring:url>
The above results in: /currentApplicationContext/url/path/more%20than%20JSTL%20c%3Aurl
Attribute Summary
Attribute
Required?
Runtime Expression?
Description
value
true
true
The URL to build. This value can include template {placeholders} that are
replaced with the URL encoded value of the named parameter. Parameters
must be defined using the param tag inside the body of this tag.
context
false
true
Specifies a remote application context path.
The default is the current application context path.
var
false
true
The name of the variable to export the URL value to.
If not specified the URL is written as output.
scope
false
true
The scope for the var. 'application', 'session', 'request' and 'page'
scopes are supported. Defaults to page scope. This attribute has no
effect unless the var attribute is also defined.
htmlEscape
false
true
Set HTML escaping for this tag, as a boolean value. Overrides the
default HTML escaping setting for the current page.
javaScriptEscape
false
true
Set JavaScript escaping for this tag, as a boolean value.
Default is false.
Author: Scott Andrews See Also: Since: 3.0
/**
* The {@code <url>} tag creates URLs. Modeled after the JSTL {@code c:url} tag with
* backwards compatibility in mind.
*
* <p>Enhancements to the JSTL functionality include:
* <ul>
* <li>URL encoded template URI variables</li>
* <li>HTML/XML escaping of URLs</li>
* <li>JavaScript escaping of URLs</li>
* </ul>
*
* <p>Template URI variables are indicated in the {@link #setValue(String) 'value'}
* attribute and marked by braces '{variableName}'. The braces and attribute name are
* replaced by the URL encoded value of a parameter defined with the spring:param tag
* in the body of the url tag. If no parameter is available the literal value is
* passed through. Params matched to template variables will not be added to the query
* string.
*
* <p>Use of the spring:param tag for URI template variables is strongly recommended
* over direct EL substitution as the values are URL encoded. Failure to properly
* encode URL can leave an application vulnerable to XSS and other injection attacks.
*
* <p>URLs can be HTML/XML escaped by setting the {@link #setHtmlEscape(boolean)
* 'htmlEscape'} attribute to 'true'. Detects an HTML escaping setting, either on
* this tag instance, the page level, or the {@code web.xml} level. The default
* is 'false'. When setting the URL value into a variable, escaping is not recommended.
*
* <p>Example usage:
* <pre class="code"><spring:url value="/url/path/{variableName}">
* <spring:param name="variableName" value="more than JSTL c:url" />
* </spring:url></pre>
*
* <p>The above results in:
* {@code /currentApplicationContext/url/path/more%20than%20JSTL%20c%3Aurl}
*
* <table>
* <caption>Attribute Summary</caption>
* <thead>
* <tr>
* <th>Attribute</th>
* <th>Required?</th>
* <th>Runtime Expression?</th>
* <th>Description</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>value</td>
* <td>true</td>
* <td>true</td>
* <td>The URL to build. This value can include template {placeholders} that are
* replaced with the URL encoded value of the named parameter. Parameters
* must be defined using the param tag inside the body of this tag.</td>
* </tr>
* <tr>
* <td>context</td>
* <td>false</td>
* <td>true</td>
* <td>Specifies a remote application context path.
* The default is the current application context path.</td>
* </tr>
* <tr>
* <td>var</td>
* <td>false</td>
* <td>true</td>
* <td>The name of the variable to export the URL value to.
* If not specified the URL is written as output.</td>
* </tr>
* <tr>
* <td>scope</td>
* <td>false</td>
* <td>true</td>
* <td>The scope for the var. 'application', 'session', 'request' and 'page'
* scopes are supported. Defaults to page scope. This attribute has no
* effect unless the var attribute is also defined.</td>
* </tr>
* <tr>
* <td>htmlEscape</td>
* <td>false</td>
* <td>true</td>
* <td>Set HTML escaping for this tag, as a boolean value. Overrides the
* default HTML escaping setting for the current page.</td>
* </tr>
* <tr>
* <td>javaScriptEscape</td>
* <td>false</td>
* <td>true</td>
* <td>Set JavaScript escaping for this tag, as a boolean value.
* Default is false.</td>
* </tr>
* </tbody>
* </table>
*
* @author Scott Andrews
* @since 3.0
* @see ParamTag
*/
@SuppressWarnings("serial")
public class UrlTag extends HtmlEscapingAwareTag implements ParamAware {
private static final String URL_TEMPLATE_DELIMITER_PREFIX = "{";
private static final String URL_TEMPLATE_DELIMITER_SUFFIX = "}";
private static final String URL_TYPE_ABSOLUTE = "://";
private List<Param> params = Collections.emptyList();
private Set<String> templateParams = Collections.emptySet();
@Nullable
private UrlType type;
@Nullable
private String value;
@Nullable
private String context;
@Nullable
private String var;
private int scope = PageContext.PAGE_SCOPE;
private boolean javaScriptEscape = false;
Set the value of the URL.
/**
* Set the value of the URL.
*/
public void setValue(String value) {
if (value.contains(URL_TYPE_ABSOLUTE)) {
this.type = UrlType.ABSOLUTE;
this.value = value;
}
else if (value.startsWith("/")) {
this.type = UrlType.CONTEXT_RELATIVE;
this.value = value;
}
else {
this.type = UrlType.RELATIVE;
this.value = value;
}
}
Set the context path for the URL.
Defaults to the current context.
/**
* Set the context path for the URL.
* Defaults to the current context.
*/
public void setContext(String context) {
if (context.startsWith("/")) {
this.context = context;
}
else {
this.context = "/" + context;
}
}
Set the variable name to expose the URL under. Defaults to rendering the
URL to the current JspWriter
/**
* Set the variable name to expose the URL under. Defaults to rendering the
* URL to the current JspWriter
*/
public void setVar(String var) {
this.var = var;
}
Set the scope to export the URL variable to. This attribute has no
meaning unless var is also defined.
/**
* Set the scope to export the URL variable to. This attribute has no
* meaning unless var is also defined.
*/
public void setScope(String scope) {
this.scope = TagUtils.getScope(scope);
}
Set JavaScript escaping for this tag, as boolean value.
Default is "false".
/**
* Set JavaScript escaping for this tag, as boolean value.
* Default is "false".
*/
public void setJavaScriptEscape(boolean javaScriptEscape) throws JspException {
this.javaScriptEscape = javaScriptEscape;
}
@Override
public void addParam(Param param) {
this.params.add(param);
}
@Override
public int doStartTagInternal() throws JspException {
this.params = new ArrayList<>();
this.templateParams = new HashSet<>();
return EVAL_BODY_INCLUDE;
}
@Override
public int doEndTag() throws JspException {
String url = createUrl();
RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor();
ServletRequest request = this.pageContext.getRequest();
if ((processor != null) && (request instanceof HttpServletRequest)) {
url = processor.processUrl((HttpServletRequest) request, url);
}
if (this.var == null) {
// print the url to the writer
try {
this.pageContext.getOut().print(url);
}
catch (IOException ex) {
throw new JspException(ex);
}
}
else {
// store the url as a variable
this.pageContext.setAttribute(this.var, url, this.scope);
}
return EVAL_PAGE;
}
Build the URL for the tag from the tag attributes and parameters.
Returns: the URL value as a String
/**
* Build the URL for the tag from the tag attributes and parameters.
* @return the URL value as a String
*/
String createUrl() throws JspException {
Assert.state(this.value != null, "No value set");
HttpServletRequest request = (HttpServletRequest) this.pageContext.getRequest();
HttpServletResponse response = (HttpServletResponse) this.pageContext.getResponse();
StringBuilder url = new StringBuilder();
if (this.type == UrlType.CONTEXT_RELATIVE) {
// add application context to url
if (this.context == null) {
url.append(request.getContextPath());
}
else {
if (this.context.endsWith("/")) {
url.append(this.context, 0, this.context.length() - 1);
}
else {
url.append(this.context);
}
}
}
if (this.type != UrlType.RELATIVE && this.type != UrlType.ABSOLUTE && !this.value.startsWith("/")) {
url.append("/");
}
url.append(replaceUriTemplateParams(this.value, this.params, this.templateParams));
url.append(createQueryString(this.params, this.templateParams, (url.indexOf("?") == -1)));
String urlStr = url.toString();
if (this.type != UrlType.ABSOLUTE) {
// Add the session identifier if needed
// (Do not embed the session identifier in a remote link!)
urlStr = response.encodeURL(urlStr);
}
// HTML and/or JavaScript escape, if demanded.
urlStr = htmlEscape(urlStr);
urlStr = (this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(urlStr) : urlStr);
return urlStr;
}
Build the query string from available parameters that have not already
been applied as template params.
The names and values of parameters are URL encoded.
Params: - params – the parameters to build the query string from
- usedParams – set of parameter names that have been applied as
template params
- includeQueryStringDelimiter – true if the query string should start
with a '?' instead of '&'
Returns: the query string
/**
* Build the query string from available parameters that have not already
* been applied as template params.
* <p>The names and values of parameters are URL encoded.
* @param params the parameters to build the query string from
* @param usedParams set of parameter names that have been applied as
* template params
* @param includeQueryStringDelimiter true if the query string should start
* with a '?' instead of '&'
* @return the query string
*/
protected String createQueryString(List<Param> params, Set<String> usedParams, boolean includeQueryStringDelimiter)
throws JspException {
String encoding = this.pageContext.getResponse().getCharacterEncoding();
StringBuilder qs = new StringBuilder();
for (Param param : params) {
if (!usedParams.contains(param.getName()) && StringUtils.hasLength(param.getName())) {
if (includeQueryStringDelimiter && qs.length() == 0) {
qs.append("?");
}
else {
qs.append("&");
}
try {
qs.append(UriUtils.encodeQueryParam(param.getName(), encoding));
if (param.getValue() != null) {
qs.append("=");
qs.append(UriUtils.encodeQueryParam(param.getValue(), encoding));
}
}
catch (UnsupportedCharsetException ex) {
throw new JspException(ex);
}
}
}
return qs.toString();
}
Replace template markers in the URL matching available parameters. The
name of matched parameters are added to the used parameters set.
Parameter values are URL encoded.
Params: - uri – the URL with template parameters to replace
- params – parameters used to replace template markers
- usedParams – set of template parameter names that have been replaced
Returns: the URL with template parameters replaced
/**
* Replace template markers in the URL matching available parameters. The
* name of matched parameters are added to the used parameters set.
* <p>Parameter values are URL encoded.
* @param uri the URL with template parameters to replace
* @param params parameters used to replace template markers
* @param usedParams set of template parameter names that have been replaced
* @return the URL with template parameters replaced
*/
protected String replaceUriTemplateParams(String uri, List<Param> params, Set<String> usedParams)
throws JspException {
String encoding = this.pageContext.getResponse().getCharacterEncoding();
for (Param param : params) {
String template = URL_TEMPLATE_DELIMITER_PREFIX + param.getName() + URL_TEMPLATE_DELIMITER_SUFFIX;
if (uri.contains(template)) {
usedParams.add(param.getName());
String value = param.getValue();
try {
uri = StringUtils.replace(uri, template,
(value != null ? UriUtils.encodePath(value, encoding) : ""));
}
catch (UnsupportedCharsetException ex) {
throw new JspException(ex);
}
}
else {
template = URL_TEMPLATE_DELIMITER_PREFIX + '/' + param.getName() + URL_TEMPLATE_DELIMITER_SUFFIX;
if (uri.contains(template)) {
usedParams.add(param.getName());
String value = param.getValue();
try {
uri = StringUtils.replace(uri, template,
(value != null ? UriUtils.encodePathSegment(value, encoding) : ""));
}
catch (UnsupportedCharsetException ex) {
throw new JspException(ex);
}
}
}
}
return uri;
}
Internal enum that classifies URLs by type.
/**
* Internal enum that classifies URLs by type.
*/
private enum UrlType {
CONTEXT_RELATIVE, RELATIVE, ABSOLUTE
}
}