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}