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}