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}