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">&lt;spring:url value="/url/path/{variableName}"&gt;
066 *   &lt;spring:param name="variableName" value="more than JSTL c:url" /&gt;
067 * &lt;/spring:url&gt;</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}