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
153                StringBuilder render = new StringBuilder();
154                if (Strings.isEmpty(protocol) == false)
155                {
156                        render.append(protocol);
157                        render.append(':');
158                }
159
160                if (Strings.isEmpty(host) == false)
161                {
162                        render.append("//");
163                        render.append(host);
164
165                        if ((port != null) && !port.equals(PROTO_TO_PORT.get(protocol)))
166                        {
167                                render.append(':');
168                                render.append(port);
169                        }
170                }
171
172                if (!(url.isFull() || url.isContextAbsolute()))
173                {
174                        render.append(request.getContextPath());
175                        render.append(request.getFilterPath());
176                }
177                return Strings.join("/", render.toString(), path.toString());
178        }
179
180        /**
181         * Gets port that should be used to render the url
182         *
183         * @param url
184         *            url being rendered
185         * @return port or {@code null} if none is set
186         */
187        protected Integer resolvePort(final Url url)
188        {
189                return choose(url.getPort(), baseUrl.getPort(), request.getClientUrl().getPort());
190        }
191
192        /**
193         * Gets the host name that should be used to render the url
194         *
195         * @param url
196         *            url being rendered
197         * @return the host name or {@code null} if none is set
198         */
199        protected String resolveHost(final Url url)
200        {
201                return choose(url.getHost(), baseUrl.getHost(), request.getClientUrl().getHost());
202        }
203
204        /**
205         * Gets the protocol that should be used to render the url
206         *
207         * @param url
208         *            url being rendered
209         * @return the protocol or {@code null} if none is set
210         */
211        protected String resolveProtocol(final Url url)
212        {
213                return choose(url.getProtocol(), baseUrl.getProtocol(), request.getClientUrl()
214                        .getProtocol());
215        }
216
217        /**
218         * Renders the Url relative to currently set Base Url.
219         *
220         * This method is only intended for Wicket URLs, because the {@link Url} object represents part
221         * of URL after Wicket Filter.
222         *
223         * For general URLs within context use {@link #renderContextRelativeUrl(String)}
224         *
225         * @param url
226         * @return Url rendered as string
227         */
228        public String renderRelativeUrl(final Url url)
229        {
230                Args.notNull(url, "url");
231
232                if (url instanceof IUrlRenderer)
233                {
234                        IUrlRenderer renderer = (IUrlRenderer)url;
235                        return renderer.renderRelativeUrl(url, getBaseUrl());
236                }
237
238                List<String> baseUrlSegments =  new ArrayList<>(getBaseUrl().getSegments());
239                List<String> urlSegments = new ArrayList<>(url.getSegments());
240
241                if (!getBaseUrl().isContextRelative())
242                {
243                        // so we remove any possible filter/context segments
244                        removeCommonPrefixes(request, baseUrlSegments);
245                }
246                removeCommonPrefixes(request, urlSegments);
247
248                List<String> newSegments = new ArrayList<>();
249
250                int common = 0;
251
252                String last = null;
253
254                for (String s : baseUrlSegments)
255                {
256                        if (!urlSegments.isEmpty() && s.equals(urlSegments.get(0)))
257                        {
258                                ++common;
259                                last = urlSegments.remove(0);
260                        }
261                        else
262                        {
263                                break;
264                        }
265                }
266
267                // we want the new URL to have at least one segment (other than possible ../)
268                if ((last != null) && (urlSegments.isEmpty() || (baseUrlSegments.size() == common)))
269                {
270                        --common;
271                        urlSegments.add(0, last);
272                }
273
274                int baseUrlSize = baseUrlSegments.size();
275                if (common + 1 == baseUrlSize && urlSegments.isEmpty())
276                {
277                        newSegments.add(".");
278                }
279                else
280                {
281                        for (int i = common + 1; i < baseUrlSize; ++i)
282                        {
283                                newSegments.add("..");
284                        }
285                }
286                newSegments.addAll(urlSegments);
287
288                Url relativeUrl = new Url(newSegments, url.getQueryParameters());
289                relativeUrl.setFragment(url.getFragment());
290                String renderedUrl = relativeUrl.toString();
291
292                // sanitize start
293                if (renderedUrl.startsWith("...") || (!renderedUrl.startsWith("..") && !renderedUrl.equals(".")))
294                {
295                        // WICKET-4260
296                        renderedUrl = "./" + renderedUrl;
297                }
298
299                // add trailing slash if the url has no query string and ends with ..
300                if (renderedUrl.indexOf('?') == -1 && (renderedUrl.endsWith("..") && renderedUrl.endsWith("...") == false))
301                {
302                        // WICKET-4401
303                        renderedUrl = renderedUrl + '/';
304                }
305
306                return renderedUrl;
307        }
308
309        /**
310         * Removes common prefixes like empty first segment, context path and filter path.
311         *
312         * @param request
313         *            the current web request
314         * @param segments
315         *            the segments to clean
316         */
317        private void removeCommonPrefixes(Request request, List<String> segments)
318        {
319                // try to remove context/filter path only if the Url starts with '/',
320                // i.e. has an empty segment in the beginning
321                if ((segments.isEmpty() || segments.get(0).isEmpty()) == false)
322                {
323                        return;
324                }
325
326                Url commonPrefix = Url.parse(request.getContextPath() + request.getFilterPath());
327                // if both context and filter path are empty, common prefixes are empty too
328                if (commonPrefix.getSegments().isEmpty())
329                {
330                        // WICKET-4920 and WICKET-4935
331                        commonPrefix.getSegments().add("");
332                }
333
334                for (int i = 0; i < commonPrefix.getSegments().size() && i < segments.size(); i++)
335                {
336                        String commonPrefixSegment = Strings.stripJSessionId(commonPrefix.getSegments().get(i));
337                        String segmentToClean = Strings.stripJSessionId(segments.get(i));
338                        if (commonPrefixSegment.equals(segmentToClean) == false)
339                        {
340                                LOG.debug("Segments '{}' do not start with common prefix '{}'", segments,
341                                        commonPrefix);
342                                return;
343                        }
344                }
345
346                for (int i = 0; i < commonPrefix.getSegments().size() && !segments.isEmpty(); i++)
347                {
348                        segments.remove(0);
349                }
350        }
351
352        /**
353         * Determines whether a URL should be rendered in its full form
354         *
355         * @param url
356         * @return {@code true} if URL should be rendered in the full form
357         */
358        protected boolean shouldRenderAsFull(final Url url)
359        {
360                if (url.shouldRenderAsFull()) {
361                        return true;
362                }
363
364                Url clientUrl = request.getClientUrl();
365
366                if (!Strings.isEmpty(url.getProtocol()) &&
367                        !url.getProtocol().equals(clientUrl.getProtocol()))
368                {
369                        return true;
370                }
371                if (!Strings.isEmpty(url.getHost()) && !url.getHost().equals(clientUrl.getHost()))
372                {
373                        return true;
374                }
375                if ((url.getPort() != null) && !url.getPort().equals(clientUrl.getPort()))
376                {
377                        return true;
378                }
379                if (url.isContextAbsolute())
380                {
381                        // do not relativize urls like "/a/b"
382                        return true;
383                }
384                return false;
385        }
386
387        /**
388         * Renders the URL within context relative to current base URL.
389         *
390         * @param url
391         * @return relative URL
392         */
393        public String renderContextRelativeUrl(String url)
394        {
395                Args.notNull(url, "url");
396
397                if (url.startsWith("/"))
398                {
399                        url = url.substring(1);
400                }
401
402                PrependingStringBuffer buffer = new PrependingStringBuffer(url);
403                for (int i = 0; i < getBaseUrl().getSegments().size() - 1; ++i)
404                {
405                        buffer.prepend("../");
406                }
407
408                buffer.prepend(request.getPrefixToContextPath());
409
410                return buffer.toString();
411        }
412
413        private static String choose(String value, final String fallback1, final String fallback2)
414        {
415                if (Strings.isEmpty(value))
416                {
417                        value = fallback1;
418                        if (Strings.isEmpty(value))
419                        {
420                                value = fallback2;
421                        }
422                }
423                return value;
424        }
425
426        private static Integer choose(Integer value, final Integer fallback1, final Integer fallback2)
427        {
428                if (value == null)
429                {
430                        value = fallback1;
431                        if (value == null)
432                        {
433                                value = fallback2;
434                        }
435                }
436                return value;
437        }
438}