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