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 java.io.IOException;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.Properties;
026
027import javax.servlet.http.HttpServletRequest;
028
029import org.apache.log4j.Logger;
030import org.apache.wiki.InternalWikiException;
031import org.apache.wiki.WikiEngine;
032import org.apache.wiki.WikiPage;
033import org.apache.wiki.WikiProvider;
034import org.apache.wiki.api.exceptions.ProviderException;
035import org.apache.wiki.auth.GroupPrincipal;
036import org.apache.wiki.parser.MarkupParser;
037import 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 */
069public 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< Map.Entry< String, Command > > i = m_specialPages.entrySet().iterator(); i.hasNext(); )
393        {
394            Map.Entry< String, Command > entry = i.next();
395            Command specialCommand = 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." , e);
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}