    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}