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}