001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.i18n.CmsMessageContainer;
036import org.opencms.site.CmsSite;
037import org.opencms.staticexport.CmsStaticExportData;
038import org.opencms.staticexport.CmsStaticExportRequest;
039import org.opencms.util.CmsRequestUtil;
040import org.opencms.util.CmsStringUtil;
041import org.opencms.util.CmsThreadLocalStack;
042import org.opencms.util.CmsUUID;
043
044import java.io.IOException;
045import java.util.HashMap;
046import java.util.Map;
047import java.util.concurrent.ConcurrentHashMap;
048import java.util.function.Consumer;
049import java.util.function.Function;
050
051import org.apache.commons.logging.Log;
052
053import jakarta.servlet.ServletConfig;
054import jakarta.servlet.ServletException;
055import jakarta.servlet.http.HttpServlet;
056import jakarta.servlet.http.HttpServletRequest;
057import jakarta.servlet.http.HttpServletResponse;
058
059/**
060 * This the main servlet of the OpenCms system.<p>
061 *
062 * From here, all operations that are results of HTTP requests are invoked.
063 * Any incoming request is handled in multiple steps:
064 *
065 * <ol><li>The requesting <code>{@link org.opencms.file.CmsUser}</code> is authenticated
066 * and a <code>{@link org.opencms.file.CmsObject}</code> with this users context information
067 * is created. This <code>{@link org.opencms.file.CmsObject}</code> is used to access all functions of OpenCms, limited by
068 * the authenticated users permissions. If the user is not identified, it is set to the default user, usually named "Guest".</li>
069 *
070 * <li>The requested <code>{@link org.opencms.file.CmsResource}</code> is loaded into OpenCms and depending on its type
071 * (and the users persmissions to display or modify it),
072 * it is send to one of the OpenCms <code>{@link org.opencms.loader.I_CmsResourceLoader}</code> implementations
073 * do be processed.</li>
074 *
075 * <li>
076 * The <code>{@link org.opencms.loader.I_CmsResourceLoader}</code> will then decide what to do with the
077 * contents of the requested <code>{@link org.opencms.file.CmsResource}</code>.
078 * In case of a JSP resource the JSP handling mechanism is invoked with the <code>{@link org.opencms.loader.CmsJspLoader}</code>,
079 * in case of an image (or another static resource) this will be returned by the <code>{@link org.opencms.loader.CmsDumpLoader}</code>
080 * etc.
081 * </li></ol>
082 *
083 * @since 6.0.0
084 *
085 * @see org.opencms.main.CmsShell
086 * @see org.opencms.file.CmsObject
087 * @see org.opencms.main.OpenCms
088 */
089public class OpenCmsServlet extends HttpServlet implements I_CmsRequestHandler {
090
091    /**
092     * Context class for storing request-dependent caches etc.
093     */
094    public static class RequestCache {
095
096        /** Cache for sitemap configurations. */
097        private Map<String, CmsADEConfigData> m_configCache = new HashMap<>();
098
099        /** Map of attributes. */
100        private Map<String, Object> m_attributes = new HashMap<>();
101
102        /** Buffer for log messages to write at the end of the request. */
103        private CmsDuplicateRemovingLogBuffer m_logBuffer = new CmsDuplicateRemovingLogBuffer();
104
105        /**
106         * Adds a log message to the log buffer.
107         *
108         * @param channel the channel
109         * @param level the log level
110         * @param message the message
111         */
112        public void addLog(String channel, String level, String message) {
113
114            m_logBuffer.add(channel, level, message);
115        }
116
117        /**
118         * Called at the end of the request.
119         */
120        public void close() {
121
122            m_logBuffer.flush();
123        }
124
125        /**
126         * Gets the attribute for the given key.
127         *
128         * @param key the key
129         * @return the attribute
130         */
131        public Object getAttribute(String key) {
132
133            return m_attributes.get(key);
134        }
135
136        /**
137         * Gets the stored attribute for the given key, or lazily initializes it with the given provider function if it doesn't exist yet.
138         *
139         * @param key the key
140         * @param provider the function to lazily initialize the entry
141         * @return the attribute
142         */
143        public Object getAttribute(String key, Function<String, Object> provider) {
144
145            return m_attributes.computeIfAbsent(key, provider);
146        }
147
148        /**
149         * Gets the cached sitemap configuration data.
150         *
151         * @param key the key
152         * @return the sitemap config from the cache
153         */
154        public CmsADEConfigData getCachedConfig(String key) {
155
156            return m_configCache.get(key);
157        }
158
159        /**
160         * Sets the attribute for the given key.
161         *
162         * @param key the key
163         * @param value the attribute value
164         */
165        public void setAttribute(String key, Object value) {
166
167            m_attributes.put(key, value);
168        }
169
170        /**
171         * Sets the cached sitemap configuration data.
172         *
173         * @param key the key
174         * @param config the sitemap configuration to cache
175         */
176        public void setCachedConfig(String key, CmsADEConfigData config) {
177
178            m_configCache.put(key, config);
179        }
180
181    }
182
183    /**
184     * Debugging information about currently running requests.
185     */
186    public static class RequestInfo {
187
188        /** Start time of the request. */
189        private long m_startTime;
190
191        /** URI of the request. */
192        private String m_uri;
193
194        /** Thread id of the thread handling the request. */
195        private long m_threadId;
196
197        /**
198         * Creates a new instance.
199         *
200         * @param uri the URI
201         * @param startTime the start time
202         */
203        public RequestInfo(String uri, long startTime) {
204
205            m_startTime = startTime;
206            m_uri = uri;
207            m_threadId = Thread.currentThread().getId();
208        }
209
210        /**
211         * Gets the request start time.
212         *
213         * @return the start time
214         */
215        public long getStartTime() {
216
217            return m_startTime;
218        }
219
220        /**
221         * Gets the thread id
222         *
223         * @return the thread id
224         */
225        public long getThreadId() {
226
227            return m_threadId;
228        }
229
230        /**
231         * Gets the URI of the request.
232         *
233         * @return the URI of the request
234         */
235        public String getUri() {
236
237            return m_uri;
238        }
239    }
240
241    /** The current request in a threadlocal. */
242    public static final CmsThreadLocalStack<HttpServletRequest> currentRequestStack = new CmsThreadLocalStack<HttpServletRequest>();
243
244    /** Map containing beans with information about currently running requests. */
245    public static final ConcurrentHashMap<CmsUUID, RequestInfo> activeRequests = new ConcurrentHashMap<>();
246
247    /** The current thread context for the request. */
248    private static final CmsThreadLocalStack<RequestCache> requestCacheStack = new CmsThreadLocalStack<RequestCache>();
249
250    /** GWT RPC services suffix. */
251    public static final String HANDLE_GWT = ".gwt";
252
253    /** Handler prefix. */
254    public static final String HANDLE_PATH = "/handle";
255
256    /** Name of the <code>DefaultWebApplication</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */
257    public static final String SERVLET_PARAM_DEFAULT_WEB_APPLICATION = "DefaultWebApplication";
258
259    /** Name of the <code>OpenCmsHome</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */
260    public static final String SERVLET_PARAM_OPEN_CMS_HOME = "OpenCmsHome";
261
262    /** Name of the <code>OpenCmsServlet</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */
263    public static final String SERVLET_PARAM_OPEN_CMS_SERVLET = "OpenCmsServlet";
264
265    /** Name of the <code>WebApplicationContext</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */
266    public static final String SERVLET_PARAM_WEB_APPLICATION_CONTEXT = "WebApplicationContext";
267
268    /** Path to handler "error page" files in the VFS. */
269    private static final String HANDLE_VFS_PATH = "/system/handler" + HANDLE_PATH;
270
271    /** Handler "error page" file suffix. */
272    private static final String HANDLE_VFS_SUFFIX = ".html";
273
274    /** Handler implementation names. */
275    private static final String[] HANDLER_NAMES = {"404"};
276
277    /** The log object for this class. */
278    private static final Log LOG = CmsLog.getLog(OpenCmsServlet.class);
279
280    /** Serial version UID required for safe serialization. */
281    private static final long serialVersionUID = 4729951599966070050L;
282
283    /** URL prefix for the built-in service handler. */
284    public static final String HANDLE_BUILTIN_SERVICE = "/handleBuiltinService/";
285
286    /**
287     * Gets a thread-local, request-specific context object for requests made to the OpenCms servlet.
288     *
289     * @return the thread context
290     */
291    public static RequestCache getRequestCache() {
292
293        RequestCache result = requestCacheStack.top();
294        return result;
295    }
296
297    /**
298     * Helper method used to conveniently access the request cache, does nothing if the request cache isn't set.
299     *
300     * @param handler the handler to pass the request cache to
301     */
302    public static void withRequestCache(Consumer<RequestCache> handler) {
303
304        RequestCache cache = getRequestCache();
305        if (cache != null) {
306            handler.accept(cache);
307        }
308    }
309
310    /**
311     * OpenCms servlet main request handling method.<p>
312     *
313     * @see jakarta.servlet.http.HttpServlet#doGet(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)
314     */
315    @Override
316    public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
317
318        // we are using stacks for these because the doGet method may be called reentrantly, e.g. when using servlet forwarding.
319        currentRequestStack.push(req);
320        requestCacheStack.push(new RequestCache());
321        final CmsUUID requestId = new CmsUUID();
322        RequestInfo reqInfo = new RequestInfo(
323            req.getRequestURL().toString() + (req.getQueryString() != null ? "?" + req.getQueryString() : ""),
324            System.currentTimeMillis());
325        activeRequests.put(requestId, reqInfo);
326        try {
327
328            // check to OpenCms runlevel
329            int runlevel = OpenCmsCore.getInstance().getRunLevel();
330
331            // write OpenCms server identification in the response header
332            res.setHeader(CmsRequestUtil.HEADER_SERVER, OpenCmsCore.getInstance().getSystemInfo().getVersion());
333
334            if (runlevel != OpenCms.RUNLEVEL_4_SERVLET_ACCESS) {
335                // not the "normal" servlet runlevel
336                if (runlevel == OpenCms.RUNLEVEL_3_SHELL_ACCESS) {
337                    // we have shell runlevel only, upgrade to servlet runlevel (required after setup wizard)
338                    init(getServletConfig());
339                } else {
340                    // illegal runlevel, we can't process requests
341                    // sending status code 403, indicating the server understood the request but refused to fulfill it
342                    res.sendError(HttpServletResponse.SC_FORBIDDEN);
343                    // goodbye
344                    return;
345                }
346            }
347
348            String path = OpenCmsCore.getPathInfo(req);
349            if (path.startsWith(HANDLE_BUILTIN_SERVICE)) {
350                // built-in services are for small AJAX-related functionality in the core that doesn't need to be configurable
351                String remainder = path.substring(HANDLE_BUILTIN_SERVICE.length() - 1); // we want a leading slash in remainder
352                OpenCmsCore.getInstance().invokeBuiltinService(remainder, req, res);
353            } else if (path.startsWith(HANDLE_PATH)) {
354                // this is a request to an OpenCms handler URI
355                invokeHandler(req, res);
356            } else if (path.endsWith(HANDLE_GWT)) {
357                // handle GWT rpc services
358                String serviceName = CmsResource.getName(path);
359                serviceName = serviceName.substring(0, serviceName.length() - HANDLE_GWT.length());
360                OpenCmsCore.getInstance().invokeGwtService(serviceName, req, res, getServletConfig());
361            } else {
362                // standard request to a URI in the OpenCms VFS
363                OpenCmsCore.getInstance().showResource(req, res);
364            }
365        } finally {
366            currentRequestStack.pop();
367            requestCacheStack.pop().close();
368            activeRequests.remove(requestId);
369        }
370    }
371
372    /**
373     * OpenCms servlet POST request handling method,
374     * will just call {@link #doGet(HttpServletRequest, HttpServletResponse)}.<p>
375     *
376     * @see jakarta.servlet.http.HttpServlet#doPost(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)
377     */
378    @Override
379    public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
380
381        doGet(req, res);
382    }
383
384    /**
385     * @see org.opencms.main.I_CmsRequestHandler#getHandlerNames()
386     */
387    public String[] getHandlerNames() {
388
389        return HANDLER_NAMES;
390    }
391
392    /**
393     * @see org.opencms.main.I_CmsRequestHandler#handle(HttpServletRequest, HttpServletResponse, String)
394     */
395    public void handle(HttpServletRequest req, HttpServletResponse res, String name)
396    throws IOException, ServletException {
397
398        int errorCode;
399        try {
400            errorCode = Integer.valueOf(name).intValue();
401        } catch (NumberFormatException nf) {
402            res.sendError(HttpServletResponse.SC_FORBIDDEN);
403            LOG.debug("Error parsing handler name.", nf);
404            return;
405        }
406        switch (errorCode) {
407            case 404:
408                CmsObject cms = null;
409                CmsStaticExportData exportData = null;
410                try {
411                    // this will be set in the root site
412                    cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserExport());
413                    exportData = OpenCms.getStaticExportManager().getExportData(req, cms);
414                } catch (CmsException e) {
415                    // unlikely to happen
416                    if (LOG.isWarnEnabled()) {
417                        LOG.warn(
418                            Messages.get().getBundle().key(
419                                Messages.LOG_INIT_CMSOBJECT_IN_HANDLER_2,
420                                name,
421                                OpenCmsCore.getPathInfo(req)),
422                            e);
423                    }
424                }
425                if (exportData != null) {
426                    try {
427                        // generate a static export request wrapper
428                        CmsStaticExportRequest exportReq = new CmsStaticExportRequest(req, exportData);
429                        // stored content and registered scaled images can bypass the generic static export setup
430                        if (!OpenCms.getStaticExportManager().tryExportStoredContent(exportReq, res, cms, exportData)
431                            && !OpenCms.getStaticExportManager().tryExportImageCache(exportReq, res, cms, exportData)) {
432                            // export the resource and set the response status according to the result
433                            res.setStatus(OpenCms.getStaticExportManager().export(exportReq, res, cms, exportData));
434                        }
435                    } catch (Throwable t) {
436                        if (LOG.isWarnEnabled()) {
437                            LOG.warn(Messages.get().getBundle().key(Messages.LOG_ERROR_EXPORT_1, exportData), t);
438                        }
439                        openErrorHandler(req, res, errorCode);
440                    }
441                } else {
442                    openErrorHandler(req, res, errorCode);
443                }
444                break;
445            default:
446                openErrorHandler(req, res, errorCode);
447        }
448    }
449
450    /**
451     * @see jakarta.servlet.Servlet#init(jakarta.servlet.ServletConfig)
452     */
453    @Override
454    public synchronized void init(ServletConfig config) throws ServletException {
455
456        super.init(config);
457        try {
458            // upgrade the runlevel
459            // usually this should have already been done by the context listener
460            // however, after a fresh install / setup this will be done from here
461            OpenCmsCore.getInstance().upgradeRunlevel(config.getServletContext());
462            // finalize OpenCms initialization
463            OpenCmsCore.getInstance().initServlet(this);
464        } catch (CmsInitException e) {
465            if (Messages.ERR_CRITICAL_INIT_WIZARD_0.equals(e.getMessageContainer().getKey())) {
466                // if wizard is still enabled - allow retry of initialization (required for setup wizard)
467                // this means the servlet init() call must be terminated by an exception
468                if (CmsServletContainerSettings.isServletThrowsException()) {
469                    throw new ServletException(e.getMessage());
470                } else {
471                    // this is needed since some servlet containers does not like the servlet to throw exceptions,
472                    // like BEA WLS 9.x and Resin
473                    LOG.error(Messages.get().getBundle().key(Messages.LOG_ERROR_GENERIC_0), e);
474                }
475            }
476        } catch (Throwable t) {
477            LOG.error(Messages.get().getBundle().key(Messages.LOG_ERROR_GENERIC_0), t);
478        }
479    }
480
481    /**
482     * Manages requests to internal OpenCms request handlers.<p>
483     *
484     * @param req the current request
485     * @param res the current response
486     * @throws ServletException in case an error occurs
487     * @throws ServletException in case an error occurs
488     * @throws IOException in case an error occurs
489     */
490    protected void invokeHandler(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
491
492        String pathInfo = OpenCmsCore.getPathInfo(req);
493        String name = pathInfo.substring(HANDLE_PATH.length());
494        I_CmsRequestHandler handler = OpenCmsCore.getInstance().getRequestHandler(name);
495        if ((handler == null) && name.contains("/")) {
496            // if the name contains a '/', also check for handlers matching the first path fragment only
497            name = name.substring(0, name.indexOf("/"));
498            handler = OpenCmsCore.getInstance().getRequestHandler(name);
499        }
500        if (handler != null) {
501            handler.handle(req, res, name);
502        } else {
503            LOG.warn("Invalid request handler call: " + pathInfo);
504            openErrorHandler(req, res, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
505        }
506    }
507
508    /**
509     * Displays an error code handler loaded from the OpenCms VFS,
510     * or if such a page does not exist,
511     * displays the default servlet container error code.<p>
512     *
513     * @param req the current request
514     * @param res the current response
515     * @param errorCode the error code to display
516     * @throws IOException if something goes wrong
517     * @throws ServletException if something goes wrong
518     */
519    protected void openErrorHandler(HttpServletRequest req, HttpServletResponse res, int errorCode)
520    throws IOException, ServletException {
521
522        String handlerUri = (new StringBuffer(64)).append(HANDLE_VFS_PATH).append(errorCode).append(
523            HANDLE_VFS_SUFFIX).toString();
524        // provide the original error code in a request attribute
525        req.setAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE, Integer.valueOf(errorCode));
526        CmsObject cms;
527        CmsFile file;
528        try {
529            // create OpenCms context, this will be set in the root site
530            cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest());
531            cms.getRequestContext().setSecureRequest(OpenCms.getSiteManager().usesSecureSite(req));
532        } catch (CmsException e) {
533            // unlikely to happen as the OpenCms "Guest" context can always be initialized
534            CmsMessageContainer container = Messages.get().container(
535                Messages.LOG_INIT_CMSOBJECT_IN_HANDLER_2,
536                Integer.valueOf(errorCode),
537                handlerUri);
538            if (LOG.isWarnEnabled()) {
539                LOG.warn(org.opencms.jsp.Messages.getLocalizedMessage(container, req), e);
540            }
541            // however, if it _does_ happen, then we really can't continue here
542            if (!res.isCommitted()) {
543                // since the handler file is not accessible, display the default error page
544                res.sendError(errorCode, e.getLocalizedMessage());
545            }
546            return;
547        }
548        try {
549            if (!tryCustomErrorPage(cms, req, res, errorCode)) {
550                cms.getRequestContext().setUri(handlerUri);
551                cms.getRequestContext().setSecureRequest(OpenCms.getSiteManager().usesSecureSite(req));
552                // read the error handler file
553                file = cms.readFile(handlerUri, CmsResourceFilter.IGNORE_EXPIRATION);
554                OpenCms.getResourceManager().loadResource(cms, file, req, res);
555            }
556        } catch (CmsException e) {
557            // unable to load error page handler VFS resource
558            CmsMessageContainer container = Messages.get().container(
559                Messages.ERR_SHOW_ERR_HANDLER_RESOURCE_2,
560                Integer.valueOf(errorCode),
561                handlerUri);
562            throw new ServletException(org.opencms.jsp.Messages.getLocalizedMessage(container, req), e);
563        }
564    }
565
566    /**
567     * Tries to load the custom error page at the given rootPath.
568     * @param cms {@link CmsObject} used for reading the resource (site root and uri get adjusted!)
569     * @param req the current request
570     * @param res the current response
571     * @param rootPath the VFS root path to the error page resource
572     * @return a flag, indicating if the error page could be loaded
573     */
574    private boolean loadCustomErrorPage(
575        CmsObject cms,
576        HttpServletRequest req,
577        HttpServletResponse res,
578        String rootPath) {
579
580        try {
581
582            // get the site of the error page resource
583            CmsSite errorSite = OpenCms.getSiteManager().getSiteForRootPath(rootPath);
584            cms.getRequestContext().setSiteRoot(errorSite.getSiteRoot());
585            String relPath = cms.getRequestContext().removeSiteRoot(rootPath);
586            if (cms.existsResource(relPath)) {
587                cms.getRequestContext().setUri(relPath);
588                OpenCms.getResourceManager().loadResource(cms, cms.readResource(relPath), req, res);
589                return true;
590            } else {
591                return false;
592            }
593        } catch (Throwable e) {
594            // something went wrong log the exception and return false
595            LOG.error(e.getMessage(), e);
596            return false;
597        }
598    }
599
600    /**
601     * Tries to load a site specific error page. If
602     * @param cms {@link CmsObject} used for reading the resource (site root and uri get adjusted!)
603     * @param req the current request
604     * @param res the current response
605     * @param errorCode the error code to display
606     * @return a flag, indicating if the custom error page could be loaded.
607     */
608    private boolean tryCustomErrorPage(CmsObject cms, HttpServletRequest req, HttpServletResponse res, int errorCode) {
609
610        String siteRoot = OpenCms.getSiteManager().matchRequest(req).getSiteRoot();
611        CmsSite site = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot);
612        if (site != null) {
613            // store current site root and URI
614            String currentSiteRoot = cms.getRequestContext().getSiteRoot();
615            String currentUri = cms.getRequestContext().getUri();
616            try {
617                if (site.getErrorPage() != null) {
618                    String rootPath = site.getErrorPage();
619                    if (loadCustomErrorPage(cms, req, res, rootPath)) {
620                        return true;
621                    }
622                }
623                String rootPath = CmsStringUtil.joinPaths(siteRoot, "/.errorpages/handle" + errorCode + ".html");
624                if (loadCustomErrorPage(cms, req, res, rootPath)) {
625                    return true;
626                }
627            } finally {
628                cms.getRequestContext().setSiteRoot(currentSiteRoot);
629                cms.getRequestContext().setUri(currentUri);
630            }
631        }
632        return false;
633    }
634}