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