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}