| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.wiki.ui; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Properties; |
| |
| import javax.servlet.http.HttpServletRequest; |
| |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.InternalWikiException; |
| import org.apache.wiki.WikiEngine; |
| import org.apache.wiki.WikiPage; |
| import org.apache.wiki.WikiProvider; |
| import org.apache.wiki.api.exceptions.ProviderException; |
| import org.apache.wiki.auth.GroupPrincipal; |
| import org.apache.wiki.parser.MarkupParser; |
| import org.apache.wiki.util.TextUtil; |
| |
| /** |
| * <p>Resolves special pages, JSPs and Commands on behalf of a |
| * WikiEngine. CommandResolver will automatically resolve page names |
| * with singular/plural variants. It can also detect the correct Command |
| * based on parameters supplied in an HTTP request, or due to the |
| * JSP being accessed.</p> |
| * <p> |
| * <p>CommandResolver's static {@link #findCommand(String)} method is |
| * the simplest method; it looks up and returns the Command matching |
| * a supplied wiki context. For example, looking up the request context |
| * <code>view</code> returns {@link PageCommand#VIEW}. Use this method |
| * to obtain static Command instances that aren't targeted at a particular |
| * page or group.</p> |
| * <p>For more complex lookups in which the caller supplies an HTTP |
| * request, {@link #findCommand(HttpServletRequest, String)} will |
| * look up and return the correct Command. The String parameter |
| * <code>defaultContext</code> supplies the request context to use |
| * if it cannot be detected. However, note that the default wiki |
| * context may be over-ridden if the request was for a "special page."</p> |
| * <p>For example, suppose the WikiEngine's properties specify a |
| * special page called <code>UserPrefs</code> |
| * that redirects to <code>UserPreferences.jsp</code>. The ordinary |
| * lookup method {@linkplain #findCommand(String)} using a supplied |
| * context <code>view</code> would return {@link PageCommand#VIEW}. But |
| * the {@linkplain #findCommand(HttpServletRequest, String)} method, |
| * when passed the same context (<code>view</code>) and an HTTP request |
| * containing the page parameter value <code>UserPrefs</code>, |
| * will instead return {@link WikiCommand#PREFS}.</p> |
| * @since 2.4.22 |
| */ |
| public final class CommandResolver |
| { |
| /** Prefix in jspwiki.properties signifying special page keys. */ |
| private static final String PROP_SPECIALPAGE = "jspwiki.specialPage."; |
| |
| /** Private map with request contexts as keys, Commands as values */ |
| private static final Map<String, Command> CONTEXTS; |
| |
| /** Private map with JSPs as keys, Commands as values */ |
| private static final Map<String, Command> JSPS; |
| |
| /** Store the JSP-to-Command and context-to-Command mappings */ |
| static |
| { |
| CONTEXTS = new HashMap<String, Command>(); |
| JSPS = new HashMap<String, Command>(); |
| Command[] commands = AbstractCommand.allCommands(); |
| for( int i = 0; i < commands.length; i++ ) |
| { |
| JSPS.put( commands[i].getJSP(), commands[i] ); |
| CONTEXTS.put( commands[i].getRequestContext(), commands[i] ); |
| } |
| } |
| |
| private final Logger m_log = Logger.getLogger( CommandResolver.class ); |
| |
| private final WikiEngine m_engine; |
| |
| /** If true, we'll also consider english plurals (+s) a match. */ |
| private final boolean m_matchEnglishPlurals; |
| |
| /** Stores special page names as keys, and Commands as values. */ |
| private final Map<String, Command> m_specialPages; |
| |
| /** |
| * Constructs a CommandResolver for a given WikiEngine. This constructor |
| * will extract the special page references for this wiki and store them in |
| * a cache used for resolution. |
| * @param engine the wiki engine |
| * @param properties the properties used to initialize the wiki |
| */ |
| public CommandResolver( WikiEngine engine, Properties properties ) |
| { |
| m_engine = engine; |
| m_specialPages = new HashMap<String, Command>(); |
| |
| // Skim through the properties and look for anything with |
| // the "special page" prefix. Create maps that allow us |
| // look up the correct Command based on special page name. |
| // If a matching command isn't found, create a RedirectCommand. |
| for(String key : properties.stringPropertyNames()) |
| { |
| if ( key.startsWith( PROP_SPECIALPAGE ) ) |
| { |
| String specialPage = key.substring( PROP_SPECIALPAGE.length() ); |
| String jsp = (String) properties.getProperty(key); |
| if ( specialPage != null && jsp != null ) |
| { |
| specialPage = specialPage.trim(); |
| jsp = jsp.trim(); |
| Command command = JSPS.get( jsp ); |
| if ( command == null ) |
| { |
| Command redirect = RedirectCommand.REDIRECT; |
| command = redirect.targetedCommand( jsp ); |
| } |
| m_specialPages.put( specialPage, command ); |
| } |
| } |
| } |
| |
| // Do we match plurals? |
| m_matchEnglishPlurals = TextUtil.getBooleanProperty( properties, WikiEngine.PROP_MATCHPLURALS, true ); |
| } |
| |
| /** |
| * Attempts to locate a wiki command for a supplied request context. |
| * The resolution technique is simple: we examine the list of |
| * Commands returned by {@link AbstractCommand#allCommands()} and |
| * return the one whose <code>requestContext</code> matches the |
| * supplied context. If the supplied context does not resolve to a known |
| * Command, this method throws an {@link IllegalArgumentException}. |
| * @param context the request context |
| * @return the resolved context |
| */ |
| public static Command findCommand( String context ) |
| { |
| Command command = CONTEXTS.get( context ); |
| if ( command == null ) |
| { |
| throw new IllegalArgumentException( "Unsupported wiki context: " + context + "." ); |
| } |
| return command; |
| } |
| |
| /** |
| * <p> |
| * Attempts to locate a Command for a supplied wiki context and HTTP |
| * request, incorporating the correct WikiPage into the command if reqiured. |
| * This method will first determine what page the user requested by |
| * delegating to {@link #extractPageFromParameter(String, HttpServletRequest)}. If |
| * this page equates to a special page, we return the Command |
| * corresponding to that page. Otherwise, this method simply returns the |
| * Command for the supplied request context. |
| * </p> |
| * <p> |
| * The reason this method attempts to resolve against special pages is |
| * because some of them resolve to contexts that may be different from the |
| * one supplied. For example, a VIEW request context for the special page |
| * "UserPreferences" should return a PREFS context instead. |
| * </p> |
| * <p> |
| * When the caller supplies a request context and HTTP request that |
| * specifies an actual wiki page (rather than a special page), this method |
| * will return a "targeted" Command that includes the resolved WikiPage |
| * as the target. (See {@link #resolvePage(HttpServletRequest, String)} |
| * for the resolution algorithm). Specifically, the Command will |
| * return a non-<code>null</code> value for its {@link AbstractCommand#getTarget()} method. |
| * </p> |
| * <p><em>Note: if this method determines that the Command is the VIEW PageCommand, |
| * then the Command returned will always be targeted to the front page.</em></p> |
| * @param request the HTTP request; if <code>null</code>, delegates |
| * to {@link #findCommand(String)} |
| * @param defaultContext the request context to use by default |
| * @return the resolved wiki command |
| */ |
| public Command findCommand( HttpServletRequest request, String defaultContext ) |
| { |
| // Corner case if request is null |
| if ( request == null ) |
| { |
| return findCommand( defaultContext ); |
| } |
| |
| Command command = null; |
| |
| // Determine the name of the page (which may be null) |
| String pageName = extractPageFromParameter( defaultContext, request ); |
| |
| // Can we find a special-page command matching the extracted page? |
| if ( pageName != null ) |
| { |
| command = m_specialPages.get( pageName ); |
| } |
| |
| // If we haven't found a matching command yet, extract the JSP path |
| // and compare to our list of special pages |
| if ( command == null ) |
| { |
| command = extractCommandFromPath( request ); |
| |
| // Otherwise: use the default context |
| if ( command == null ) |
| { |
| command = CONTEXTS.get( defaultContext ); |
| if ( command == null ) |
| { |
| throw new IllegalArgumentException( "Wiki context " + defaultContext + " is illegal." ); |
| } |
| } |
| } |
| |
| // For PageCommand.VIEW, default to front page if a page wasn't supplied |
| if( PageCommand.VIEW.equals( command ) && pageName == null ) |
| { |
| pageName = m_engine.getFrontPage(); |
| } |
| |
| // These next blocks handle targeting requirements |
| |
| // If we were passed a page parameter, try to resolve it |
| if ( command instanceof PageCommand && pageName != null ) |
| { |
| // If there's a matching WikiPage, "wrap" the command |
| WikiPage page = resolvePage( request, pageName ); |
| if ( page != null ) |
| { |
| return command.targetedCommand( page ); |
| } |
| } |
| |
| // If "create group" command, target this wiki |
| String wiki = m_engine.getApplicationName(); |
| if ( WikiCommand.CREATE_GROUP.equals( command ) ) |
| { |
| return WikiCommand.CREATE_GROUP.targetedCommand( wiki ); |
| } |
| |
| // If group command, see if we were passed a group name |
| if ( command instanceof GroupCommand ) |
| { |
| String groupName = request.getParameter( "group" ); |
| groupName = TextUtil.replaceEntities( groupName ); |
| if ( groupName != null && groupName.length() > 0 ) |
| { |
| GroupPrincipal group = new GroupPrincipal( groupName ); |
| return command.targetedCommand( group ); |
| } |
| } |
| |
| // No page provided; return an "ordinary" command |
| return command; |
| } |
| |
| /** |
| * <p> |
| * Returns the correct page name, or <code>null</code>, if no such page can be found. |
| * Aliases are considered. |
| * </p> |
| * <p> |
| * In some cases, page names can refer to other pages. For example, when you |
| * have matchEnglishPlurals set, then a page name "Foobars" will be |
| * transformed into "Foobar", should a page "Foobars" not exist, but the |
| * page "Foobar" would. This method gives you the correct page name to refer |
| * to. |
| * </p> |
| * <p> |
| * This facility can also be used to rewrite any page name, for example, by |
| * using aliases. It can also be used to check the existence of any page. |
| * </p> |
| * @since 2.4.20 |
| * @param page the page name. |
| * @return The rewritten page name, or <code>null</code>, if the page does not exist. |
| * @throws ProviderException if the underlyng page provider that locates pages |
| * throws an exception |
| */ |
| public String getFinalPageName( String page ) throws ProviderException |
| { |
| boolean isThere = simplePageExists( page ); |
| String finalName = page; |
| |
| if ( !isThere && m_matchEnglishPlurals ) |
| { |
| if ( page.endsWith( "s" ) ) |
| { |
| finalName = page.substring( 0, page.length() - 1 ); |
| } |
| else |
| { |
| finalName += "s"; |
| } |
| |
| isThere = simplePageExists( finalName ); |
| } |
| |
| if( !isThere ) |
| { |
| finalName = MarkupParser.wikifyLink( page ); |
| isThere = simplePageExists(finalName); |
| |
| if( !isThere && m_matchEnglishPlurals ) |
| { |
| if( finalName.endsWith( "s" ) ) |
| { |
| finalName = finalName.substring( 0, finalName.length() - 1 ); |
| } |
| else |
| { |
| finalName += "s"; |
| } |
| |
| isThere = simplePageExists( finalName ); |
| } |
| } |
| |
| return isThere ? finalName : null; |
| } |
| |
| /** |
| * <p> |
| * If the page is a special page, this method returns a direct URL to that |
| * page; otherwise, it returns <code>null</code>. |
| * </p> |
| * <p> |
| * Special pages are non-existant references to other pages. For example, |
| * you could define a special page reference "RecentChanges" which would |
| * always be redirected to "RecentChanges.jsp" instead of trying to find a |
| * Wiki page called "RecentChanges". |
| * </p> |
| * @param page the page name ro search for |
| * @return the URL of the special page, if the supplied page is one, or <code>null</code> |
| */ |
| public String getSpecialPageReference( String page ) |
| { |
| Command command = m_specialPages.get( page ); |
| |
| if ( command != null ) |
| { |
| return m_engine.getURLConstructor() |
| .makeURL( command.getRequestContext(), command.getURLPattern(), true, null ); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Extracts a Command based on the JSP path of an HTTP request. |
| * If the JSP requested matches a Command's <code>getJSP()</code> |
| * value, that Command is returned. |
| * @param request the HTTP request |
| * @return the resolved Command, or <code>null</code> if not found |
| */ |
| protected Command extractCommandFromPath( HttpServletRequest request ) |
| { |
| String jsp = request.getServletPath(); |
| |
| // Take everything to right of initial / and left of # or ? |
| int hashMark = jsp.indexOf( '#' ); |
| if ( hashMark != -1 ) |
| { |
| jsp = jsp.substring( 0, hashMark ); |
| } |
| int questionMark = jsp.indexOf( '?' ); |
| if ( questionMark != -1 ) |
| { |
| jsp = jsp.substring( 0, questionMark ); |
| } |
| if ( jsp.startsWith( "/" ) ) |
| { |
| jsp = jsp.substring( 1 ); |
| } |
| |
| // Find special page reference? |
| for( Iterator< Map.Entry< String, Command > > i = m_specialPages.entrySet().iterator(); i.hasNext(); ) |
| { |
| Map.Entry< String, Command > entry = i.next(); |
| Command specialCommand = entry.getValue(); |
| if ( specialCommand.getJSP().equals( jsp ) ) |
| { |
| return specialCommand; |
| } |
| } |
| |
| // Still haven't found a matching command? |
| // Ok, see if we match against our standard list of JSPs |
| if ( jsp.length() > 0 && JSPS.containsKey( jsp ) ) |
| { |
| return JSPS.get( jsp ); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Determines the correct wiki page based on a supplied request context and |
| * HTTP request. This method attempts to determine the page requested by a |
| * user, taking into acccount special pages. The resolution algorithm will: |
| * <ul> |
| * <li>Extract the page name from the URL according to the rules for the |
| * current {@link org.apache.wiki.url.URLConstructor}. If a page name was |
| * passed in the request, return the correct name after taking into account |
| * potential plural matches.</li> |
| * <li>If the extracted page name is <code>null</code>, attempt to see |
| * if a "special page" was intended by examining the servlet path. For |
| * example, the request path "/UserPreferences.jsp" will resolve to |
| * "UserPreferences."</li> |
| * <li>If neither of these methods work, this method returns |
| * <code>null</code></li> |
| * </ul> |
| * @param requestContext the request context |
| * @param request the HTTP request |
| * @return the resolved page name |
| */ |
| protected String extractPageFromParameter( String requestContext, HttpServletRequest request ) |
| { |
| String page; |
| |
| // Extract the page name from the URL directly |
| try |
| { |
| page = m_engine.getURLConstructor().parsePage( requestContext, request, m_engine.getContentEncoding() ); |
| if ( page != null ) |
| { |
| try |
| { |
| // Look for singular/plural variants; if one |
| // not found, take the one the user supplied |
| String finalPage = getFinalPageName( page ); |
| if ( finalPage != null ) |
| { |
| page = finalPage; |
| } |
| } |
| catch( ProviderException e ) |
| { |
| // FIXME: Should not ignore! |
| } |
| return page; |
| } |
| } |
| catch( IOException e ) |
| { |
| m_log.error( "Unable to create context", e ); |
| throw new InternalWikiException( "Big internal booboo, please check logs." ); |
| } |
| |
| // Didn't resolve; return null |
| return null; |
| } |
| |
| /** |
| * Looks up and returns the correct, versioned WikiPage based on a supplied |
| * page name and optional <code>version</code> parameter passed in an HTTP |
| * request. If the <code>version</code> parameter does not exist in the |
| * request, the latest version is returned. |
| * @param request the HTTP request |
| * @param page the name of the page to look up; this page <em>must</em> exist |
| * @return the wiki page |
| */ |
| protected WikiPage resolvePage( HttpServletRequest request, String page ) |
| { |
| // See if the user included a version parameter |
| WikiPage wikipage; |
| int version = WikiProvider.LATEST_VERSION; |
| String rev = request.getParameter( "version" ); |
| |
| if ( rev != null ) |
| { |
| try |
| { |
| version = Integer.parseInt( rev ); |
| } |
| catch( NumberFormatException e ) |
| { |
| // 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 |
| // it here so that the admin does not get tons of mail. |
| } |
| } |
| |
| wikipage = m_engine.getPage( page, version ); |
| |
| if ( wikipage == null ) |
| { |
| page = MarkupParser.cleanLink( page ); |
| wikipage = new WikiPage( m_engine, page ); |
| } |
| return wikipage; |
| } |
| |
| /** |
| * Determines whether a "page" exists by examining the list of special pages |
| * and querying the page manager. |
| * @param page the page to seek |
| * @return <code>true</code> if the page exists, <code>false</code> |
| * otherwise |
| * @throws ProviderException if the underlyng page provider that locates pages |
| * throws an exception |
| */ |
| protected boolean simplePageExists( String page ) throws ProviderException |
| { |
| if ( m_specialPages.containsKey( page ) ) |
| { |
| return true; |
| } |
| return m_engine.getPageManager().pageExists( page ); |
| } |
| |
| } |