001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.request;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.wicket.util.lang.Args;
025import org.apache.wicket.util.string.PrependingStringBuffer;
026import org.apache.wicket.util.string.Strings;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030/**
031 * Takes care of rendering URLs.
032 * <p>
033 * Normally Urls are rendered relative to the base Url. Base Url is normally Url of the page being
034 * rendered. However, during Ajax request and redirect to buffer rendering the BaseUrl needs to be
035 * adjusted.
036 * 
037 * @author Matej Knopp
038 * @author Igor Vaynberg
039 */
040public class UrlRenderer
041{
042        private static final Logger LOG = LoggerFactory.getLogger(UrlRenderer.class);
043
044        private static final Map<String, Integer> PROTO_TO_PORT = new HashMap<>();
045        static
046        {
047                PROTO_TO_PORT.put("http", 80);
048                PROTO_TO_PORT.put("https", 443);
049        }
050
051        private final Request request;
052        private Url baseUrl;
053
054        /**
055         * Construct.
056         * 
057         * @param request
058         *            Request that serves as the base for rendering urls
059         */
060        public UrlRenderer(final Request request)
061        {
062                this.request = request;
063                baseUrl = request.getClientUrl();
064        }
065
066        /**
067         * Sets the base Url. All generated URLs will be relative to this Url.
068         * 
069         * @param base
070         * @return original base Url
071         */
072        public Url setBaseUrl(final Url base)
073        {
074                Args.notNull(base, "base");
075
076                Url original = baseUrl;
077                baseUrl = base;
078                return original;
079        }
080
081        /**
082         * Returns the base Url.
083         * 
084         * @return base Url
085         */
086        public Url getBaseUrl()
087        {
088                return baseUrl;
089        }
090
091        /**
092         * Renders the Url
093         * 
094         * @param url
095         * @return Url rendered as string
096         */
097        public String renderUrl(final Url url)
098        {
099                final String renderedUrl;
100                if (shouldRenderAsFull(url))
101                {
102                        if (!(url.isFull() || url.isContextAbsolute()))
103                        {
104                                String relativeUrl = renderRelativeUrl(url);
105                                Url relative = Url.parse(relativeUrl, url.getCharset());
106                                relative.setHost(url.getHost());
107                                relative.setPort(url.getPort());
108                                relative.setProtocol(url.getProtocol());
109                                renderedUrl = renderFullUrl(relative);
110                        }
111                        else
112                        {
113                                renderedUrl = renderFullUrl(url);
114                        }
115                }
116                else
117                {
118                        renderedUrl = renderRelativeUrl(url);
119                }
120                return renderedUrl;
121        }
122
123        /**
124         * Renders a full URL in the {@code protocol://hostname:port/path} format
125         * 
126         * @param url
127         * @return rendered URL
128         */
129        public String renderFullUrl(final Url url)
130        {
131                if (url instanceof IUrlRenderer)
132                {
133                        IUrlRenderer renderer = (IUrlRenderer)url;
134                        return renderer.renderFullUrl(url, getBaseUrl());
135                }
136
137                final String protocol = resolveProtocol(url);
138                final String host = resolveHost(url);
139                final Integer port = resolvePort(url);
140
141                final StringBuilder path;
142                if (url.isFull() || url.isContextAbsolute())
143                {
144                        path = new StringBuilder(url.canonical().toString());
145                }
146                else
147                {
148                        Url base = new Url(baseUrl);
149                        base.resolveRelative(url);
150                        path = new StringBuilder(base.toString());
151                }
152                if (url.getFragment() != null)
153                {
154                        path.append('#').append(url.getFragment());
155                }
156
157                StringBuilder render = new StringBuilder();
158                if (Strings.isEmpty(protocol) == false)
159                {
160                        render.append(protocol);
161                        render.append(':');
162                }
163
164                if (Strings.isEmpty(host) == false)
165                {
166                        render.append("//");
167                        render.append(host);
168
169                        if ((port != null) && !port.equals(PROTO_TO_PORT.get(protocol)))
170                        {
171                                render.append(':');
172                                render.append(port);
173                        }
174                }
175
176                if (!(url.isFull() || url.isContextAbsolute()))
177                {
178                        render.append(request.getContextPath());
179                        render.append(request.getFilterPath());
180                }
181                return Strings.join("/", render.toString(), path.toString());
182        }
183
184        /**
185         * Gets port that should be used to render the url
186         * 
187         * @param url
188         *            url being rendered
189         * @return port or {@code null} if none is set
190         */
191        protected Integer resolvePort(final Url url)
192        {
193                return choose(url.getPort(), baseUrl.getPort(), request.getClientUrl().getPort());
194        }
195
196        /**
197         * Gets the host name that should be used to render the url
198         * 
199         * @param url
200         *            url being rendered
201         * @return the host name or {@code null} if none is set
202         */
203        protected String resolveHost(final Url url)
204        {
205                return choose(url.getHost(), baseUrl.getHost(), request.getClientUrl().getHost());
206        }
207
208        /**
209         * Gets the protocol that should be used to render the url
210         * 
211         * @param url
212         *            url being rendered
213         * @return the protocol or {@code null} if none is set
214         */
215        protected String resolveProtocol(final Url url)
216        {
217                return choose(url.getProtocol(), baseUrl.getProtocol(), request.getClientUrl()
218                        .getProtocol());
219        }
220
221        /**
222         * Renders the Url relative to currently set Base Url.
223         * 
224         * This method is only intended for Wicket URLs, because the {@link Url} object represents part
225         * of URL after Wicket Filter.
226         * 
227         * For general URLs within context use {@link #renderContextRelativeUrl(String)}
228         * 
229         * @param url
230         * @return Url rendered as string
231         */
232        public String renderRelativeUrl(final Url url)
233        {
234                Args.notNull(url, "url");
235
236                if (url instanceof IUrlRenderer)
237                {
238                        IUrlRenderer renderer = (IUrlRenderer)url;
239                        return renderer.renderRelativeUrl(url, getBaseUrl());
240                }
241
242                List<String> baseUrlSegments =  new ArrayList<>(getBaseUrl().getSegments());
243                List<String> urlSegments = new ArrayList<>(url.getSegments());
244
245                if (!getBaseUrl().isContextRelative())
246                {
247                        // so we remove any possible filter/context segments
248                        removeCommonPrefixes(request, baseUrlSegments);
249                }
250                removeCommonPrefixes(request, urlSegments);
251
252                List<String> newSegments = new ArrayList<>();
253
254                int common = 0;
255
256                String last = null;
257
258                for (String s : baseUrlSegments)
259                {
260                        if (!urlSegments.isEmpty() && s.equals(urlSegments.get(0)))
261                        {
262                                ++common;
263                                last = urlSegments.remove(0);
264                        }
265                        else
266                        {
267                                break;
268                        }
269                }
270
271                // we want the new URL to have at least one segment (other than possible ../)
272                if ((last != null) && (urlSegments.isEmpty() || (baseUrlSegments.size() == common)))
273                {
274                        --common;
275                        urlSegments.add(0, last);
276                }
277
278                int baseUrlSize = baseUrlSegments.size();
279                if (common + 1 == baseUrlSize && urlSegments.isEmpty())
280                {
281                        newSegments.add(".");
282                }
283                else
284                {
285                        for (int i = common + 1; i < baseUrlSize; ++i)
286                        {
287                                newSegments.add("..");
288                        }
289                }
290                newSegments.addAll(urlSegments);
291
292                Url relativeUrl = new Url(newSegments, url.getQueryParameters());
293                relativeUrl.setFragment(url.getFragment());
294                String renderedUrl = relativeUrl.toString();
295
296                // sanitize start
297                if (renderedUrl.startsWith("...") || (!renderedUrl.startsWith("..") && !renderedUrl.equals(".")))
298                {
299                        // WICKET-4260
300                        renderedUrl = "./" + renderedUrl;
301                }
302
303                // add trailing slash if the url has no query string and ends with ..
304                if (renderedUrl.indexOf('?') == -1 && (renderedUrl.endsWith("..") && renderedUrl.endsWith("...") == false))
305                {
306                        // WICKET-4401
307                        renderedUrl = renderedUrl + '/';
308                }
309
310                return renderedUrl;
311        }
312
313        /**
314         * Removes common prefixes like empty first segment, context path and filter path.
315         * 
316         * @param request
317         *            the current web request
318         * @param segments
319         *            the segments to clean
320         */
321        private void removeCommonPrefixes(Request request, List<String> segments)
322        {
323                // try to remove context/filter path only if the Url starts with '/',
324                // i.e. has an empty segment in the beginning
325                if ((segments.isEmpty() || segments.get(0).isEmpty()) == false)
326                {
327                        return;
328                }
329
330                Url commonPrefix = Url.parse(request.getContextPath() + request.getFilterPath());
331                // if both context and filter path are empty, common prefixes are empty too
332                if (commonPrefix.getSegments().isEmpty())
333                {
334                        // WICKET-4920 and WICKET-4935
335                        commonPrefix.getSegments().add("");
336                }
337
338                for (int i = 0; i < commonPrefix.getSegments().size() && i < segments.size(); i++)
339                {
340                        String commonPrefixSegment = Strings.stripJSessionId(commonPrefix.getSegments().get(i));
341                        String segmentToClean = Strings.stripJSessionId(segments.get(i));
342                        if (commonPrefixSegment.equals(segmentToClean) == false)
343                        {
344                                LOG.debug("Segments '{}' do not start with common prefix '{}'", segments,
345                                        commonPrefix);
346                                return;
347                        }
348                }
349
350                for (int i = 0; i < commonPrefix.getSegments().size() && !segments.isEmpty(); i++)
351                {
352                        segments.remove(0);
353                }
354        }
355
356        /**
357         * Determines whether a URL should be rendered in its full form
358         * 
359         * @param url
360         * @return {@code true} if URL should be rendered in the full form
361         */
362        protected boolean shouldRenderAsFull(final Url url)
363        {
364                if (url.shouldRenderAsFull()) {
365                        return true;
366                }
367
368                Url clientUrl = request.getClientUrl();
369
370                if (!Strings.isEmpty(url.getProtocol()) &&
371                        !url.getProtocol().equals(clientUrl.getProtocol()))
372                {
373                        return true;
374                }
375                if (!Strings.isEmpty(url.getHost()) && !url.getHost().equals(clientUrl.getHost()))
376                {
377                        return true;
378                }
379                if ((url.getPort() != null) && !url.getPort().equals(clientUrl.getPort()))
380                {
381                        return true;
382                }
383                if (url.isContextAbsolute())
384                {
385                        // do not relativize urls like "/a/b"
386                        return true;
387                }
388                return false;
389        }
390
391        /**
392         * Renders the URL within context relative to current base URL.
393         * 
394         * @param url
395         * @return relative URL
396         */
397        public String renderContextRelativeUrl(String url)
398        {
399                Args.notNull(url, "url");
400
401                if (url.startsWith("/"))
402                {
403                        url = url.substring(1);
404                }
405
406                PrependingStringBuffer buffer = new PrependingStringBuffer(url);
407                for (int i = 0; i < getBaseUrl().getSegments().size() - 1; ++i)
408                {
409                        buffer.prepend("../");
410                }
411
412                buffer.prepend(request.getPrefixToContextPath());
413
414                return buffer.toString();
415        }
416
417        private static String choose(String value, final String fallback1, final String fallback2)
418        {
419                if (Strings.isEmpty(value))
420                {
421                        value = fallback1;
422                        if (Strings.isEmpty(value))
423                        {
424                                value = fallback2;
425                        }
426                }
427                return value;
428        }
429
430        private static Integer choose(Integer value, final Integer fallback1, final Integer fallback2)
431        {
432                if (value == null)
433                {
434                        value = fallback1;
435                        if (value == null)
436                        {
437                                value = fallback2;
438                        }
439                }
440                return value;
441        }
442}