001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://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: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://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.types.CmsResourceTypePlain;
039import org.opencms.gwt.shared.I_CmsUploadConstants;
040import org.opencms.i18n.CmsMessages;
041import org.opencms.json.JSONArray;
042import org.opencms.json.JSONException;
043import org.opencms.json.JSONObject;
044import org.opencms.jsp.CmsJspBean;
045import org.opencms.loader.CmsLoaderException;
046import org.opencms.lock.CmsLockException;
047import org.opencms.main.CmsException;
048import org.opencms.main.CmsLog;
049import org.opencms.main.OpenCms;
050import org.opencms.security.CmsSecurityException;
051import org.opencms.util.CmsCollectionsGenericWrapper;
052import org.opencms.util.CmsRequestUtil;
053import org.opencms.util.CmsStringUtil;
054import org.opencms.util.CmsUUID;
055
056import java.io.File;
057import java.io.UnsupportedEncodingException;
058import java.net.URLDecoder;
059import java.util.ArrayList;
060import java.util.Collections;
061import java.util.HashMap;
062import java.util.List;
063import java.util.Map;
064
065import javax.servlet.http.HttpServletRequest;
066import javax.servlet.http.HttpServletResponse;
067import javax.servlet.jsp.PageContext;
068
069import org.apache.commons.fileupload.FileItem;
070import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;
071import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException;
072import org.apache.commons.fileupload.disk.DiskFileItemFactory;
073import org.apache.commons.fileupload.servlet.ServletFileUpload;
074import org.apache.commons.lang3.StringUtils;
075import org.apache.commons.logging.Log;
076
077/**
078 * Bean to be used in JSP scriptlet code that provides
079 * access to the upload functionality.<p>
080 *
081 * @since 8.0.0
082 */
083public class CmsUploadBean extends CmsJspBean {
084
085    /** The default upload timeout. */
086    public static final int DEFAULT_UPLOAD_TIMEOUT = 20000;
087
088    /** Key name for the session attribute that stores the id of the current listener. */
089    public static final String SESSION_ATTRIBUTE_LISTENER_ID = "__CmsUploadBean.LISTENER";
090
091    /** The log object for this class. */
092    private static final Log LOG = CmsLog.getLog(CmsUploadBean.class);
093
094    /** A static map of all listeners. */
095    private static Map<CmsUUID, CmsUploadListener> m_listeners = new HashMap<CmsUUID, CmsUploadListener>();
096
097    /** The gwt message bundle. */
098    private CmsMessages m_bundle = org.opencms.ade.upload.Messages.get().getBundle();
099
100    /** Signals that the start method is called. */
101    private boolean m_called;
102
103    /** A list of the file items to upload. */
104    private List<FileItem> m_multiPartFileItems;
105
106    /** The map of parameters read from the current request. */
107    private Map<String, String[]> m_parameterMap;
108
109    /** The names by id of the resources that have been created successfully. */
110    private HashMap<String, String> m_resourcesCreated = new HashMap<String, String>();
111
112    /** A CMS context for the root site. */
113    private CmsObject m_rootCms;
114
115    /** The server side upload delay. */
116    private int m_uploadDelay;
117
118    /** The upload hook URI. */
119    private String m_uploadHook;
120
121    /**
122     * Constructor, with parameters.<p>
123     *
124     * @param context the JSP page context object
125     * @param req the JSP request
126     * @param res the JSP response
127     *
128     * @throws CmsException if something goes wrong
129     */
130    public CmsUploadBean(PageContext context, HttpServletRequest req, HttpServletResponse res)
131    throws CmsException {
132
133        super();
134        init(context, req, res);
135
136        m_rootCms = OpenCms.initCmsObject(getCmsObject());
137        m_rootCms.getRequestContext().setSiteRoot("");
138    }
139
140    /**
141     * Returns the listener for given CmsUUID.<p>
142     *
143     * @param listenerId the uuid
144     *
145     * @return the according listener
146     */
147    public static CmsUploadListener getCurrentListener(CmsUUID listenerId) {
148
149        return m_listeners.get(listenerId);
150    }
151
152    /**
153     * Returns the VFS path for the given filename and folder.<p>
154     *
155     * @param cms the cms object
156     * @param fileName the filename to combine with the folder
157     * @param folder the folder to combine with the filename
158     *
159     * @return the VFS path for the given filename and folder
160     */
161    public static String getNewResourceName(CmsObject cms, String fileName, String folder) {
162
163        String newResname = CmsResource.getName(fileName.replace('\\', '/'));
164        newResname = cms.getRequestContext().getFileTranslator().translateResource(newResname);
165        newResname = folder + newResname;
166        return newResname;
167    }
168
169    /**
170     * Sets the uploadDelay.<p>
171     *
172     * @param uploadDelay the uploadDelay to set
173     */
174    public void setUploadDelay(int uploadDelay) {
175
176        m_uploadDelay = uploadDelay;
177    }
178
179    /**
180     * Starts the upload.<p>
181     *
182     * @return the response String (JSON)
183     */
184    public String start() {
185
186        // ensure that this method can only be called once
187        if (m_called) {
188            throw new UnsupportedOperationException();
189        }
190        m_called = true;
191
192        // create a upload listener
193        CmsUploadListener listener = createListener();
194        try {
195            // try to parse the request
196            parseRequest(listener);
197            // try to create the resources on the VFS
198            createResources(listener);
199            // trigger update offline indexes, important for gallery search
200            OpenCms.getSearchManager().updateOfflineIndexes();
201        } catch (CmsException e) {
202            // an error occurred while creating the resources on the VFS, create a special error message
203            LOG.error(e.getMessage(), e);
204            return generateResponse(Boolean.FALSE, getCreationErrorMessage(), formatStackTrace(e));
205        } catch (CmsUploadException e) {
206            // an expected error occurred while parsing the request, the error message is already set in the exception
207            LOG.debug(e.getMessage(), e);
208            return generateResponse(Boolean.FALSE, e.getMessage(), formatStackTrace(e));
209        } catch (Throwable e) {
210            // an unexpected error occurred while parsing the request, create a non-specific error message
211            LOG.error(e.getMessage(), e);
212            String message = m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_UNEXPECTED_0);
213            return generateResponse(Boolean.FALSE, message, formatStackTrace(e));
214        } finally {
215            removeListener(listener.getId());
216        }
217        // the upload was successful inform the user about success
218        return generateResponse(Boolean.TRUE, m_bundle.key(org.opencms.ade.upload.Messages.LOG_UPLOAD_SUCCESS_0), "");
219    }
220
221    /**
222     * Creates a upload listener and puts it into the static map.<p>
223     *
224     * @return the listener
225     */
226    private CmsUploadListener createListener() {
227
228        CmsUploadListener listener = new CmsUploadListener(getRequest().getContentLength());
229        listener.setDelay(m_uploadDelay);
230        m_listeners.put(listener.getId(), listener);
231        getRequest().getSession().setAttribute(SESSION_ATTRIBUTE_LISTENER_ID, listener.getId());
232        return listener;
233    }
234
235    /**
236     * Creates the resources.<p>
237     * @param listener the listener
238     *
239     * @throws CmsException if something goes wrong
240     * @throws UnsupportedEncodingException in case the encoding is not supported
241     */
242    private void createResources(CmsUploadListener listener) throws CmsException, UnsupportedEncodingException {
243
244        CmsObject cms = getCmsObject();
245        String[] isRootPathVals = m_parameterMap.get(I_CmsUploadConstants.UPLOAD_IS_ROOT_PATH_FIELD_NAME);
246        if ((isRootPathVals != null) && (isRootPathVals.length > 0) && Boolean.parseBoolean(isRootPathVals[0])) {
247            cms = m_rootCms;
248        }
249        // get the target folder
250        String targetFolder = getTargetFolder(cms);
251        m_uploadHook = OpenCms.getWorkplaceManager().getUploadHook(cms, targetFolder);
252
253        List<String> filesToUnzip = getFilesToUnzip();
254
255        // iterate over the list of files to upload and create each single resource
256        for (FileItem fileItem : m_multiPartFileItems) {
257            if ((fileItem != null) && (!fileItem.isFormField())) {
258                // read the content of the file
259                byte[] content = fileItem.get();
260                fileItem.delete();
261
262                // determine the new resource name
263                String fileName = m_parameterMap.get(
264                    fileItem.getFieldName() + I_CmsUploadConstants.UPLOAD_FILENAME_ENCODED_SUFFIX)[0];
265                fileName = URLDecoder.decode(fileName, "UTF-8");
266
267                if (filesToUnzip.contains(CmsResource.getName(fileName.replace('\\', '/')))) {
268                    // import the zip
269                    CmsImportFolder importZip = new CmsImportFolder();
270                    try {
271                        importZip.importZip(content, targetFolder, cms, false);
272                    } finally {
273                        // get the created resource names
274                        for (CmsResource importedResource : importZip.getImportedResources()) {
275                            m_resourcesCreated.put(
276                                importedResource.getStructureId().toString(),
277                                importedResource.getName());
278                        }
279                    }
280                } else {
281                    // create the resource
282                    CmsResource importedResource = createSingleResource(cms, fileName, targetFolder, content);
283                    // add the name of the created resource to the list of successful created resources
284                    m_resourcesCreated.put(importedResource.getStructureId().toString(), importedResource.getName());
285                }
286
287                if (listener.isCanceled()) {
288                    throw listener.getException();
289                }
290            }
291        }
292    }
293
294    /**
295     * Creates a single resource and returns the new resource.<p>
296     *
297     * @param cms the CMS context to use
298     * @param fileName the name of the resource to create
299     * @param targetFolder the folder to store the new resource
300     * @param content the content of the resource to create
301     *
302     * @return the new resource
303     *
304     * @throws CmsException if something goes wrong
305     * @throws CmsLoaderException if something goes wrong
306     * @throws CmsDbSqlException if something goes wrong
307     */
308    @SuppressWarnings("deprecation")
309    private CmsResource createSingleResource(CmsObject cms, String fileName, String targetFolder, byte[] content)
310    throws CmsException, CmsLoaderException, CmsDbSqlException {
311
312        String newResname = getNewResourceName(cms, fileName, targetFolder);
313        CmsResource createdResource = null;
314
315        // determine Title property value to set on new resource
316        String title = fileName;
317        if (title.lastIndexOf('.') != -1) {
318            title = title.substring(0, title.lastIndexOf('.'));
319        }
320
321        // fileName really shouldn't contain the full path, but for some reason it does sometimes when the client is
322        // running on IE7, so we eliminate anything before and including the last slash or backslash in the title
323        // before setting it as a property.
324
325        int backslashIndex = title.lastIndexOf('\\');
326        if (backslashIndex != -1) {
327            title = title.substring(backslashIndex + 1);
328        }
329
330        int slashIndex = title.lastIndexOf('/');
331        if (slashIndex != -1) {
332            title = title.substring(slashIndex + 1);
333        }
334
335        List<CmsProperty> properties = new ArrayList<CmsProperty>(1);
336        CmsProperty titleProp = new CmsProperty();
337        titleProp.setName(CmsPropertyDefinition.PROPERTY_TITLE);
338        if (OpenCms.getWorkplaceManager().isDefaultPropertiesOnStructure()) {
339            titleProp.setStructureValue(title);
340        } else {
341            titleProp.setResourceValue(title);
342        }
343        properties.add(titleProp);
344
345        int plainId = OpenCms.getResourceManager().getResourceType(
346            CmsResourceTypePlain.getStaticTypeName()).getTypeId();
347        if (!cms.existsResource(newResname, CmsResourceFilter.IGNORE_EXPIRATION)) {
348            // if the resource does not exist, create it
349
350            try {
351                // create the resource
352                int resTypeId = OpenCms.getResourceManager().getDefaultTypeForName(newResname).getTypeId();
353                createdResource = cms.createResource(newResname, resTypeId, content, properties);
354                try {
355                    cms.unlockResource(newResname);
356                } catch (CmsLockException e) {
357                    LOG.info("Couldn't unlock uploaded file", e);
358                }
359            } catch (CmsSecurityException e) {
360                // in case of not enough permissions, try to create a plain text file
361                createdResource = cms.createResource(newResname, plainId, content, properties);
362                cms.unlockResource(newResname);
363            } catch (CmsDbSqlException sqlExc) {
364                // SQL error, probably the file is too large for the database settings, delete file
365                cms.lockResource(newResname);
366                cms.deleteResource(newResname, CmsResource.DELETE_PRESERVE_SIBLINGS);
367                throw sqlExc;
368            } catch (OutOfMemoryError e) {
369                // the file is to large try to clear up
370                cms.lockResource(newResname);
371                cms.deleteResource(newResname, CmsResource.DELETE_PRESERVE_SIBLINGS);
372                throw e;
373            }
374
375        } else {
376            // if the resource already exists, replace it
377            CmsResource res = cms.readResource(newResname, CmsResourceFilter.ALL);
378            boolean wasLocked = false;
379            try {
380                if (!cms.getLock(res).isOwnedBy(cms.getRequestContext().getCurrentUser())) {
381                    cms.lockResource(res);
382                    wasLocked = true;
383                }
384                CmsFile file = cms.readFile(res);
385                byte[] contents = file.getContents();
386                try {
387                    cms.replaceResource(newResname, res.getTypeId(), content, null);
388                    createdResource = res;
389                } catch (CmsDbSqlException sqlExc) {
390                    // SQL error, probably the file is too large for the database settings, restore content
391                    file.setContents(contents);
392                    cms.writeFile(file);
393                    throw sqlExc;
394                } catch (OutOfMemoryError e) {
395                    // the file is to large try to clear up
396                    file.setContents(contents);
397                    cms.writeFile(file);
398                    throw e;
399                }
400            } finally {
401                if (wasLocked) {
402                    cms.unlockResource(res);
403                }
404            }
405        }
406        return createdResource;
407    }
408
409    /**
410     * Returns the stacktrace of the given exception as String.<p>
411     *
412     * @param e the exception
413     *
414     * @return the stacktrace as String
415     */
416    private String formatStackTrace(Throwable e) {
417        return StringUtils.join(CmsLog.render(e), '\n');
418    }
419
420    /**
421     * Generates a JSON object and returns its String representation for the response.<p>
422     *
423     * @param success <code>true</code> if the upload was successful
424     * @param message the message to display
425     * @param stacktrace the stack trace in case of an error
426     *
427     * @return the the response String
428     */
429    private String generateResponse(Boolean success, String message, String stacktrace) {
430
431        JSONObject result = new JSONObject();
432        try {
433            result.put(I_CmsUploadConstants.KEY_SUCCESS, success);
434            result.put(I_CmsUploadConstants.KEY_MESSAGE, message);
435            result.put(I_CmsUploadConstants.KEY_STACKTRACE, stacktrace);
436            result.put(I_CmsUploadConstants.KEY_REQUEST_SIZE, getRequest().getContentLength());
437            result.put(I_CmsUploadConstants.KEY_UPLOADED_FILES, new JSONArray(m_resourcesCreated.keySet()));
438            result.put(I_CmsUploadConstants.KEY_UPLOADED_FILE_NAMES, new JSONArray(m_resourcesCreated.values()));
439            if (m_uploadHook != null) {
440                result.put(I_CmsUploadConstants.KEY_UPLOAD_HOOK, m_uploadHook);
441            }
442        } catch (JSONException e) {
443            LOG.error(m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_JSON_0), e);
444        }
445        return result.toString();
446    }
447
448    /**
449     * Returns the error message if an error occurred during the creation of resources in the VFS.<p>
450     *
451     * @return the error message
452     */
453    private String getCreationErrorMessage() {
454
455        String message = new String();
456        if (!m_resourcesCreated.isEmpty()) {
457            // some resources have been created, tell the user which resources were created successfully
458            StringBuffer buf = new StringBuffer(64);
459            for (String name : m_resourcesCreated.values()) {
460                buf.append("<br />");
461                buf.append(name);
462            }
463            message = m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_CREATING_1, buf.toString());
464        } else {
465            // no resources have been created on the VFS
466            message = m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_CREATING_0);
467        }
468        return message;
469    }
470
471    /**
472     * Gets the list of file names that should be unziped.<p>
473     *
474     * @return the list of file names that should be unziped
475     *
476     * @throws UnsupportedEncodingException if something goes wrong
477     */
478    private List<String> getFilesToUnzip() throws UnsupportedEncodingException {
479
480        if (m_parameterMap.get(I_CmsUploadConstants.UPLOAD_UNZIP_FILES_FIELD_NAME) != null) {
481            String[] filesToUnzip = m_parameterMap.get(I_CmsUploadConstants.UPLOAD_UNZIP_FILES_FIELD_NAME);
482            if (filesToUnzip != null) {
483                List<String> result = new ArrayList<String>();
484                for (String filename : filesToUnzip) {
485                    result.add(URLDecoder.decode(filename, "UTF-8"));
486                }
487                return result;
488            }
489        }
490        return Collections.emptyList();
491    }
492
493    /**
494     * Returns the target folder for the new resource,
495     * if the given folder does not exist root folder
496     * of the current site is returned.<p>
497     *
498     * @param cms the CMS context to use
499     *
500     * @return the target folder for the new resource
501     *
502     * @throws CmsException if something goes wrong
503     */
504    private String getTargetFolder(CmsObject cms) throws CmsException {
505
506        // get the target folder on the vfs
507        CmsResource target = cms.readResource("/", CmsResourceFilter.IGNORE_EXPIRATION);
508        if (m_parameterMap.get(I_CmsUploadConstants.UPLOAD_TARGET_FOLDER_FIELD_NAME) != null) {
509            String targetFolder = m_parameterMap.get(I_CmsUploadConstants.UPLOAD_TARGET_FOLDER_FIELD_NAME)[0];
510            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(targetFolder)) {
511                if (cms.existsResource(targetFolder)) {
512                    CmsResource tmpTarget = cms.readResource(targetFolder, CmsResourceFilter.IGNORE_EXPIRATION);
513                    if (tmpTarget.isFolder()) {
514                        target = tmpTarget;
515                    }
516                }
517            }
518        }
519        String targetFolder = cms.getRequestContext().removeSiteRoot(target.getRootPath());
520        if (!targetFolder.endsWith("/")) {
521            // add folder separator to currentFolder
522            targetFolder += "/";
523        }
524        return targetFolder;
525    }
526
527    /**
528     * Parses the request.<p>
529     *
530     * Stores the file items and the request parameters in a local variable if present.<p>
531     *
532     * @param listener the upload listener
533     *
534     * @throws Exception if anything goes wrong
535     */
536    private void parseRequest(CmsUploadListener listener) throws Exception {
537
538        // check if the request is a multipart request
539        if (!ServletFileUpload.isMultipartContent(getRequest())) {
540            // no multipart request: Abort the upload
541            throw new CmsUploadException(m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_NO_MULTIPART_0));
542        }
543
544        // this was indeed a multipart form request, read the files
545        m_multiPartFileItems = readMultipartFileItems(listener);
546
547        // check if there were any multipart file items in the request
548        if ((m_multiPartFileItems == null) || m_multiPartFileItems.isEmpty()) {
549            // no file items found stop process
550            throw new CmsUploadException(m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_NO_FILEITEMS_0));
551        }
552
553        // there are file items in the request, get the request parameters
554        m_parameterMap = CmsRequestUtil.readParameterMapFromMultiPart(
555            getCmsObject().getRequestContext().getEncoding(),
556            m_multiPartFileItems);
557
558        listener.setFinished(true);
559    }
560
561    /**
562     * Parses a request of the form <code>multipart/form-data</code>.<p>
563     *
564     * The result list will contain items of type <code>{@link FileItem}</code>.
565     * If the request has no file items, then <code>null</code> is returned.<p>
566     *
567     * @param listener the upload listener
568     *
569     * @return the list of <code>{@link FileItem}</code> extracted from the multipart request,
570     *      or <code>null</code> if the request has no file items
571     *
572     * @throws Exception if anything goes wrong
573     */
574    private List<FileItem> readMultipartFileItems(CmsUploadListener listener) throws Exception {
575
576        DiskFileItemFactory factory = new DiskFileItemFactory();
577        // maximum size that will be stored in memory
578        factory.setSizeThreshold(4096);
579        // the location for saving data that is larger than the threshold
580        File temp = new File(OpenCms.getSystemInfo().getPackagesRfsPath());
581        if (temp.exists() || temp.mkdirs()) {
582            // make sure the folder exists
583            factory.setRepository(temp);
584        }
585
586        // create a file upload servlet
587        ServletFileUpload fu = new ServletFileUpload(factory);
588        // set the listener
589        fu.setProgressListener(listener);
590        // set encoding to correctly handle special chars (e.g. in filenames)
591        fu.setHeaderEncoding(getRequest().getCharacterEncoding());
592        // set the maximum size for a single file (value is in bytes)
593        long maxFileSizeBytes = OpenCms.getWorkplaceManager().getFileBytesMaxUploadSize(getCmsObject());
594        if (maxFileSizeBytes > 0) {
595            fu.setFileSizeMax(maxFileSizeBytes);
596        }
597
598        // try to parse the request
599        try {
600            return CmsCollectionsGenericWrapper.list(fu.parseRequest(getRequest()));
601        } catch (SizeLimitExceededException e) {
602            // request size is larger than maximum allowed request size, throw an error
603            Integer actualSize = new Integer((int)(e.getActualSize() / 1024));
604            Integer maxSize = new Integer((int)(e.getPermittedSize() / 1024));
605            throw new CmsUploadException(
606                m_bundle.key(org.opencms.ade.upload.Messages.ERR_UPLOAD_REQUEST_SIZE_LIMIT_2, actualSize, maxSize),
607                e);
608        } catch (FileSizeLimitExceededException e) {
609            // file size is larger than maximum allowed file size, throw an error
610            Integer actualSize = new Integer((int)(e.getActualSize() / 1024));
611            Integer maxSize = new Integer((int)(e.getPermittedSize() / 1024));
612            throw new CmsUploadException(
613                m_bundle.key(
614                    org.opencms.ade.upload.Messages.ERR_UPLOAD_FILE_SIZE_LIMIT_3,
615                    actualSize,
616                    e.getFileName(),
617                    maxSize),
618                e);
619        }
620    }
621
622    /**
623     * Remove the listener active in this session.
624     *
625     * @param listenerId the id of the listener to remove
626     */
627    private void removeListener(CmsUUID listenerId) {
628
629        getRequest().getSession().removeAttribute(SESSION_ATTRIBUTE_LISTENER_ID);
630        m_listeners.remove(listenerId);
631    }
632}