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.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.InternalWikiException; 024import org.apache.wiki.api.core.Command; 025import org.apache.wiki.api.core.Engine; 026import org.apache.wiki.api.core.Page; 027import org.apache.wiki.api.exceptions.ProviderException; 028import org.apache.wiki.api.providers.WikiProvider; 029import org.apache.wiki.api.spi.Wiki; 030import org.apache.wiki.auth.GroupPrincipal; 031import org.apache.wiki.pages.PageManager; 032import org.apache.wiki.parser.MarkupParser; 033import org.apache.wiki.url.URLConstructor; 034import org.apache.wiki.util.TextUtil; 035 036import javax.servlet.http.HttpServletRequest; 037import java.io.IOException; 038import java.util.HashMap; 039import java.util.Map; 040import java.util.Properties; 041 042 043/** 044 * <p>Default implementation for {@link CommandResolver}</p> 045 * 046 * @since 2.4.22 047 */ 048public class DefaultCommandResolver implements CommandResolver { 049 050 /** Private map with request contexts as keys, Commands as values */ 051 private static final Map< String, Command > CONTEXTS; 052 053 /** Private map with JSPs as keys, Commands as values */ 054 private static final Map< String, Command > JSPS; 055 056 /* Store the JSP-to-Command and context-to-Command mappings */ 057 static { 058 CONTEXTS = new HashMap<>(); 059 JSPS = new HashMap<>(); 060 final Command[] commands = AllCommands.get(); 061 for( final Command command : commands ) { 062 JSPS.put( command.getJSP(), command ); 063 CONTEXTS.put( command.getRequestContext(), command ); 064 } 065 } 066 067 private static final Logger LOG = LogManager.getLogger( DefaultCommandResolver.class ); 068 069 private final Engine m_engine; 070 071 /** If true, we'll also consider english plurals (+s) a match. */ 072 private final boolean m_matchEnglishPlurals; 073 074 /** Stores special page names as keys, and Commands as values. */ 075 private final Map<String, Command> m_specialPages; 076 077 /** 078 * Constructs a CommandResolver for a given Engine. This constructor will extract the special page references for this wiki and 079 * store them in a cache used for resolution. 080 * 081 * @param engine the wiki engine 082 * @param properties the properties used to initialize the wiki 083 */ 084 public DefaultCommandResolver( final Engine engine, final Properties properties ) { 085 m_engine = engine; 086 m_specialPages = new HashMap<>(); 087 088 // Skim through the properties and look for anything with the "special page" prefix. Create maps that allow us look up 089 // the correct Command based on special page name. If a matching command isn't found, create a RedirectCommand. 090 for( final String key : properties.stringPropertyNames() ) { 091 if ( key.startsWith( PROP_SPECIALPAGE ) ) { 092 String specialPage = key.substring( PROP_SPECIALPAGE.length() ); 093 String jsp = properties.getProperty( key ); 094 if ( jsp != null ) { 095 specialPage = specialPage.trim(); 096 jsp = jsp.trim(); 097 Command command = JSPS.get( jsp ); 098 if ( command == null ) { 099 final Command redirect = RedirectCommand.REDIRECT; 100 command = redirect.targetedCommand( jsp ); 101 } 102 m_specialPages.put( specialPage, command ); 103 } 104 } 105 } 106 107 // Do we match plurals? 108 m_matchEnglishPlurals = TextUtil.getBooleanProperty( properties, Engine.PROP_MATCHPLURALS, true ); 109 } 110 111 /** 112 * {@inheritDoc} 113 */ 114 @Override 115 public Command findCommand( final HttpServletRequest request, final String defaultContext ) { 116 // Corner case if request is null 117 if ( request == null ) { 118 return CommandResolver.findCommand( defaultContext ); 119 } 120 121 Command command = null; 122 123 // Determine the name of the page (which may be null) 124 String pageName = extractPageFromParameter( defaultContext, request ); 125 126 // Can we find a special-page command matching the extracted page? 127 if ( pageName != null ) { 128 command = m_specialPages.get( pageName ); 129 } 130 131 // If we haven't found a matching command yet, extract the JSP path and compare to our list of special pages 132 if ( command == null ) { 133 command = extractCommandFromPath( request ); 134 135 // Otherwise: use the default context 136 if ( command == null ) { 137 command = CONTEXTS.get( defaultContext ); 138 if ( command == null ) { 139 throw new IllegalArgumentException( "Wiki context " + defaultContext + " is illegal." ); 140 } 141 } 142 } 143 144 // For PageCommand.VIEW, default to front page if a page wasn't supplied 145 if( PageCommand.VIEW.equals( command ) && pageName == null ) { 146 pageName = m_engine.getFrontPage(); 147 } 148 149 // These next blocks handle targeting requirements 150 151 // If we were passed a page parameter, try to resolve it 152 if ( command instanceof PageCommand && pageName != null ) { 153 // If there's a matching WikiPage, "wrap" the command 154 final Page page = resolvePage( request, pageName ); 155 return command.targetedCommand( page ); 156 } 157 158 // If "create group" command, target this wiki 159 final String wiki = m_engine.getApplicationName(); 160 if ( WikiCommand.CREATE_GROUP.equals( command ) ) { 161 return WikiCommand.CREATE_GROUP.targetedCommand( wiki ); 162 } 163 164 // If group command, see if we were passed a group name 165 if( command instanceof GroupCommand ) { 166 String groupName = request.getParameter( "group" ); 167 groupName = TextUtil.replaceEntities( groupName ); 168 if ( groupName != null && !groupName.isEmpty() ) { 169 final GroupPrincipal group = new GroupPrincipal( groupName ); 170 return command.targetedCommand( group ); 171 } 172 } 173 174 // No page provided; return an "ordinary" command 175 return command; 176 } 177 178 /** 179 * {@inheritDoc} 180 */ 181 @Override 182 public String getFinalPageName( final String page ) throws ProviderException { 183 boolean isThere = simplePageExists( page ); 184 String finalName = page; 185 186 if ( !isThere && m_matchEnglishPlurals ) { 187 if ( page.endsWith( "s" ) ) { 188 finalName = page.substring( 0, page.length() - 1 ); 189 } else { 190 finalName += "s"; 191 } 192 193 isThere = simplePageExists( finalName ); 194 } 195 196 if( !isThere ) { 197 finalName = MarkupParser.wikifyLink( page ); 198 isThere = simplePageExists(finalName); 199 200 if( !isThere && m_matchEnglishPlurals ) { 201 if( finalName.endsWith( "s" ) ) { 202 finalName = finalName.substring( 0, finalName.length() - 1 ); 203 } else { 204 finalName += "s"; 205 } 206 207 isThere = simplePageExists( finalName ); 208 } 209 } 210 211 return isThere ? finalName : null; 212 } 213 214 /** 215 * {@inheritDoc} 216 */ 217 @Override 218 public String getSpecialPageReference( final String page ) { 219 final Command command = m_specialPages.get( page ); 220 if ( command != null ) { 221 return m_engine.getManager( URLConstructor.class ).makeURL( command.getRequestContext(), command.getURLPattern(), null ); 222 } 223 224 return null; 225 } 226 227 /** 228 * Extracts a Command based on the JSP path of an HTTP request. If the JSP requested matches a Command's <code>getJSP()</code> 229 * value, that Command is returned. 230 * 231 * @param request the HTTP request 232 * @return the resolved Command, or <code>null</code> if not found 233 */ 234 protected Command extractCommandFromPath( final HttpServletRequest request ) { 235 String jsp = request.getServletPath(); 236 237 // Take everything to right of initial / and left of # or ? 238 final int hashMark = jsp.indexOf( '#' ); 239 if ( hashMark != -1 ) { 240 jsp = jsp.substring( 0, hashMark ); 241 } 242 final int questionMark = jsp.indexOf( '?' ); 243 if ( questionMark != -1 ) { 244 jsp = jsp.substring( 0, questionMark ); 245 } 246 if ( jsp.startsWith( "/" ) ) { 247 jsp = jsp.substring( 1 ); 248 } 249 250 // Find special page reference? 251 for( final Map.Entry< String, Command > entry : m_specialPages.entrySet() ) { 252 final Command specialCommand = entry.getValue(); 253 if( specialCommand.getJSP().equals( jsp ) ) { 254 return specialCommand; 255 } 256 } 257 258 // Still haven't found a matching command? Ok, see if we match against our standard list of JSPs 259 if ( !jsp.isEmpty() && JSPS.containsKey( jsp ) ) { 260 return JSPS.get( jsp ); 261 } 262 263 return null; 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 @Override 270 public String extractPageFromParameter( final String requestContext, final HttpServletRequest request ) { 271 // Extract the page name from the URL directly 272 try { 273 String page = m_engine.getManager( URLConstructor.class ).parsePage( requestContext, request, m_engine.getContentEncoding() ); 274 if ( page != null ) { 275 try { 276 // Look for singular/plural variants; if one not found, take the one the user supplied 277 final String finalPage = getFinalPageName( page ); 278 if ( finalPage != null ) { 279 page = finalPage; 280 } 281 } catch( final ProviderException e ) { 282 // FIXME: Should not ignore! 283 } 284 return page; 285 } 286 } catch( final IOException e ) { 287 LOG.error( "Unable to create context", e ); 288 throw new InternalWikiException( "Big internal booboo, please check logs." , e ); 289 } 290 291 // Didn't resolve; return null 292 return null; 293 } 294 295 /** 296 * {@inheritDoc} 297 */ 298 @Override 299 public Page resolvePage( final HttpServletRequest request, String page ) { 300 // See if the user included a version parameter 301 int version = WikiProvider.LATEST_VERSION; 302 final String rev = request.getParameter( "version" ); 303 if ( rev != null ) { 304 try { 305 version = Integer.parseInt( rev ); 306 } catch( final NumberFormatException e ) { 307 // This happens a lot with bots or other guys who are trying to test if we are vulnerable to e.g. XSS attacks. We catch 308 // it here so that the admin does not get tons of mail. 309 } 310 } 311 312 Page wikipage = m_engine.getManager( PageManager.class ).getPage( page, version ); 313 if ( wikipage == null ) { 314 page = MarkupParser.cleanLink( page ); 315 wikipage = Wiki.contents().page( m_engine, page ); 316 } 317 return wikipage; 318 } 319 320 /** 321 * Determines whether a "page" exists by examining the list of special pages and querying the page manager. 322 * 323 * @param page the page to seek 324 * @return <code>true</code> if the page exists, <code>false</code> otherwise 325 * @throws ProviderException if the underlyng page provider that locates pages 326 * throws an exception 327 */ 328 protected boolean simplePageExists( final String page ) throws ProviderException { 329 if ( m_specialPages.containsKey( page ) ) { 330 return true; 331 } 332 return m_engine.getManager( PageManager.class ).pageExists( page ); 333 } 334 335}