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.jsp.util;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsPropertyDefinition;
032import org.opencms.flex.CmsFlexController;
033import org.opencms.flex.CmsFlexRequest;
034import org.opencms.flex.CmsFlexResponse;
035import org.opencms.i18n.CmsAcceptLanguageHeaderParser;
036import org.opencms.i18n.CmsMessages;
037import org.opencms.jsp.CmsJspActionElement;
038import org.opencms.main.CmsLog;
039import org.opencms.main.OpenCms;
040import org.opencms.security.CmsRole;
041import org.opencms.site.CmsSite;
042import org.opencms.staticexport.CmsLinkManager;
043import org.opencms.util.CmsRequestUtil;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.workplace.CmsWorkplace;
046
047import java.util.Date;
048import java.util.List;
049import java.util.Locale;
050import java.util.Map;
051
052import org.apache.commons.logging.Log;
053
054import jakarta.servlet.RequestDispatcher;
055import jakarta.servlet.http.HttpServletRequest;
056import jakarta.servlet.http.HttpServletResponse;
057import jakarta.servlet.jsp.JspException;
058import jakarta.servlet.jsp.PageContext;
059
060/**
061 * This bean provides methods to generate customized http status error pages, e.g. to handle 404 (not found) errors.<p>
062 *
063 * The JSPs using this bean are placed in the OpenCms VFS folder /system/handler/.<p>
064 *
065 * @since 6.0
066 */
067public class CmsJspStatusBean extends CmsJspActionElement {
068
069    /** Request attribute key for the error message. */
070    public static final String ERROR_MESSAGE = RequestDispatcher.ERROR_MESSAGE;
071
072    /** Request attribute key for the error request URI. */
073    public static final String ERROR_REQUEST_URI = RequestDispatcher.ERROR_REQUEST_URI;
074
075    /** Request attribute key for the error servlet name. */
076    public static final String ERROR_SERVLET_NAME = RequestDispatcher.ERROR_SERVLET_NAME;
077
078    /** Request attribute key for the error status code. */
079    public static final String ERROR_STATUS_CODE = RequestDispatcher.ERROR_STATUS_CODE;
080
081    /** Default name for an unknown error status code. */
082    public static final String UNKKNOWN_STATUS_CODE = "unknown";
083
084    /** The OpenCms VFS path containing the handler files. */
085    public static final String VFS_FOLDER_HANDLER = CmsWorkplace.VFS_PATH_SYSTEM + "handler/";
086
087    /** The log object for this class. */
088    private static final Log LOG = CmsLog.getLog(CmsJspStatusBean.class);
089
090    /** The error message. */
091    private String m_errorMessage;
092
093    /** The thrown exception. */
094    private Throwable m_exception;
095
096    /** The Locale to use for displayed messages. */
097    private Locale m_locale;
098
099    /** Contains all possible parameters usable by localized messages. */
100    private Object[] m_localizeParameters;
101
102    /** The localized messages to use on the page. */
103    private CmsMessages m_messages;
104
105    /** The request URI. */
106    private String m_requestUri;
107
108    /** The servlet name. */
109    private String m_servletName;
110
111    /** The site root of the requested resource. */
112    private String m_siteRoot;
113
114    /** The status code. */
115    private Integer m_statusCode;
116
117    /** The status code as message. */
118    private String m_statusCodeMessage;
119
120    /** The URI used for template part inclusion. */
121    private String m_templateUri;
122
123    /**
124     * Empty constructor, required for every JavaBean.
125     */
126    public CmsJspStatusBean() {
127
128        super();
129    }
130
131    /**
132     * Constructor, with parameters.
133     *
134     * @param context the JSP page context object
135     * @param req the JSP request
136     * @param res the JSP response
137     */
138    public CmsJspStatusBean(PageContext context, HttpServletRequest req, HttpServletResponse res) {
139
140        super(context, req, res);
141        initMembers(req, null);
142    }
143
144    /**
145     * Constructor, with parameters.
146     *
147     * @param context the JSP page context object
148     * @param req the JSP request
149     * @param res the JSP response
150     * @param t the exception that lead to the error
151     */
152    public CmsJspStatusBean(PageContext context, HttpServletRequest req, HttpServletResponse res, Throwable t) {
153
154        super(context, req, res);
155        initMembers(req, t);
156    }
157
158    /**
159     * Tries to forward to the configured error page for the current site root if present.<p>
160     *
161     * @param rootPath the resource to be used as error page
162     *
163     * @return <code>true</code> if the forward was successful performed, <code>false</code> otherwise
164     *
165     * @deprecated will not work for container pages and other pages using cms:include tags
166     */
167    @Deprecated
168    public boolean forwardToErrorPage(String rootPath) {
169
170        try {
171
172            // get the site of the the error page resource
173            CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(rootPath);
174
175            // initialize a CMS object for the given resource
176            CmsObject clone = OpenCms.initCmsObject(getCmsObject());
177            if (site != null) {
178                clone.getRequestContext().setSiteRoot(site.getSiteRoot());
179            }
180            String relPath = clone.getRequestContext().removeSiteRoot(rootPath);
181            clone.getRequestContext().setUri(relPath);
182
183            // create a new flex controller together with its flex request/response
184            // initialized with the context of the error page
185            CmsFlexController ori = CmsFlexController.getController(getRequest());
186            CmsFlexController controller = new CmsFlexController(
187                clone,
188                clone.readResource(relPath),
189                ori.getCmsCache(),
190                getRequest(),
191                getResponse(),
192                false,
193                false);
194            // controller.setForwardMode(true);
195            CmsFlexController.setController(getRequest(), controller);
196            CmsFlexRequest f_req = new CmsFlexRequest(getRequest(), controller);
197            CmsFlexResponse f_res = new CmsFlexResponse(getResponse(), controller, false, false);
198            controller.push(f_req, f_res);
199
200            // send the forward
201            CmsRequestUtil.forwardRequest(
202                OpenCms.getStaticExportManager().getVfsPrefix() + relPath,
203                getRequest(),
204                getResponse());
205
206        } catch (Throwable e) {
207            // something went wrong log the exception and return false
208            LOG.error(e.getMessage(), e);
209            return false;
210        }
211        // return success flag
212        return true;
213    }
214
215    /**
216     * Returns the error message.<p>
217     *
218     * @return the error message
219     */
220    public String getErrorMessage() {
221
222        return m_errorMessage;
223    }
224
225    /**
226     * Returns the exception.<p>
227     *
228     * @return the exception
229     */
230    public Throwable getException() {
231
232        return m_exception;
233    }
234
235    /**
236     * Returns the locale to use for the error page.<p>
237     *
238     * @return the locale to use for the error page
239     */
240    public Locale getLocale() {
241
242        return m_locale;
243    }
244
245    /**
246     * Returns the processed output of the specified element of an OpenCms page.<p>
247     *
248     * The page to get the content from is looked up in the property value "template-elements".
249     * If no value is found, the page is read from the "contents/" subfolder of the handler folder.<p>
250     *
251     * For each status code, an individual page can be created by naming it "content${STATUSCODE}.html".
252     * If the individual page can not be found, the content is read from "contentunknown.html".<p>
253     *
254     * @param element name of the element
255     * @return the processed output of the specified element of an OpenCms page
256     */
257    public String getPageContent(String element) {
258
259        // Determine the folder to read the contents from
260        String contentFolder = property(CmsPropertyDefinition.PROPERTY_TEMPLATE_ELEMENTS, "search", "");
261        if (CmsStringUtil.isEmpty(contentFolder)) {
262            contentFolder = VFS_FOLDER_HANDLER + "contents/";
263        }
264
265        // determine the file to read the contents from
266        String fileName = "content" + getStatusCodeMessage() + ".html";
267        if (!getCmsObject().existsResource(contentFolder + fileName)) {
268            // special file does not exist, use generic one
269            fileName = "content" + UNKKNOWN_STATUS_CODE + ".html";
270        }
271
272        // get the content
273        return getContent(contentFolder + fileName, element, getLocale());
274    }
275
276    /**
277     * Returns the absolute path of the requested resource in the VFS of OpenCms.<p>
278     *
279     * @return the absolute path of the requested resource in the VFS of OpenCms
280     */
281    public String getRequestResourceName() {
282
283        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(getRequestUri())) {
284            return CmsLinkManager.removeOpenCmsContext(getRequestUri());
285        }
286        return getRequestUri();
287    }
288
289    /**
290     * Returns the request Uri.<p>
291     *
292     * @return the request Uri
293     */
294    public String getRequestUri() {
295
296        return m_requestUri;
297    }
298
299    /**
300     * Returns the full Workplace resource path to the selected resource.<p>
301     *
302     * @param resourceName the name of the resource to get the resource path for
303     *
304     * @return the full Workplace resource path to the selected resource
305     */
306    public String getResourceUri(String resourceName) {
307
308        return CmsWorkplace.getResourceUri(resourceName);
309    }
310
311    /**
312     * Returns the servlet name.<p>
313     *
314     * @return the servlet name
315     */
316    public String getServletName() {
317
318        return m_servletName;
319    }
320
321    /**
322     * Returns the site root of the requested resource.<p>
323     *
324     * @return the site root of the requested resource
325     */
326    public String getSiteRoot() {
327
328        return m_siteRoot;
329    }
330
331    /**
332     * Returns the status code.<p>
333     *
334     * @return the status code
335     */
336    public Integer getStatusCode() {
337
338        return m_statusCode;
339    }
340
341    /**
342     * Returns the status code message.<p>
343     *
344     * @return the status code message
345     */
346    public String getStatusCodeMessage() {
347
348        return m_statusCodeMessage;
349    }
350
351    /**
352     * Returns the URI used for template part inclusion.<p>
353     *
354     * @return the URI used for template part inclusion
355     */
356    public String getTemplateUri() {
357
358        if (CmsStringUtil.isEmptyOrWhitespaceOnly(m_templateUri)) {
359            m_templateUri = "/";
360        }
361        return m_templateUri;
362    }
363
364    /**
365     * Include a template part to display on the error page.<p>
366     *
367     * @param target the target uri of the file in the OpenCms VFS (can be relative or absolute)
368     * @param element the element (template selector) to display from the target
369     *
370     * @throws JspException in case there were problems including the target
371     */
372    public void includeTemplatePart(String target, String element) throws JspException {
373
374        includeTemplatePart(target, element, null);
375    }
376
377    /**
378     * Include a template part to display on the error page.<p>
379     *
380     * @param target the target uri of the file in the OpenCms VFS (can be relative or absolute)
381     * @param element the element (template selector) to display from the target
382     * @param parameterMap a map of the request parameters
383     *
384     * @throws JspException in case there were problems including the target
385     */
386    public void includeTemplatePart(String target, String element, Map<String, Object> parameterMap)
387    throws JspException {
388
389        // store current site root and URI
390        String currentSiteRoot = getRequestContext().getSiteRoot();
391        String currentUri = getRequestContext().getUri();
392
393        try {
394            // set site root and URI to display template part correct
395            getRequestContext().setSiteRoot(getSiteRoot());
396            getRequestContext().setUri(getTemplateUri());
397            // include the template part
398            include(target, element, parameterMap);
399        } finally {
400            // reset site root and requested URI to status JSP
401            getRequestContext().setSiteRoot(currentSiteRoot);
402            getRequestContext().setUri(currentUri);
403        }
404    }
405
406    /**
407     * Returns the localized resource string for a given message key.<p>
408     *
409     * If the key was not found in the bundle, the return value is
410     * <code>"??? " + keyName + " ???"</code>.<p>
411     *
412     * The key can use the following parameters for formatting:
413     * <ul>
414     * <li>0: the HTTP status code</li>
415     * <li>1: the requested URI</li>
416     * <li>2: the generated error message</li>
417     * <li>3: the servlet name</li>
418     * <li>4: the date of the request</li>
419     * </ul>
420     *
421     * @param keyName the key for the desired string
422     * @return the resource string for the given key
423     */
424    public String key(String keyName) {
425
426        return key(keyName, null);
427    }
428
429    /**
430     * Returns the localized resource string for a given message key.<p>
431     *
432     * For a detailed parameter description, see {@link CmsJspStatusBean#key(String)}.<p>
433     *
434     * @param keyName the key for the desired string
435     * @param defaultKeyName the default key for the desired string, used if the keyName delivered no resource string
436     * @return the resource string for the given key
437     */
438    public String key(String keyName, String defaultKeyName) {
439
440        String value = getMessages().key(keyName, getLocalizeParameters());
441        if (value.startsWith(CmsMessages.UNKNOWN_KEY_EXTENSION) && CmsStringUtil.isNotEmpty(defaultKeyName)) {
442            value = getMessages().key(defaultKeyName, getLocalizeParameters());
443        }
444        return CmsStringUtil.escapeHtml(value);
445    }
446
447    /**
448     * Returns the localized resource string for a given message key depending on the HTTP status.<p>
449     *
450     * Automatically adds a status suffix for the key to get, eg. "keyname" gets the suffix "_404" for a 404 status.
451     * For a detailed parameter description, see {@link CmsJspStatusBean#key(String)}.<p>
452     *
453     * @param keyName the key for the desired string
454     * @return the resource string for the given key
455     */
456    public String keyStatus(String keyName) {
457
458        keyName += "_";
459        return key(keyName + getStatusCodeMessage(), keyName + UNKKNOWN_STATUS_CODE);
460    }
461
462    /**
463     * Writes the exception into the 'opencms.log', if the exception is not <code>null</code>.<p>
464     */
465    public void logException() {
466
467        if (m_exception != null) {
468            LOG.error(m_exception.getMessage(), m_exception);
469        }
470    }
471
472    /**
473     * Sets the URI used for template part inclusion.<p>
474     *
475     * @param templateUri the URI used for template part inclusion
476     */
477    public void setTemplateUri(String templateUri) {
478
479        m_templateUri = templateUri;
480    }
481
482    /**
483     * Returns true if the current user has the "DEVELOPER" role and can view the exception stacktrace.<p>
484     *
485     * @return true if the current user has the "DEVELOPER" role and can view the exception stacktrace
486     */
487    public boolean showException() {
488
489        return OpenCms.getRoleManager().hasRole(getCmsObject(), CmsRole.DEVELOPER);
490    }
491
492    /**
493     * Returns the parameter object for localization.<p>
494     *
495     * @return the parameter object for localization
496     *
497     * @see #key(String) for a more detailed object description
498     */
499    protected Object[] getLocalizeParameters() {
500
501        if (m_localizeParameters == null) {
502            m_localizeParameters = new Object[] {
503                getStatusCodeMessage(),
504                getRequestUri(),
505                getErrorMessage(),
506                getServletName(),
507                new Date(getRequestContext().getRequestTime())};
508        }
509        return m_localizeParameters;
510    }
511
512    /**
513     * Returns the initialized messages object to read localized messages from.<p>
514     *
515     * @return the initialized messages object to read localized messages from
516     */
517    protected CmsMessages getMessages() {
518
519        if (m_messages == null) {
520            // initialize the localized messages
521            m_messages = new CmsMessages(Messages.get().getBundleName(), getLocale().toString());
522        }
523        return m_messages;
524    }
525
526    /**
527     * Initializes the members of this bean with the information retrieved from the current request.<p>
528     *
529     * @param req the JSP request
530     * @param t the exception that lead to the error
531     */
532    protected void initMembers(HttpServletRequest req, Throwable t) {
533
534        // get the status error attribute values from the request
535        m_servletName = (String)req.getAttribute(ERROR_SERVLET_NAME);
536        m_errorMessage = (String)req.getAttribute(ERROR_MESSAGE);
537        m_requestUri = (String)req.getAttribute(ERROR_REQUEST_URI);
538        m_statusCode = (Integer)req.getAttribute(ERROR_STATUS_CODE);
539
540        if ((m_statusCode == null) || (m_requestUri == null)) {
541            // check if the error request is invoked via Apache/HTTPd ErrorDocument and mod_jk
542
543            // to use this you need to add the following to "jk.conf":
544
545            // JkEnvVar REDIRECT_URL none
546            // JkEnvVar REDIRECT_STATUS none
547            // JkEnvVar REDIRECT_SERVLET_NAME OpenCmsServlet
548
549            String jkUri = (String)req.getAttribute("REDIRECT_URL");
550            String jkStatusCode = (String)req.getAttribute("REDIRECT_STATUS");
551            String jkServletName = (String)req.getAttribute("REDIRECT_SERVLET_NAME");
552            try {
553                if (!"none".equals(jkStatusCode) && !"none".equals(jkUri)) {
554                    m_servletName = jkServletName;
555                    m_requestUri = jkUri;
556                    m_statusCode = Integer.valueOf(jkStatusCode);
557                }
558            } catch (NullPointerException e) {
559                // attibute not set, ignore
560            } catch (NumberFormatException e) {
561                // status code not a number, ignore
562            }
563        }
564
565        // get the status code as String
566        if (m_statusCode != null) {
567            m_statusCodeMessage = String.valueOf(m_statusCode.intValue());
568        } else {
569            m_statusCodeMessage = UNKKNOWN_STATUS_CODE;
570        }
571
572        m_exception = t;
573
574        // determine the best locale to use from the users browser settings
575        CmsAcceptLanguageHeaderParser parser = new CmsAcceptLanguageHeaderParser(
576            req,
577            OpenCms.getWorkplaceManager().getDefaultLocale());
578        List<Locale> acceptedLocales = parser.getAcceptedLocales();
579        List<Locale> workplaceLocales = OpenCms.getWorkplaceManager().getLocales();
580        m_locale = OpenCms.getLocaleManager().getFirstMatchingLocale(acceptedLocales, workplaceLocales);
581        if (m_locale == null) {
582            // no match found - use OpenCms default locale
583            m_locale = OpenCms.getWorkplaceManager().getDefaultLocale();
584        }
585        getCmsObject().getRequestContext().setLocale(m_locale);
586
587        // store the site root of the request
588        m_siteRoot = OpenCms.getSiteManager().matchRequest(req).getSiteRoot();
589    }
590
591    /**
592     * Sets the error message.<p>
593     *
594     * @param errorMessage the error message to set
595     */
596    protected void setErrorMessage(String errorMessage) {
597
598        m_errorMessage = errorMessage;
599    }
600
601    /**
602     * Sets the exception.<p>
603     *
604     * @param exception the exception to set
605     */
606    protected void setException(Throwable exception) {
607
608        m_exception = exception;
609    }
610
611    /**
612     * Sets the locale to use for the error page.<p>
613     *
614     * @param locale the locale to use for the error page
615     */
616    protected void setLocale(Locale locale) {
617
618        m_locale = locale;
619        getCmsObject().getRequestContext().setLocale(m_locale);
620    }
621
622    /**
623     * Sets the parameter object for localization.<p>
624     *
625     * @param localizeParameters the parameter object for localization
626     */
627    protected void setLocalizeParameters(Object[] localizeParameters) {
628
629        m_localizeParameters = localizeParameters;
630    }
631
632    /**
633     * Sets the initialized messages object to read localized messages from.<p>
634     *
635     * @param messages the initialized messages object to read localized messages from
636     */
637    protected void setMessages(CmsMessages messages) {
638
639        m_messages = messages;
640    }
641
642    /**
643     * Sets the request Uri.<p>
644     *
645     * @param requestUri the request Uri to set
646     */
647    protected void setRequestUri(String requestUri) {
648
649        m_requestUri = requestUri;
650    }
651
652    /**
653     * Sets the servlet name.<p>
654     *
655     * @param servletName the servlet name to set
656     */
657    protected void setServletName(String servletName) {
658
659        m_servletName = servletName;
660    }
661
662    /**
663     * Sets the site root of the requested resource.<p>
664     *
665     * @param siteRoot the site root of the requested resource
666     */
667    protected void setSiteRoot(String siteRoot) {
668
669        m_siteRoot = siteRoot;
670    }
671
672    /**
673     * Sets the status code.<p>
674     *
675     * @param statusCode the status code to set
676     */
677    protected void setStatusCode(Integer statusCode) {
678
679        m_statusCode = statusCode;
680    }
681
682    /**
683     * Sets the status code message.<p>
684     *
685     * @param statusCodeMessage the status code message to set
686     */
687    protected void setStatusCodeMessage(String statusCodeMessage) {
688
689        m_statusCodeMessage = statusCodeMessage;
690    }
691}