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.api.core.Command;
024import org.apache.wiki.api.core.Engine;
025import org.apache.wiki.api.core.Page;
026import org.apache.wiki.api.exceptions.ProviderException;
027import org.apache.wiki.api.providers.WikiProvider;
028import org.apache.wiki.api.spi.Wiki;
029import org.apache.wiki.auth.GroupPrincipal;
030import org.apache.wiki.pages.PageManager;
031import org.apache.wiki.parser.MarkupParser;
032import org.apache.wiki.url.URLConstructor;
033import org.apache.wiki.util.TextUtil;
034
035import javax.servlet.http.HttpServletRequest;
036import java.io.IOException;
037import java.util.HashMap;
038import java.util.Map;
039import java.util.Properties;
040
041
042/**
043 * <p>Default implementation for {@link CommandResolver}</p>
044 *
045 * @since 2.4.22
046 */
047public final class DefaultCommandResolver implements CommandResolver {
048
049    /** Private map with request contexts as keys, Commands as values */
050    private static final Map< String, Command > CONTEXTS;
051
052    /** Private map with JSPs as keys, Commands as values */
053    private static final Map< String, Command > JSPS;
054
055    /* Store the JSP-to-Command and context-to-Command mappings */
056    static {
057        CONTEXTS = new HashMap<>();
058        JSPS = new HashMap<>();
059        final Command[] commands = AllCommands.get();
060        for( final Command command : commands ) {
061            JSPS.put( command.getJSP(), command );
062            CONTEXTS.put( command.getRequestContext(), command );
063        }
064    }
065
066    private static final Logger LOG = Logger.getLogger( DefaultCommandResolver.class );
067
068    private final Engine m_engine;
069
070    /** If true, we'll also consider english plurals (+s) a match. */
071    private final boolean m_matchEnglishPlurals;
072
073    /** Stores special page names as keys, and Commands as values. */
074    private final Map<String, Command> m_specialPages;
075
076    /**
077     * Constructs a CommandResolver for a given Engine. This constructor will extract the special page references for this wiki and
078     * store them in a cache used for resolution.
079     *
080     * @param engine the wiki engine
081     * @param properties the properties used to initialize the wiki
082     */
083    public DefaultCommandResolver( final Engine engine, final Properties properties ) {
084        m_engine = engine;
085        m_specialPages = new HashMap<>();
086
087        // Skim through the properties and look for anything with the "special page" prefix. Create maps that allow us look up
088        // the correct Command based on special page name. If a matching command isn't found, create a RedirectCommand.
089        for( final String key : properties.stringPropertyNames() ) {
090            if ( key.startsWith( PROP_SPECIALPAGE ) ) {
091                String specialPage = key.substring( PROP_SPECIALPAGE.length() );
092                String jsp = properties.getProperty( key );
093                if ( jsp != null ) {
094                    specialPage = specialPage.trim();
095                    jsp = jsp.trim();
096                    Command command = JSPS.get( jsp );
097                    if ( command == null ) {
098                        final Command redirect = RedirectCommand.REDIRECT;
099                        command = redirect.targetedCommand( jsp );
100                    }
101                    m_specialPages.put( specialPage, command );
102                }
103            }
104        }
105
106        // Do we match plurals?
107        m_matchEnglishPlurals = TextUtil.getBooleanProperty( properties, Engine.PROP_MATCHPLURALS, true );
108    }
109
110    /**
111     * {@inheritDoc}
112     */
113    @Override
114    public Command findCommand( final HttpServletRequest request, final String defaultContext ) {
115        // Corner case if request is null
116        if ( request == null ) {
117            return CommandResolver.findCommand( defaultContext );
118        }
119
120        Command command = null;
121
122        // Determine the name of the page (which may be null)
123        String pageName = extractPageFromParameter( defaultContext, request );
124
125        // Can we find a special-page command matching the extracted page?
126        if ( pageName != null ) {
127            command = m_specialPages.get( pageName );
128        }
129
130        // If we haven't found a matching command yet, extract the JSP path and compare to our list of special pages
131        if ( command == null ) {
132            command = extractCommandFromPath( request );
133
134            // Otherwise: use the default context
135            if ( command == null ) {
136                command = CONTEXTS.get( defaultContext );
137                if ( command == null ) {
138                    throw new IllegalArgumentException( "Wiki context " + defaultContext + " is illegal." );
139                }
140            }
141        }
142
143        // For PageCommand.VIEW, default to front page if a page wasn't supplied
144        if( PageCommand.VIEW.equals( command ) && pageName == null ) {
145            pageName = m_engine.getFrontPage();
146        }
147
148        // These next blocks handle targeting requirements
149
150        // If we were passed a page parameter, try to resolve it
151        if ( command instanceof PageCommand && pageName != null ) {
152            // If there's a matching WikiPage, "wrap" the command
153            final Page page = resolvePage( request, pageName );
154            return command.targetedCommand( page );
155        }
156
157        // If "create group" command, target this wiki
158        final String wiki = m_engine.getApplicationName();
159        if ( WikiCommand.CREATE_GROUP.equals( command ) ) {
160            return WikiCommand.CREATE_GROUP.targetedCommand( wiki );
161        }
162
163        // If group command, see if we were passed a group name
164        if( command instanceof GroupCommand ) {
165            String groupName = request.getParameter( "group" );
166            groupName = TextUtil.replaceEntities( groupName );
167            if ( groupName != null && groupName.length() > 0 ) {
168                final GroupPrincipal group = new GroupPrincipal( groupName );
169                return command.targetedCommand( group );
170            }
171        }
172
173        // No page provided; return an "ordinary" command
174        return command;
175    }
176
177    /**
178     * {@inheritDoc}
179     */
180    @Override
181    public String getFinalPageName( final String page ) throws ProviderException {
182        boolean isThere = simplePageExists( page );
183        String  finalName = page;
184
185        if ( !isThere && m_matchEnglishPlurals ) {
186            if ( page.endsWith( "s" ) ) {
187                finalName = page.substring( 0, page.length() - 1 );
188            } else {
189                finalName += "s";
190            }
191
192            isThere = simplePageExists( finalName );
193        }
194
195        if( !isThere ) {
196            finalName = MarkupParser.wikifyLink( page );
197            isThere = simplePageExists(finalName);
198
199            if( !isThere && m_matchEnglishPlurals ) {
200                if( finalName.endsWith( "s" ) ) {
201                    finalName = finalName.substring( 0, finalName.length() - 1 );
202                } else {
203                    finalName += "s";
204                }
205
206                isThere = simplePageExists( finalName );
207            }
208        }
209
210        return isThere ? finalName : null;
211    }
212
213    /**
214     * {@inheritDoc}
215     */
216    @Override
217    public String getSpecialPageReference( final String page ) {
218        final Command command = m_specialPages.get( page );
219        if ( command != null ) {
220            return m_engine.getManager( URLConstructor.class ).makeURL( command.getRequestContext(), command.getURLPattern(), null );
221        }
222
223        return null;
224    }
225
226    /**
227     * Extracts a Command based on the JSP path of an HTTP request. If the JSP requested matches a Command's <code>getJSP()</code>
228     * value, that Command is returned.
229     *
230     * @param request the HTTP request
231     * @return the resolved Command, or <code>null</code> if not found
232     */
233    protected Command extractCommandFromPath( final HttpServletRequest request ) {
234        String jsp = request.getServletPath();
235
236        // Take everything to right of initial / and left of # or ?
237        final int hashMark = jsp.indexOf( '#' );
238        if ( hashMark != -1 ) {
239            jsp = jsp.substring( 0, hashMark );
240        }
241        final int questionMark = jsp.indexOf( '?' );
242        if ( questionMark != -1 ) {
243            jsp = jsp.substring( 0, questionMark );
244        }
245        if ( jsp.startsWith( "/" ) ) {
246            jsp = jsp.substring( 1 );
247        }
248
249        // Find special page reference?
250        for( final Map.Entry< String, Command > entry : m_specialPages.entrySet() ) {
251            final Command specialCommand = entry.getValue();
252            if( specialCommand.getJSP().equals( jsp ) ) {
253                return specialCommand;
254            }
255        }
256
257        // Still haven't found a matching command? Ok, see if we match against our standard list of JSPs
258        if ( jsp.length() > 0 && JSPS.containsKey( jsp ) ) {
259            return JSPS.get( jsp );
260        }
261
262        return null;
263    }
264
265    /**
266     * {@inheritDoc}
267     */
268    @Override
269    public String extractPageFromParameter( final String requestContext, final HttpServletRequest request ) {
270        // Extract the page name from the URL directly
271        try {
272            String page = m_engine.getManager( URLConstructor.class ).parsePage( requestContext, request, m_engine.getContentEncoding() );
273            if ( page != null ) {
274                try {
275                    // Look for singular/plural variants; if one not found, take the one the user supplied
276                    final String finalPage = getFinalPageName( page );
277                    if ( finalPage != null ) {
278                        page = finalPage;
279                    }
280                } catch( final ProviderException e ) {
281                    // FIXME: Should not ignore!
282                }
283                return page;
284            }
285        } catch( final IOException e ) {
286            LOG.error( "Unable to create context", e );
287            throw new InternalWikiException( "Big internal booboo, please check logs." , e );
288        }
289
290        // Didn't resolve; return null
291        return null;
292    }
293
294    /**
295     * {@inheritDoc}
296     */
297    @Override
298    public Page resolvePage( final HttpServletRequest request, String page ) {
299        // See if the user included a version parameter
300        int version = WikiProvider.LATEST_VERSION;
301        final String rev = request.getParameter( "version" );
302        if ( rev != null ) {
303            try {
304                version = Integer.parseInt( rev );
305            } catch( final NumberFormatException e ) {
306                // 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
307                // it here so that the admin does not get tons of mail.
308            }
309        }
310
311        Page wikipage = m_engine.getManager( PageManager.class ).getPage( page, version );
312        if ( wikipage == null ) {
313            page = MarkupParser.cleanLink( page );
314            wikipage = Wiki.contents().page( m_engine, page );
315        }
316        return wikipage;
317    }
318
319    /**
320     * Determines whether a "page" exists by examining the list of special pages and querying the page manager.
321     *
322     * @param page the page to seek
323     * @return <code>true</code> if the page exists, <code>false</code> otherwise
324     * @throws ProviderException if the underlyng page provider that locates pages
325     * throws an exception
326     */
327    protected boolean simplePageExists( final String page ) throws ProviderException {
328        if ( m_specialPages.containsKey( page ) ) {
329            return true;
330        }
331        return m_engine.getManager( PageManager.class ).pageExists( page );
332    }
333
334}