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.util;
029
030import org.opencms.flex.CmsFlexRequest;
031import org.opencms.flex.CmsFlexResponse;
032import org.opencms.i18n.CmsEncoder;
033import org.opencms.json.JSONArray;
034import org.opencms.json.JSONException;
035import org.opencms.json.JSONObject;
036import org.opencms.jsp.CmsJspActionElement;
037import org.opencms.main.CmsLog;
038import org.opencms.main.OpenCms;
039import org.opencms.site.CmsSiteMatcher;
040
041import java.io.IOException;
042import java.io.UnsupportedEncodingException;
043import java.net.URI;
044import java.net.URLDecoder;
045import java.nio.charset.Charset;
046import java.util.ArrayList;
047import java.util.Enumeration;
048import java.util.HashMap;
049import java.util.Iterator;
050import java.util.List;
051import java.util.Map;
052import java.util.Map.Entry;
053
054import org.apache.commons.fileupload2.core.DiskFileItem;
055import org.apache.commons.fileupload2.core.DiskFileItemFactory;
056import org.apache.commons.fileupload2.core.FileUploadException;
057import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletDiskFileUpload;
058import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletFileUpload;
059import org.apache.commons.logging.Log;
060
061import com.google.common.collect.ArrayListMultimap;
062import com.google.common.collect.Multimap;
063
064import jakarta.servlet.ServletException;
065import jakarta.servlet.ServletRequest;
066import jakarta.servlet.ServletResponse;
067import jakarta.servlet.http.Cookie;
068import jakarta.servlet.http.HttpServletRequest;
069import jakarta.servlet.http.HttpServletRequestWrapper;
070import jakarta.servlet.http.HttpServletResponse;
071import jakarta.servlet.http.HttpServletResponseWrapper;
072import jakarta.servlet.http.HttpSession;
073
074/**
075 * Provides utility functions for dealing with values a <code>{@link HttpServletRequest}</code>.<p>
076 *
077 * @since 6.0.0
078 */
079public final class CmsRequestUtil {
080
081    /** Request attribute that contains the original error code. */
082    public static final String ATTRIBUTE_ERRORCODE = "org.opencms.util.CmsErrorCode";
083
084    /** Flag to enable / disable backlink checks. */
085    public static boolean backlinkCheckEnabled = true;
086
087    /** HTTP Accept Header for the cms:device-tag. */
088    public static final String HEADER_ACCEPT = "Accept";
089
090    /** HTTP Header "Accept-Ranges". */
091    public static final String HEADER_ACCEPT_RANGES = "Accept-Ranges";
092
093    /** HTTP Accept-Charset Header for internal requests used during static export. */
094    public static final String HEADER_ACCEPT_CHARSET = "Accept-Charset";
095
096    /** HTTP Accept-Language Header for internal requests used during static export. */
097    public static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
098
099    /** HTTP Header "Cache-Control". */
100    public static final String HEADER_CACHE_CONTROL = "Cache-Control";
101
102    /** HTTP Header "Connection". */
103    public static final String HEADER_CONNECTION = "Connection";
104
105    /** The "Content-Disposition" http header. */
106    public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
107
108    /** The "Content-Length" http header. */
109    public static final String HEADER_CONTENT_LENGTH = "Content-Length";
110
111    /** The "Content-Range" http header. */
112    public static final String HEADER_CONTENT_RANGE = "Content-Range";
113
114    /** The "Content-Type" http header. */
115    public static final String HEADER_CONTENT_TYPE = "Content-Type";
116
117    /** HTTP Header "ETag". */
118    public static final String HEADER_ETAG = "ETag";
119
120    /** HTTP Header "Expires". */
121    public static final String HEADER_EXPIRES = "Expires";
122
123    /** HTTP Header "If-Range". */
124    public static final String HEADER_IF_RANGE = "If-Range";
125
126    /** HTTP Header "If-Modified-Since". */
127    public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
128
129    /** HTTP Header "If-None-Match". */
130    public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
131
132    /** The Header that stores the session id (used by OpenCms upload applet). */
133    public static final String HEADER_JSESSIONID = "JSESSIONID";
134
135    /** HTTP Header "Last-Modified". */
136    public static final String HEADER_LAST_MODIFIED = "Last-Modified";
137
138    /** HTTP Header "Location". */
139    public static final String HEADER_LOCATION = "Location";
140
141    /** HTTP Header for internal requests used during static export. */
142    public static final String HEADER_OPENCMS_EXPORT = "OpenCms-Export";
143
144    /** HTTP Header "Pragma". */
145    public static final String HEADER_PRAGMA = "Pragma";
146
147    /** HTTP Header "Range". */
148    public static final String HEADER_RANGE = "Range";
149
150    /** HTTP Header "Server". */
151    public static final String HEADER_SERVER = "Server";
152
153    /** HTTP Header "user-agent". */
154    public static final String HEADER_USER_AGENT = "user-agent";
155
156    /** HTTP Header value "max-age=" (for "Cache-Control"). */
157    public static final String HEADER_VALUE_MAX_AGE = "max-age=";
158
159    /** HTTP Header value "must-revalidate" (for "Cache-Control"). */
160    public static final String HEADER_VALUE_MUST_REVALIDATE = "must-revalidate";
161
162    /** HTTP Header value "no-cache" (for "Cache-Control"). */
163    public static final String HEADER_VALUE_NO_CACHE = "no-cache";
164
165    /** HTTP Header value "no-store" (for "Cache-Control"). */
166    public static final String HEADER_VALUE_NO_STORE = "no-store";
167
168    /** HTTP Header "WWW-Authenticate". */
169    public static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
170
171    /** Identifier for x-forwarded-for (i.e. proxied) request headers. */
172    public static final String HEADER_X_FORWARDED_FOR = "x-forwarded-for";
173
174    /** Assignment char between parameter name and values. */
175    public static final String PARAMETER_ASSIGNMENT = "=";
176
177    /** Delimiter char between parameters. */
178    public static final String PARAMETER_DELIMITER = "&";
179
180    /** Delimiter char between url and query. */
181    public static final String URL_DELIMITER = "?";
182
183    /** The prefix for &amp. */
184    private static final String AMP = "amp;";
185
186    /** The log object for this class. */
187    private static final Log LOG = CmsLog.getLog(CmsRequestUtil.class);
188
189    /**
190     * Default constructor (empty), private because this class has only
191     * static methods.<p>
192     */
193    private CmsRequestUtil() {
194
195        // empty
196    }
197
198    /**
199     * Appends a request parameter to the given URL.<p>
200     *
201     * This method takes care about the adding the parameter as an additional
202     * parameter (appending <code>&amp;param=value</code>) or as the first parameter
203     * (appending <code>?param=value</code>).<p>
204     *
205     * @param url the URL where to append the parameter to
206     * @param paramName the paramter name to append
207     * @param paramValue the parameter value to append
208     *
209     * @return the URL with the given parameter appended
210     */
211    public static String appendParameter(String url, String paramName, String paramValue) {
212
213        if (CmsStringUtil.isEmpty(url)) {
214            return null;
215        }
216        int pos = url.indexOf(URL_DELIMITER);
217        StringBuffer result = new StringBuffer(256);
218        result.append(url);
219        if (pos >= 0) {
220            // url already has parameters
221            result.append(PARAMETER_DELIMITER);
222        } else {
223            // url does not have parameters
224            result.append(URL_DELIMITER);
225        }
226        result.append(paramName);
227        result.append(PARAMETER_ASSIGNMENT);
228        result.append(paramValue);
229        return result.toString();
230    }
231
232    /**
233     * Appends a map of request parameters to the given URL.<p>
234     *
235     * The map can contains values of <code>String[]</code> or
236     * simple <code>String</code> values.<p>
237     *
238     * This method takes care about the adding the parameter as an additional
239     * parameter (appending <code>&amp;param=value</code>) or as the first parameter
240     * (appending <code>?param=value</code>).<p>
241     *
242     * @param url the URL where to append the parameter to
243     * @param params the parameters to append
244     * @param encode if <code>true</code>, the parameter values are encoded before they are appended
245     *
246     * @return the URL with the given parameter appended
247     */
248    public static String appendParameters(String url, Map<String, String[]> params, boolean encode) {
249
250        if (CmsStringUtil.isEmpty(url)) {
251            return null;
252        }
253        if ((params == null) || params.isEmpty()) {
254            return url;
255        }
256        int pos = url.indexOf(URL_DELIMITER);
257        StringBuffer result = new StringBuffer(256);
258        result.append(url);
259        if (pos >= 0) {
260            // url already has parameters
261            result.append(PARAMETER_DELIMITER);
262        } else {
263            // url does not have parameters
264            result.append(URL_DELIMITER);
265        }
266        // ensure all values are of type String[]
267        Iterator<Map.Entry<String, String[]>> i = params.entrySet().iterator();
268        while (i.hasNext()) {
269            Map.Entry<String, String[]> entry = i.next();
270            String key = entry.getKey();
271            Object value = entry.getValue();
272            // generics where added later, so make sure that the value really is a String[]
273            String[] values = value instanceof String[] ? (String[])value : new String[] {value.toString()};
274            for (int j = 0; j < values.length; j++) {
275                String strValue = values[j];
276                if (encode) {
277                    strValue = CmsEncoder.encode(strValue);
278                }
279                result.append(key);
280                result.append(PARAMETER_ASSIGNMENT);
281                result.append(strValue);
282                if ((j + 1) < values.length) {
283                    result.append(PARAMETER_DELIMITER);
284                }
285            }
286            if (i.hasNext()) {
287                result.append(PARAMETER_DELIMITER);
288            }
289        }
290        return result.toString();
291    }
292
293    /**
294     * Checks that the given link is a valid backlink for editors.
295     *
296     * <p>This means that the link is either just a path, or starts with a scheme/domain/port that is either registered in the
297     * site configuration, or is the same as that of the current request.
298     *
299     * @param backlink the link to check
300     * @param optionalRequest the current request - may be null if no check against the current request is desired
301     * @return true if the link is a valid backlink
302     */
303    public static boolean checkBacklink(String backlink, HttpServletRequest optionalRequest) {
304
305        if (!backlinkCheckEnabled) {
306            return true;
307        }
308
309        if (CmsStringUtil.isEmptyOrWhitespaceOnly(backlink)) {
310            return true;
311        }
312
313        if (backlink.startsWith("javascript")) {
314            return false;
315        }
316
317        if (backlink.startsWith("/")) {
318            return true;
319        }
320        CmsSiteMatcher matcher = new CmsSiteMatcher(backlink, 0);
321        if (OpenCms.getSiteManager().isMatching(matcher)) {
322            return true;
323        }
324
325        if (optionalRequest != null) {
326            if (optionalRequest.getServerName().equals(matcher.getServerName())) {
327                return true;
328            }
329        }
330        return false;
331
332    }
333
334    /**
335     * Creates a valid request parameter map from the given map,
336     * most notably changing the values form <code>String</code>
337     * to <code>String[]</code> if required.<p>
338     *
339     * If the given parameter map is <code>null</code>, then <code>null</code> is returned.<p>
340     *
341     * @param params the map of parameters to create a parameter map from
342     * @return the created parameter map, all values will be instances of <code>String[]</code>
343     */
344    public static Map<String, String[]> createParameterMap(Map<String, ?> params) {
345
346        if (params == null) {
347            return null;
348        }
349        Map<String, String[]> result = new HashMap<String, String[]>();
350        Iterator<?> i = params.entrySet().iterator();
351        while (i.hasNext()) {
352            @SuppressWarnings("unchecked")
353            Map.Entry<String, ?> entry = (Entry<String, ?>)i.next();
354            String key = entry.getKey();
355            Object values = entry.getValue();
356            if (values instanceof String[]) {
357                result.put(key, (String[])values);
358            } else {
359                if (values != null) {
360                    result.put(key, new String[] {values.toString()});
361                }
362            }
363        }
364        return result;
365    }
366
367    /**
368     * Parses the parameters of the given request query part and creates a parameter map out of them.<p>
369     *
370     * Please note: This does not parse a full request URI/URL, only the query part that
371     * starts after the "?". For example, in the URI <code>/system/index.html?a=b&amp;c=d</code>,
372     * the query part is <code>a=b&amp;c=d</code>.<p>
373     *
374     * If the given String is empty, an empty map is returned.<p>
375     *
376     * @param query the query to parse
377     * @return the parameter map created from the query
378     */
379    public static Map<String, String[]> createParameterMap(String query) {
380
381        return createParameterMap(query, false, null);
382    }
383
384    /**
385     * Parses the parameters of the given request query part, optionally decodes them, and creates a parameter map out of them.<p>
386     *
387     * Please note: This does not parse a full request URI/URL, only the query part that
388     * starts after the "?". For example, in the URI <code>/system/index.html?a=b&amp;c=d</code>,
389     * the query part is <code>a=b&amp;c=d</code>.<p>
390     *
391     * If the given String is empty, an empty map is returned.<p>
392     *
393     * @param query the query to parse
394     * @param decodeParameters a flag, indicating if the parameters should be decoded.
395     * @param encoding the character encoding used while decoding. If <code>null</code>, the default character encoding is used.
396     * @return the parameter map created from the query
397     */
398    public static Map<String, String[]> createParameterMap(String query, boolean decodeParameters, String encoding) {
399
400        if (CmsStringUtil.isEmpty(query)) {
401            // empty query
402            return new HashMap<String, String[]>();
403        }
404        if (query.charAt(0) == URL_DELIMITER.charAt(0)) {
405            // remove leading '?' if required
406            query = query.substring(1);
407        }
408        // cut along the different parameters
409        String[] params = CmsStringUtil.splitAsArray(query, PARAMETER_DELIMITER);
410        Map<String, String[]> parameters = new HashMap<String, String[]>(params.length);
411        for (int i = 0; i < params.length; i++) {
412            String key = null;
413            String value = null;
414            // get key and value, separated by a '='
415            int pos = params[i].indexOf(PARAMETER_ASSIGNMENT);
416            if (pos > 0) {
417                key = params[i].substring(0, pos);
418                value = params[i].substring(pos + 1);
419            } else if (pos < 0) {
420                key = params[i];
421                value = "";
422            }
423            // adjust the key if it starts with "amp;"
424            // this happens when "&amp;" is used instead of a simple "&"
425            if ((key != null) && (key.startsWith(AMP))) {
426                key = key.substring(AMP.length());
427            }
428            // now make sure the values are of type String[]
429            if (key != null) {
430                if (decodeParameters) {
431                    key = CmsEncoder.decode(key, encoding);
432                    value = CmsEncoder.decode(value, encoding);
433                }
434                String[] values = parameters.get(key);
435                if (values == null) {
436                    // this is the first value, create new array
437                    values = new String[] {value};
438                } else {
439                    // append to the existing value array
440                    String[] copy = new String[values.length + 1];
441                    System.arraycopy(values, 0, copy, 0, values.length);
442                    copy[copy.length - 1] = value;
443                    values = copy;
444                }
445                parameters.put(key, values);
446            }
447        }
448        return parameters;
449    }
450
451    /**
452     * Sets HTTP response headers to disable embedding in iframes on different domains.
453     *
454     * @param response the response on which to set the HTTP headers
455     */
456    public static void disableCrossSiteFrameEmbedding(HttpServletResponse response) {
457
458        response.addHeader("Content-Security-Policy", "frame-ancestors 'self';");
459        response.addHeader("X-Frame-Options", "SAMEORIGIN");
460    }
461
462    /**
463     * Returns all parameters of the given request
464     * as a request parameter URL String, that is in the form <code>key1=value1&amp;key2=value2</code> etc.
465     *
466     * The result will be encoded using the <code>{@link CmsEncoder#encode(String)}</code> function.<p>
467     *
468     * @param req the request to read the parameters from
469     *
470     * @return all initialized parameters of the given request as request parameter URL String
471     */
472    public static String encodeParams(HttpServletRequest req) {
473
474        StringBuffer result = new StringBuffer(512);
475        Map<String, String[]> params = CmsCollectionsGenericWrapper.map(req.getParameterMap());
476        Iterator<Map.Entry<String, String[]>> i = params.entrySet().iterator();
477        while (i.hasNext()) {
478            Map.Entry<String, String[]> entry = i.next();
479            String param = entry.getKey();
480            String[] values = entry.getValue();
481            for (int j = 0; j < values.length; j++) {
482                result.append(param);
483                result.append("=");
484                result.append(CmsEncoder.encode(values[j]));
485                if ((j + 1) < values.length) {
486                    result.append("&");
487                }
488            }
489            if (i.hasNext()) {
490                result.append("&");
491            }
492        }
493        return CmsEncoder.encode(result.toString());
494    }
495
496    /**
497     * Encodes the given URI, with all parameters from the given request appended.<p>
498     *
499     * The result will be encoded using the <code>{@link CmsEncoder#encode(String)}</code> function.<p>
500     *
501     * @param req the request where to read the parameters from
502     * @param uri the URI to encode
503     * @return the encoded URI, with all parameters from the given request appended
504     */
505    public static String encodeParamsWithUri(String uri, HttpServletRequest req) {
506
507        String result;
508        String params = encodeParams(req);
509        if (CmsStringUtil.isNotEmpty(params)) {
510            result = CmsEncoder.encode(uri + "?") + params;
511        } else {
512            result = CmsEncoder.encode(uri);
513        }
514        return result;
515    }
516
517    /**
518     * Forwards the response to the given target, which may contain parameters appended like for example <code>?a=b&amp;c=d</code>.<p>
519     *
520     * Please note: If possible, use <code>{@link #forwardRequest(String, Map, HttpServletRequest, HttpServletResponse)}</code>
521     * where the parameters are passed as a map, since the parsing of the parameters may introduce issues with encoding
522     * and is in general much less effective.<p>
523     *
524     * The parsing of parameters will likely fail for "large values" (e.g. full blown web forms with &lt;textarea&gt;
525     * elements etc. Use this method only if you know that the target will just contain up to 3 parameters which
526     * are relatively short and have no encoding or line break issues.<p>
527     *
528     * @param target the target to forward to (may contain parameters like <code>?a=b&amp;c=d</code>)
529     * @param req the request to forward
530     * @param res the response to forward
531     *
532     * @throws IOException in case the forwarding fails
533     * @throws ServletException in case the forwarding fails
534     */
535    public static void forwardRequest(String target, HttpServletRequest req, HttpServletResponse res)
536    throws IOException, ServletException {
537
538        // clear the current parameters
539        CmsUriSplitter uri = new CmsUriSplitter(target);
540        Map<String, String[]> params = createParameterMap(uri.getQuery());
541        forwardRequest(uri.getPrefix(), params, req, res);
542    }
543
544    /**
545     * Forwards the response to the given target, with the provided parameter map.<p>
546     *
547     * The target URI must NOT have parameters appended like for example <code>?a=b&amp;c=d</code>.
548     * The values in the provided map must be of type <code>String[]</code>. If required, use
549     * <code>{@link #createParameterMap(Map)}</code> before calling this method to make sure
550     * all values are actually of the required array type.<p>
551     *
552     * @param target the target to forward to (may NOT contain parameters like <code>?a=b&amp;c=d</code>)
553     * @param params the parameter map (the values must be of type <code>String[]</code>
554     * @param req the request to forward
555     * @param res the response to forward
556     *
557     * @throws IOException in case the forwarding fails
558     * @throws ServletException in case the forwarding fails
559     */
560    public static void forwardRequest(
561        String target,
562        Map<String, String[]> params,
563        HttpServletRequest req,
564        HttpServletResponse res)
565    throws IOException, ServletException {
566
567        // get the flex request (which the servlet container may have wrapped) so the parameter map can be accessed
568        CmsFlexRequest f_req = getFlexRequest(req);
569        // set the parameters
570        f_req.setParameterMap(params);
571        // check for links "into" OpenCms, these may need the webapp name to be removed
572        String vfsPrefix = OpenCms.getStaticExportManager().getVfsPrefix();
573        if (target.startsWith(vfsPrefix)) {
574            // remove VFS prefix (will also work for empty vfs prefix in ROOT webapp case with proxy rules)
575            target = target.substring(vfsPrefix.length());
576            // append the servlet name
577            target = OpenCms.getSystemInfo().getServletPath() + target;
578        }
579        // forward the request
580        f_req.getRequestDispatcher(target).forward(f_req, res);
581    }
582
583    /**
584     * Exactly like getAttributeMap, but incorrectly spelled.<p>
585     *
586     * Kept for backward compatibility.
587     *
588     * @param req the request
589     *
590     * @return the attribute map
591     */
592    public static Map<String, Object> getAtrributeMap(ServletRequest req) {
593
594        return getAttributeMap(req);
595    }
596
597    /**
598     * Returns a map with all request attributes.<p>
599     *
600     * @param req the request
601     *
602     * @return the attribute map
603     */
604    public static Map<String, Object> getAttributeMap(ServletRequest req) {
605
606        CmsFlexRequest flexReq = getFlexRequest(req);
607        if (flexReq != null) {
608            return flexReq.getAttributeMap();
609        }
610        Map<String, Object> attrs = new HashMap<String, Object>();
611        Enumeration<String> atrrEnum = CmsCollectionsGenericWrapper.enumeration(req.getAttributeNames());
612        while (atrrEnum.hasMoreElements()) {
613            String key = atrrEnum.nextElement();
614            Object value = req.getAttribute(key);
615            attrs.put(key, value);
616        }
617        return attrs;
618    }
619
620    /**
621     * Returns the value of the cookie with the given name.<p/>
622     *
623     * @param jsp the CmsJspActionElement to use
624     * @param name the name of the cookie
625     *
626     * @return the value of the cookie with the given name or null, if no cookie exists with the name
627     */
628    public static String getCookieValue(CmsJspActionElement jsp, String name) {
629
630        Cookie[] cookies = jsp.getRequest().getCookies();
631        return getCookieValue(cookies, name);
632    }
633
634    /**
635     * Gets the value of a specific cookie from an array of cookies.<p>
636     *
637     * @param cookies the cookie array
638     * @param name the name of the cookie we want
639     *
640     * @return the cookie value, or null if cookie with the given name wasn't found
641     */
642    public static String getCookieValue(Cookie[] cookies, String name) {
643
644        for (int i = 0; (cookies != null) && (i < cookies.length); i++) {
645            if (name.equalsIgnoreCase(cookies[i].getName())) {
646                return cookies[i].getValue();
647            }
648        }
649        return null;
650    }
651
652    /**
653     * Tries to extract a wrapped CmsFlexRequest from a potentially nested servlet request object.
654     *
655     * @param request the request to get the flex request from
656     * @return the flex request, or null if none was found
657     */
658    public static CmsFlexRequest getFlexRequest(ServletRequest request) {
659
660        while ((request != null) && !(request instanceof CmsFlexRequest)) {
661            if (request instanceof HttpServletRequestWrapper) {
662                request = ((HttpServletRequestWrapper)request).getRequest();
663            } else {
664                // container response implementation that's not a wrapper
665                return null;
666            }
667        }
668
669        // works for both cases of the while loop exit condition
670        return (CmsFlexRequest)request;
671    }
672
673    /**
674     * Tries to extract a wrapped CmsFlexResponse from a potentially nested servlet response object.
675     *
676     * @param response the response to get the flex response from
677     * @return the flex response, or null if none was found
678     */
679    public static CmsFlexResponse getFlexResponse(ServletResponse response) {
680
681        while ((response != null) && !(response instanceof CmsFlexResponse)) {
682            if (response instanceof HttpServletResponseWrapper) {
683                response = ((HttpServletResponseWrapper)response).getResponse();
684            } else {
685                // container response implementation that's not a wrapper
686                return null;
687            }
688        }
689
690        // works for both cases of the while loop exit condition
691        return (CmsFlexResponse)response;
692    }
693
694    /**
695     * Converts the given parameter map into an JSON object.<p>
696     *
697     * @param params the parameters map to convert
698     *
699     * @return the JSON representation of the given parameter map
700     */
701    public static JSONObject getJsonParameterMap(Map<String, String[]> params) {
702
703        JSONObject result = new JSONObject();
704        for (Map.Entry<String, String[]> entry : params.entrySet()) {
705            String paramKey = entry.getKey();
706            JSONArray paramValue = new JSONArray();
707            for (int i = 0, l = entry.getValue().length; i < l; i++) {
708                paramValue.put(entry.getValue()[i]);
709            }
710            try {
711                result.putOpt(paramKey, paramValue);
712            } catch (JSONException e) {
713                // should never happen
714                LOG.warn(e.getLocalizedMessage(), e);
715            }
716        }
717        return result;
718    }
719
720    /**
721     * Reads value from the request parameters,
722     * will return <code>null</code> if the value is not available or only white space.<p>
723     *
724     * The value of the request will also be decoded using <code>{@link CmsEncoder#decode(String)}</code>
725     * and also trimmed using <code>{@link String#trim()}</code>.<p>
726     *
727     * @param request the request to read the parameter from
728     * @param paramName the parameter name to read
729     *
730     * @return the request parameter value for the given parameter
731     */
732    public static String getNotEmptyDecodedParameter(HttpServletRequest request, String paramName) {
733
734        String result = getNotEmptyParameter(request, paramName);
735        if (result != null) {
736            result = CmsEncoder.decode(result.trim());
737        }
738        return result;
739    }
740
741    /**
742     * Reads value from the request parameters,
743     * will return <code>null</code> if the value is not available or only white space.<p>
744     *
745     * @param request the request to read the parameter from
746     * @param paramName the parameter name to read
747     *
748     * @return the request parameter value for the given parameter
749     */
750    public static String getNotEmptyParameter(HttpServletRequest request, String paramName) {
751
752        String result = request.getParameter(paramName);
753        if (CmsStringUtil.isEmptyOrWhitespaceOnly(result)) {
754            result = null;
755        }
756        return result;
757    }
758
759    /**
760     * Returns a map with all request parameters.<p>
761     *
762     * If the given request wraps a flex request, the parameters of that flex request are returned:
763     * a request wrapper added by the servlet container does not necessarily report the parameters
764     * of the request it wraps.<p>
765     *
766     * @param req the request
767     *
768     * @return the parameter map
769     */
770    public static Map<String, String[]> getParameterMap(ServletRequest req) {
771
772        CmsFlexRequest flexReq = getFlexRequest(req);
773        if (flexReq != null) {
774            return flexReq.getParameterMap();
775        }
776        return req.getParameterMap();
777    }
778
779    /**
780     * Converts the given JSON object into a valid parameter map.<p>
781     *
782     * @param params the JSON object to convert
783     *
784     * @return the parameter map from the given JSON object
785     */
786    public static Map<String, String[]> getParameterMapFromJSON(JSONObject params) {
787
788        Map<String, String[]> result = new HashMap<String, String[]>();
789        Iterator<String> itKeys = params.keys();
790        while (itKeys.hasNext()) {
791            String key = itKeys.next();
792            JSONArray paramValue = params.optJSONArray(key);
793            result.put(key, new String[paramValue.length()]);
794            for (int i = 0, l = paramValue.length(); i < l; i++) {
795                result.get(key)[i] = paramValue.optString(i);
796            }
797        }
798        return result;
799    }
800
801    /**
802     * Parses parameter map from the given URI.<p>
803     *
804     * @param uri the URI
805     * @return the parameter map
806     */
807    public static Multimap<String, String> getParameters(URI uri) {
808
809        return getParametersFromRawQuery(uri.getRawQuery());
810    }
811
812    /**
813     * Parses the parameter map from a raw query string.<p>
814     *
815     * @param rawQuery the raw query string
816     *
817     * @return the parameter map
818     */
819    public static Multimap<String, String> getParametersFromRawQuery(String rawQuery) {
820
821        Multimap<String, String> result = ArrayListMultimap.create();
822        if (rawQuery != null) {
823            for (String keyValuePair : CmsStringUtil.splitAsList(rawQuery, "&")) {
824                try {
825                    String decodedKeyValue = URLDecoder.decode(keyValuePair, "UTF-8");
826                    int eqPos = decodedKeyValue.indexOf("=");
827                    if (eqPos < 0) {
828                        decodedKeyValue = decodedKeyValue + "=";
829                        eqPos = decodedKeyValue.indexOf("=");
830                    }
831                    String key = decodedKeyValue.substring(0, eqPos);
832                    String value = decodedKeyValue.substring(eqPos + 1);
833                    result.put(key, value);
834                } catch (UnsupportedEncodingException e) {
835                    // UTF8 should be present
836                }
837            }
838        }
839        return result;
840    }
841
842    /**
843     * Returns the link without parameters from a String that is formatted for a GET request.<p>
844     *
845     * @param url the URL to remove the parameters from
846     * @return the URL without any parameters
847     */
848    public static String getRequestLink(String url) {
849
850        if (CmsStringUtil.isEmpty(url)) {
851            return null;
852        }
853        int pos = url.indexOf(URL_DELIMITER);
854        if (pos >= 0) {
855            return url.substring(0, pos);
856        }
857        return url;
858
859    }
860
861    /**
862     * Reads an object from the session of the given HTTP request.<p>
863     *
864     * A session will be initialized if the request does not currently have a session.
865     * As a result, the request will always have a session after this method has been called.<p>
866     *
867     * Will return <code>null</code> if no corresponding object is found in the session.<p>
868     *
869     * @param request the request to get the session from
870     * @param key the key of the object to read from the session
871     * @return the object received form the session, or <code>null</code>
872     */
873    public static Object getSessionValue(HttpServletRequest request, String key) {
874
875        HttpSession session = request.getSession(true);
876        return session.getAttribute(key);
877    }
878
879    /**
880     * Parses a request of the form <code>multipart/form-data</code>.
881     *
882     * The result list will contain items of type <code>{@link DiskFileItem}</code>.
883     * If the request is not of type <code>multipart/form-data</code>, then <code>null</code> is returned.<p>
884     *
885     * @param request the HTTP servlet request to parse
886     *
887     * @return the list of <code>{@link DiskFileItem}</code> extracted from the multipart request,
888     *      or <code>null</code> if the request was not of type <code>multipart/form-data</code>
889     */
890    public static List<DiskFileItem> readMultipartFileItems(HttpServletRequest request) {
891
892        return readMultipartFileItems(request, OpenCms.getSystemInfo().getPackagesRfsPath());
893    }
894
895    /**
896     * Parses a request of the form <code>multipart/form-data</code>.
897     *
898     * The result list will contain items of type <code>{@link DiskFileItem}</code>.
899     * If the request is not of type <code>multipart/form-data</code>, then <code>null</code> is returned.<p>
900     *
901     * @param request the HTTP servlet request to parse
902     * @param tempFolderPath the real file system path to the temp file folder
903     *
904     * @return the list of <code>{@link DiskFileItem}</code> extracted from the multipart request,
905     *      or <code>null</code> if the request was not of type <code>multipart/form-data</code>
906     */
907    public static List<DiskFileItem> readMultipartFileItems(HttpServletRequest request, String tempFolderPath) {
908
909        if (!JakartaServletFileUpload.isMultipartContent(request)) {
910            return null;
911        }
912        DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
913        builder.setPath(tempFolderPath);
914        DiskFileItemFactory factory = builder.get();
915        JakartaServletDiskFileUpload fu = new JakartaServletDiskFileUpload(factory);
916        // set encoding to correctly handle special chars (e.g. in filenames)
917        fu.setHeaderCharset(Charset.forName(request.getCharacterEncoding()));
918        List<DiskFileItem> result = new ArrayList<DiskFileItem>();
919        try {
920            List<DiskFileItem> items = CmsCollectionsGenericWrapper.list(fu.parseRequest(request));
921            if (items != null) {
922                result = items;
923            }
924        } catch (FileUploadException e) {
925            LOG.error(Messages.get().getBundle().key(Messages.LOG_PARSE_MULIPART_REQ_FAILED_0), e);
926        }
927        return result;
928    }
929
930    /**
931     * Creates a "standard" request parameter map from the values of a
932     * <code>multipart/form-data</code> request.<p>
933     *
934     * @param encoding the encoding to use when creating the values
935     * @param multiPartFileItems the list of parsed multi part file items
936     *
937     * @return a map containing all non-file request parameters
938     *
939     * @see #readMultipartFileItems(HttpServletRequest)
940     */
941    public static Map<String, String[]> readParameterMapFromMultiPart(
942        String encoding,
943        List<DiskFileItem> multiPartFileItems) {
944
945        Map<String, String[]> parameterMap = new HashMap<String, String[]>();
946        Iterator<DiskFileItem> i = multiPartFileItems.iterator();
947        while (i.hasNext()) {
948            DiskFileItem item = i.next();
949            String name = item.getFieldName();
950            String value = null;
951            if ((name != null) && (item.getName() == null)) {
952                try {
953                    // only put to map if current item is no file and not null
954                    try {
955                        value = item.getString(Charset.forName(encoding));
956                    } catch (UnsupportedEncodingException e) {
957                        LOG.error(Messages.get().getBundle().key(Messages.LOG_ENC_MULTIPART_REQ_ERROR_0), e);
958                        value = item.getString();
959                    }
960                } catch (IOException e) {
961                    LOG.error(e.getLocalizedMessage(), e);
962                    continue;
963                }
964                if (parameterMap.containsKey(name)) {
965
966                    // append value to parameter values array
967                    String[] oldValues = parameterMap.get(name);
968                    String[] newValues = new String[oldValues.length + 1];
969                    System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
970                    newValues[oldValues.length] = value;
971                    parameterMap.put(name, newValues);
972
973                } else {
974                    parameterMap.put(name, new String[] {value});
975                }
976            }
977        }
978        return parameterMap;
979    }
980
981    /**
982     * Redirects the response to the target link using a "301 - Moved Permanently" header.<p>
983     *
984     * This implementation will work only on JSP pages in OpenCms that use the default JSP loader implementation.<p>
985     *
986     * @param jsp the OpenCms JSP context
987     * @param target the target link
988     */
989    public static void redirectPermanently(CmsJspActionElement jsp, String target) {
990
991        target = OpenCms.getLinkManager().substituteLink(jsp.getCmsObject(), target);
992        jsp.getResponse().setHeader(HEADER_CONNECTION, "close");
993        try {
994            HttpServletResponse response = jsp.getResponse();
995            CmsFlexResponse flexRes = CmsRequestUtil.getFlexResponse(response);
996            if (flexRes != null) {
997                flexRes.sendRedirect(target, true);
998            } else {
999                response.setHeader("Location", target);
1000                response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
1001            }
1002        } catch (IOException e) {
1003            LOG.error(Messages.get().getBundle().key(Messages.ERR_IOERROR_0), e);
1004        }
1005    }
1006
1007    /**
1008     * Redirects the response to the target link.<p>
1009     *
1010     * Use this method instead of {@link jakarta.servlet.http.HttpServletResponse#sendRedirect(java.lang.String)}
1011     * to avoid relative links with secure sites (and issues with apache).<p>
1012     *
1013     * @param jsp the OpenCms JSP context
1014     * @param target the target link
1015     *
1016     * @throws IOException if something goes wrong during redirection
1017     */
1018    public static void redirectRequestSecure(CmsJspActionElement jsp, String target) throws IOException {
1019
1020        jsp.getResponse().sendRedirect(OpenCms.getLinkManager().substituteLink(jsp.getCmsObject(), target, null, true));
1021    }
1022
1023    /**
1024     * Removes an object from the session of the given http request.<p>
1025     *
1026     * A session will be initialized if the request does not currently have a session.
1027     * As a result, the request will always have a session after this method has been called.<p>
1028     *
1029     * @param request the request to get the session from
1030     * @param key the key of the object to be removed from the session
1031     */
1032    public static void removeSessionValue(HttpServletRequest request, String key) {
1033
1034        HttpSession session = request.getSession(true);
1035        session.removeAttribute(key);
1036    }
1037
1038    /**
1039     * Sets the value of a specific cookie.<p>
1040     * If no cookie exists with the value, a new cookie will be created.
1041     *
1042     * @param jsp the CmsJspActionElement to use
1043     * @param name the name of the cookie
1044     * @param value the value of the cookie
1045     */
1046    public static void setCookieValue(CmsJspActionElement jsp, String name, String value) {
1047
1048        Cookie[] cookies = jsp.getRequest().getCookies();
1049        for (int i = 0; (cookies != null) && (i < cookies.length); i++) {
1050            if (name.equalsIgnoreCase(cookies[i].getName())) {
1051                cookies[i].setValue(value);
1052                return;
1053            }
1054        }
1055        Cookie cookie = new Cookie(name, value);
1056        jsp.getResponse().addCookie(cookie);
1057    }
1058
1059    /**
1060     * Sets headers to the given response to prevent client side caching.<p>
1061     *
1062     * The following headers are set:<p>
1063     * <code>
1064     * Cache-Control: max-age=0<br>
1065     * Cache-Control: must-revalidate<br>
1066     * Pragma: no-cache
1067     * </code>
1068     *
1069     * @param res the request where to set the no-cache headers
1070     */
1071    public static void setNoCacheHeaders(HttpServletResponse res) {
1072
1073        res.setHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_MAX_AGE + "0");
1074        res.addHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_MUST_REVALIDATE);
1075        res.addHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_NO_CACHE);
1076        res.addHeader(CmsRequestUtil.HEADER_CACHE_CONTROL, CmsRequestUtil.HEADER_VALUE_NO_STORE);
1077        res.setHeader(CmsRequestUtil.HEADER_PRAGMA, CmsRequestUtil.HEADER_VALUE_NO_CACHE);
1078    }
1079
1080    /**
1081     * Adds an object to the session of the given HTTP request.<p>
1082     *
1083     * A session will be initialized if the request does not currently have a session.
1084     * As a result, the request will always have a session after this method has been called.<p>
1085     *
1086     * @param request the request to get the session from
1087     * @param key the key of the object to be stored in the session
1088     * @param value the object to be stored in the session
1089     */
1090    public static void setSessionValue(HttpServletRequest request, String key, Object value) {
1091
1092        HttpSession session = request.getSession(true);
1093        session.setAttribute(key, value);
1094    }
1095}