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 }