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, 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.ade.upload;
029
030import org.opencms.db.CmsDbSqlException;
031import org.opencms.db.CmsImportFolder;
032import org.opencms.file.CmsFile;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsProperty;
035import org.opencms.file.CmsPropertyDefinition;
036import org.opencms.file.CmsResource;
037import org.opencms.file.CmsResourceFilter;
038import org.opencms.file.collectors.A_CmsResourceCollector;
039import org.opencms.file.collectors.I_CmsCollectorPostCreateHandler;
040import org.opencms.file.types.CmsResourceTypeFolder;
041import org.opencms.file.types.CmsResourceTypePlain;
042import org.opencms.gwt.shared.CmsUploadRestrictionInfo;
043import org.opencms.gwt.shared.I_CmsUploadConstants;
044import org.opencms.i18n.CmsMessages;
045import org.opencms.json.JSONArray;
046import org.opencms.json.JSONException;
047import org.opencms.json.JSONObject;
048import org.opencms.jsp.CmsJspBean;
049import org.opencms.loader.CmsLoaderException;
050import org.opencms.lock.CmsLockException;
051import org.opencms.main.CmsException;
052import org.opencms.main.CmsLog;
053import org.opencms.main.OpenCms;
054import org.opencms.security.CmsSecurityException;
055import org.opencms.util.CmsCollectionsGenericWrapper;
056import org.opencms.util.CmsPair;
057import org.opencms.util.CmsRequestUtil;
058import org.opencms.util.CmsStringUtil;
059import org.opencms.util.CmsUUID;
060
061import java.io.ByteArrayInputStream;
062import java.io.File;
063import java.io.IOException;
064import java.io.InputStream;
065import java.io.UnsupportedEncodingException;
066import java.net.URLDecoder;
067import java.nio.charset.Charset;
068import java.util.ArrayList;
069import java.util.Collections;
070import java.util.HashMap;
071import java.util.List;
072import java.util.Map;
073
074import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
075import org.apache.commons.compress.archivers.zip.ZipFile;
076import org.apache.commons.fileupload2.core.DiskFileItem;
077import org.apache.commons.fileupload2.core.DiskFileItemFactory;
078import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
079import org.apache.commons.fileupload2.core.FileUploadSizeException;
080import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletDiskFileUpload;
081import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletFileUpload;
082import org.apache.commons.lang3.StringUtils;
083import org.apache.commons.logging.Log;
084
085import com.google.common.collect.HashMultimap;
086
087import jakarta.servlet.http.HttpServletRequest;
088import jakarta.servlet.http.HttpServletResponse;
089import jakarta.servlet.jsp.PageContext;
090
091/**
092 * Bean to be used in JSP scriptlet code that provides
093 * access to the upload functionality.<p>
094 *
095 * @since 8.0.0
096 */
097public class CmsUploadBean extends CmsJspBean {
098
099    /**
100     * Supplies an input stream (used by the virus scanning mechanism).
101     */
102    private interface InputStreamProvider {
103
104        /**
105         * Gets the input stream.
106         *
107         * <p>Don't call this more than once.
108         *
109         * @return the input stream
110         * @throws IOException if something IO related fails
111         */
112        InputStream getStream() throws IOException;
113    }
114
115    /** The default upload timeout. */
116    public static final int DEFAULT_UPLOAD_TIMEOUT = 20000;
117
118    /** Key name for the session attribute that stores the id of the current listener. */
119    public static final String SESSION_ATTRIBUTE_LISTENER_ID = "__CmsUploadBean.LISTENER";
120
121    /** The log object for this class. */
122    private static final Log LOG = CmsLog.getLog(CmsUploadBean.class);
123
124    /** A static map of all listeners. */
125    private static Map<CmsUUID, CmsUploadListener> m_listeners = new HashMap<CmsUUID, CmsUploadListener>();
126
127    /** The gwt message bundle. */
128    private CmsMessages m_bundle = org.opencms.ade.upload.Messages.get().getBundle();
129
130    /** Signals that the start method is called. */
131    private boolean m_called;
132
133    /** A list of the file items to upload. */
134    private List<DiskFileItem> m_multiPartFileItems;
135
136    /** The map of parameters read from the current request. */
137    private Map<String, String[]> m_parameterMap;
138
139    /** The names by id of the resources that have been created successfully. */
140    private HashMap<CmsUUID, String> m_resourcesCreated = new HashMap<CmsUUID, String>();
141
142    /** A CMS context for the root site. */
143    private CmsObject m_rootCms;
144
145    /** The virus scanner instance to use for uploads. */
146    private I_CmsVirusScanner m_scanner;
147
148    /** The server side upload delay. */
149    private int m_uploadDelay;
150
151    /** The upload hook URI. */
152    private String m_uploadHook;
153
154    private CmsUploadRestrictionInfo m_uploadRestrictionInfo;
155
156    /** The viruses found while processing the uploads, with the file names as keys. */
157    private HashMultimap<String, String> m_viruses = HashMultimap.create();
158
159    /**
160     * Constructor, with parameters.<p>
161     *
162     * @param context the JSP page context object
163     * @param req the JSP request
164     * @param res the JSP response
165     *
166     * @throws CmsException if something goes wrong
167     */
168    public CmsUploadBean(PageContext context, HttpServletRequest req, HttpServletResponse res)
169    throws CmsException {
170
171        super();
172        init(context, req, res);
173
174        m_rootCms = OpenCms.initCmsObject(getCmsObject());
175        m_rootCms.getRequestContext().setSiteRoot("");
176        m_uploadRestrictionInfo = OpenCms.getWorkplaceManager().getUploadRestriction().getUploadRestrictionInfo(
177            m_rootCms);
178        if (OpenCms.getWorkplaceManager().isVirusScannerEnabled()) {
179            m_scanner = OpenCms.getWorkplaceManager().getVirusScanner();
180        }
181    }
182
183    /**
184     * Returns the listener for given CmsUUID.<p>
185     *
186     * @param listenerId the uuid
187     *
188     * @return the according listener
189     */
190    public static CmsUploadListener getCurrentListener(CmsUUID listenerId) {
191
192        return m_listeners.get(listenerId);
193    }
194
195    /**
196     * Returns the VFS path for the given filename and folder.<p>
197     *
198     * @param cms the cms object
199     * @param fileName the filename to combine with the folder
200     * @param folder the folder to combine with the filename
201     * @param keepFileNames skip file name translation if true
202     *
203     * @return the VFS path for the given filename and folder
204     */
205    public static String getNewResourceName(CmsObject cms, String fileName, String folder, boolean keepFileNames) {
206
207        String newResname = CmsResource.getName(fileName.replace('\\', '/'));
208        if (!keepFileNames) {
209            newResname = cms.getRequestContext().getFileTranslator().translateResource(newResname);
210        }
211        newResname = folder + newResname;
212        return newResname;
213    }
214
215    /**
216     * Sets the uploadDelay.<p>
217     *
218     * @param uploadDelay the uploadDelay to set
219     */
220    public void setUploadDelay(int uploadDelay) {
221
222        m_uploadDelay = uploadDelay;
223    }
224
225    /**
226     * Starts the upload.<p>
227     *
228     * @return the response String (JSON)
229     */
230    public String start() {
231
232        // ensure that this method can only be called once
233        if (m_called) {
234            throw new UnsupportedOperationException();
235        }
236        m_called = true;
237
238        // create a upload listener
239        CmsUploadListener listener = createListener();
240        try {
241            // try to parse the request
242            parseRequest(listener);
243            // try to create the resources on the VFS
244            createResources(listener);
245            // trigger update offline indexes, important for gallery search
246            OpenCms.getSearchManager().updateOfflineIndexes();
247        } catch (CmsException e) {
248            // an error occurred while creating the resources on the VFS, create a special error message
249            LOG.error(e.getMessage(), e);
250            return generateResponse(Boolean.FALSE, getCreationErrorMessage(), formatStackTrace(e));
251        } catch (CmsUploadException e) {
252            // an expected error occurred while parsing the request, the error message is already set in the exception
253            LOG.debug(e.getMessage(), e);
254            return generateResponse(Boolean.FALSE, e.getMessage(), formatStackTrace(e));
255        } catch (Throwable e) {
256            // an unexpected error occurred while parsing the request, create a non-specific error message
257            LOG.error(e.getMessage(), e);
258            String message = m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_UNEXPECTED_0);
259            return generateResponse(Boolean.FALSE, message, formatStackTrace(e));
260        } finally {
261            removeListener(listener.getId());
262        }
263        // the upload was successful inform the user about success
264        return generateResponse(Boolean.TRUE, m_bundle.key(org.opencms.ade.upload.Messages.LOG_UPLOAD_SUCCESS_0), "");
265    }
266
267    /**
268     * Creates a upload listener and puts it into the static map.<p>
269     *
270     * @return the listener
271     */
272    private CmsUploadListener createListener() {
273
274        CmsUploadListener listener = new CmsUploadListener(getRequest().getContentLength());
275        listener.setDelay(m_uploadDelay);
276        m_listeners.put(listener.getId(), listener);
277        getRequest().getSession().setAttribute(SESSION_ATTRIBUTE_LISTENER_ID, listener.getId());
278        return listener;
279    }
280
281    /**
282     * Creates the resources.<p>
283     * @param listener the listener
284     *
285     * @throws CmsException if something goes wrong
286     * @throws UnsupportedEncodingException in case the encoding is not supported
287     */
288    private void createResources(CmsUploadListener listener) throws CmsException, UnsupportedEncodingException {
289
290        CmsObject cms = getCmsObject();
291        String[] isRootPathVals = m_parameterMap.get(I_CmsUploadConstants.UPLOAD_IS_ROOT_PATH_FIELD_NAME);
292        if ((isRootPathVals != null) && (isRootPathVals.length > 0) && Boolean.parseBoolean(isRootPathVals[0])) {
293            cms = m_rootCms;
294        }
295        // get the target folder
296        String targetFolder = getTargetFolder(cms);
297        m_uploadHook = OpenCms.getWorkplaceManager().getUploadHook(cms, targetFolder);
298        if (m_scanner != null) {
299            m_scanner.test();
300        }
301
302        List<String> filesToUnzip = getFilesToUnzip();
303
304        // iterate over the list of files to upload and create each single resource
305        for (DiskFileItem fileItem : m_multiPartFileItems) {
306            if ((fileItem != null) && (!fileItem.isFormField())) {
307                // read the content of the file
308                byte[] content = null;
309                try {
310                    content = fileItem.get();
311                    fileItem.delete();
312                } catch (IOException e) {
313                    LOG.error(e.getLocalizedMessage(), e);
314                    continue;
315                }
316
317                // determine the new resource name
318                String fileName = m_parameterMap.get(
319                    fileItem.getFieldName() + I_CmsUploadConstants.UPLOAD_FILENAME_ENCODED_SUFFIX)[0];
320                String originalFileName = m_parameterMap.get(
321                    fileItem.getFieldName() + I_CmsUploadConstants.UPLOAD_ORIGINAL_FILENAME_ENCODED_SUFFIX)[0];
322                String context = "(file: " + originalFileName + ")";
323                List<String> viruses = scan(context, content);
324
325                fileName = URLDecoder.decode(fileName, "UTF-8");
326                originalFileName = URLDecoder.decode(originalFileName, "UTF-8");
327                if (viruses.size() > 0) {
328                    m_viruses.putAll(originalFileName, viruses);
329                } else {
330                    if (filesToUnzip.contains(CmsResource.getName(fileName.replace('\\', '/')))) {
331
332                        // scan all contents of ZIP before creating anything in the VFS
333                        boolean foundVirus = false;
334                        try {
335                            ZipFile zip = ZipFile.builder().setByteArray(content).get();
336                            for (ZipArchiveEntry entry : (Iterable<ZipArchiveEntry>)() -> zip.getEntries().asIterator()) {
337                                if (entry.isDirectory() || entry.isUnixSymlink()) {
338                                    continue;
339                                }
340                                String zipEntryContext = "(file: "
341                                    + originalFileName
342                                    + ", entry: "
343                                    + entry.getName()
344                                    + ")";
345                                viruses = scan(zipEntryContext, zip, entry);
346                                if (viruses.size() > 0) {
347                                    foundVirus = true;
348                                    m_viruses.putAll(originalFileName, viruses);
349                                    break;
350                                }
351                            }
352                        } catch (IOException e) {
353                            LOG.error(e.getLocalizedMessage(), e);
354                        }
355                        if (foundVirus) {
356                            continue;
357                        }
358
359                        // import the zip
360                        CmsImportFolder importZip = new CmsImportFolder();
361                        try {
362                            importZip.importZip(content, targetFolder, cms, false);
363                        } finally {
364                            // get the created resource names
365                            for (CmsResource importedResource : importZip.getImportedResources()) {
366                                m_resourcesCreated.put(importedResource.getStructureId(), importedResource.getName());
367                            }
368                        }
369                    } else {
370                        // create the resource
371                        CmsResource importedResource = createSingleResource(cms, fileName, targetFolder, content);
372                        if (importedResource != null) {
373                            // add the name of the created resource to the list of successful created resources
374                            m_resourcesCreated.put(importedResource.getStructureId(), importedResource.getName());
375                        }
376                    }
377                }
378
379                if (listener.isCanceled()) {
380                    throw listener.getException();
381                }
382            }
383        }
384
385        String postCreateHandlerStr = getPostCreateHandler();
386        if (postCreateHandlerStr != null) {
387            try {
388                CmsPair<String, String> classAndConfig = I_CmsCollectorPostCreateHandler.splitClassAndConfig(
389                    postCreateHandlerStr);
390                String className = classAndConfig.getFirst();
391                String config = classAndConfig.getSecond();
392                I_CmsCollectorPostCreateHandler handler = A_CmsResourceCollector.getPostCreateHandler(className);
393                for (Map.Entry<CmsUUID, String> resourceEntry : m_resourcesCreated.entrySet()) {
394                    try {
395                        CmsUUID structureId = resourceEntry.getKey();
396                        CmsResource resource = cms.readResource(structureId, CmsResourceFilter.IGNORE_EXPIRATION);
397                        if (!resource.isFolder()) {
398                            handler.onCreate(cms, resource, false, config);
399                        }
400                    } catch (Exception e) {
401                        LOG.error(e.getLocalizedMessage(), e);
402                    }
403                }
404            } catch (Exception e) {
405                LOG.error(e.getLocalizedMessage(), e);
406            }
407
408        }
409    }
410
411    /**
412     * Creates a single resource and returns the new resource.<p>
413     *
414     * @param cms the CMS context to use
415     * @param fileName the name of the resource to create
416     * @param targetFolder the folder to store the new resource
417     * @param content the content of the resource to create
418     *
419     * @return the new resource
420     *
421     * @throws CmsException if something goes wrong
422     * @throws CmsLoaderException if something goes wrong
423     * @throws CmsDbSqlException if something goes wrong
424     */
425    @SuppressWarnings("deprecation")
426    private CmsResource createSingleResource(CmsObject cms, String fileName, String targetFolder, byte[] content)
427    throws CmsException, CmsLoaderException, CmsDbSqlException {
428
429        String folderRootPath = cms.getRequestContext().addSiteRoot(targetFolder);
430        if (!m_uploadRestrictionInfo.isUploadEnabled(folderRootPath)) {
431            LOG.error("Upload not enabled for folder " + targetFolder);
432            return null;
433        }
434
435        String newResname = getNewResourceName(cms, fileName, targetFolder, isKeepFileNames());
436        CmsResource createdResource = null;
437
438        // determine Title property value to set on new resource
439        String title = fileName;
440        if (title.lastIndexOf('.') != -1) {
441            title = title.substring(0, title.lastIndexOf('.'));
442        }
443
444        // fileName really shouldn't contain the full path, but for some reason it does sometimes when the client is
445        // running on IE7, so we eliminate anything before and including the last slash or backslash in the title
446        // before setting it as a property.
447
448        int backslashIndex = title.lastIndexOf('\\');
449        if (backslashIndex != -1) {
450            title = title.substring(backslashIndex + 1);
451        }
452
453        int slashIndex = title.lastIndexOf('/');
454        if (slashIndex != -1) {
455            title = title.substring(slashIndex + 1);
456        }
457
458        List<CmsProperty> properties = new ArrayList<CmsProperty>(1);
459        CmsProperty titleProp = new CmsProperty();
460        titleProp.setName(CmsPropertyDefinition.PROPERTY_TITLE);
461        if (OpenCms.getWorkplaceManager().isDefaultPropertiesOnStructure()) {
462            titleProp.setStructureValue(title);
463        } else {
464            titleProp.setResourceValue(title);
465        }
466        properties.add(titleProp);
467
468        int plainId = OpenCms.getResourceManager().getResourceType(
469            CmsResourceTypePlain.getStaticTypeName()).getTypeId();
470        if (!cms.existsResource(newResname, CmsResourceFilter.IGNORE_EXPIRATION)) {
471            // if the resource does not exist, create it
472
473            try {
474                // create the resource
475                int resTypeId = OpenCms.getResourceManager().getDefaultTypeForName(newResname).getTypeId();
476                createdResource = cms.createResource(newResname, resTypeId, content, properties);
477                try {
478                    cms.unlockResource(newResname);
479                } catch (CmsLockException e) {
480                    LOG.info("Couldn't unlock uploaded file", e);
481                }
482            } catch (CmsSecurityException e) {
483                // in case of not enough permissions, try to create a plain text file
484                createdResource = cms.createResource(newResname, plainId, content, properties);
485                cms.unlockResource(newResname);
486            } catch (CmsDbSqlException sqlExc) {
487                // SQL error, probably the file is too large for the database settings, delete file
488                cms.lockResource(newResname);
489                cms.deleteResource(newResname, CmsResource.DELETE_PRESERVE_SIBLINGS);
490                throw sqlExc;
491            } catch (OutOfMemoryError e) {
492                // the file is to large try to clear up
493                cms.lockResource(newResname);
494                cms.deleteResource(newResname, CmsResource.DELETE_PRESERVE_SIBLINGS);
495                throw e;
496            }
497
498        } else {
499            // if the resource already exists, replace it
500            CmsResource res = cms.readResource(newResname, CmsResourceFilter.ALL);
501            boolean wasLocked = false;
502            try {
503                if (!cms.getLock(res).isOwnedBy(cms.getRequestContext().getCurrentUser())) {
504                    cms.lockResource(res);
505                    wasLocked = true;
506                }
507                CmsFile file = cms.readFile(res);
508                byte[] contents = file.getContents();
509                try {
510                    cms.replaceResource(newResname, res.getTypeId(), content, null);
511                    createdResource = res;
512                } catch (CmsDbSqlException sqlExc) {
513                    // SQL error, probably the file is too large for the database settings, restore content
514                    file.setContents(contents);
515                    cms.writeFile(file);
516                    throw sqlExc;
517                } catch (OutOfMemoryError e) {
518                    // the file is to large try to clear up
519                    file.setContents(contents);
520                    cms.writeFile(file);
521                    throw e;
522                }
523            } finally {
524                if (wasLocked) {
525                    cms.unlockResource(res);
526                }
527            }
528        }
529        return createdResource;
530    }
531
532    /**
533     * Creates the upload target folder.
534     *
535     * @param cms the CMS context
536     * @param targetFolder the upload target folder
537     * @return the new folder
538     * @throws CmsException if something goes wrong
539     */
540    private CmsResource createTargetFolder(CmsObject cms, String targetFolder) throws CmsException {
541
542        List<String> parentFolders = new ArrayList<>();
543        String currentFolder = targetFolder;
544        while ((currentFolder != null) && !cms.existsResource(currentFolder, CmsResourceFilter.IGNORE_EXPIRATION)) {
545            parentFolders.add(currentFolder);
546            currentFolder = CmsResource.getParentFolder(currentFolder);
547        }
548        Collections.reverse(parentFolders);
549        CmsResource lastCreated = null;
550        CmsResource firstCreated = null;
551        for (String parentFolder : parentFolders) {
552
553            CmsResource createdFolder = cms.createResource(
554                parentFolder,
555                OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName()));
556            lastCreated = createdFolder;
557            if (firstCreated == null) {
558                firstCreated = createdFolder;
559            }
560        }
561        cms.unlockResource(firstCreated);
562        return lastCreated;
563
564    }
565
566    /**
567     * Returns the stacktrace of the given exception as String.<p>
568     *
569     * @param e the exception
570     *
571     * @return the stacktrace as String
572     */
573    private String formatStackTrace(Throwable e) {
574
575        return StringUtils.join(CmsLog.render(e), '\n');
576    }
577
578    /**
579     * Generates a JSON object and returns its String representation for the response.<p>
580     *
581     * @param success <code>true</code> if the upload was successful
582     * @param message the message to display
583     * @param stacktrace the stack trace in case of an error
584     *
585     * @return the the response String
586     */
587    private String generateResponse(Boolean success, String message, String stacktrace) {
588
589        JSONObject result = new JSONObject();
590        try {
591            result.put(I_CmsUploadConstants.KEY_SUCCESS, success);
592            result.put(I_CmsUploadConstants.KEY_MESSAGE, message);
593            result.put(I_CmsUploadConstants.KEY_STACKTRACE, stacktrace);
594            result.put(I_CmsUploadConstants.KEY_REQUEST_SIZE, getRequest().getContentLength());
595
596            // UUIDs get converted to strings when generating the JSON text
597            result.put(I_CmsUploadConstants.KEY_UPLOADED_FILES, new JSONArray(m_resourcesCreated.keySet()));
598
599            result.put(I_CmsUploadConstants.KEY_UPLOADED_FILE_NAMES, new JSONArray(m_resourcesCreated.values()));
600
601            JSONObject virusWarnings = new JSONObject();
602            for (String filename : m_viruses.keys()) {
603                JSONArray viruses = new JSONArray(new ArrayList<>(m_viruses.get(filename)));
604                virusWarnings.put(filename, viruses);
605            }
606            result.put(I_CmsUploadConstants.ATTR_VIRUS_WARNINGS, virusWarnings);
607
608            if (m_uploadHook != null) {
609                result.put(I_CmsUploadConstants.KEY_UPLOAD_HOOK, m_uploadHook);
610            }
611        } catch (JSONException e) {
612            LOG.error(m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_JSON_0), e);
613        }
614        return result.toString();
615    }
616
617    /**
618     * Returns the error message if an error occurred during the creation of resources in the VFS.<p>
619     *
620     * @return the error message
621     */
622    private String getCreationErrorMessage() {
623
624        String message = new String();
625        if (!m_resourcesCreated.isEmpty()) {
626            // some resources have been created, tell the user which resources were created successfully
627            StringBuffer buf = new StringBuffer(64);
628            for (String name : m_resourcesCreated.values()) {
629                buf.append("<br />");
630                buf.append(name);
631            }
632            message = m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_CREATING_1, buf.toString());
633        } else {
634            // no resources have been created on the VFS
635            message = m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_CREATING_0);
636        }
637        return message;
638    }
639
640    /**
641     * Gets the list of file names that should be unziped.<p>
642     *
643     * @return the list of file names that should be unziped
644     *
645     * @throws UnsupportedEncodingException if something goes wrong
646     */
647    private List<String> getFilesToUnzip() throws UnsupportedEncodingException {
648
649        if (m_parameterMap.get(I_CmsUploadConstants.UPLOAD_UNZIP_FILES_FIELD_NAME) != null) {
650            String[] filesToUnzip = m_parameterMap.get(I_CmsUploadConstants.UPLOAD_UNZIP_FILES_FIELD_NAME);
651            if (filesToUnzip != null) {
652                List<String> result = new ArrayList<String>();
653                for (String filename : filesToUnzip) {
654                    result.add(URLDecoder.decode(filename, "UTF-8"));
655                }
656                return result;
657            }
658        }
659        return Collections.emptyList();
660    }
661
662    /**
663     * Gets the post-create handler.
664     *
665     * @return the post-create handler
666     */
667    private String getPostCreateHandler() {
668
669        String[] values = m_parameterMap.get(I_CmsUploadConstants.POST_CREATE_HANDLER);
670        return ((values != null) && (values.length > 0)) ? values[0] : null;
671    }
672
673    /**
674     * Returns the target folder for the new resource,
675     * if the given folder does not exist root folder
676     * of the current site is returned.<p>
677     *
678     * @param cms the CMS context to use
679     *
680     * @return the target folder for the new resource
681     *
682     * @throws CmsException if something goes wrong
683     */
684    private String getTargetFolder(CmsObject cms) throws CmsException {
685
686        // get the target folder on the vfs
687        CmsResource target = cms.readResource("/", CmsResourceFilter.IGNORE_EXPIRATION);
688        if (m_parameterMap.get(I_CmsUploadConstants.UPLOAD_TARGET_FOLDER_FIELD_NAME) != null) {
689            String targetFolder = m_parameterMap.get(I_CmsUploadConstants.UPLOAD_TARGET_FOLDER_FIELD_NAME)[0];
690            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(targetFolder)) {
691                if (cms.existsResource(targetFolder, CmsResourceFilter.IGNORE_EXPIRATION)) {
692                    CmsResource tmpTarget = cms.readResource(targetFolder, CmsResourceFilter.IGNORE_EXPIRATION);
693                    if (tmpTarget.isFolder()) {
694                        target = tmpTarget;
695                    }
696                } else {
697                    target = createTargetFolder(cms, targetFolder);
698                }
699            }
700        }
701        String targetFolder = cms.getRequestContext().removeSiteRoot(target.getRootPath());
702        if (!targetFolder.endsWith("/")) {
703            // add folder separator to currentFolder
704            targetFolder += "/";
705        }
706        return targetFolder;
707    }
708
709    /**
710     * Returns true if file name translation should be skipped for the upload.
711     *
712     * <p>This is mainly used for the file replacement dialog.
713     *
714     * @return true if file name translation should be skipped
715     */
716    private boolean isKeepFileNames() {
717
718        String[] values = m_parameterMap.get(I_CmsUploadConstants.KEEP_FILE_NAMES);
719        boolean result = (values != null) && (values.length > 0) && Boolean.parseBoolean(values[0]);
720        return result;
721    }
722
723    /**
724     * Parses the request.<p>
725     *
726     * Stores the file items and the request parameters in a local variable if present.<p>
727     *
728     * @param listener the upload listener
729     *
730     * @throws Exception if anything goes wrong
731     */
732    private void parseRequest(CmsUploadListener listener) throws Exception {
733
734        // check if the request is a multipart request
735        if (!JakartaServletFileUpload.isMultipartContent(getRequest())) {
736            // no multipart request: Abort the upload
737            throw new CmsUploadException(m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_NO_MULTIPART_0));
738        }
739
740        // this was indeed a multipart form request, read the files
741        m_multiPartFileItems = readMultipartFileItems(listener);
742
743        // check if there were any multipart file items in the request
744        if ((m_multiPartFileItems == null) || m_multiPartFileItems.isEmpty()) {
745            // no file items found stop process
746            throw new CmsUploadException(m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_NO_FILEITEMS_0));
747        }
748
749        // there are file items in the request, get the request parameters
750        m_parameterMap = CmsRequestUtil.readParameterMapFromMultiPart(
751            getCmsObject().getRequestContext().getEncoding(),
752            m_multiPartFileItems);
753
754        listener.setFinished(true);
755    }
756
757    /**
758     * Parses a request of the form <code>multipart/form-data</code>.<p>
759     *
760     * The result list will contain items of type <code>{@link DiskFileItem}</code>.
761     * If the request has no file items, then <code>null</code> is returned.<p>
762     *
763     * @param listener the upload listener
764     *
765     * @return the list of <code>{@link DiskFileItem}</code> extracted from the multipart request,
766     *      or <code>null</code> if the request has no file items
767     *
768     * @throws Exception if anything goes wrong
769     */
770    private List<DiskFileItem> readMultipartFileItems(CmsUploadListener listener) throws Exception {
771
772        DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
773
774        // the location for saving data that is larger than the threshold
775        File temp = new File(OpenCms.getSystemInfo().getPackagesRfsPath());
776        if (temp.exists() || temp.mkdirs()) {
777            // make sure the folder exists
778            builder.setPath(temp.getAbsolutePath());
779        }
780        DiskFileItemFactory factory = builder.get();
781
782        // create a file upload servlet
783        JakartaServletDiskFileUpload fu = new JakartaServletDiskFileUpload(factory);
784        // set the listener
785        fu.setProgressListener(listener);
786        // set encoding to correctly handle special chars (e.g. in filenames)
787        Charset.forName(getRequest().getCharacterEncoding());
788        fu.setHeaderCharset(Charset.forName(getRequest().getCharacterEncoding()));
789        // set the maximum size for a single file (value is in bytes)
790        long maxFileSizeBytes = OpenCms.getWorkplaceManager().getFileBytesMaxUploadSize(getCmsObject());
791        if (maxFileSizeBytes > 0) {
792            fu.setMaxFileSize(maxFileSizeBytes);
793        }
794
795        // try to parse the request
796        try {
797            return CmsCollectionsGenericWrapper.list(fu.parseRequest(getRequest()));
798        } catch (FileUploadByteCountLimitException e) {
799            // file size is larger than maximum allowed file size, throw an error
800            Integer actualSize = Integer.valueOf((int)(e.getActualSize() / 1024));
801            Integer maxSize = Integer.valueOf((int)(e.getPermitted() / 1024));
802            throw new CmsUploadException(
803                m_bundle.key(
804                    org.opencms.ade.upload.Messages.ERR_UPLOAD_FILE_SIZE_LIMIT_3,
805                    actualSize,
806                    e.getFileName(),
807                    maxSize),
808                e);
809        } catch (FileUploadSizeException e) {
810            // request size is larger than maximum allowed request size, throw an error
811            Integer actualSize = Integer.valueOf((int)(e.getActualSize() / 1024));
812            Integer maxSize = Integer.valueOf((int)(e.getPermitted() / 1024));
813            throw new CmsUploadException(
814                m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_REQUEST_SIZE_LIMIT_2, actualSize, maxSize),
815                e);
816        }
817    }
818
819    /**
820     * Remove the listener active in this session.
821     *
822     * @param listenerId the id of the listener to remove
823     */
824    private void removeListener(CmsUUID listenerId) {
825
826        getRequest().getSession().removeAttribute(SESSION_ATTRIBUTE_LISTENER_ID);
827        m_listeners.remove(listenerId);
828    }
829
830    /**
831     * Scans a byte buffer for viruses if possible.
832     * @param data the file data
833     * @return the list of detected viruses
834     */
835    private List<String> scan(String context, byte[] data) {
836
837        return scanInternal(context, () -> new ByteArrayInputStream(data));
838    }
839
840    /**
841     * Scans a zip file entry for viruses if possible.
842     * @param zip the zip file
843     * @param entry the entry
844     * @return the list of detected viruses
845     */
846    private List<String> scan(String context, ZipFile zip, ZipArchiveEntry entry) {
847
848        return scanInternal(context, () -> zip.getInputStream(entry));
849    }
850
851    /**
852     * Internal helper method for calling the virus scanner implementation and handling logging.
853     *
854     * @param context the context string to include into the log; contains information about what is being scanned
855     * @param streamProvider supplier for the data stream to be scanned
856     *
857     * @return the list of found virus names
858     */
859    private List<String> scanInternal(String context, InputStreamProvider streamProvider) {
860
861        if (m_scanner != null) {
862            List<String> result = null;
863            long t1 = System.currentTimeMillis();
864            try (InputStream stream = streamProvider.getStream()) {
865                result = m_scanner.scan(stream);
866            } catch (IOException e) {
867                result = Collections.emptyList();
868            } finally {
869                long t2 = System.currentTimeMillis();
870                CmsVirusScannerLog.LOG.debug("Virus scan in context " + context + " took " + (t2 - t1) + "ms");
871            }
872            if (result.isEmpty()) {
873                CmsVirusScannerLog.LOG.info("Found no viruses, context=" + context);
874            } else {
875                CmsVirusScannerLog.LOG.warn(
876                    "Found viruses: "
877                        + result
878                        + ", context="
879                        + context
880                        + ", user="
881                        + getCmsObject().getRequestContext().getCurrentUser().getName());
882            }
883            return result;
884        } else {
885            return Collections.emptyList();
886        }
887    }
888
889}