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}