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    }