    001/*
    002
    003    Licensed to the Apache Software Foundation (ASF) under one
    004    or more contributor license agreements.  See the NOTICE file
    005    distributed with this work for additional information
    006    regarding copyright ownership.  The ASF licenses this file
    007    to you under the Apache License, Version 2.0 (the
    008    "License"); you may not use this file except in compliance
    009    with the License.  You may obtain a copy of the License at
    010
    011       http://www.apache.org/licenses/LICENSE-2.0
    012
    013    Unless required by applicable law or agreed to in writing,
    014    software distributed under the License is distributed on an
    015    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    016    KIND, either express or implied.  See the License for the
    017    specific language governing permissions and limitations
    018    under the License.
    019 */
    020package org.apache.wiki.ajax;
    021
    022import org.apache.commons.lang3.StringUtils;
    023import org.apache.logging.log4j.LogManager;
    024import org.apache.logging.log4j.Logger;
    025import org.apache.wiki.api.core.Engine;
    026import org.apache.wiki.api.spi.Wiki;
    027import org.apache.wiki.auth.AuthorizationManager;
    028import org.apache.wiki.auth.permissions.PagePermission;
    029import org.apache.wiki.util.TextUtil;
    030
    031import jakarta.servlet.ServletConfig;
    032import jakarta.servlet.ServletException;
    033import jakarta.servlet.http.HttpServlet;
    034import jakarta.servlet.http.HttpServletRequest;
    035import jakarta.servlet.http.HttpServletResponse;
    036import java.io.IOException;
    037import java.security.Permission;
    038import java.util.ArrayList;
    039import java.util.Arrays;
    040import java.util.List;
    041import java.util.Map;
    042import java.util.concurrent.ConcurrentHashMap;
    043import org.apache.wiki.api.core.Session;
    044import org.apache.wiki.auth.SessionMonitor;
    045import org.apache.wiki.event.WikiEventManager;
    046import org.apache.wiki.event.WikiSecurityEvent;
    047import org.apache.wiki.search.DefaultSearchManager;
    048import org.apache.wiki.security.EventUtil;
    049
    050
    051/**
    052 * This provides a simple ajax servlet for handling /ajax/<ClassName> requests. HttpServlet classes need to be registered using
    053 * {@link WikiAjaxDispatcherServlet#registerServlet(WikiAjaxServlet)}
    054 *
    055 * @since 2.10.2-svn12
    056 */
    057public class WikiAjaxDispatcherServlet extends HttpServlet {
    058
    059    private static final long serialVersionUID = 1L;
    060    private static final Map< String, AjaxServletContainer > ajaxServlets = new ConcurrentHashMap<>();
    061    private static final Logger LOG = LogManager.getLogger( WikiAjaxDispatcherServlet.class.getName() );
    062    private String PATH_AJAX = "/ajax/";
    063    private Engine m_engine;
    064
    065    /**
    066     * {@inheritDoc}
    067     *
    068     * This sets the AjaxPath to "/ajax/" as configured in "jspwiki.ajax.url.prefix".
    069     * Note: Do not change this without also changing the web.xml file.
    070     */
    071    @Override
    072    public void init( final ServletConfig config ) throws ServletException {
    073        super.init( config );
    074        m_engine = Wiki.engine().find( config );
    075        PATH_AJAX = "/" + TextUtil.getStringProperty( m_engine.getWikiProperties(), "jspwiki.ajax.url.prefix", "ajax" ) + "/";
    076        LOG.info( "WikiAjaxDispatcherServlet initialized." );
    077    }
    078
    079    /**
    080     * Register a {@link WikiAjaxServlet} using the servlet mapping as the alias
    081     */
    082    public static void registerServlet( final WikiAjaxServlet servlet ) {
    083        registerServlet( servlet.getServletMapping(), servlet );
    084    }
    085
    086    /**
    087     * Register a {@link WikiAjaxServlet} with a specific alias, and default permission {@link PagePermission#VIEW}.
    088     */
    089    public static void registerServlet( final String alias, final WikiAjaxServlet servlet ) {
    090        registerServlet( alias, servlet, PagePermission.VIEW );
    091    }
    092
    093    /**
    094     * Regster a {@link WikiAjaxServlet} given an alias, the servlet, and the permission.
    095     * This creates a temporary bundle object called {@link WikiAjaxDispatcherServlet.AjaxServletContainer}
    096     *
    097     * @param alias the uri link to this servlet
    098     * @param servlet the servlet being registered
    099     * @param perm the permission required to execute the servlet.
    100     */
    101    public static void registerServlet( final String alias, final WikiAjaxServlet servlet, final Permission perm ) {
    102        LOG.info( "WikiAjaxDispatcherServlet registering " + alias + "=" + servlet + " perm=" + perm );
    103        ajaxServlets.put( alias, new AjaxServletContainer( alias, servlet, perm ) );
    104    }
    105
    106    /**
    107     * Calls {@link #performAction}
    108     */
    109    @Override
    110    public void doPost( final HttpServletRequest req, final HttpServletResponse res ) throws IOException, ServletException {
    111        performAction( req, res );
    112    }
    113
    114    /**
    115     * Calls {@link #performAction}
    116     */
    117    @Override
    118    public void doGet( final HttpServletRequest req, final HttpServletResponse res ) throws IOException, ServletException {
    119        performAction( req, res );
    120    }
    121
    122    /**
    123     * The main method which get the requestURI "/ajax/<ServletName>", gets the {@link #getServletName} and finds the servlet using
    124     * {@link #findServletByName}. It then calls {@link WikiAjaxServlet#service} method.
    125     *
    126     * @param req the inbound request
    127     * @param res the outbound response
    128     * @throws IOException if WikiEngine's content encoding is valid
    129     * @throws ServletException if no registered servlet can be found
    130     */
    131    private void performAction( final HttpServletRequest req, final HttpServletResponse res ) throws IOException, ServletException {
    132        final String path = req.getRequestURI();
    133        final String servletName = getServletName( path );
    134        if( servletName != null) {
    135            final AjaxServletContainer container = findServletContainer( servletName );
    136            if( container != null ) {
    137                final WikiAjaxServlet servlet = container.servlet;
    138                Session wikiSession = SessionMonitor.getInstance(m_engine).find(req.getSession());
    139                if ( validatePermission( req, container ) ) {
    140                    res.setCharacterEncoding( m_engine.getContentEncoding().displayName() );
    141                    final String actionName = AjaxUtil.getNextPathPart( req.getRequestURI(), servlet.getServletMapping() );
    142                    if (!(servlet instanceof DefaultSearchManager.PluginSearch)) {
    143                        final String xsrfToken = req.getParameter("X-XSRF-TOKEN");
    144                        if (!wikiSession.antiCsrfToken().equals(xsrfToken)) {
    145                            res.sendError(400, "X-XSRF-TOKEN missing or invalid.");
    146                            WikiEventManager.fireEvent(this,
    147                                    EventUtil.applyFrom(new WikiSecurityEvent(wikiSession, WikiSecurityEvent.ACCESS_DENIED,
    148                                            req.getUserPrincipal(), "X-XSRF-TOKEN missing or invalid."), req));
    149                            return;
    150                        }
    151                    }
    152                    LOG.debug( "actionName=" + actionName );
    153                    final String params = req.getParameter( "params" );
    154                    LOG.debug( "params=" + params );
    155                    List< String > paramValues = new ArrayList<>();
    156                    if( params != null ) {
    157                        if( StringUtils.isNotBlank( params ) ) {
    158                            paramValues = Arrays.asList( params.trim().split( "," ) );
    159                        }
    160                    }
    161                    servlet.service( req, res, actionName, paramValues );
    162                } else {
    163                    LOG.warn("Servlet container " + container + " not authorised. Permission required.");
    164                    WikiEventManager.fireEvent(this,
    165                            EventUtil.applyFrom(new WikiSecurityEvent(wikiSession, WikiSecurityEvent.ACCESS_DENIED,
    166                                    req.getUserPrincipal(), container.permission), req));
    167                    res.sendError(400, "Denied.");
    168                    return;
    169                }
    170            } else {
    171                LOG.warn( "No registered class for servletName=" + servletName + " in path=" + path );
    172                res.sendError(400, "Unknown Service.");
    173                //should this be an auditable event?
    174                //throw new ServletException( "No registered class for servletName=" + servletName );
    175            }
    176        }
    177    }
    178
    179    /**
    180     * Validate the permission of the {@link WikiAjaxServlet} using the {@link AuthorizationManager#checkPermission}
    181     *
    182     * @param req the servlet request
    183     * @param container the container info of the servlet
    184     * @return true if permission is valid
    185     */
    186    private boolean validatePermission( final HttpServletRequest req, final AjaxServletContainer container ) {
    187        final Engine e = Wiki.engine().find( req.getSession().getServletContext(), null );
    188        boolean valid = false;
    189        if( container != null ) {
    190            valid = e.getManager( AuthorizationManager.class ).checkPermission( Wiki.session().find( e, req ), container.permission );
    191        }
    192        return valid;
    193    }
    194
    195    /**
    196     * Get the ServletName from the requestURI "/ajax/<ServletName>", using {@link AjaxUtil#getNextPathPart}.
    197     *
    198     * @param path The requestURI, which must contains "/ajax/<ServletName>" in the path
    199     * @return The ServletName for the requestURI, or null
    200     * @throws ServletException if the path is invalid
    201     */
    202    public String getServletName( final String path ) throws ServletException {
    203        return AjaxUtil.getNextPathPart( path, PATH_AJAX );
    204    }
    205
    206    /**
    207     * Find the {@link AjaxServletContainer} as registered in {@link #registerServlet}.
    208     *
    209     * @param servletAlias the name of the servlet from {@link #getServletName}
    210     * @return The first servlet found, or null.
    211     */
    212    private AjaxServletContainer findServletContainer( final String servletAlias ) {
    213        return ajaxServlets.get( servletAlias );
    214    }
    215
    216    /**
    217     * Find the {@link WikiAjaxServlet} given the servletAlias that it was registered with.
    218     *
    219     * @param servletAlias the value provided to {@link #registerServlet}
    220     * @return the {@link WikiAjaxServlet} given the servletAlias that it was registered with.
    221     */
    222    public WikiAjaxServlet findServletByName( final String servletAlias ) {
    223        final AjaxServletContainer container = ajaxServlets.get( servletAlias );
    224        if( container != null ) {
    225            return container.servlet;
    226        }
    227        return null;
    228    }
    229
    230    private static class AjaxServletContainer {
    231
    232        final String alias;
    233        final WikiAjaxServlet servlet;
    234        final Permission permission;
    235
    236        public AjaxServletContainer( final String alias, final WikiAjaxServlet servlet, final Permission permission ) {
    237            this.alias = alias;
    238            this.servlet = servlet;
    239            this.permission = permission;
    240        }
    241
    242        @Override
    243        public String toString() {
    244            return getClass().getSimpleName() + " " + alias + "=" + servlet.getClass().getSimpleName() + " permission=" + permission;
    245        }
    246
    247    }
    248
    249}