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.loader; 029 030import org.opencms.configuration.CmsConfigurationException; 031import org.opencms.configuration.CmsParameterConfiguration; 032import org.opencms.db.storage.I_CmsStorageDelivery; 033import org.opencms.file.CmsFile; 034import org.opencms.file.CmsObject; 035import org.opencms.file.CmsResource; 036import org.opencms.file.CmsStoredContentInfo; 037import org.opencms.flex.CmsFlexController; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsLog; 040import org.opencms.main.OpenCms; 041import org.opencms.security.CmsPermissionSet; 042import org.opencms.util.CmsRequestUtil; 043import org.opencms.util.CmsStringUtil; 044import org.opencms.workplace.CmsWorkplaceManager; 045 046import java.io.IOException; 047import java.io.OutputStream; 048import java.util.Iterator; 049import java.util.Locale; 050 051import jakarta.servlet.ServletRequest; 052import jakarta.servlet.ServletResponse; 053import jakarta.servlet.http.HttpServletRequest; 054import jakarta.servlet.http.HttpServletResponse; 055 056/** 057 * Dump loader for binary or other unprocessed resource types.<p> 058 * 059 * This loader is also used to deliver static sub-elements of pages processed 060 * by other loaders.<p> 061 * 062 * @since 6.0.0 063 */ 064public class CmsDumpLoader 065implements I_CmsResourceLoader, I_CmsStaticExportStreamLoader, I_CmsStoredContentDirectDeliveryLoader { 066 067 /** 068 * Output stream which writes all bytes to two target streams.<p> 069 */ 070 private static class CmsTeeOutputStream extends OutputStream { 071 072 /** The first output stream. */ 073 private final OutputStream m_first; 074 075 /** The second output stream. */ 076 private final OutputStream m_second; 077 078 /** 079 * Creates a new tee output stream.<p> 080 * 081 * @param first the first output stream 082 * @param second the second output stream 083 */ 084 CmsTeeOutputStream(OutputStream first, OutputStream second) { 085 086 m_first = first; 087 m_second = second; 088 } 089 090 /** 091 * @see java.io.OutputStream#flush() 092 */ 093 @Override 094 public void flush() throws IOException { 095 096 m_first.flush(); 097 m_second.flush(); 098 } 099 100 /** 101 * @see java.io.OutputStream#write(byte[], int, int) 102 */ 103 @Override 104 public void write(byte[] buffer, int offset, int length) throws IOException { 105 106 m_first.write(buffer, offset, length); 107 m_second.write(buffer, offset, length); 108 } 109 110 /** 111 * @see java.io.OutputStream#write(int) 112 */ 113 @Override 114 public void write(int value) throws IOException { 115 116 m_first.write(value); 117 m_second.write(value); 118 } 119 } 120 121 /** The id of this loader. */ 122 public static final int RESOURCE_LOADER_ID = 1; 123 124 /** The maximum age for dumped contents in the clients cache. */ 125 private static long m_clientCacheMaxAge; 126 127 /** The resource loader configuration. */ 128 private CmsParameterConfiguration m_configuration; 129 130 /** 131 * The constructor of the class is empty and does nothing.<p> 132 */ 133 public CmsDumpLoader() { 134 135 m_configuration = new CmsParameterConfiguration(); 136 } 137 138 /** 139 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String) 140 */ 141 public void addConfigurationParameter(String paramName, String paramValue) { 142 143 m_configuration.put(paramName, paramValue); 144 } 145 146 /** 147 * Tries to deliver externally stored content directly to the servlet response.<p> 148 * 149 * @param cms the CMS context 150 * @param resource the resource 151 * @param req the request 152 * @param res the response 153 * 154 * @return <code>true</code> if the response was handled 155 * 156 * @throws IOException in case writing to the response fails 157 * @throws CmsException in case storage delivery fails 158 */ 159 public boolean deliverStoredContent( 160 CmsObject cms, 161 CmsResource resource, 162 HttpServletRequest req, 163 HttpServletResponse res) 164 throws IOException, CmsException { 165 166 if ((req == null) || (res == null)) { 167 return false; 168 } 169 if (!cms.hasPermissions(resource, CmsPermissionSet.ACCESS_READ)) { 170 return false; 171 } 172 CmsStoredContentInfo info = cms.readStoredContentInfo(resource); 173 if ((info == null) || !info.isExternallyStored()) { 174 return false; 175 } 176 I_CmsStorageDelivery storage = cms.getStoredContentDelivery(info.getStorage()); 177 if (storage == null) { 178 return false; 179 } 180 String mimetype = OpenCms.getResourceManager().getMimeType( 181 resource.getName(), 182 cms.getRequestContext().getEncoding()); 183 if (mimetype != null) { 184 res.setContentType(mimetype); 185 } 186 CmsStoredContentDeliveryHelper.DeliveryResult result = new CmsStoredContentDeliveryHelper( 187 getClientCacheMaxAge()).deliver(info, storage, req, res); 188 return result != CmsStoredContentDeliveryHelper.DeliveryResult.NOT_DELIVERABLE; 189 } 190 191 /** 192 * Destroy this ResourceLoder, this is a NOOP so far.<p> 193 */ 194 public void destroy() { 195 196 // NOOP 197 } 198 199 /** 200 * @see org.opencms.loader.I_CmsResourceLoader#dump(org.opencms.file.CmsObject, org.opencms.file.CmsResource, java.lang.String, java.util.Locale, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 201 */ 202 public byte[] dump( 203 CmsObject cms, 204 CmsResource resource, 205 String element, 206 Locale locale, 207 HttpServletRequest req, 208 HttpServletResponse res) 209 throws CmsException { 210 211 return cms.readFile(resource).getContents(); 212 } 213 214 /** 215 * @see org.opencms.loader.I_CmsResourceLoader#export(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 216 */ 217 public byte[] export(CmsObject cms, CmsResource resource, HttpServletRequest req, HttpServletResponse res) 218 throws IOException, CmsException { 219 220 CmsFile file = cms.readFile(resource); 221 222 // if no request and response are given, the resource only must be exported and no 223 // output must be generated 224 if ((req != null) && (res != null)) { 225 // overwrite headers if set as default 226 setStaticExportHeaders(res); 227 load(cms, file, req, res); 228 } 229 230 return file.getContents(); 231 } 232 233 /** 234 * Tries to deliver externally stored content directly during static export.<p> 235 * 236 * @param cms the CMS context 237 * @param resource the resource 238 * @param req the request 239 * @param res the response 240 * 241 * @return <code>true</code> if the response was handled 242 * 243 * @throws IOException in case writing to the response fails 244 * @throws CmsException in case storage delivery fails 245 */ 246 @Override 247 public boolean exportStoredContentTo( 248 CmsObject cms, 249 CmsResource resource, 250 HttpServletRequest req, 251 HttpServletResponse res) 252 throws IOException, CmsException { 253 254 if ((req == null) || (res == null)) { 255 return false; 256 } 257 setStaticExportHeaders(res); 258 return deliverStoredContent(cms, resource, req, res); 259 } 260 261 /** 262 * @see org.opencms.loader.I_CmsStaticExportStreamLoader#exportTo(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse, java.io.OutputStream) 263 */ 264 @Override 265 public void exportTo( 266 CmsObject cms, 267 CmsResource resource, 268 HttpServletRequest req, 269 HttpServletResponse res, 270 OutputStream exportOut) 271 throws IOException, CmsException { 272 273 OutputStream out = exportOut; 274 if ((req != null) && (res != null)) { 275 setStaticExportHeaders(res); 276 if (!canSendLastModifiedHeader(resource, req, res)) { 277 prepareResponse(resource, resource.getLength(), req, res); 278 out = new CmsTeeOutputStream(exportOut, res.getOutputStream()); 279 } 280 } 281 cms.readFileContentTo(resource, out); 282 } 283 284 /** 285 * Will always return <code>null</code> since this loader does not 286 * need to be configured.<p> 287 * 288 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 289 */ 290 public CmsParameterConfiguration getConfiguration() { 291 292 // return the configuration in an immutable form 293 return m_configuration; 294 } 295 296 /** 297 * @see org.opencms.loader.I_CmsResourceLoader#getLoaderId() 298 */ 299 public int getLoaderId() { 300 301 return RESOURCE_LOADER_ID; 302 } 303 304 /** 305 * Return a String describing the ResourceLoader, 306 * which is (localized to the system default locale) 307 * <code>"The OpenCms default resource loader for unprocessed files"</code>.<p> 308 * 309 * @return a describing String for the ResourceLoader 310 */ 311 public String getResourceLoaderInfo() { 312 313 return Messages.get().getBundle().key(Messages.GUI_LOADER_DUMB_DEFAULT_DESC_0); 314 } 315 316 /** 317 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration() 318 */ 319 public void initConfiguration() throws CmsConfigurationException { 320 321 Object maxAge = m_configuration.get("client.cache.maxage"); 322 if (maxAge == null) { 323 m_clientCacheMaxAge = -1; 324 } else { 325 m_clientCacheMaxAge = Long.parseLong(String.valueOf(maxAge)); 326 } 327 328 if (CmsLog.INIT.isInfoEnabled()) { 329 if (maxAge != null) { 330 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_CLIENT_CACHE_MAX_AGE_1, maxAge)); 331 } 332 CmsLog.INIT.info( 333 Messages.get().getBundle().key(Messages.INIT_LOADER_INITIALIZED_1, this.getClass().getName())); 334 } 335 } 336 337 /** 338 * @see org.opencms.loader.I_CmsResourceLoader#isStaticExportEnabled() 339 */ 340 public boolean isStaticExportEnabled() { 341 342 return true; 343 } 344 345 /** 346 * @see org.opencms.loader.I_CmsResourceLoader#isStaticExportProcessable() 347 */ 348 public boolean isStaticExportProcessable() { 349 350 return false; 351 } 352 353 /** 354 * @see org.opencms.loader.I_CmsStoredContentDirectDeliveryLoader#isStoredContentDirectDeliveryEnabled(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 355 */ 356 @Override 357 public boolean isStoredContentDirectDeliveryEnabled( 358 CmsObject cms, 359 CmsResource resource, 360 HttpServletRequest req, 361 HttpServletResponse res) 362 throws CmsException { 363 364 return true; 365 } 366 367 /** 368 * @see org.opencms.loader.I_CmsResourceLoader#isUsableForTemplates() 369 */ 370 public boolean isUsableForTemplates() { 371 372 return false; 373 } 374 375 /** 376 * @see org.opencms.loader.I_CmsResourceLoader#isUsingUriWhenLoadingTemplate() 377 */ 378 public boolean isUsingUriWhenLoadingTemplate() { 379 380 return false; 381 } 382 383 /** 384 * @see org.opencms.loader.I_CmsResourceLoader#load(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse) 385 */ 386 public void load(CmsObject cms, CmsResource resource, HttpServletRequest req, HttpServletResponse res) 387 throws IOException, CmsException { 388 389 if (tryDeliverStoredContent(cms, resource, req, res)) { 390 return; 391 } 392 393 if (canSendLastModifiedHeader(resource, req, res)) { 394 // no further processing required 395 return; 396 } 397 398 prepareResponse(resource, resource.getLength(), req, res); 399 400 service(cms, resource, req, res); 401 } 402 403 /** 404 * @see org.opencms.loader.I_CmsResourceLoader#service(org.opencms.file.CmsObject, org.opencms.file.CmsResource, jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse) 405 */ 406 public void service(CmsObject cms, CmsResource resource, ServletRequest req, ServletResponse res) 407 throws CmsException, IOException { 408 409 cms.readFileContentTo(resource, res.getOutputStream()); 410 } 411 412 /** 413 * Checks if the requested resource must be send to the client by checking the "If-Modified-Since" http header.<p> 414 * 415 * If the resource has not been modified, the "304 - not modified" 416 * header is send to the client and <code>true</code> 417 * is returned, otherwise nothing is send and <code>false</code> is returned.<p> 418 * 419 * @param resource the resource to check 420 * @param req the current request 421 * @param res the current response 422 * 423 * @return <code>true</code> if the "304 - not modified" header has been send to the client 424 */ 425 protected boolean canSendLastModifiedHeader(CmsResource resource, HttpServletRequest req, HttpServletResponse res) { 426 427 // resource state must be unchanged 428 if (resource.getState().isUnchanged() 429 // the request must not have been send by a workplace user (we can't use "304 - not modified" in workplace 430 && !CmsWorkplaceManager.isWorkplaceUser(req) 431 // last modified header must match the time form the resource 432 && CmsFlexController.isNotModifiedSince(req, resource.getDateLastModified())) { 433 long now = System.currentTimeMillis(); 434 if ((resource.getDateReleased() < now) && (resource.getDateExpired() > now)) { 435 // resource is available and not expired 436 CmsFlexController.setDateExpiresHeader(res, resource.getDateExpired(), m_clientCacheMaxAge); 437 // set status 304 - not modified 438 res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 439 return true; 440 } 441 } 442 return false; 443 } 444 445 /** 446 * Returns the maximum age for dumped contents in the clients cache.<p> 447 * 448 * @return the maximum age for dumped contents in the clients cache 449 */ 450 protected long getClientCacheMaxAge() { 451 452 return m_clientCacheMaxAge; 453 } 454 455 /** 456 * Prepares the servlet response headers for sending the given resource.<p> 457 * 458 * @param resource the resource to send 459 * @param contentLength the content length to set 460 * @param req the current request 461 * @param res the current response 462 * 463 */ 464 protected void prepareResponse( 465 CmsResource resource, 466 int contentLength, 467 HttpServletRequest req, 468 HttpServletResponse res) { 469 470 // set response status to "200 - OK" (required for static export "on-demand") 471 res.setStatus(HttpServletResponse.SC_OK); 472 // set content length header 473 res.setContentLength(contentLength); 474 475 if (CmsWorkplaceManager.isWorkplaceUser(req)) { 476 // prevent caching for Workplace users 477 res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, System.currentTimeMillis()); 478 CmsRequestUtil.setNoCacheHeaders(res); 479 } else { 480 // set date last modified header 481 res.setDateHeader(CmsRequestUtil.HEADER_LAST_MODIFIED, resource.getDateLastModified()); 482 483 // set "Expires" only if cache control is not already set 484 if (!res.containsHeader(CmsRequestUtil.HEADER_CACHE_CONTROL)) { 485 long expireTime = resource.getDateExpired(); 486 if (expireTime == CmsResource.DATE_EXPIRED_DEFAULT) { 487 expireTime--; 488 // flex controller will automatically reduce this to a reasonable value 489 } 490 // now set "Expires" header 491 CmsFlexController.setDateExpiresHeader(res, expireTime, m_clientCacheMaxAge); 492 } 493 } 494 } 495 496 /** 497 * Sets the configured static export headers on the response.<p> 498 * 499 * @param res the response 500 */ 501 private void setStaticExportHeaders(HttpServletResponse res) { 502 503 Iterator<String> headers = OpenCms.getStaticExportManager().getExportHeaders().listIterator(); 504 while (headers.hasNext()) { 505 String header = headers.next(); 506 String[] parts = CmsStringUtil.splitAsArray(header, ':'); 507 if (parts.length == 2) { 508 res.setHeader(parts[0], parts[1]); 509 } 510 } 511 } 512 513 /** 514 * Tries to deliver externally stored content directly from the storage backend.<p> 515 * 516 * @param cms the CMS context 517 * @param resource the resource 518 * @param req the request 519 * @param res the response 520 * 521 * @return <code>true</code> if the response was handled 522 * 523 * @throws IOException in case writing to the response fails 524 * @throws CmsException in case storage delivery fails 525 */ 526 private boolean tryDeliverStoredContent( 527 CmsObject cms, 528 CmsResource resource, 529 HttpServletRequest req, 530 HttpServletResponse res) 531 throws IOException, CmsException { 532 533 return deliverStoredContent(cms, resource, req, res); 534 } 535}