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