001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.ui; 020 021import org.apache.log4j.Logger; 022import org.apache.wiki.InternalWikiException; 023import org.apache.wiki.WikiEngine; 024import org.apache.wiki.WikiPage; 025import org.apache.wiki.WikiProvider; 026import org.apache.wiki.api.exceptions.ProviderException; 027import org.apache.wiki.auth.GroupPrincipal; 028import org.apache.wiki.parser.MarkupParser; 029import org.apache.wiki.util.TextUtil; 030 031import javax.servlet.http.HttpServletRequest; 032import java.io.IOException; 033import java.util.HashMap; 034import java.util.Iterator; 035import java.util.Map; 036import java.util.Properties; 037 038/** 039 * <p>Resolves special pages, JSPs and Commands on behalf of a 040 * WikiEngine. CommandResolver will automatically resolve page names 041 * with singular/plural variants. It can also detect the correct Command 042 * based on parameters supplied in an HTTP request, or due to the 043 * JSP being accessed.</p> 044 * <p> 045 * <p>CommandResolver's static {@link #findCommand(String)} method is 046 * the simplest method; it looks up and returns the Command matching 047 * a supplied wiki context. For example, looking up the request context 048 * <code>view</code> returns {@link PageCommand#VIEW}. Use this method 049 * to obtain static Command instances that aren't targeted at a particular 050 * page or group.</p> 051 * <p>For more complex lookups in which the caller supplies an HTTP 052 * request, {@link #findCommand(HttpServletRequest, String)} will 053 * look up and return the correct Command. The String parameter 054 * <code>defaultContext</code> supplies the request context to use 055 * if it cannot be detected. However, note that the default wiki 056 * context may be over-ridden if the request was for a "special page."</p> 057 * <p>For example, suppose the WikiEngine's properties specify a 058 * special page called <code>UserPrefs</code> 059 * that redirects to <code>UserPreferences.jsp</code>. The ordinary 060 * lookup method {@linkplain #findCommand(String)} using a supplied 061 * context <code>view</code> would return {@link PageCommand#VIEW}. But 062 * the {@linkplain #findCommand(HttpServletRequest, String)} method, 063 * when passed the same context (<code>view</code>) and an HTTP request 064 * containing the page parameter value <code>UserPrefs</code>, 065 * will instead return {@link WikiCommand#PREFS}.</p> 066 * @since 2.4.22 067 */ 068public final class CommandResolver 069{ 070 /** Prefix in jspwiki.properties signifying special page keys. */ 071 private static final String PROP_SPECIALPAGE = "jspwiki.specialPage."; 072 073 /** Private map with request contexts as keys, Commands as values */ 074 private static final Map<String, Command> CONTEXTS; 075 076 /** Private map with JSPs as keys, Commands as values */ 077 private static final Map<String, Command> JSPS; 078 079 /** Store the JSP-to-Command and context-to-Command mappings */ 080 static 081 { 082 CONTEXTS = new HashMap<String, Command>(); 083 JSPS = new HashMap<String, Command>(); 084 Command[] commands = AbstractCommand.allCommands(); 085 for( int i = 0; i < commands.length; i++ ) 086 { 087 JSPS.put( commands[i].getJSP(), commands[i] ); 088 CONTEXTS.put( commands[i].getRequestContext(), commands[i] ); 089 } 090 } 091 092 private final Logger m_log = Logger.getLogger( CommandResolver.class ); 093 094 private final WikiEngine m_engine; 095 096 /** If true, we'll also consider english plurals (+s) a match. */ 097 private final boolean m_matchEnglishPlurals; 098 099 /** Stores special page names as keys, and Commands as values. */ 100 private final Map<String, Command> m_specialPages; 101 102 /** 103 * Constructs a CommandResolver for a given WikiEngine. This constructor 104 * will extract the special page references for this wiki and store them in 105 * a cache used for resolution. 106 * @param engine the wiki engine 107 * @param properties the properties used to initialize the wiki 108 */ 109 public CommandResolver( WikiEngine engine, Properties properties ) 110 { 111 m_engine = engine; 112 m_specialPages = new HashMap<String, Command>(); 113 114 // Skim through the properties and look for anything with 115 // the "special page" prefix. Create maps that allow us 116 // look up the correct Command based on special page name. 117 // If a matching command isn't found, create a RedirectCommand. 118 for(String key : properties.stringPropertyNames()) 119 { 120 if ( key.startsWith( PROP_SPECIALPAGE ) ) 121 { 122 String specialPage = key.substring( PROP_SPECIALPAGE.length() ); 123 String jsp = (String) properties.getProperty(key); 124 if ( specialPage != null && jsp != null ) 125 { 126 specialPage = specialPage.trim(); 127 jsp = jsp.trim(); 128 Command command = JSPS.get( jsp ); 129 if ( command == null ) 130 { 131 Command redirect = RedirectCommand.REDIRECT; 132 command = redirect.targetedCommand( jsp ); 133 } 134 m_specialPages.put( specialPage, command ); 135 } 136 } 137 } 138 139 // Do we match plurals? 140 m_matchEnglishPlurals = TextUtil.getBooleanProperty( properties, WikiEngine.PROP_MATCHPLURALS, true ); 141 } 142 143 /** 144 * Attempts to locate a wiki command for a supplied request context. 145 * The resolution technique is simple: we examine the list of 146 * Commands returned by {@link AbstractCommand#allCommands()} and 147 * return the one whose <code>requestContext</code> matches the 148 * supplied context. If the supplied context does not resolve to a known 149 * Command, this method throws an {@link IllegalArgumentException}. 150 * @param context the request context 151 * @return the resolved context 152 */ 153 public static Command findCommand( String context ) 154 { 155 Command command = CONTEXTS.get( context ); 156 if ( command == null ) 157 { 158 throw new IllegalArgumentException( "Unsupported wiki context: " + context + "." ); 159 } 160 return command; 161 } 162 163 /** 164 * <p> 165 * Attempts to locate a Command for a supplied wiki context and HTTP 166 * request, incorporating the correct WikiPage into the command if reqiured. 167 * This method will first determine what page the user requested by 168 * delegating to {@link #extractPageFromParameter(String, HttpServletRequest)}. If 169 * this page equates to a special page, we return the Command 170 * corresponding to that page. Otherwise, this method simply returns the 171 * Command for the supplied request context. 172 * </p> 173 * <p> 174 * The reason this method attempts to resolve against special pages is 175 * because some of them resolve to contexts that may be different from the 176 * one supplied. For example, a VIEW request context for the special page 177 * "UserPreferences" should return a PREFS context instead. 178 * </p> 179 * <p> 180 * When the caller supplies a request context and HTTP request that 181 * specifies an actual wiki page (rather than a special page), this method 182 * will return a "targeted" Command that includes the resolved WikiPage 183 * as the target. (See {@link #resolvePage(HttpServletRequest, String)} 184 * for the resolution algorithm). Specifically, the Command will 185 * return a non-<code>null</code> value for its {@link AbstractCommand#getTarget()} method. 186 * </p> 187 * <p><em>Note: if this method determines that the Command is the VIEW PageCommand, 188 * then the Command returned will always be targeted to the front page.</em></p> 189 * @param request the HTTP request; if <code>null</code>, delegates 190 * to {@link #findCommand(String)} 191 * @param defaultContext the request context to use by default 192 * @return the resolved wiki command 193 */ 194 public Command findCommand( HttpServletRequest request, String defaultContext ) 195 { 196 // Corner case if request is null 197 if ( request == null ) 198 { 199 return findCommand( defaultContext ); 200 } 201 202 Command command = null; 203 204 // Determine the name of the page (which may be null) 205 String pageName = extractPageFromParameter( defaultContext, request ); 206 207 // Can we find a special-page command matching the extracted page? 208 if ( pageName != null ) 209 { 210 command = m_specialPages.get( pageName ); 211 } 212 213 // If we haven't found a matching command yet, extract the JSP path 214 // and compare to our list of special pages 215 if ( command == null ) 216 { 217 command = extractCommandFromPath( request ); 218 219 // Otherwise: use the default context 220 if ( command == null ) 221 { 222 command = CONTEXTS.get( defaultContext ); 223 if ( command == null ) 224 { 225 throw new IllegalArgumentException( "Wiki context " + defaultContext + " is illegal." ); 226 } 227 } 228 } 229 230 // For PageCommand.VIEW, default to front page if a page wasn't supplied 231 if( PageCommand.VIEW.equals( command ) && pageName == null ) 232 { 233 pageName = m_engine.getFrontPage(); 234 } 235 236 // These next blocks handle targeting requirements 237 238 // If we were passed a page parameter, try to resolve it 239 if ( command instanceof PageCommand && pageName != null ) 240 { 241 // If there's a matching WikiPage, "wrap" the command 242 WikiPage page = resolvePage( request, pageName ); 243 if ( page != null ) 244 { 245 return command.targetedCommand( page ); 246 } 247 } 248 249 // If "create group" command, target this wiki 250 String wiki = m_engine.getApplicationName(); 251 if ( WikiCommand.CREATE_GROUP.equals( command ) ) 252 { 253 return WikiCommand.CREATE_GROUP.targetedCommand( wiki ); 254 } 255 256 // If group command, see if we were passed a group name 257 if ( command instanceof GroupCommand ) 258 { 259 String groupName = request.getParameter( "group" ); 260 groupName = TextUtil.replaceEntities( groupName ); 261 if ( groupName != null && groupName.length() > 0 ) 262 { 263 GroupPrincipal group = new GroupPrincipal( groupName ); 264 return command.targetedCommand( group ); 265 } 266 } 267 268 // No page provided; return an "ordinary" command 269 return command; 270 } 271 272 /** 273 * <p> 274 * Returns the correct page name, or <code>null</code>, if no such page can be found. 275 * Aliases are considered. 276 * </p> 277 * <p> 278 * In some cases, page names can refer to other pages. For example, when you 279 * have matchEnglishPlurals set, then a page name "Foobars" will be 280 * transformed into "Foobar", should a page "Foobars" not exist, but the 281 * page "Foobar" would. This method gives you the correct page name to refer 282 * to. 283 * </p> 284 * <p> 285 * This facility can also be used to rewrite any page name, for example, by 286 * using aliases. It can also be used to check the existence of any page. 287 * </p> 288 * @since 2.4.20 289 * @param page the page name. 290 * @return The rewritten page name, or <code>null</code>, if the page does not exist. 291 * @throws ProviderException if the underlyng page provider that locates pages 292 * throws an exception 293 */ 294 public String getFinalPageName( String page ) throws ProviderException 295 { 296 boolean isThere = simplePageExists( page ); 297 String finalName = page; 298 299 if ( !isThere && m_matchEnglishPlurals ) 300 { 301 if ( page.endsWith( "s" ) ) 302 { 303 finalName = page.substring( 0, page.length() - 1 ); 304 } 305 else 306 { 307 finalName += "s"; 308 } 309 310 isThere = simplePageExists( finalName ); 311 } 312 313 if( !isThere ) 314 { 315 finalName = MarkupParser.wikifyLink( page ); 316 isThere = simplePageExists(finalName); 317 318 if( !isThere && m_matchEnglishPlurals ) 319 { 320 if( finalName.endsWith( "s" ) ) 321 { 322 finalName = finalName.substring( 0, finalName.length() - 1 ); 323 } 324 else 325 { 326 finalName += "s"; 327 } 328 329 isThere = simplePageExists( finalName ); 330 } 331 } 332 333 return isThere ? finalName : null; 334 } 335 336 /** 337 * <p> 338 * If the page is a special page, this method returns a direct URL to that 339 * page; otherwise, it returns <code>null</code>. 340 * </p> 341 * <p> 342 * Special pages are non-existant references to other pages. For example, 343 * you could define a special page reference "RecentChanges" which would 344 * always be redirected to "RecentChanges.jsp" instead of trying to find a 345 * Wiki page called "RecentChanges". 346 * </p> 347 * @param page the page name ro search for 348 * @return the URL of the special page, if the supplied page is one, or <code>null</code> 349 */ 350 public String getSpecialPageReference( String page ) 351 { 352 Command command = m_specialPages.get( page ); 353 354 if ( command != null ) 355 { 356 return m_engine.getURLConstructor() 357 .makeURL( command.getRequestContext(), command.getURLPattern(), true, null ); 358 } 359 360 return null; 361 } 362 363 /** 364 * Extracts a Command based on the JSP path of an HTTP request. 365 * If the JSP requested matches a Command's <code>getJSP()</code> 366 * value, that Command is returned. 367 * @param request the HTTP request 368 * @return the resolved Command, or <code>null</code> if not found 369 */ 370 protected Command extractCommandFromPath( HttpServletRequest request ) 371 { 372 String jsp = request.getServletPath(); 373 374 // Take everything to right of initial / and left of # or ? 375 int hashMark = jsp.indexOf( '#' ); 376 if ( hashMark != -1 ) 377 { 378 jsp = jsp.substring( 0, hashMark ); 379 } 380 int questionMark = jsp.indexOf( '?' ); 381 if ( questionMark != -1 ) 382 { 383 jsp = jsp.substring( 0, questionMark ); 384 } 385 if ( jsp.startsWith( "/" ) ) 386 { 387 jsp = jsp.substring( 1 ); 388 } 389 390 // Find special page reference? 391 for( Iterator< Map.Entry< String, Command > > i = m_specialPages.entrySet().iterator(); i.hasNext(); ) 392 { 393 Map.Entry< String, Command > entry = i.next(); 394 Command specialCommand = entry.getValue(); 395 if ( specialCommand.getJSP().equals( jsp ) ) 396 { 397 return specialCommand; 398 } 399 } 400 401 // Still haven't found a matching command? 402 // Ok, see if we match against our standard list of JSPs 403 if ( jsp.length() > 0 && JSPS.containsKey( jsp ) ) 404 { 405 return JSPS.get( jsp ); 406 } 407 408 return null; 409 } 410 411 /** 412 * Determines the correct wiki page based on a supplied request context and 413 * HTTP request. This method attempts to determine the page requested by a 414 * user, taking into acccount special pages. The resolution algorithm will: 415 * <ul> 416 * <li>Extract the page name from the URL according to the rules for the 417 * current {@link org.apache.wiki.url.URLConstructor}. If a page name was 418 * passed in the request, return the correct name after taking into account 419 * potential plural matches.</li> 420 * <li>If the extracted page name is <code>null</code>, attempt to see 421 * if a "special page" was intended by examining the servlet path. For 422 * example, the request path "/UserPreferences.jsp" will resolve to 423 * "UserPreferences."</li> 424 * <li>If neither of these methods work, this method returns 425 * <code>null</code></li> 426 * </ul> 427 * @param requestContext the request context 428 * @param request the HTTP request 429 * @return the resolved page name 430 */ 431 protected String extractPageFromParameter( final String requestContext, final HttpServletRequest request ) { 432 String page; 433 434 // Extract the page name from the URL directly 435 try { 436 page = m_engine.getURLConstructor().parsePage( requestContext, request, m_engine.getContentEncoding() ); 437 if ( page != null ) { 438 try { 439 // Look for singular/plural variants; if one 440 // not found, take the one the user supplied 441 final String finalPage = getFinalPageName( page ); 442 if ( finalPage != null ) { 443 page = finalPage; 444 } 445 } catch( final ProviderException e ) { 446 // FIXME: Should not ignore! 447 } 448 return page; 449 } 450 } catch( final IOException e ) { 451 m_log.error( "Unable to create context", e ); 452 throw new InternalWikiException( "Big internal booboo, please check logs." , e); 453 } 454 455 // Didn't resolve; return null 456 return null; 457 } 458 459 /** 460 * Looks up and returns the correct, versioned WikiPage based on a supplied 461 * page name and optional <code>version</code> parameter passed in an HTTP 462 * request. If the <code>version</code> parameter does not exist in the 463 * request, the latest version is returned. 464 * @param request the HTTP request 465 * @param page the name of the page to look up; this page <em>must</em> exist 466 * @return the wiki page 467 */ 468 protected WikiPage resolvePage( HttpServletRequest request, String page ) 469 { 470 // See if the user included a version parameter 471 WikiPage wikipage; 472 int version = WikiProvider.LATEST_VERSION; 473 String rev = request.getParameter( "version" ); 474 475 if ( rev != null ) 476 { 477 try 478 { 479 version = Integer.parseInt( rev ); 480 } 481 catch( NumberFormatException e ) 482 { 483 // This happens a lot with bots or other guys who are trying 484 // to test if we are vulnerable to e.g. XSS attacks. We catch 485 // it here so that the admin does not get tons of mail. 486 } 487 } 488 489 wikipage = m_engine.getPage( page, version ); 490 491 if ( wikipage == null ) 492 { 493 page = MarkupParser.cleanLink( page ); 494 wikipage = new WikiPage( m_engine, page ); 495 } 496 return wikipage; 497 } 498 499 /** 500 * Determines whether a "page" exists by examining the list of special pages 501 * and querying the page manager. 502 * @param page the page to seek 503 * @return <code>true</code> if the page exists, <code>false</code> 504 * otherwise 505 * @throws ProviderException if the underlyng page provider that locates pages 506 * throws an exception 507 */ 508 protected boolean simplePageExists( String page ) throws ProviderException 509 { 510 if ( m_specialPages.containsKey( page ) ) 511 { 512 return true; 513 } 514 return m_engine.getPageManager().pageExists( page ); 515 } 516 517}