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 GmbH & Co. KG, 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 * This file is based on: 028 * - org.apache.catalina.servlets.WebdavServlet 029 * - org.apache.catalina.servlets.DefaultServlet 030 * from the Apache Tomcat project. 031 * 032 * Licensed to the Apache Software Foundation (ASF) under one or more 033 * contributor license agreements. See the NOTICE file distributed with 034 * this work for additional information regarding copyright ownership. 035 * The ASF licenses this file to You under the Apache License, Version 2.0 036 * (the "License"); you may not use this file except in compliance with 037 * the License. You may obtain a copy of the License at 038 * 039 * http://www.apache.org/licenses/LICENSE-2.0 040 * 041 * Unless required by applicable law or agreed to in writing, software 042 * distributed under the License is distributed on an "AS IS" BASIS, 043 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 044 * See the License for the specific language governing permissions and 045 * limitations under the License. 046 */ 047 048package org.opencms.webdav; 049 050import org.opencms.file.CmsVfsResourceAlreadyExistsException; 051import org.opencms.file.CmsVfsResourceNotFoundException; 052import org.opencms.i18n.CmsEncoder; 053import org.opencms.main.CmsException; 054import org.opencms.main.CmsLog; 055import org.opencms.main.OpenCms; 056import org.opencms.repository.A_CmsRepository; 057import org.opencms.repository.CmsRepositoryLockInfo; 058import org.opencms.repository.I_CmsRepositoryItem; 059import org.opencms.repository.I_CmsRepositorySession; 060import org.opencms.security.CmsSecurityException; 061import org.opencms.util.CmsRequestUtil; 062 063import java.io.BufferedInputStream; 064import java.io.ByteArrayInputStream; 065import java.io.ByteArrayOutputStream; 066import java.io.File; 067import java.io.FileInputStream; 068import java.io.IOException; 069import java.io.InputStream; 070import java.io.InputStreamReader; 071import java.io.OutputStreamWriter; 072import java.io.PrintWriter; 073import java.io.RandomAccessFile; 074import java.io.Reader; 075import java.io.UnsupportedEncodingException; 076import java.io.Writer; 077import java.security.MessageDigest; 078import java.security.NoSuchAlgorithmException; 079import java.text.DateFormat; 080import java.text.SimpleDateFormat; 081import java.util.ArrayList; 082import java.util.BitSet; 083import java.util.Date; 084import java.util.Hashtable; 085import java.util.Iterator; 086import java.util.List; 087import java.util.Locale; 088import java.util.Map; 089import java.util.Map.Entry; 090import java.util.Stack; 091import java.util.StringTokenizer; 092import java.util.TimeZone; 093import java.util.Vector; 094 095import javax.servlet.ServletException; 096import javax.servlet.ServletOutputStream; 097import javax.servlet.UnavailableException; 098import javax.servlet.http.HttpServlet; 099import javax.servlet.http.HttpServletRequest; 100import javax.servlet.http.HttpServletResponse; 101 102import org.apache.commons.codec.binary.Base64; 103import org.apache.commons.codec.net.URLCodec; 104import org.apache.commons.logging.Log; 105 106import org.dom4j.Document; 107import org.dom4j.DocumentException; 108import org.dom4j.DocumentHelper; 109import org.dom4j.Element; 110import org.dom4j.Namespace; 111import org.dom4j.Node; 112import org.dom4j.QName; 113import org.dom4j.io.SAXReader; 114import org.xml.sax.InputSource; 115 116/** 117 * Servlet which adds support for WebDAV level 2.<p> 118 * 119 * @since 6.5.6 120 */ 121public class CmsWebdavServlet extends HttpServlet { 122 123 /** Basic authorization prefix constant. */ 124 public static final String AUTHORIZATION_BASIC_PREFIX = "BASIC "; 125 126 /** Size of file transfer buffer in bytes. */ 127 public static final int BUFFER_SIZE = 4096; 128 129 /** Credentials separator constant. */ 130 public static final String SEPARATOR_CREDENTIALS = ":"; 131 132 /** Date format for the last modified date. */ 133 protected static final DateFormat HTTP_DATE_FORMAT; 134 135 /** Date format for the creation date. */ 136 protected static final DateFormat ISO8601_FORMAT; 137 138 /** MD5 message digest provider. */ 139 protected static MessageDigest m_md5Helper; 140 141 /** The MD5 helper object for this class. */ 142 protected static final CmsMD5Encoder MD5_ENCODER = new CmsMD5Encoder(); 143 144 /** WebDAV method: COPY. */ 145 protected static final String METHOD_COPY = "COPY"; 146 147 /** HTTP Method: DELETE. */ 148 protected static final String METHOD_DELETE = "DELETE"; 149 150 /** HTTP Method: GET. */ 151 protected static final String METHOD_GET = "GET"; 152 153 /** HTTP Method: HEAD. */ 154 protected static final String METHOD_HEAD = "HEAD"; 155 156 /** WebDAV method: LOCK. */ 157 protected static final String METHOD_LOCK = "LOCK"; 158 159 /** WebDAV method: MKCOL. */ 160 protected static final String METHOD_MKCOL = "MKCOL"; 161 162 /** WebDAV method: MOVE. */ 163 protected static final String METHOD_MOVE = "MOVE"; 164 165 /** HTTP Method: OPTIONS. */ 166 protected static final String METHOD_OPTIONS = "OPTIONS"; 167 168 /** HTTP Method: POST. */ 169 protected static final String METHOD_POST = "POST"; 170 171 /** WebDAV method: PROPFIND. */ 172 protected static final String METHOD_PROPFIND = "PROPFIND"; 173 174 /** WebDAV method: PROPPATCH. */ 175 protected static final String METHOD_PROPPATCH = "PROPPATCH"; 176 177 /** HTTP Method: PUT. */ 178 protected static final String METHOD_PUT = "PUT"; 179 180 /** HTTP Method: TRACE. */ 181 protected static final String METHOD_TRACE = "TRACE"; 182 183 /** WebDAV method: UNLOCK. */ 184 protected static final String METHOD_UNLOCK = "UNLOCK"; 185 186 /** MIME multipart separation string. */ 187 protected static final String MIME_SEPARATION = "CATALINA_MIME_BOUNDARY"; 188 189 /** Chars which are safe for urls. */ 190 protected static final BitSet URL_SAFE_CHARS; 191 192 /** Name of the servlet attribute to get the path to the temp directory. */ 193 private static final String ATT_SERVLET_TEMPDIR = "javax.servlet.context.tempdir"; 194 195 /** The text to use as basic realm. */ 196 private static final String BASIC_REALM = "OpenCms WebDAV Servlet"; 197 198 /** Default namespace. */ 199 private static final String DEFAULT_NAMESPACE = "DAV:"; 200 201 /** The text to send if the depth is inifinity. */ 202 private static final String DEPTH_INFINITY = "Infinity"; 203 204 /** PROPFIND - Display all properties. */ 205 private static final int FIND_ALL_PROP = 1; 206 207 /** PROPFIND - Specify a property mask. */ 208 private static final int FIND_BY_PROPERTY = 0; 209 210 /** PROPFIND - Return property names. */ 211 private static final int FIND_PROPERTY_NAMES = 2; 212 213 /** Full range marker. */ 214 private static ArrayList<CmsWebdavRange> FULL_RANGE = new ArrayList<CmsWebdavRange>(); 215 216 /** The name of the header "allow". */ 217 private static final String HEADER_ALLOW = "Allow"; 218 219 /** The name of the header "authorization". */ 220 private static final String HEADER_AUTHORIZATION = "Authorization"; 221 222 /** The name of the header "content-length". */ 223 private static final String HEADER_CONTENTLENGTH = "content-length"; 224 225 /** The name of the header "Content-Range". */ 226 private static final String HEADER_CONTENTRANGE = "Content-Range"; 227 228 /** The name of the header "Depth". */ 229 private static final String HEADER_DEPTH = "Depth"; 230 231 /** The name of the header "Destination". */ 232 private static final String HEADER_DESTINATION = "Destination"; 233 234 /** The name of the header "ETag". */ 235 private static final String HEADER_ETAG = "ETag"; 236 237 /** The name of the header "If-Range". */ 238 private static final String HEADER_IFRANGE = "If-Range"; 239 240 /** The name of the header "Last-Modified". */ 241 private static final String HEADER_LASTMODIFIED = "Last-Modified"; 242 243 /** The name of the header "Lock-Token". */ 244 private static final String HEADER_LOCKTOKEN = "Lock-Token"; 245 246 /** The name of the header "Overwrite". */ 247 private static final String HEADER_OVERWRITE = "Overwrite"; 248 249 /** The name of the header "Range". */ 250 private static final String HEADER_RANGE = "Range"; 251 252 /** The name of the init parameter in the web.xml to allow listing. */ 253 private static final String INIT_PARAM_LIST = "listings"; 254 255 /** The name of the init parameter in the web.xml to set read only. */ 256 private static final String INIT_PARAM_READONLY = "readonly"; 257 258 /** The name of the init-param where the repository class is defined. */ 259 private static final String INIT_PARAM_REPOSITORY = "repository"; 260 261 /** Create a new lock. */ 262 private static final int LOCK_CREATION = 0; 263 264 /** Refresh lock. */ 265 private static final int LOCK_REFRESH = 1; 266 267 /** The log object for this class. */ 268 private static final Log LOG = CmsLog.getLog(CmsWebdavServlet.class); 269 270 /** The repository used from this servlet. */ 271 private static A_CmsRepository m_repository; 272 273 /** The unique serial id for this class. */ 274 private static final long serialVersionUID = -122598983283724306L; 275 276 /** The name of the tag "activelock" in the WebDAV protocol. */ 277 private static final String TAG_ACTIVELOCK = "activelock"; 278 279 /** The name of the tag "collection" in the WebDAV protocol. */ 280 private static final String TAG_COLLECTION = "collection"; 281 282 /** The name of the tag "getcontentlanguage" in the WebDAV protocol. */ 283 private static final String TAG_CONTENTLANGUAGE = "getcontentlanguage"; 284 285 /** The name of the tag "getcontentlength" in the WebDAV protocol. */ 286 private static final String TAG_CONTENTLENGTH = "getcontentlength"; 287 288 /** The name of the tag "getcontenttype" in the WebDAV protocol. */ 289 private static final String TAG_CONTENTTYPE = "getcontenttype"; 290 291 /** The name of the tag "creationdate" in the WebDAV protocol. */ 292 private static final String TAG_CREATIONDATE = "creationdate"; 293 294 /** The name of the tag "depth" in the WebDAV protocol. */ 295 private static final String TAG_DEPTH = "depth"; 296 297 /** The name of the tag "displayname" in the WebDAV protocol. */ 298 private static final String TAG_DISPLAYNAME = "displayname"; 299 300 /** The name of the tag "getetag" in the WebDAV protocol. */ 301 private static final String TAG_ETAG = "getetag"; 302 303 /** The name of the tag "href" in the WebDAV protocol. */ 304 private static final String TAG_HREF = "href"; 305 306 /** The name of the tag "getlastmodified" in the WebDAV protocol. */ 307 private static final String TAG_LASTMODIFIED = "getlastmodified"; 308 309 /** The name of the tag "lockdiscovery" in the WebDAV protocol. */ 310 private static final String TAG_LOCKDISCOVERY = "lockdiscovery"; 311 312 /** The name of the tag "lockentry" in the WebDAV protocol. */ 313 private static final String TAG_LOCKENTRY = "lockentry"; 314 315 /** The name of the tag "lockscope" in the WebDAV protocol. */ 316 private static final String TAG_LOCKSCOPE = "lockscope"; 317 318 /** The name of the tag "locktoken" in the WebDAV protocol. */ 319 private static final String TAG_LOCKTOKEN = "locktoken"; 320 321 /** The name of the tag "locktype" in the WebDAV protocol. */ 322 private static final String TAG_LOCKTYPE = "locktype"; 323 324 /** The name of the tag "multistatus" in the WebDAV protocol. */ 325 private static final String TAG_MULTISTATUS = "multistatus"; 326 327 /** The name of the tag "owner" in the WebDAV protocol. */ 328 private static final String TAG_OWNER = "owner"; 329 330 /** The name of the tag "prop" in the WebDAV protocol. */ 331 private static final String TAG_PROP = "prop"; 332 333 /** The name of the tag "propstat" in the WebDAV protocol. */ 334 private static final String TAG_PROPSTAT = "propstat"; 335 336 /** The name of the tag "resourcetype" in the WebDAV protocol. */ 337 private static final String TAG_RESOURCETYPE = "resourcetype"; 338 339 /** The name of the tag "response" in the WebDAV protocol. */ 340 private static final String TAG_RESPONSE = "response"; 341 342 /** The name of the tag "source" in the WebDAV protocol. */ 343 private static final String TAG_SOURCE = "source"; 344 345 /** The name of the tag "status" in the WebDAV protocol. */ 346 private static final String TAG_STATUS = "status"; 347 348 /** The name of the tag "supportedlock" in the WebDAV protocol. */ 349 private static final String TAG_SUPPORTEDLOCK = "supportedlock"; 350 351 /** The name of the tag "timeout" in the WebDAV protocol. */ 352 private static final String TAG_TIMEOUT = "timeout"; 353 354 /** The text to send if the timeout is infinite. */ 355 private static final String TIMEOUT_INFINITE = "Infinite"; 356 357 /** The input buffer size to use when serving resources. */ 358 protected int m_input = 2048; 359 360 /** The output buffer size to use when serving resources. */ 361 protected int m_output = 2048; 362 363 /** Should we generate directory listings? */ 364 private boolean m_listings; 365 366 /** Read only flag. By default, it's set to true. */ 367 private boolean m_readOnly = true; 368 369 /** Secret information used to generate reasonably secure lock ids. */ 370 private String m_secret = "catalina"; 371 372 /** The session which handles the action made with WebDAV. */ 373 private I_CmsRepositorySession m_session; 374 375 /** The name of the user found in the authorization header. */ 376 private String m_username; 377 378 static { 379 URL_SAFE_CHARS = new BitSet(); 380 URL_SAFE_CHARS.set('a', 'z' + 1); 381 URL_SAFE_CHARS.set('A', 'Z' + 1); 382 URL_SAFE_CHARS.set('0', '9' + 1); 383 URL_SAFE_CHARS.set('-'); 384 URL_SAFE_CHARS.set('_'); 385 URL_SAFE_CHARS.set('.'); 386 URL_SAFE_CHARS.set('*'); 387 URL_SAFE_CHARS.set('/'); 388 URL_SAFE_CHARS.set(':'); 389 390 ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); 391 ISO8601_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); 392 393 HTTP_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); 394 HTTP_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); 395 } 396 397 /** 398 * Adds an xml element to the given parent and sets the appropriate namespace and 399 * prefix.<p> 400 * 401 * @param parent the parent node to add the element 402 * @param name the name of the new element 403 * 404 * @return the created element with the given name which was added to the given parent 405 */ 406 public static Element addElement(Element parent, String name) { 407 408 return parent.addElement(new QName(name, Namespace.get("D", DEFAULT_NAMESPACE))); 409 } 410 411 /** 412 * Initialize this servlet.<p> 413 * 414 * @throws ServletException if something goes wrong 415 */ 416 @Override 417 public void init() throws ServletException { 418 419 if (LOG.isInfoEnabled()) { 420 LOG.info(Messages.get().getBundle().key(Messages.LOG_INIT_WEBDAV_SERVLET_0)); 421 } 422 423 String value = null; 424 425 // init parameter: listings 426 try { 427 value = getServletConfig().getInitParameter(INIT_PARAM_LIST); 428 if (value != null) { 429 m_listings = Boolean.valueOf(value).booleanValue(); 430 } 431 } catch (Exception e) { 432 if (LOG.isErrorEnabled()) { 433 LOG.error( 434 Messages.get().getBundle().key(Messages.LOG_READ_INIT_PARAM_ERROR_2, INIT_PARAM_LIST, value), 435 e); 436 } 437 } 438 439 if (LOG.isInfoEnabled()) { 440 LOG.info( 441 Messages.get().getBundle().key( 442 Messages.LOG_READ_INIT_PARAM_2, 443 INIT_PARAM_LIST, 444 Boolean.valueOf(m_listings))); 445 } 446 447 // init parameter: read only 448 try { 449 value = getServletConfig().getInitParameter(INIT_PARAM_READONLY); 450 if (value != null) { 451 m_readOnly = Boolean.valueOf(value).booleanValue(); 452 } 453 } catch (Exception e) { 454 if (LOG.isErrorEnabled()) { 455 LOG.error( 456 Messages.get().getBundle().key(Messages.LOG_READ_INIT_PARAM_ERROR_2, INIT_PARAM_READONLY, value), 457 e); 458 } 459 } 460 461 if (LOG.isInfoEnabled()) { 462 LOG.info( 463 Messages.get().getBundle().key( 464 Messages.LOG_READ_INIT_PARAM_2, 465 INIT_PARAM_READONLY, 466 Boolean.valueOf(m_readOnly))); 467 } 468 469 // Load the MD5 helper used to calculate signatures. 470 try { 471 m_md5Helper = MessageDigest.getInstance("MD5"); 472 } catch (NoSuchAlgorithmException e) { 473 474 if (LOG.isErrorEnabled()) { 475 LOG.error(Messages.get().getBundle().key(Messages.ERR_MD5_NOT_AVAILABLE_0), e); 476 } 477 478 throw new UnavailableException(Messages.get().getBundle().key(Messages.ERR_MD5_NOT_AVAILABLE_0)); 479 } 480 481 // Instantiate repository from init-param 482 String repositoryName = getInitParameter(INIT_PARAM_REPOSITORY); 483 if (repositoryName == null) { 484 485 if (LOG.isErrorEnabled()) { 486 LOG.error(Messages.get().getBundle().key(Messages.ERR_INIT_PARAM_MISSING_1, INIT_PARAM_REPOSITORY)); 487 } 488 489 throw new ServletException( 490 Messages.get().getBundle().key(Messages.ERR_INIT_PARAM_MISSING_1, INIT_PARAM_REPOSITORY)); 491 } 492 493 m_repository = OpenCms.getRepositoryManager().getRepository(repositoryName, A_CmsRepository.class); 494 if (m_repository == null) { 495 496 if (LOG.isErrorEnabled()) { 497 LOG.error(Messages.get().getBundle().key(Messages.ERR_REPOSITORY_NOT_FOUND_1, repositoryName)); 498 } 499 500 throw new ServletException( 501 Messages.get().getBundle().key(Messages.ERR_REPOSITORY_NOT_FOUND_1, repositoryName)); 502 } 503 504 if (LOG.isInfoEnabled()) { 505 LOG.info(Messages.get().getBundle().key(Messages.LOG_USE_REPOSITORY_1, repositoryName)); 506 } 507 } 508 509 /** 510 * Copy the contents of the specified input stream to the specified 511 * output stream, and ensure that both streams are closed before returning 512 * (even in the face of an exception).<p> 513 * 514 * @param item the RepositoryItem 515 * @param is the input stream to copy from 516 * @param writer the writer to write to 517 * 518 * @throws IOException if an input/output error occurs 519 */ 520 protected void copy(I_CmsRepositoryItem item, InputStream is, PrintWriter writer) throws IOException { 521 522 IOException exception = null; 523 524 InputStream resourceInputStream = null; 525 if (!item.isCollection()) { 526 resourceInputStream = new ByteArrayInputStream(item.getContent()); 527 } else { 528 resourceInputStream = is; 529 } 530 531 Reader reader = new InputStreamReader(resourceInputStream); 532 533 // Copy the input stream to the output stream 534 exception = copyRange(reader, writer); 535 536 // Clean up the reader 537 try { 538 reader.close(); 539 } catch (Exception e) { 540 if (LOG.isErrorEnabled()) { 541 LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_READER_0), e); 542 } 543 } 544 545 // Rethrow any exception that has occurred 546 if (exception != null) { 547 throw exception; 548 } 549 } 550 551 /** 552 * Copy the contents of the specified input stream to the specified 553 * output stream, and ensure that both streams are closed before returning 554 * (even in the face of an exception).<p> 555 * 556 * @param item the RepositoryItem 557 * @param is the input stream to copy from 558 * @param ostream the output stream to write to 559 * 560 * @throws IOException if an input/output error occurs 561 */ 562 protected void copy(I_CmsRepositoryItem item, InputStream is, ServletOutputStream ostream) throws IOException { 563 564 IOException exception = null; 565 InputStream resourceInputStream = null; 566 567 // Optimization: If the binary content has already been loaded, send 568 // it directly 569 if (!item.isCollection()) { 570 byte[] buffer = item.getContent(); 571 if (buffer != null) { 572 ostream.write(buffer, 0, buffer.length); 573 return; 574 } 575 resourceInputStream = new ByteArrayInputStream(item.getContent()); 576 } else { 577 resourceInputStream = is; 578 } 579 580 InputStream istream = new BufferedInputStream(resourceInputStream, m_input); 581 582 // Copy the input stream to the output stream 583 exception = copyRange(istream, ostream); 584 585 // Clean up the input stream 586 try { 587 istream.close(); 588 } catch (Exception e) { 589 if (LOG.isErrorEnabled()) { 590 LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_INPUT_STREAM_0), e); 591 } 592 } 593 594 // Rethrow any exception that has occurred 595 if (exception != null) { 596 throw exception; 597 } 598 } 599 600 /** 601 * Copy the contents of the specified input stream to the specified 602 * output stream, and ensure that both streams are closed before returning 603 * (even in the face of an exception).<p> 604 * 605 * @param item the RepositoryItem 606 * @param writer the writer to write to 607 * @param range the range the client wants to retrieve 608 * 609 * @throws IOException if an input/output error occurs 610 */ 611 protected void copy(I_CmsRepositoryItem item, PrintWriter writer, CmsWebdavRange range) throws IOException { 612 613 IOException exception = null; 614 615 InputStream resourceInputStream = new ByteArrayInputStream(item.getContent()); 616 617 Reader reader = new InputStreamReader(resourceInputStream); 618 exception = copyRange(reader, writer, range.getStart(), range.getEnd()); 619 620 // Clean up the input stream 621 try { 622 reader.close(); 623 } catch (Exception e) { 624 if (LOG.isErrorEnabled()) { 625 LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_READER_0), e); 626 } 627 } 628 629 // Rethrow any exception that has occurred 630 if (exception != null) { 631 throw exception; 632 } 633 } 634 635 /** 636 * Copy the contents of the specified input stream to the specified 637 * output stream, and ensure that both streams are closed before returning 638 * (even in the face of an exception).<p> 639 * 640 * @param item the RepositoryItem 641 * @param writer the writer to write to 642 * @param ranges iterator of the ranges the client wants to retrieve 643 * @param contentType the content type of the resource 644 * 645 * @throws IOException if an input/output error occurs 646 */ 647 protected void copy( 648 I_CmsRepositoryItem item, 649 PrintWriter writer, 650 Iterator<CmsWebdavRange> ranges, 651 String contentType) throws IOException { 652 653 IOException exception = null; 654 655 while ((exception == null) && (ranges.hasNext())) { 656 657 InputStream resourceInputStream = new ByteArrayInputStream(item.getContent()); 658 659 Reader reader = new InputStreamReader(resourceInputStream); 660 CmsWebdavRange currentRange = ranges.next(); 661 662 // Writing MIME header. 663 writer.println(); 664 writer.println("--" + MIME_SEPARATION); 665 if (contentType != null) { 666 writer.println("Content-Type: " + contentType); 667 } 668 writer.println( 669 "Content-Range: bytes " 670 + currentRange.getStart() 671 + "-" 672 + currentRange.getEnd() 673 + "/" 674 + currentRange.getLength()); 675 writer.println(); 676 677 // Printing content 678 exception = copyRange(reader, writer, currentRange.getStart(), currentRange.getEnd()); 679 680 try { 681 reader.close(); 682 } catch (Exception e) { 683 if (LOG.isErrorEnabled()) { 684 LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_READER_0), e); 685 } 686 } 687 688 } 689 690 writer.println(); 691 writer.print("--" + MIME_SEPARATION + "--"); 692 693 // Rethrow any exception that has occurred 694 if (exception != null) { 695 throw exception; 696 } 697 } 698 699 /** 700 * Copy the contents of the specified input stream to the specified 701 * output stream, and ensure that both streams are closed before returning 702 * (even in the face of an exception).<p> 703 * 704 * @param item the RepositoryItem 705 * @param ostream the output stream to write to 706 * @param range the range the client wants to retrieve 707 * 708 * @throws IOException if an input/output error occurs 709 */ 710 protected void copy(I_CmsRepositoryItem item, ServletOutputStream ostream, CmsWebdavRange range) 711 throws IOException { 712 713 IOException exception = null; 714 715 InputStream resourceInputStream = new ByteArrayInputStream(item.getContent()); 716 InputStream istream = new BufferedInputStream(resourceInputStream, m_input); 717 exception = copyRange(istream, ostream, range.getStart(), range.getEnd()); 718 719 // Clean up the input stream 720 try { 721 istream.close(); 722 } catch (Exception e) { 723 if (LOG.isErrorEnabled()) { 724 LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_INPUT_STREAM_0), e); 725 } 726 } 727 728 // Rethrow any exception that has occurred 729 if (exception != null) { 730 throw exception; 731 } 732 } 733 734 /** 735 * Copy the contents of the specified input stream to the specified 736 * output stream, and ensure that both streams are closed before returning 737 * (even in the face of an exception).<p> 738 * 739 * @param item the RepositoryItem 740 * @param ostream the output stream to write to 741 * @param ranges iterator of the ranges the client wants to retrieve 742 * @param contentType the content type of the resource 743 * 744 * @throws IOException if an input/output error occurs 745 */ 746 protected void copy( 747 I_CmsRepositoryItem item, 748 ServletOutputStream ostream, 749 Iterator<CmsWebdavRange> ranges, 750 String contentType) throws IOException { 751 752 IOException exception = null; 753 754 while ((exception == null) && (ranges.hasNext())) { 755 756 InputStream resourceInputStream = new ByteArrayInputStream(item.getContent()); 757 InputStream istream = new BufferedInputStream(resourceInputStream, m_input); 758 759 CmsWebdavRange currentRange = ranges.next(); 760 761 // Writing MIME header. 762 ostream.println(); 763 ostream.println("--" + MIME_SEPARATION); 764 if (contentType != null) { 765 ostream.println("Content-Type: " + contentType); 766 } 767 ostream.println( 768 "Content-Range: bytes " 769 + currentRange.getStart() 770 + "-" 771 + currentRange.getEnd() 772 + "/" 773 + currentRange.getLength()); 774 ostream.println(); 775 776 // Printing content 777 exception = copyRange(istream, ostream, currentRange.getStart(), currentRange.getEnd()); 778 779 try { 780 istream.close(); 781 } catch (Exception e) { 782 if (LOG.isErrorEnabled()) { 783 LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_INPUT_STREAM_0), e); 784 } 785 } 786 787 } 788 789 ostream.println(); 790 ostream.print("--" + MIME_SEPARATION + "--"); 791 792 // Rethrow any exception that has occurred 793 if (exception != null) { 794 throw exception; 795 } 796 } 797 798 /** 799 * Copy the contents of the specified input stream to the specified 800 * output stream, and ensure that both streams are closed before returning 801 * (even in the face of an exception).<p> 802 * 803 * @param istream the input stream to read from 804 * @param ostream the output stream to write to 805 * 806 * @return the exception which occurred during processing 807 */ 808 protected IOException copyRange(InputStream istream, ServletOutputStream ostream) { 809 810 // Copy the input stream to the output stream 811 IOException exception = null; 812 byte[] buffer = new byte[m_input]; 813 int len = buffer.length; 814 while (true) { 815 try { 816 len = istream.read(buffer); 817 if (len == -1) { 818 break; 819 } 820 ostream.write(buffer, 0, len); 821 } catch (IOException e) { 822 exception = e; 823 len = -1; 824 break; 825 } 826 } 827 return exception; 828 } 829 830 /** 831 * Copy the contents of the specified input stream to the specified 832 * output stream, and ensure that both streams are closed before returning 833 * (even in the face of an exception).<p> 834 * 835 * @param istream the input stream to read from 836 * @param ostream the output stream to write to 837 * @param start the start of the range which will be copied 838 * @param end the end of the range which will be copied 839 * 840 * @return the exception which occurred during processing 841 */ 842 protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) { 843 844 if (LOG.isDebugEnabled()) { 845 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_BYTES_2, new Long(start), new Long(end))); 846 } 847 848 try { 849 istream.skip(start); 850 } catch (IOException e) { 851 return e; 852 } 853 854 IOException exception = null; 855 long bytesToRead = (end - start) + 1; 856 857 byte[] buffer = new byte[m_input]; 858 int len = buffer.length; 859 while ((bytesToRead > 0) && (len >= buffer.length)) { 860 try { 861 len = istream.read(buffer); 862 if (bytesToRead >= len) { 863 ostream.write(buffer, 0, len); 864 bytesToRead -= len; 865 } else { 866 ostream.write(buffer, 0, (int)bytesToRead); 867 bytesToRead = 0; 868 } 869 } catch (IOException e) { 870 exception = e; 871 len = -1; 872 } 873 874 if (len < buffer.length) { 875 break; 876 } 877 } 878 879 return exception; 880 } 881 882 /** 883 * Copy the contents of the specified input stream to the specified 884 * output stream, and ensure that both streams are closed before returning 885 * (even in the face of an exception).<p> 886 * 887 * @param reader the reader to read from 888 * @param writer the writer to write to 889 * 890 * @return the exception which occurred during processing 891 */ 892 protected IOException copyRange(Reader reader, PrintWriter writer) { 893 894 // Copy the input stream to the output stream 895 IOException exception = null; 896 char[] buffer = new char[m_input]; 897 int len = buffer.length; 898 while (true) { 899 try { 900 len = reader.read(buffer); 901 if (len == -1) { 902 break; 903 } 904 writer.write(buffer, 0, len); 905 } catch (IOException e) { 906 exception = e; 907 len = -1; 908 break; 909 } 910 } 911 return exception; 912 } 913 914 /** 915 * Copy the contents of the specified input stream to the specified 916 * output stream, and ensure that both streams are closed before returning 917 * (even in the face of an exception).<p> 918 * 919 * @param reader the reader to read from 920 * @param writer the writer to write to 921 * @param start the start of the range which will be copied 922 * @param end the end of the range which will be copied 923 * 924 * @return the exception which occurred during processing 925 */ 926 protected IOException copyRange(Reader reader, PrintWriter writer, long start, long end) { 927 928 try { 929 reader.skip(start); 930 } catch (IOException e) { 931 return e; 932 } 933 934 IOException exception = null; 935 long bytesToRead = (end - start) + 1; 936 937 char[] buffer = new char[m_input]; 938 int len = buffer.length; 939 while ((bytesToRead > 0) && (len >= buffer.length)) { 940 try { 941 len = reader.read(buffer); 942 if (bytesToRead >= len) { 943 writer.write(buffer, 0, len); 944 bytesToRead -= len; 945 } else { 946 writer.write(buffer, 0, (int)bytesToRead); 947 bytesToRead = 0; 948 } 949 } catch (IOException e) { 950 exception = e; 951 len = -1; 952 } 953 954 if (len < buffer.length) { 955 break; 956 } 957 } 958 959 return exception; 960 } 961 962 /** 963 * Process a COPY WebDAV request for the specified resource.<p> 964 * 965 * @param req the servlet request we are processing 966 * @param resp the servlet response we are creating 967 */ 968 protected void doCopy(HttpServletRequest req, HttpServletResponse resp) { 969 970 // Check if webdav is set to read only 971 if (m_readOnly) { 972 973 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 974 975 if (LOG.isDebugEnabled()) { 976 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 977 } 978 979 return; 980 } 981 982 // Get the source path to copy 983 String src = getRelativePath(req); 984 985 // Check if source exists 986 if (!m_session.exists(src)) { 987 988 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 989 990 if (LOG.isDebugEnabled()) { 991 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src)); 992 } 993 994 return; 995 } 996 997 // Get the destination path to copy to 998 String dest = parseDestinationHeader(req); 999 if (dest == null) { 1000 1001 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1002 1003 if (LOG.isDebugEnabled()) { 1004 LOG.debug(Messages.get().getBundle().key(Messages.LOG_PARSE_DEST_HEADER_0)); 1005 } 1006 1007 return; 1008 } 1009 1010 // source and destination are the same 1011 if (dest.equals(src)) { 1012 1013 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1014 1015 if (LOG.isDebugEnabled()) { 1016 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SRC_DEST_EQUALS_0)); 1017 } 1018 1019 return; 1020 } 1021 1022 // Parsing overwrite header 1023 boolean overwrite = parseOverwriteHeader(req); 1024 1025 // If the destination exists, then it's a conflict 1026 if ((m_session.exists(dest)) && (!overwrite)) { 1027 1028 resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED); 1029 1030 if (LOG.isDebugEnabled()) { 1031 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEST_PATH_EXISTS_1, dest)); 1032 } 1033 1034 return; 1035 } 1036 1037 if ((!m_session.exists(dest)) && (overwrite)) { 1038 resp.setStatus(CmsWebdavStatus.SC_CREATED); 1039 } 1040 1041 // Copying source to destination 1042 try { 1043 1044 if (LOG.isDebugEnabled()) { 1045 LOG.debug(Messages.get().getBundle().key(Messages.LOG_COPY_ITEM_2, src, dest)); 1046 } 1047 1048 m_session.copy(src, dest, overwrite); 1049 } catch (CmsSecurityException sex) { 1050 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1051 1052 if (LOG.isDebugEnabled()) { 1053 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0)); 1054 } 1055 1056 return; 1057 } catch (CmsVfsResourceAlreadyExistsException raeex) { 1058 1059 // should never happen 1060 resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED); 1061 1062 if (LOG.isDebugEnabled()) { 1063 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, dest)); 1064 } 1065 1066 return; 1067 } catch (CmsVfsResourceNotFoundException rnfex) { 1068 1069 // should never happen 1070 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1071 1072 if (LOG.isDebugEnabled()) { 1073 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src)); 1074 } 1075 1076 return; 1077 } catch (CmsException ex) { 1078 resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR); 1079 1080 if (LOG.isErrorEnabled()) { 1081 LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "COPY", src), ex); 1082 } 1083 return; 1084 } 1085 1086 if (LOG.isDebugEnabled()) { 1087 LOG.debug(Messages.get().getBundle().key(Messages.LOG_COPY_SUCCESS_0)); 1088 } 1089 } 1090 1091 /** 1092 * Process a DELETE WebDAV request for the specified resource.<p> 1093 * 1094 * @param req the servlet request we are processing 1095 * @param resp the servlet response we are creating 1096 * 1097 * @throws IOException if an input/output error occurs 1098 */ 1099 @Override 1100 protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { 1101 1102 // Get the path to delete 1103 String path = getRelativePath(req); 1104 1105 // Check if webdav is set to read only 1106 if (m_readOnly) { 1107 1108 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1109 1110 if (LOG.isDebugEnabled()) { 1111 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 1112 } 1113 1114 return; 1115 } 1116 1117 // Check if path exists 1118 boolean exists = m_session.exists(path); 1119 if (!exists) { 1120 1121 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1122 1123 if (LOG.isDebugEnabled()) { 1124 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path)); 1125 } 1126 1127 return; 1128 } 1129 1130 // Check if resource is locked 1131 if (isLocked(req)) { 1132 1133 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 1134 1135 if (LOG.isDebugEnabled()) { 1136 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path)); 1137 } 1138 1139 return; 1140 } 1141 1142 // Check if resources found in the tree of the path are locked 1143 Hashtable<String, Integer> errorList = new Hashtable<String, Integer>(); 1144 1145 checkChildLocks(req, path, errorList); 1146 if (!errorList.isEmpty()) { 1147 sendReport(req, resp, errorList); 1148 1149 if (LOG.isDebugEnabled()) { 1150 Iterator<String> iter = errorList.keySet().iterator(); 1151 while (iter.hasNext()) { 1152 String errorPath = iter.next(); 1153 LOG.debug(Messages.get().getBundle().key(Messages.LOG_CHILD_LOCKED_1, errorPath)); 1154 } 1155 } 1156 1157 return; 1158 } 1159 1160 // Delete the resource 1161 try { 1162 1163 if (LOG.isDebugEnabled()) { 1164 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_ITEM_0)); 1165 } 1166 1167 m_session.delete(path); 1168 } catch (CmsVfsResourceNotFoundException rnfex) { 1169 1170 // should never happen 1171 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1172 return; 1173 } catch (CmsSecurityException sex) { 1174 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1175 1176 if (LOG.isDebugEnabled()) { 1177 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0)); 1178 } 1179 1180 return; 1181 } catch (CmsException ex) { 1182 resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR); 1183 1184 if (LOG.isErrorEnabled()) { 1185 LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "DELETE", path), ex); 1186 } 1187 1188 return; 1189 } 1190 1191 if (LOG.isDebugEnabled()) { 1192 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_SUCCESS_0)); 1193 } 1194 1195 resp.setStatus(CmsWebdavStatus.SC_NO_CONTENT); 1196 } 1197 1198 /** 1199 * Process a GET request for the specified resource.<p> 1200 * 1201 * @param request the servlet request we are processing 1202 * @param response the servlet response we are creating 1203 * 1204 * @throws IOException if an input/output error occurs 1205 */ 1206 @Override 1207 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 1208 1209 // Serve the requested resource, including the data content 1210 serveResource(request, response, true); 1211 } 1212 1213 /** 1214 * Process a HEAD request for the specified resource.<p> 1215 * 1216 * @param request the servlet request we are processing 1217 * @param response the servlet response we are creating 1218 * 1219 * @throws IOException if an input/output error occurs 1220 */ 1221 @Override 1222 protected void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException { 1223 1224 // Serve the requested resource, without the data content 1225 serveResource(request, response, false); 1226 } 1227 1228 /** 1229 * Process a LOCK WebDAV request for the specified resource.<p> 1230 * 1231 * @param req the servlet request we are processing 1232 * @param resp the servlet response we are creating 1233 * 1234 * @throws IOException if an input/output error occurs 1235 */ 1236 @SuppressWarnings("unchecked") 1237 protected void doLock(HttpServletRequest req, HttpServletResponse resp) throws IOException { 1238 1239 String path = getRelativePath(req); 1240 1241 // Check if webdav is set to read only 1242 if (m_readOnly) { 1243 1244 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1245 1246 if (LOG.isDebugEnabled()) { 1247 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 1248 } 1249 1250 return; 1251 } 1252 1253 // Check if resource is locked 1254 if (isLocked(req)) { 1255 1256 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 1257 1258 if (LOG.isDebugEnabled()) { 1259 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path)); 1260 } 1261 1262 return; 1263 } 1264 1265 CmsRepositoryLockInfo lock = new CmsRepositoryLockInfo(); 1266 1267 // Parsing depth header 1268 String depthStr = req.getHeader(HEADER_DEPTH); 1269 if (depthStr == null) { 1270 lock.setDepth(CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE); 1271 } else { 1272 if (depthStr.equals("0")) { 1273 lock.setDepth(0); 1274 } else { 1275 lock.setDepth(CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE); 1276 } 1277 } 1278 1279 // Parsing timeout header 1280 int lockDuration = CmsRepositoryLockInfo.TIMEOUT_INFINITE_VALUE; 1281 lock.setExpiresAt(System.currentTimeMillis() + (lockDuration * 1000)); 1282 1283 int lockRequestType = LOCK_CREATION; 1284 1285 Element lockInfoNode = null; 1286 1287 try { 1288 SAXReader saxReader = new SAXReader(); 1289 Document document = saxReader.read(new InputSource(req.getInputStream())); 1290 1291 // Get the root element of the document 1292 Element rootElement = document.getRootElement(); 1293 lockInfoNode = rootElement; 1294 } catch (Exception e) { 1295 lockRequestType = LOCK_REFRESH; 1296 } 1297 1298 if (lockInfoNode != null) { 1299 1300 // Reading lock information 1301 Iterator<Element> iter = lockInfoNode.elementIterator(); 1302 1303 Element lockScopeNode = null; 1304 Element lockTypeNode = null; 1305 Element lockOwnerNode = null; 1306 1307 while (iter.hasNext()) { 1308 Element currentElem = iter.next(); 1309 switch (currentElem.getNodeType()) { 1310 case Node.TEXT_NODE: 1311 break; 1312 case Node.ELEMENT_NODE: 1313 String nodeName = currentElem.getName(); 1314 if (nodeName.endsWith(TAG_LOCKSCOPE)) { 1315 lockScopeNode = currentElem; 1316 } 1317 if (nodeName.endsWith(TAG_LOCKTYPE)) { 1318 lockTypeNode = currentElem; 1319 } 1320 if (nodeName.endsWith(TAG_OWNER)) { 1321 lockOwnerNode = currentElem; 1322 } 1323 break; 1324 default: 1325 break; 1326 } 1327 } 1328 1329 if (lockScopeNode != null) { 1330 1331 iter = lockScopeNode.elementIterator(); 1332 while (iter.hasNext()) { 1333 Element currentElem = iter.next(); 1334 switch (currentElem.getNodeType()) { 1335 case Node.TEXT_NODE: 1336 break; 1337 case Node.ELEMENT_NODE: 1338 String tempScope = currentElem.getName(); 1339 if (tempScope.indexOf(':') != -1) { 1340 lock.setScope(tempScope.substring(tempScope.indexOf(':') + 1)); 1341 } else { 1342 lock.setScope(tempScope); 1343 } 1344 break; 1345 default: 1346 break; 1347 } 1348 } 1349 1350 if (lock.getScope() == null) { 1351 1352 // Bad request 1353 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1354 } 1355 1356 } else { 1357 1358 // Bad request 1359 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1360 } 1361 1362 if (lockTypeNode != null) { 1363 1364 iter = lockTypeNode.elementIterator(); 1365 while (iter.hasNext()) { 1366 Element currentElem = iter.next(); 1367 switch (currentElem.getNodeType()) { 1368 case Node.TEXT_NODE: 1369 break; 1370 case Node.ELEMENT_NODE: 1371 String tempType = currentElem.getName(); 1372 if (tempType.indexOf(':') != -1) { 1373 lock.setType(tempType.substring(tempType.indexOf(':') + 1)); 1374 } else { 1375 lock.setType(tempType); 1376 } 1377 break; 1378 default: 1379 break; 1380 } 1381 } 1382 1383 if (lock.getType() == null) { 1384 1385 // Bad request 1386 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1387 } 1388 1389 } else { 1390 1391 // Bad request 1392 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1393 } 1394 1395 if (lockOwnerNode != null) { 1396 1397 iter = lockOwnerNode.elementIterator(); 1398 while (iter.hasNext()) { 1399 Element currentElem = iter.next(); 1400 switch (currentElem.getNodeType()) { 1401 case Node.TEXT_NODE: 1402 lock.setOwner(lock.getOwner() + currentElem.getStringValue()); 1403 break; 1404 case Node.ELEMENT_NODE: 1405 lock.setOwner(lock.getOwner() + currentElem.getStringValue()); 1406 break; 1407 default: 1408 break; 1409 } 1410 } 1411 1412 if (lock.getOwner() == null) { 1413 1414 // Bad request 1415 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1416 } 1417 1418 } else { 1419 lock.setOwner(""); 1420 } 1421 1422 } 1423 1424 lock.setPath(path); 1425 lock.setUsername(m_username); 1426 1427 if (lockRequestType == LOCK_REFRESH) { 1428 1429 CmsRepositoryLockInfo currentLock = m_session.getLock(path); 1430 if (currentLock == null) { 1431 lockRequestType = LOCK_CREATION; 1432 } 1433 } 1434 1435 if (lockRequestType == LOCK_CREATION) { 1436 1437 try { 1438 1439 if (LOG.isDebugEnabled()) { 1440 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_1, lock.getOwner())); 1441 } 1442 1443 boolean result = m_session.lock(path, lock); 1444 if (result) { 1445 1446 // Add the Lock-Token header as by RFC 2518 8.10.1 1447 // - only do this for newly created locks 1448 resp.addHeader(HEADER_LOCKTOKEN, "<opaquelocktoken:" + generateLockToken(req, lock) + ">"); 1449 1450 if (LOG.isDebugEnabled()) { 1451 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_FAILED_0)); 1452 } 1453 1454 } else { 1455 1456 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 1457 1458 if (LOG.isDebugEnabled()) { 1459 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_SUCCESS_0)); 1460 } 1461 1462 return; 1463 } 1464 } catch (CmsVfsResourceNotFoundException rnfex) { 1465 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1466 1467 if (LOG.isDebugEnabled()) { 1468 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path)); 1469 } 1470 1471 return; 1472 } catch (CmsSecurityException sex) { 1473 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1474 1475 if (LOG.isDebugEnabled()) { 1476 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0)); 1477 } 1478 1479 return; 1480 } catch (CmsException ex) { 1481 resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR); 1482 1483 if (LOG.isErrorEnabled()) { 1484 LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "LOCK", path), ex); 1485 } 1486 1487 return; 1488 } 1489 } 1490 1491 // Set the status, then generate the XML response containing 1492 // the lock information 1493 Document doc = DocumentHelper.createDocument(); 1494 Element propElem = doc.addElement(new QName(TAG_PROP, Namespace.get(DEFAULT_NAMESPACE))); 1495 1496 Element lockElem = addElement(propElem, TAG_LOCKDISCOVERY); 1497 addLockElement(lock, lockElem, generateLockToken(req, lock)); 1498 1499 resp.setStatus(CmsWebdavStatus.SC_OK); 1500 resp.setContentType("text/xml; charset=UTF-8"); 1501 1502 Writer writer = resp.getWriter(); 1503 doc.write(writer); 1504 writer.close(); 1505 } 1506 1507 /** 1508 * Process a MKCOL WebDAV request for the specified resource.<p> 1509 * 1510 * @param req the servlet request we are processing 1511 * @param resp the servlet response we are creating 1512 * 1513 * @throws IOException if an input/output error occurs 1514 */ 1515 protected void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws IOException { 1516 1517 String path = getRelativePath(req); 1518 1519 // Check if Webdav is read only 1520 if (m_readOnly) { 1521 1522 if (LOG.isDebugEnabled()) { 1523 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 1524 } 1525 1526 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1527 return; 1528 } 1529 1530 // Check if resource is locked 1531 if (isLocked(req)) { 1532 1533 if (LOG.isDebugEnabled()) { 1534 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path)); 1535 } 1536 1537 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 1538 return; 1539 } 1540 1541 boolean exists = m_session.exists(path); 1542 1543 // Can't create a collection if a resource already exists at the given path 1544 if (exists) { 1545 1546 if (LOG.isDebugEnabled()) { 1547 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, path)); 1548 } 1549 1550 // Get allowed methods 1551 StringBuffer methodsAllowed = determineMethodsAllowed(getRelativePath(req)); 1552 resp.addHeader(HEADER_ALLOW, methodsAllowed.toString()); 1553 resp.setStatus(CmsWebdavStatus.SC_METHOD_NOT_ALLOWED); 1554 return; 1555 } 1556 1557 if (req.getInputStream().available() > 0) { 1558 try { 1559 new SAXReader().read(req.getInputStream()); 1560 1561 // TODO: Process this request body (from Apache Tomcat) 1562 resp.setStatus(CmsWebdavStatus.SC_NOT_IMPLEMENTED); 1563 return; 1564 1565 } catch (DocumentException de) { 1566 1567 // Parse error - assume invalid content 1568 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1569 1570 if (LOG.isDebugEnabled()) { 1571 LOG.debug(Messages.get().getBundle().key(Messages.LOG_INVALID_CONTENT_0)); 1572 } 1573 1574 return; 1575 } 1576 } 1577 1578 // call session to create collection 1579 try { 1580 1581 if (LOG.isDebugEnabled()) { 1582 LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_COLLECTION_0)); 1583 } 1584 1585 m_session.create(path); 1586 } catch (CmsVfsResourceAlreadyExistsException raeex) { 1587 1588 // should never happen, because it was checked if the item exists before 1589 resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED); 1590 1591 if (LOG.isDebugEnabled()) { 1592 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, path)); 1593 } 1594 1595 return; 1596 } catch (CmsSecurityException sex) { 1597 1598 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1599 1600 if (LOG.isDebugEnabled()) { 1601 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0)); 1602 } 1603 1604 return; 1605 } catch (CmsException ex) { 1606 1607 resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR); 1608 1609 if (LOG.isErrorEnabled()) { 1610 LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "MKCOL", path), ex); 1611 } 1612 1613 return; 1614 } 1615 1616 if (LOG.isDebugEnabled()) { 1617 LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_SUCCESS_0)); 1618 } 1619 1620 resp.setStatus(CmsWebdavStatus.SC_CREATED); 1621 } 1622 1623 /** 1624 * Process a MOVE WebDAV request for the specified resource.<p> 1625 * 1626 * @param req the servlet request we are processing 1627 * @param resp the servlet response we are creating 1628 */ 1629 protected void doMove(HttpServletRequest req, HttpServletResponse resp) { 1630 1631 // Get source path 1632 String src = getRelativePath(req); 1633 1634 // Check if Webdav is read only 1635 if (m_readOnly) { 1636 1637 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1638 1639 if (LOG.isDebugEnabled()) { 1640 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 1641 } 1642 1643 return; 1644 } 1645 1646 // Check if resource is locked 1647 if (isLocked(req)) { 1648 1649 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 1650 1651 if (LOG.isDebugEnabled()) { 1652 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, src)); 1653 } 1654 1655 return; 1656 } 1657 1658 // Parsing destination header 1659 String dest = parseDestinationHeader(req); 1660 if (dest == null) { 1661 1662 resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST); 1663 1664 if (LOG.isDebugEnabled()) { 1665 LOG.debug(Messages.get().getBundle().key(Messages.LOG_PARSE_DEST_HEADER_0)); 1666 } 1667 1668 return; 1669 } 1670 1671 // source and destination are the same 1672 if (dest.equals(src)) { 1673 1674 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1675 1676 if (LOG.isDebugEnabled()) { 1677 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SRC_DEST_EQUALS_0)); 1678 } 1679 1680 return; 1681 } 1682 1683 // Parsing overwrite header 1684 boolean overwrite = parseOverwriteHeader(req); 1685 1686 // Check if source exists 1687 if (!m_session.exists(src)) { 1688 1689 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1690 1691 if (LOG.isDebugEnabled()) { 1692 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src)); 1693 } 1694 1695 return; 1696 } 1697 1698 // If the destination exists, then it's a conflict 1699 if ((m_session.exists(dest)) && (!overwrite)) { 1700 1701 resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED); 1702 1703 if (LOG.isDebugEnabled()) { 1704 LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEST_PATH_EXISTS_1, dest)); 1705 } 1706 1707 return; 1708 } 1709 1710 if ((!m_session.exists(dest)) && (overwrite)) { 1711 resp.setStatus(CmsWebdavStatus.SC_CREATED); 1712 } 1713 1714 // trigger move in session handler 1715 try { 1716 1717 if (LOG.isDebugEnabled()) { 1718 LOG.debug(Messages.get().getBundle().key(Messages.LOG_MOVE_ITEM_2, src, dest)); 1719 } 1720 1721 m_session.move(src, dest, overwrite); 1722 } catch (CmsVfsResourceNotFoundException rnfex) { 1723 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1724 1725 if (LOG.isDebugEnabled()) { 1726 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src)); 1727 } 1728 1729 return; 1730 } catch (CmsSecurityException sex) { 1731 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1732 1733 if (LOG.isDebugEnabled()) { 1734 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0)); 1735 } 1736 1737 return; 1738 } catch (CmsVfsResourceAlreadyExistsException raeex) { 1739 resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED); 1740 1741 if (LOG.isDebugEnabled()) { 1742 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, dest)); 1743 } 1744 1745 return; 1746 } catch (CmsException ex) { 1747 resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR); 1748 1749 if (LOG.isErrorEnabled()) { 1750 LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "MOVE", src), ex); 1751 } 1752 1753 return; 1754 } 1755 1756 if (LOG.isDebugEnabled()) { 1757 LOG.debug(Messages.get().getBundle().key(Messages.LOG_MOVE_ITEM_SUCCESS_0)); 1758 } 1759 } 1760 1761 /** 1762 * Process a OPTIONS WebDAV request for the specified resource.<p> 1763 * 1764 * @param req the servlet request we are processing 1765 * @param resp the servlet response we are creating 1766 */ 1767 @Override 1768 protected void doOptions(HttpServletRequest req, HttpServletResponse resp) { 1769 1770 resp.addHeader("DAV", "1,2"); 1771 1772 StringBuffer methodsAllowed = determineMethodsAllowed(getRelativePath(req)); 1773 1774 resp.addHeader(HEADER_ALLOW, methodsAllowed.toString()); 1775 resp.addHeader("MS-Author-Via", "DAV"); 1776 } 1777 1778 /** 1779 * Process a PROPFIND WebDAV request for the specified resource.<p> 1780 * 1781 * @param req the servlet request we are processing 1782 * @param resp the servlet response we are creating 1783 * 1784 * @throws IOException if an input/output error occurs 1785 */ 1786 protected void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws IOException { 1787 1788 String path = getRelativePath(req); 1789 1790 if (!m_listings) { 1791 1792 // Get allowed methods 1793 StringBuffer methodsAllowed = determineMethodsAllowed(getRelativePath(req)); 1794 1795 resp.addHeader(HEADER_ALLOW, methodsAllowed.toString()); 1796 resp.setStatus(CmsWebdavStatus.SC_METHOD_NOT_ALLOWED); 1797 return; 1798 } 1799 1800 // Properties which are to be displayed. 1801 List<String> properties = new Vector<String>(); 1802 1803 // Propfind depth 1804 int depth = CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE; 1805 1806 // Propfind type 1807 int type = FIND_ALL_PROP; 1808 1809 String depthStr = req.getHeader(HEADER_DEPTH); 1810 1811 if (depthStr == null) { 1812 depth = CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE; 1813 } else { 1814 if (depthStr.equals("0")) { 1815 depth = 0; 1816 } else if (depthStr.equals("1")) { 1817 depth = 1; 1818 } else if (depthStr.equalsIgnoreCase(DEPTH_INFINITY)) { 1819 depth = CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE; 1820 } 1821 } 1822 1823 Element propNode = null; 1824 1825 try { 1826 SAXReader saxReader = new SAXReader(); 1827 Document document = saxReader.read(req.getInputStream()); 1828 1829 // Get the root element of the document 1830 Element rootElement = document.getRootElement(); 1831 @SuppressWarnings("unchecked") 1832 Iterator<Element> iter = rootElement.elementIterator(); 1833 1834 while (iter.hasNext()) { 1835 Element currentElem = iter.next(); 1836 switch (currentElem.getNodeType()) { 1837 case Node.TEXT_NODE: 1838 break; 1839 case Node.ELEMENT_NODE: 1840 if (currentElem.getName().endsWith("prop")) { 1841 type = FIND_BY_PROPERTY; 1842 propNode = currentElem; 1843 } 1844 if (currentElem.getName().endsWith("propname")) { 1845 type = FIND_PROPERTY_NAMES; 1846 } 1847 if (currentElem.getName().endsWith("allprop")) { 1848 type = FIND_ALL_PROP; 1849 } 1850 break; 1851 default: 1852 break; 1853 } 1854 } 1855 } catch (Exception e) { 1856 // Most likely there was no content : we use the defaults. 1857 } 1858 1859 if (propNode != null) { 1860 if (type == FIND_BY_PROPERTY) { 1861 @SuppressWarnings("unchecked") 1862 Iterator<Element> iter = propNode.elementIterator(); 1863 while (iter.hasNext()) { 1864 Element currentElem = iter.next(); 1865 switch (currentElem.getNodeType()) { 1866 case Node.TEXT_NODE: 1867 break; 1868 case Node.ELEMENT_NODE: 1869 String nodeName = currentElem.getName(); 1870 String propertyName = null; 1871 if (nodeName.indexOf(':') != -1) { 1872 propertyName = nodeName.substring(nodeName.indexOf(':') + 1); 1873 } else { 1874 propertyName = nodeName; 1875 } 1876 // href is a live property which is handled differently 1877 properties.add(propertyName); 1878 break; 1879 default: 1880 break; 1881 } 1882 } 1883 } 1884 } 1885 1886 boolean exists = m_session.exists(path); 1887 if (!exists) { 1888 1889 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1890 1891 if (LOG.isDebugEnabled()) { 1892 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path)); 1893 } 1894 1895 return; 1896 } 1897 1898 I_CmsRepositoryItem item = null; 1899 try { 1900 item = m_session.getItem(path); 1901 } catch (CmsException e) { 1902 resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND); 1903 return; 1904 } 1905 1906 resp.setStatus(CmsWebdavStatus.SC_MULTI_STATUS); 1907 resp.setContentType("text/xml; charset=UTF-8"); 1908 1909 // Create multistatus object 1910 Document doc = DocumentHelper.createDocument(); 1911 Element multiStatusElem = doc.addElement(new QName(TAG_MULTISTATUS, Namespace.get("D", DEFAULT_NAMESPACE))); 1912 1913 if (depth == 0) { 1914 parseProperties(req, multiStatusElem, item, type, properties); 1915 } else { 1916 // The stack always contains the object of the current level 1917 Stack<I_CmsRepositoryItem> stack = new Stack<I_CmsRepositoryItem>(); 1918 stack.push(item); 1919 1920 // Stack of the objects one level below 1921 Stack<I_CmsRepositoryItem> stackBelow = new Stack<I_CmsRepositoryItem>(); 1922 1923 while ((!stack.isEmpty()) && (depth >= 0)) { 1924 1925 I_CmsRepositoryItem currentItem = stack.pop(); 1926 parseProperties(req, multiStatusElem, currentItem, type, properties); 1927 1928 if ((currentItem.isCollection()) && (depth > 0)) { 1929 1930 try { 1931 List<I_CmsRepositoryItem> list = m_session.list(currentItem.getName()); 1932 Iterator<I_CmsRepositoryItem> iter = list.iterator(); 1933 while (iter.hasNext()) { 1934 I_CmsRepositoryItem element = iter.next(); 1935 stackBelow.push(element); 1936 } 1937 1938 } catch (CmsException e) { 1939 1940 resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 1941 1942 if (LOG.isErrorEnabled()) { 1943 LOG.error( 1944 Messages.get().getBundle().key(Messages.LOG_LIST_ITEMS_ERROR_1, currentItem.getName()), 1945 e); 1946 } 1947 1948 return; 1949 } 1950 } 1951 1952 if (stack.isEmpty()) { 1953 depth--; 1954 stack = stackBelow; 1955 stackBelow = new Stack<I_CmsRepositoryItem>(); 1956 } 1957 } 1958 } 1959 1960 Writer writer = resp.getWriter(); 1961 doc.write(writer); 1962 writer.close(); 1963 } 1964 1965 /** 1966 * Process a PROPPATCH WebDAV request for the specified resource.<p> 1967 * 1968 * Not implemented yet.<p> 1969 * 1970 * @param req the servlet request we are processing 1971 * @param resp the servlet response we are creating 1972 */ 1973 protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) { 1974 1975 // Check if Webdav is read only 1976 if (m_readOnly) { 1977 1978 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 1979 1980 if (LOG.isDebugEnabled()) { 1981 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 1982 } 1983 1984 return; 1985 } 1986 1987 // Check if resource is locked 1988 if (isLocked(req)) { 1989 1990 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 1991 1992 if (LOG.isDebugEnabled()) { 1993 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, getRelativePath(req))); 1994 } 1995 1996 return; 1997 } 1998 1999 resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); 2000 } 2001 2002 /** 2003 * Process a POST request for the specified resource.<p> 2004 * 2005 * @param req the servlet request we are processing 2006 * @param resp the servlet response we are creating 2007 * 2008 * @throws IOException if an input/output error occurs 2009 */ 2010 @Override 2011 protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { 2012 2013 String path = getRelativePath(req); 2014 2015 // Check if webdav is set to read only 2016 if (m_readOnly) { 2017 2018 resp.setStatus(HttpServletResponse.SC_FORBIDDEN); 2019 2020 if (LOG.isDebugEnabled()) { 2021 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 2022 } 2023 2024 return; 2025 } 2026 2027 // Check if resource is locked 2028 if (isLocked(req)) { 2029 2030 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 2031 2032 if (LOG.isDebugEnabled()) { 2033 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path)); 2034 } 2035 2036 return; 2037 } 2038 2039 boolean exists = m_session.exists(path); 2040 boolean result = true; 2041 2042 // Temp. content file used to support partial PUT 2043 File contentFile = null; 2044 2045 CmsWebdavRange range = parseContentRange(req, resp); 2046 2047 InputStream resourceInputStream = null; 2048 2049 // Append data specified in ranges to existing content for this 2050 // resource - create a temp. file on the local filesystem to 2051 // perform this operation 2052 // Assume just one range is specified for now 2053 if (range != null) { 2054 contentFile = executePartialPut(req, range, path); 2055 resourceInputStream = new FileInputStream(contentFile); 2056 } else { 2057 resourceInputStream = req.getInputStream(); 2058 } 2059 2060 try { 2061 2062 // FIXME: Add attributes(from Apache Tomcat) 2063 if (LOG.isDebugEnabled()) { 2064 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SAVE_ITEM_0)); 2065 } 2066 2067 m_session.save(path, resourceInputStream, exists); 2068 } catch (Exception e) { 2069 2070 if (LOG.isErrorEnabled()) { 2071 LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "PUT", path), e); 2072 } 2073 2074 result = false; 2075 resp.setStatus(HttpServletResponse.SC_CONFLICT); 2076 } 2077 2078 // Bugzilla 40326: at this point content file should be safe to delete 2079 // as it's no longer referenced. Let's not rely on deleteOnExit because 2080 // it's a memory leak, as noted in this Bugzilla issue. 2081 if (contentFile != null) { 2082 try { 2083 contentFile.delete(); 2084 } catch (Exception e) { 2085 if (LOG.isErrorEnabled()) { 2086 LOG.error(Messages.get().getBundle().key(Messages.LOG_DELETE_TEMP_FILE_0), e); 2087 } 2088 } 2089 } 2090 2091 if (result) { 2092 2093 if (LOG.isDebugEnabled()) { 2094 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SAVE_SUCCESS_0)); 2095 } 2096 2097 if (exists) { 2098 resp.setStatus(HttpServletResponse.SC_NO_CONTENT); 2099 } else { 2100 resp.setStatus(HttpServletResponse.SC_CREATED); 2101 } 2102 } 2103 } 2104 2105 /** 2106 * Process a UNLOCK WebDAV request for the specified resource.<p> 2107 * 2108 * @param req the servlet request we are processing 2109 * @param resp the servlet response we are creating 2110 */ 2111 protected void doUnlock(HttpServletRequest req, HttpServletResponse resp) { 2112 2113 String path = getRelativePath(req); 2114 2115 // Check if Webdav is read only 2116 if (m_readOnly) { 2117 2118 resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN); 2119 2120 if (LOG.isDebugEnabled()) { 2121 LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0)); 2122 } 2123 2124 return; 2125 } 2126 2127 // Check if resource is locked 2128 if (isLocked(req)) { 2129 2130 resp.setStatus(CmsWebdavStatus.SC_LOCKED); 2131 2132 if (LOG.isDebugEnabled()) { 2133 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path)); 2134 } 2135 2136 return; 2137 } 2138 2139 if (LOG.isDebugEnabled()) { 2140 LOG.debug(Messages.get().getBundle().key(Messages.LOG_UNLOCK_ITEM_0)); 2141 } 2142 2143 m_session.unlock(path); 2144 2145 resp.setStatus(CmsWebdavStatus.SC_NO_CONTENT); 2146 } 2147 2148 /** 2149 * Handle a partial PUT.<p> 2150 * 2151 * New content specified in request is appended to 2152 * existing content in oldRevisionContent (if present). This code does 2153 * not support simultaneous partial updates to the same resource.<p> 2154 * 2155 * @param req the servlet request we are processing 2156 * @param range the range of the content in the file 2157 * @param path the path where to find the resource 2158 * 2159 * @return the new content file with the appended data 2160 * 2161 * @throws IOException if an input/output error occurs 2162 */ 2163 protected File executePartialPut(HttpServletRequest req, CmsWebdavRange range, String path) throws IOException { 2164 2165 // Append data specified in ranges to existing content for this 2166 // resource - create a temp. file on the local filesystem to 2167 // perform this operation 2168 File tempDir = (File)getServletContext().getAttribute(ATT_SERVLET_TEMPDIR); 2169 2170 // Convert all '/' characters to '.' in resourcePath 2171 String convertedResourcePath = path.replace('/', '.'); 2172 File contentFile = new File(tempDir, convertedResourcePath); 2173 contentFile.createNewFile(); 2174 2175 RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw"); 2176 2177 InputStream oldResourceStream = null; 2178 try { 2179 I_CmsRepositoryItem item = m_session.getItem(path); 2180 2181 oldResourceStream = new ByteArrayInputStream(item.getContent()); 2182 } catch (CmsException e) { 2183 if (LOG.isErrorEnabled()) { 2184 LOG.error(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path), e); 2185 } 2186 } 2187 2188 // Copy data in oldRevisionContent to contentFile 2189 if (oldResourceStream != null) { 2190 2191 int numBytesRead; 2192 byte[] copyBuffer = new byte[BUFFER_SIZE]; 2193 while ((numBytesRead = oldResourceStream.read(copyBuffer)) != -1) { 2194 randAccessContentFile.write(copyBuffer, 0, numBytesRead); 2195 } 2196 2197 oldResourceStream.close(); 2198 } 2199 2200 randAccessContentFile.setLength(range.getLength()); 2201 2202 // Append data in request input stream to contentFile 2203 randAccessContentFile.seek(range.getStart()); 2204 int numBytesRead; 2205 byte[] transferBuffer = new byte[BUFFER_SIZE]; 2206 BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE); 2207 while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) { 2208 randAccessContentFile.write(transferBuffer, 0, numBytesRead); 2209 } 2210 randAccessContentFile.close(); 2211 requestBufInStream.close(); 2212 2213 return contentFile; 2214 } 2215 2216 /** 2217 * Get the ETag associated with a file.<p> 2218 * 2219 * @param item the WebDavItem 2220 * 2221 * @return the created ETag for the resource attributes 2222 */ 2223 protected String getETag(I_CmsRepositoryItem item) { 2224 2225 return "\"" + item.getContentLength() + "-" + item.getLastModifiedDate() + "\""; 2226 } 2227 2228 /** 2229 * Parse the range header.<p> 2230 * 2231 * @param request the servlet request we are processing 2232 * @param response the servlet response we are creating 2233 * @param item the WebdavItem with the information 2234 * 2235 * @return Vector of ranges 2236 */ 2237 protected ArrayList<CmsWebdavRange> parseRange( 2238 HttpServletRequest request, 2239 HttpServletResponse response, 2240 I_CmsRepositoryItem item) { 2241 2242 // Checking If-Range 2243 String headerValue = request.getHeader(HEADER_IFRANGE); 2244 2245 if (headerValue != null) { 2246 2247 long headerValueTime = (-1L); 2248 try { 2249 headerValueTime = request.getDateHeader(HEADER_IFRANGE); 2250 } catch (Exception e) { 2251 // noop 2252 } 2253 2254 String eTag = getETag(item); 2255 long lastModified = item.getLastModifiedDate(); 2256 2257 if (headerValueTime == (-1L)) { 2258 2259 // If the ETag the client gave does not match the entity 2260 // etag, then the entire entity is returned. 2261 if (!eTag.equals(headerValue.trim())) { 2262 return FULL_RANGE; 2263 } 2264 2265 } else { 2266 2267 // If the timestamp of the entity the client got is older than 2268 // the last modification date of the entity, the entire entity 2269 // is returned. 2270 if (lastModified > (headerValueTime + 1000)) { 2271 return FULL_RANGE; 2272 } 2273 } 2274 } 2275 2276 long fileLength = item.getContentLength(); 2277 2278 if (fileLength == 0) { 2279 return null; 2280 } 2281 2282 // Retrieving the range header (if any is specified 2283 String rangeHeader = request.getHeader(HEADER_RANGE); 2284 2285 if (rangeHeader == null) { 2286 return null; 2287 } 2288 2289 // bytes is the only range unit supported (and I don't see the point 2290 // of adding new ones). 2291 if (!rangeHeader.startsWith("bytes")) { 2292 response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength); 2293 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 2294 return null; 2295 } 2296 2297 rangeHeader = rangeHeader.substring(6); 2298 2299 // Vector which will contain all the ranges which are successfully parsed. 2300 ArrayList<CmsWebdavRange> result = new ArrayList<CmsWebdavRange>(); 2301 StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ","); 2302 2303 // Parsing the range list 2304 while (commaTokenizer.hasMoreTokens()) { 2305 String rangeDefinition = commaTokenizer.nextToken().trim(); 2306 2307 CmsWebdavRange currentRange = new CmsWebdavRange(); 2308 currentRange.setLength(fileLength); 2309 2310 int dashPos = rangeDefinition.indexOf('-'); 2311 2312 if (dashPos == -1) { 2313 response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength); 2314 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 2315 return null; 2316 } 2317 2318 if (dashPos == 0) { 2319 2320 try { 2321 long offset = Long.parseLong(rangeDefinition); 2322 currentRange.setStart(fileLength + offset); 2323 currentRange.setEnd(fileLength - 1); 2324 } catch (NumberFormatException e) { 2325 response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength); 2326 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 2327 return null; 2328 } 2329 2330 } else { 2331 2332 try { 2333 currentRange.setStart(Long.parseLong(rangeDefinition.substring(0, dashPos))); 2334 if (dashPos < (rangeDefinition.length() - 1)) { 2335 currentRange.setEnd( 2336 Long.parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()))); 2337 } else { 2338 currentRange.setEnd(fileLength - 1); 2339 } 2340 } catch (NumberFormatException e) { 2341 response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength); 2342 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 2343 return null; 2344 } 2345 2346 } 2347 2348 if (!currentRange.validate()) { 2349 response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength); 2350 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); 2351 return null; 2352 } 2353 2354 result.add(currentRange); 2355 } 2356 2357 return result; 2358 } 2359 2360 /** 2361 * Return an InputStream to an HTML representation of the contents 2362 * of this directory.<p> 2363 * 2364 * @param contextPath context path to which our internal paths are relative 2365 * @param path the path of the resource to render the html for 2366 * 2367 * @return an input stream with the rendered html 2368 * 2369 * @throws IOException if an input/output error occurs 2370 */ 2371 protected InputStream renderHtml(String contextPath, String path) throws IOException { 2372 2373 String name = path; 2374 // Prepare a writer to a buffered area 2375 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 2376 OutputStreamWriter osWriter = null; 2377 try { 2378 osWriter = new OutputStreamWriter(stream, "UTF8"); 2379 } catch (Exception e) { 2380 2381 // Should never happen 2382 osWriter = new OutputStreamWriter(stream); 2383 } 2384 PrintWriter writer = new PrintWriter(osWriter); 2385 2386 StringBuffer sb = new StringBuffer(); 2387 2388 // rewriteUrl(contextPath) is expensive. cache result for later reuse 2389 String rewrittenContextPath = rewriteUrl(contextPath); 2390 2391 // Render the page header 2392 sb.append("<html>\r\n"); 2393 sb.append("<head>\r\n"); 2394 sb.append("<title>"); 2395 sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_TITLE_1, name)); 2396 sb.append("</title>\r\n"); 2397 2398 // TODO: add opencms css style 2399 sb.append("<STYLE><!--"); 2400 sb.append( 2401 "H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " 2402 + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " 2403 + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " 2404 + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " 2405 + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " 2406 + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" 2407 + "A {color : black;}" 2408 + "A.name {color : black;}" 2409 + "HR {color : #525D76;}"); 2410 sb.append("--></STYLE> "); 2411 2412 sb.append("</head>\r\n"); 2413 sb.append("<body>"); 2414 sb.append("<h1>"); 2415 sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_TITLE_1, name)); 2416 2417 sb.append("</h1>"); 2418 sb.append("<HR size=\"1\" noshade=\"noshade\">"); 2419 2420 sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n"); 2421 2422 // Render the column headings 2423 sb.append("<tr>\r\n"); 2424 sb.append("<td align=\"left\"><font size=\"+1\"><strong>"); 2425 sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_FILENAME_0)); 2426 sb.append("</strong></font></td>\r\n"); 2427 sb.append("<td align=\"center\"><font size=\"+1\"><strong>"); 2428 sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_SIZE_0)); 2429 sb.append("</strong></font></td>\r\n"); 2430 sb.append("<td align=\"right\"><font size=\"+1\"><strong>"); 2431 sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_LASTMODIFIED_0)); 2432 sb.append("</strong></font></td>\r\n"); 2433 sb.append("</tr>"); 2434 2435 boolean shade = false; 2436 2437 // Render the link to our parent (if required) 2438 String parentDirectory = name; 2439 if (parentDirectory.endsWith("/")) { 2440 parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1); 2441 } 2442 int slash = parentDirectory.lastIndexOf('/'); 2443 if (slash >= 0) { 2444 2445 String parent = parentDirectory.substring(0, slash); 2446 2447 sb.append("<tr"); 2448 if (shade) { 2449 sb.append(" bgcolor=\"#eeeeee\""); 2450 } 2451 sb.append(">\r\n"); 2452 shade = !shade; 2453 2454 sb.append("<td align=\"left\"> \r\n"); 2455 sb.append("<a href=\""); 2456 sb.append(rewrittenContextPath); 2457 if (parent.equals("")) { 2458 parent = "/"; 2459 } 2460 sb.append(rewriteUrl(parent)); 2461 if (!parent.endsWith("/")) { 2462 sb.append("/"); 2463 } 2464 sb.append("\"><tt>"); 2465 sb.append(".."); 2466 sb.append("</tt></a></td>\r\n"); 2467 2468 sb.append("<td align=\"right\"><tt>"); 2469 sb.append(" "); 2470 sb.append("</tt></td>\r\n"); 2471 2472 sb.append("<td align=\"right\"><tt>"); 2473 sb.append(" "); 2474 sb.append("</tt></td>\r\n"); 2475 2476 sb.append("</tr>\r\n"); 2477 } 2478 2479 try { 2480 2481 // Render the directory entries within this directory 2482 List<I_CmsRepositoryItem> list = m_session.list(path); 2483 Iterator<I_CmsRepositoryItem> iter = list.iterator(); 2484 while (iter.hasNext()) { 2485 2486 I_CmsRepositoryItem childItem = iter.next(); 2487 2488 String resourceName = childItem.getName(); 2489 if (resourceName.endsWith("/")) { 2490 resourceName = resourceName.substring(0, resourceName.length() - 1); 2491 } 2492 slash = resourceName.lastIndexOf('/'); 2493 if (slash > -1) { 2494 resourceName = resourceName.substring(slash + 1, resourceName.length()); 2495 } 2496 2497 sb.append("<tr"); 2498 if (shade) { 2499 sb.append(" bgcolor=\"#eeeeee\""); 2500 } 2501 sb.append(">\r\n"); 2502 shade = !shade; 2503 2504 sb.append("<td align=\"left\"> \r\n"); 2505 sb.append("<a href=\""); 2506 sb.append(rewrittenContextPath); 2507 //resourceName = rewriteUrl(name + resourceName); 2508 sb.append(rewriteUrl(name + resourceName)); 2509 if (childItem.isCollection()) { 2510 sb.append("/"); 2511 } 2512 sb.append("\"><tt>"); 2513 sb.append(CmsEncoder.escapeXml(resourceName)); 2514 if (childItem.isCollection()) { 2515 sb.append("/"); 2516 } 2517 sb.append("</tt></a></td>\r\n"); 2518 2519 sb.append("<td align=\"right\"><tt>"); 2520 if (childItem.isCollection()) { 2521 sb.append(" "); 2522 } else { 2523 sb.append(renderSize(childItem.getContentLength())); 2524 } 2525 sb.append("</tt></td>\r\n"); 2526 2527 sb.append("<td align=\"right\"><tt>"); 2528 sb.append(HTTP_DATE_FORMAT.format(new Date(childItem.getLastModifiedDate()))); 2529 sb.append("</tt></td>\r\n"); 2530 2531 sb.append("</tr>\r\n"); 2532 } 2533 2534 } catch (CmsException e) { 2535 2536 // Something went wrong 2537 e.printStackTrace(); 2538 } 2539 2540 // Render the page footer 2541 sb.append("</table>\r\n"); 2542 2543 sb.append("<HR size=\"1\" noshade=\"noshade\">"); 2544 sb.append("</body>\r\n"); 2545 sb.append("</html>\r\n"); 2546 2547 // Return an input stream to the underlying bytes 2548 writer.write(sb.toString()); 2549 writer.flush(); 2550 return (new ByteArrayInputStream(stream.toByteArray())); 2551 } 2552 2553 /** 2554 * Render the specified file size (in bytes).<p> 2555 * 2556 * @param size file size (in bytes) 2557 * 2558 * @return a string with the given size formatted to output to user 2559 */ 2560 protected String renderSize(long size) { 2561 2562 long leftSide = size / 1024; 2563 long rightSide = (size % 1024) / 103; // Makes 1 digit 2564 if ((leftSide == 0) && (rightSide == 0) && (size > 0)) { 2565 rightSide = 1; 2566 } 2567 2568 return ("" + leftSide + "." + rightSide + " kb"); 2569 } 2570 2571 /** 2572 * URL rewriter.<p> 2573 * 2574 * @param path path which has to be rewritten 2575 * 2576 * @return a string with the encoded path 2577 * 2578 * @throws UnsupportedEncodingException if something goes wrong while encoding the url 2579 */ 2580 protected String rewriteUrl(String path) throws UnsupportedEncodingException { 2581 2582 return new String(URLCodec.encodeUrl(URL_SAFE_CHARS, path.getBytes("ISO-8859-1"))); 2583 } 2584 2585 /** 2586 * Serve the specified resource, optionally including the data content.<p> 2587 * 2588 * @param request the servlet request we are processing 2589 * @param response the servlet response we are creating 2590 * @param content should the content be included? 2591 * 2592 * @throws IOException if an input/output error occurs 2593 */ 2594 protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content) 2595 throws IOException { 2596 2597 // Identify the requested resource path 2598 String path = getRelativePath(request); 2599 if (LOG.isDebugEnabled()) { 2600 if (content) { 2601 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_1, path)); 2602 } else { 2603 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_HEADER_1, path)); 2604 } 2605 } 2606 2607 I_CmsRepositoryItem item = null; 2608 try { 2609 item = m_session.getItem(path); 2610 } catch (CmsException ex) { 2611 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 2612 2613 if (LOG.isDebugEnabled()) { 2614 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path)); 2615 } 2616 2617 return; 2618 } 2619 2620 // If the resource is not a collection, and the resource path 2621 // ends with "/" or "\", return NOT FOUND 2622 if (!item.isCollection()) { 2623 if (path.endsWith("/") || (path.endsWith("\\"))) { 2624 2625 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 2626 2627 if (LOG.isDebugEnabled()) { 2628 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path)); 2629 } 2630 2631 return; 2632 } 2633 } 2634 2635 // Find content type. 2636 String contentType = item.getMimeType(); 2637 if (contentType == null) { 2638 contentType = getServletContext().getMimeType(item.getName()); 2639 } 2640 2641 ArrayList<CmsWebdavRange> ranges = null; 2642 long contentLength = -1L; 2643 2644 if (item.isCollection()) { 2645 2646 // Skip directory listings if we have been configured to suppress them 2647 if (!m_listings) { 2648 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 2649 return; 2650 } 2651 contentType = "text/html;charset=UTF-8"; 2652 2653 } else { 2654 2655 // Parse range specifier 2656 ranges = parseRange(request, response, item); 2657 2658 // ETag header 2659 response.setHeader(HEADER_ETAG, getETag(item)); 2660 2661 // Last-Modified header 2662 response.setHeader(HEADER_LASTMODIFIED, HTTP_DATE_FORMAT.format(new Date(item.getLastModifiedDate()))); 2663 2664 // Get content length 2665 contentLength = item.getContentLength(); 2666 2667 // Special case for zero length files, which would cause a 2668 // (silent) ISE when setting the output buffer size 2669 if (contentLength == 0L) { 2670 content = false; 2671 } 2672 2673 } 2674 2675 ServletOutputStream ostream = null; 2676 PrintWriter writer = null; 2677 2678 if (content) { 2679 2680 // Trying to retrieve the servlet output stream 2681 try { 2682 ostream = response.getOutputStream(); 2683 } catch (IllegalStateException e) { 2684 2685 // If it fails, we try to get a Writer instead if we're 2686 // trying to serve a text file 2687 if ((contentType == null) || (contentType.startsWith("text")) || (contentType.endsWith("xml"))) { 2688 writer = response.getWriter(); 2689 } else { 2690 throw e; 2691 } 2692 } 2693 2694 } 2695 2696 if ((item.isCollection()) 2697 || (((ranges == null) || (ranges.isEmpty())) && (request.getHeader(HEADER_RANGE) == null)) 2698 || (ranges == FULL_RANGE)) { 2699 2700 // Set the appropriate output headers 2701 if (contentType != null) { 2702 if (LOG.isDebugEnabled()) { 2703 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_CONTENT_TYPE_1, contentType)); 2704 } 2705 response.setContentType(contentType); 2706 } 2707 2708 if ((!item.isCollection()) && (contentLength >= 0)) { 2709 if (LOG.isDebugEnabled()) { 2710 LOG.debug( 2711 Messages.get().getBundle().key( 2712 Messages.LOG_SERVE_ITEM_CONTENT_LENGTH_1, 2713 new Long(contentLength))); 2714 } 2715 2716 if (contentLength < Integer.MAX_VALUE) { 2717 response.setContentLength((int)contentLength); 2718 } else { 2719 2720 // Set the content-length as String to be able to use a long 2721 response.setHeader(HEADER_CONTENTLENGTH, "" + contentLength); 2722 } 2723 } 2724 2725 InputStream renderResult = null; 2726 if (item.isCollection()) { 2727 2728 if (content) { 2729 // Serve the directory browser 2730 renderResult = renderHtml(request.getContextPath() + request.getServletPath(), item.getName()); 2731 } 2732 2733 } 2734 2735 // Copy the input stream to our output stream (if requested) 2736 if (content) { 2737 try { 2738 response.setBufferSize(m_output); 2739 } catch (IllegalStateException e) { 2740 // Silent catch 2741 } 2742 if (ostream != null) { 2743 copy(item, renderResult, ostream); 2744 } else { 2745 copy(item, renderResult, writer); 2746 } 2747 } 2748 2749 } else { 2750 2751 if ((ranges == null) || (ranges.isEmpty())) { 2752 return; 2753 } 2754 2755 // Partial content response. 2756 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); 2757 2758 if (ranges.size() == 1) { 2759 2760 CmsWebdavRange range = ranges.get(0); 2761 response.addHeader( 2762 HEADER_CONTENTRANGE, 2763 "bytes " + range.getStart() + "-" + range.getEnd() + "/" + range.getLength()); 2764 long length = (range.getEnd() - range.getStart()) + 1; 2765 if (length < Integer.MAX_VALUE) { 2766 response.setContentLength((int)length); 2767 } else { 2768 // Set the content-length as String to be able to use a long 2769 response.setHeader(HEADER_CONTENTLENGTH, "" + length); 2770 } 2771 2772 if (contentType != null) { 2773 if (LOG.isDebugEnabled()) { 2774 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_CONTENT_TYPE_1, contentType)); 2775 } 2776 response.setContentType(contentType); 2777 } 2778 2779 if (content) { 2780 try { 2781 response.setBufferSize(m_output); 2782 } catch (IllegalStateException e) { 2783 // Silent catch 2784 } 2785 if (ostream != null) { 2786 copy(item, ostream, range); 2787 } else { 2788 copy(item, writer, range); 2789 } 2790 } 2791 2792 } else { 2793 2794 response.setContentType("multipart/byteranges; boundary=" + MIME_SEPARATION); 2795 2796 if (content) { 2797 try { 2798 response.setBufferSize(m_output); 2799 } catch (IllegalStateException e) { 2800 // Silent catch 2801 } 2802 if (ostream != null) { 2803 copy(item, ostream, ranges.iterator(), contentType); 2804 } else { 2805 copy(item, writer, ranges.iterator(), contentType); 2806 } 2807 } 2808 2809 } 2810 2811 } 2812 } 2813 2814 /** 2815 * Handles the special WebDAV methods.<p> 2816 * 2817 * @param req the servlet request we are processing 2818 * @param resp the servlet response we are creating 2819 * 2820 * @throws IOException if an input/output error occurs 2821 * @throws ServletException if a servlet-specified error occurs 2822 */ 2823 @Override 2824 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 2825 2826 String method = req.getMethod(); 2827 2828 if (LOG.isDebugEnabled()) { 2829 String path = getRelativePath(req); 2830 LOG.debug("[" + method + "] " + path); 2831 } 2832 2833 // check authorization 2834 String auth = req.getHeader(HEADER_AUTHORIZATION); 2835 if ((auth == null) || !auth.toUpperCase().startsWith(AUTHORIZATION_BASIC_PREFIX)) { 2836 2837 // no authorization data is available 2838 requestAuthorization(resp); 2839 2840 if (LOG.isDebugEnabled()) { 2841 LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_AUTHORIZATION_0)); 2842 } 2843 2844 return; 2845 } 2846 2847 // get encoded user and password, following after "BASIC " 2848 String base64Token = auth.substring(6); 2849 2850 // decode it, using base 64 decoder 2851 String token = new String(Base64.decodeBase64(base64Token.getBytes())); 2852 String password = null; 2853 int pos = token.indexOf(SEPARATOR_CREDENTIALS); 2854 if (pos != -1) { 2855 m_username = token.substring(0, pos); 2856 password = token.substring(pos + 1); 2857 } 2858 2859 // get session 2860 try { 2861 m_session = m_repository.login(m_username, password); 2862 } catch (CmsException ex) { 2863 m_session = null; 2864 } 2865 2866 if (m_session == null) { 2867 2868 if (LOG.isDebugEnabled()) { 2869 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOGIN_FAILED_1, m_username)); 2870 } 2871 2872 resp.setStatus(HttpServletResponse.SC_FORBIDDEN); 2873 return; 2874 } 2875 2876 if (method.equals(METHOD_PROPFIND)) { 2877 doPropfind(req, resp); 2878 } else if (method.equals(METHOD_PROPPATCH)) { 2879 doProppatch(req, resp); 2880 } else if (method.equals(METHOD_MKCOL)) { 2881 doMkcol(req, resp); 2882 } else if (method.equals(METHOD_COPY)) { 2883 doCopy(req, resp); 2884 } else if (method.equals(METHOD_MOVE)) { 2885 doMove(req, resp); 2886 } else if (method.equals(METHOD_LOCK)) { 2887 doLock(req, resp); 2888 } else if (method.equals(METHOD_UNLOCK)) { 2889 doUnlock(req, resp); 2890 } else { 2891 2892 // DefaultServlet processing 2893 super.service(req, resp); 2894 } 2895 2896 } 2897 2898 /** 2899 * Generate a dom element from the given information with all needed subelements to 2900 * add to the parent.<p> 2901 * 2902 * @param lock the lock with the information to create the subelements 2903 * @param parent the parent element where to add the created element 2904 * @param lockToken the lock token to use 2905 */ 2906 private void addLockElement(CmsRepositoryLockInfo lock, Element parent, String lockToken) { 2907 2908 Element activeLockElem = addElement(parent, TAG_ACTIVELOCK); 2909 addElement(addElement(activeLockElem, TAG_LOCKTYPE), lock.getType()); 2910 addElement(addElement(activeLockElem, TAG_LOCKSCOPE), lock.getScope()); 2911 2912 if (lock.getDepth() == CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE) { 2913 addElement(activeLockElem, TAG_DEPTH).addText(DEPTH_INFINITY); 2914 } else { 2915 addElement(activeLockElem, TAG_DEPTH).addText("0"); 2916 } 2917 2918 Element ownerElem = addElement(activeLockElem, TAG_OWNER); 2919 addElement(ownerElem, TAG_HREF).addText(lock.getOwner()); 2920 2921 if (lock.getExpiresAt() == CmsRepositoryLockInfo.TIMEOUT_INFINITE_VALUE) { 2922 addElement(activeLockElem, TAG_TIMEOUT).addText(TIMEOUT_INFINITE); 2923 } else { 2924 long timeout = (lock.getExpiresAt() - System.currentTimeMillis()) / 1000; 2925 addElement(activeLockElem, TAG_TIMEOUT).addText("Second-" + timeout); 2926 } 2927 2928 Element lockTokenElem = addElement(activeLockElem, TAG_LOCKTOKEN); 2929 addElement(lockTokenElem, TAG_HREF).addText("opaquelocktoken:" + lockToken); 2930 } 2931 2932 /** 2933 * Checks if the items in the path or in a subpath are locked.<p> 2934 * 2935 * @param req the servlet request we are processing 2936 * @param path the path to check the items for locks 2937 * @param errorList the error list where to put the found errors 2938 */ 2939 private void checkChildLocks(HttpServletRequest req, String path, Hashtable<String, Integer> errorList) { 2940 2941 List<I_CmsRepositoryItem> list = null; 2942 try { 2943 list = m_session.list(path); 2944 } catch (CmsException e) { 2945 if (LOG.isErrorEnabled()) { 2946 LOG.error(Messages.get().getBundle().key(Messages.LOG_LIST_ITEMS_ERROR_1, path), e); 2947 } 2948 errorList.put(path, new Integer(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR)); 2949 return; 2950 } 2951 2952 Iterator<I_CmsRepositoryItem> iter = list.iterator(); 2953 while (iter.hasNext()) { 2954 I_CmsRepositoryItem element = iter.next(); 2955 2956 if (isLocked(element.getName())) { 2957 errorList.put(element.getName(), new Integer(CmsWebdavStatus.SC_LOCKED)); 2958 } else { 2959 if (element.isCollection()) { 2960 checkChildLocks(req, element.getName(), errorList); 2961 } 2962 } 2963 } 2964 } 2965 2966 /** 2967 * Determines the methods normally allowed for the resource.<p> 2968 * 2969 * @param path the path to the resource 2970 * 2971 * @return a StringBuffer with the WebDAV methods allowed for the resource at the given path 2972 */ 2973 private StringBuffer determineMethodsAllowed(String path) { 2974 2975 StringBuffer methodsAllowed = new StringBuffer(); 2976 boolean exists = true; 2977 I_CmsRepositoryItem item = null; 2978 try { 2979 item = m_session.getItem(path); 2980 } catch (CmsException e) { 2981 exists = false; 2982 } 2983 2984 if (!exists) { 2985 2986 methodsAllowed.append(METHOD_OPTIONS); 2987 methodsAllowed.append(", ").append(METHOD_PUT); 2988 methodsAllowed.append(", ").append(METHOD_MKCOL); 2989 methodsAllowed.append(", ").append(METHOD_LOCK); 2990 return methodsAllowed; 2991 } 2992 2993 // add standard http methods 2994 methodsAllowed.append(METHOD_OPTIONS); 2995 methodsAllowed.append(", ").append(METHOD_GET); 2996 methodsAllowed.append(", ").append(METHOD_HEAD); 2997 methodsAllowed.append(", ").append(METHOD_POST); 2998 methodsAllowed.append(", ").append(METHOD_DELETE); 2999 methodsAllowed.append(", ").append(METHOD_TRACE); 3000 3001 // add special WebDAV methods 3002 methodsAllowed.append(", ").append(METHOD_LOCK); 3003 methodsAllowed.append(", ").append(METHOD_UNLOCK); 3004 methodsAllowed.append(", ").append(METHOD_MOVE); 3005 methodsAllowed.append(", ").append(METHOD_COPY); 3006 methodsAllowed.append(", ").append(METHOD_PROPPATCH); 3007 3008 if (m_listings) { 3009 methodsAllowed.append(", ").append(METHOD_PROPFIND); 3010 } 3011 3012 if (item != null) { 3013 if (!item.isCollection()) { 3014 methodsAllowed.append(", ").append(METHOD_PUT); 3015 } 3016 } 3017 return methodsAllowed; 3018 } 3019 3020 /** 3021 * Print the lock discovery information associated with a path.<p> 3022 * 3023 * @param path the path to the resource 3024 * @param elem the dom element where to add the lock discovery elements 3025 * @param req the servlet request we are processing 3026 * 3027 * @return true if at least one lock was displayed 3028 */ 3029 private boolean generateLockDiscovery(String path, Element elem, HttpServletRequest req) { 3030 3031 CmsRepositoryLockInfo lock = m_session.getLock(path); 3032 3033 if (lock != null) { 3034 3035 Element lockElem = addElement(elem, TAG_LOCKDISCOVERY); 3036 addLockElement(lock, lockElem, generateLockToken(req, lock)); 3037 3038 return true; 3039 } 3040 3041 return false; 3042 } 3043 3044 /** 3045 * Generates a lock token out of the lock and some information out of the 3046 * request to make it unique.<p> 3047 * 3048 * @param req the servlet request we are processing 3049 * @param lock the lock with the information for the lock token 3050 * 3051 * @return the generated lock token 3052 */ 3053 private String generateLockToken(HttpServletRequest req, CmsRepositoryLockInfo lock) { 3054 3055 String lockTokenStr = req.getServletPath() 3056 + "-" 3057 + req.getUserPrincipal() 3058 + "-" 3059 + lock.getOwner() 3060 + "-" 3061 + lock.getPath() 3062 + "-" 3063 + m_secret; 3064 3065 return MD5_ENCODER.encode(m_md5Helper.digest(lockTokenStr.getBytes())); 3066 } 3067 3068 /** 3069 * Return the relative path associated with this servlet.<p> 3070 * 3071 * @param request the servlet request we are processing 3072 * 3073 * @return the relative path of the resource 3074 */ 3075 private String getRelativePath(HttpServletRequest request) { 3076 3077 String result = request.getPathInfo(); 3078 if (result == null) { 3079 //result = request.getServletPath(); 3080 } 3081 if ((result == null) || (result.equals(""))) { 3082 result = "/"; 3083 } 3084 return (result); 3085 } 3086 3087 /** 3088 * Check to see if a resource is currently write locked.<p> 3089 * 3090 * @param req the servlet request we are processing 3091 * 3092 * @return true if the resource is locked otherwise false 3093 */ 3094 private boolean isLocked(HttpServletRequest req) { 3095 3096 return isLocked(getRelativePath(req)); 3097 } 3098 3099 /** 3100 * Check to see if a resource is currently write locked.<p> 3101 * 3102 * @param path the path where to find the resource to check the lock 3103 * 3104 * @return true if the resource is locked otherwise false 3105 */ 3106 private boolean isLocked(String path) { 3107 3108 // get lock for path 3109 CmsRepositoryLockInfo lock = m_session.getLock(path); 3110 if (lock == null) { 3111 return false; 3112 } 3113 3114 // check if found lock fits to the lock token from request 3115 // String currentToken = "<opaquelocktoken:" + generateLockToken(req, lock) + ">"; 3116 // if (currentToken.equals(parseLockTokenHeader(req))) { 3117 // return false; 3118 // } 3119 3120 if (lock.getUsername().equals(m_username)) { 3121 return false; 3122 } 3123 3124 return true; 3125 } 3126 3127 /** 3128 * Return a context-relative path, beginning with a "/".<p> 3129 * 3130 * That represents the canonical version of the specified path after ".." 3131 * and "." elements are resolved out. If the specified path attempts to go 3132 * outside the boundaries of the current context (i.e. too many ".." path 3133 * elements are present), return <code>null</code> instead.<p> 3134 * 3135 * @param path the path to be normalized 3136 * 3137 * @return the normalized path 3138 */ 3139 private String normalize(String path) { 3140 3141 if (path == null) { 3142 return null; 3143 } 3144 3145 // Create a place for the normalized path 3146 String normalized = path; 3147 3148 if (normalized.equals("/.")) { 3149 return "/"; 3150 } 3151 3152 // Normalize the slashes and add leading slash if necessary 3153 if (normalized.indexOf('\\') >= 0) { 3154 normalized = normalized.replace('\\', '/'); 3155 } 3156 3157 if (!normalized.startsWith("/")) { 3158 normalized = "/" + normalized; 3159 } 3160 3161 // Resolve occurrences of "//" in the normalized path 3162 while (true) { 3163 int index = normalized.indexOf("//"); 3164 if (index < 0) { 3165 break; 3166 } 3167 normalized = normalized.substring(0, index) + normalized.substring(index + 1); 3168 } 3169 3170 // Resolve occurrences of "/./" in the normalized path 3171 while (true) { 3172 int index = normalized.indexOf("/./"); 3173 if (index < 0) { 3174 break; 3175 } 3176 normalized = normalized.substring(0, index) + normalized.substring(index + 2); 3177 } 3178 3179 // Resolve occurrences of "/../" in the normalized path 3180 while (true) { 3181 int index = normalized.indexOf("/../"); 3182 if (index < 0) { 3183 break; 3184 } 3185 if (index == 0) { 3186 return (null); // Trying to go outside our context 3187 } 3188 3189 int index2 = normalized.lastIndexOf('/', index - 1); 3190 normalized = normalized.substring(0, index2) + normalized.substring(index + 3); 3191 } 3192 3193 // Return the normalized path that we have completed 3194 return (normalized); 3195 } 3196 3197 /** 3198 * Parse the content-range header.<p> 3199 * 3200 * @param request the servlet request we are processing 3201 * @param response the servlet response we are creating 3202 * 3203 * @return the range of the content read from the header 3204 */ 3205 private CmsWebdavRange parseContentRange(HttpServletRequest request, HttpServletResponse response) { 3206 3207 // Retrieving the content-range header (if any is specified 3208 String rangeHeader = request.getHeader(HEADER_CONTENTRANGE); 3209 3210 if (rangeHeader == null) { 3211 return null; 3212 } 3213 3214 // bytes is the only range unit supported 3215 if (!rangeHeader.startsWith("bytes")) { 3216 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 3217 return null; 3218 } 3219 3220 rangeHeader = rangeHeader.substring(6).trim(); 3221 3222 int dashPos = rangeHeader.indexOf('-'); 3223 int slashPos = rangeHeader.indexOf('/'); 3224 3225 if (dashPos == -1) { 3226 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 3227 return null; 3228 } 3229 3230 if (slashPos == -1) { 3231 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 3232 return null; 3233 } 3234 3235 CmsWebdavRange range = new CmsWebdavRange(); 3236 3237 try { 3238 range.setStart(Long.parseLong(rangeHeader.substring(0, dashPos))); 3239 range.setEnd(Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos))); 3240 range.setLength(Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()))); 3241 } catch (NumberFormatException e) { 3242 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 3243 return null; 3244 } 3245 3246 if (!range.validate()) { 3247 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 3248 return null; 3249 } 3250 3251 return range; 3252 } 3253 3254 /** 3255 * Reads the information about a destination path out of the header of the 3256 * request.<p> 3257 * 3258 * @param req the servlet request we are processing 3259 * 3260 * @return the destination path 3261 */ 3262 private String parseDestinationHeader(HttpServletRequest req) { 3263 3264 // Parsing destination header 3265 String destinationPath = req.getHeader(HEADER_DESTINATION); 3266 3267 if (destinationPath == null) { 3268 return null; 3269 } 3270 3271 // Remove url encoding from destination 3272 destinationPath = CmsEncoder.decode(destinationPath, "UTF8"); 3273 3274 int protocolIndex = destinationPath.indexOf("://"); 3275 if (protocolIndex >= 0) { 3276 3277 // if the Destination URL contains the protocol, we can safely 3278 // trim everything upto the first "/" character after "://" 3279 int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4); 3280 if (firstSeparator < 0) { 3281 destinationPath = "/"; 3282 } else { 3283 destinationPath = destinationPath.substring(firstSeparator); 3284 } 3285 } else { 3286 String hostName = req.getServerName(); 3287 if ((hostName != null) && (destinationPath.startsWith(hostName))) { 3288 destinationPath = destinationPath.substring(hostName.length()); 3289 } 3290 3291 int portIndex = destinationPath.indexOf(":"); 3292 if (portIndex >= 0) { 3293 destinationPath = destinationPath.substring(portIndex); 3294 } 3295 3296 if (destinationPath.startsWith(":")) { 3297 int firstSeparator = destinationPath.indexOf("/"); 3298 if (firstSeparator < 0) { 3299 destinationPath = "/"; 3300 } else { 3301 destinationPath = destinationPath.substring(firstSeparator); 3302 } 3303 } 3304 } 3305 3306 // Normalise destination path (remove '.' and '..') 3307 destinationPath = normalize(destinationPath); 3308 3309 String contextPath = req.getContextPath(); 3310 if ((contextPath != null) && (destinationPath.startsWith(contextPath))) { 3311 destinationPath = destinationPath.substring(contextPath.length()); 3312 } 3313 3314 String pathInfo = req.getPathInfo(); 3315 if (pathInfo != null) { 3316 String servletPath = req.getServletPath(); 3317 if ((servletPath != null) && (destinationPath.startsWith(servletPath))) { 3318 destinationPath = destinationPath.substring(servletPath.length()); 3319 } 3320 } 3321 3322 return destinationPath; 3323 } 3324 3325 /** 3326 * Reads the information about overwriting out of the header of the 3327 * request.<p> 3328 * 3329 * @param req the servlet request we are processing 3330 * 3331 * @return true if overwrite was set in the header otherwise false 3332 */ 3333 private boolean parseOverwriteHeader(HttpServletRequest req) { 3334 3335 boolean overwrite = true; 3336 String overwriteHeader = req.getHeader(HEADER_OVERWRITE); 3337 3338 if (overwriteHeader != null) { 3339 if (overwriteHeader.equalsIgnoreCase("T")) { 3340 overwrite = true; 3341 } else { 3342 overwrite = false; 3343 } 3344 } 3345 3346 return overwrite; 3347 } 3348 3349 /** 3350 * Propfind helper method.<p> 3351 * 3352 * @param req the servlet request 3353 * @param elem the parent element where to add the generated subelements 3354 * @param item the current item where to parse the properties 3355 * @param type the propfind type 3356 * @param propertiesVector if the propfind type is find properties by 3357 * name, then this Vector contains those properties 3358 */ 3359 private void parseProperties( 3360 HttpServletRequest req, 3361 Element elem, 3362 I_CmsRepositoryItem item, 3363 int type, 3364 List<String> propertiesVector) { 3365 3366 String path = item.getName(); 3367 Element responseElem = addElement(elem, TAG_RESPONSE); 3368 3369 String status = "HTTP/1.1 " 3370 + CmsWebdavStatus.SC_OK 3371 + " " 3372 + CmsWebdavStatus.getStatusText(CmsWebdavStatus.SC_OK); 3373 3374 // Generating href element 3375 Element hrefElem = addElement(responseElem, TAG_HREF); 3376 3377 String href = req.getContextPath() + req.getServletPath(); 3378 if ((href.endsWith("/")) && (path.startsWith("/"))) { 3379 href += path.substring(1); 3380 } else { 3381 href += path; 3382 } 3383 3384 try { 3385 hrefElem.addText(rewriteUrl(href)); 3386 } catch (UnsupportedEncodingException ex) { 3387 return; 3388 } 3389 3390 String resourceName = path; 3391 3392 Element propstatElem = addElement(responseElem, TAG_PROPSTAT); 3393 Element propElem = addElement(propstatElem, TAG_PROP); 3394 3395 switch (type) { 3396 3397 case FIND_ALL_PROP: 3398 3399 addElement(propElem, TAG_CREATIONDATE).addText(ISO8601_FORMAT.format(new Date(item.getCreationDate()))); 3400 addElement(propElem, TAG_DISPLAYNAME).addCDATA(resourceName); 3401 3402 // properties only for files (no collections) 3403 if (!item.isCollection()) { 3404 3405 addElement(propElem, TAG_LASTMODIFIED).addText( 3406 HTTP_DATE_FORMAT.format(new Date(item.getLastModifiedDate()))); 3407 3408 addElement(propElem, TAG_CONTENTLENGTH).addText(String.valueOf(item.getContentLength())); 3409 3410 String contentType = getServletContext().getMimeType(item.getName()); 3411 if (contentType != null) { 3412 addElement(propElem, TAG_CONTENTTYPE).addText(contentType); 3413 } 3414 addElement(propElem, TAG_ETAG).addText(getETag(item)); 3415 addElement(propElem, TAG_RESOURCETYPE); 3416 } else { 3417 addElement(addElement(propElem, TAG_RESOURCETYPE), TAG_COLLECTION); 3418 } 3419 3420 addElement(propElem, TAG_SOURCE).addText(""); 3421 3422 Element suppLockElem = addElement(propElem, TAG_SUPPORTEDLOCK); 3423 Element lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY); 3424 addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_EXCLUSIVE); 3425 addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE); 3426 lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY); 3427 addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_SHARED); 3428 addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE); 3429 3430 generateLockDiscovery(path, propElem, req); 3431 3432 addElement(propstatElem, TAG_STATUS).addText(status); 3433 3434 break; 3435 3436 case FIND_PROPERTY_NAMES: 3437 3438 addElement(propElem, TAG_CREATIONDATE); 3439 addElement(propElem, TAG_DISPLAYNAME); 3440 if (!item.isCollection()) { 3441 3442 addElement(propElem, TAG_CONTENTLANGUAGE); 3443 addElement(propElem, TAG_CONTENTLENGTH); 3444 addElement(propElem, TAG_CONTENTTYPE); 3445 addElement(propElem, TAG_ETAG); 3446 } 3447 addElement(propElem, TAG_LASTMODIFIED); 3448 addElement(propElem, TAG_RESOURCETYPE); 3449 addElement(propElem, TAG_SOURCE); 3450 addElement(propElem, TAG_LOCKDISCOVERY); 3451 3452 addElement(propstatElem, TAG_STATUS).addText(status); 3453 3454 break; 3455 3456 case FIND_BY_PROPERTY: 3457 3458 List<String> propertiesNotFound = new Vector<String>(); 3459 3460 // Parse the list of properties 3461 Iterator<String> iter = propertiesVector.iterator(); 3462 while (iter.hasNext()) { 3463 String property = iter.next(); 3464 3465 if (property.equals(TAG_CREATIONDATE)) { 3466 addElement(propElem, TAG_CREATIONDATE).addText( 3467 ISO8601_FORMAT.format(new Date(item.getCreationDate()))); 3468 } else if (property.equals(TAG_DISPLAYNAME)) { 3469 addElement(propElem, TAG_DISPLAYNAME).addCDATA(resourceName); 3470 } else if (property.equals(TAG_CONTENTLANGUAGE)) { 3471 if (item.isCollection()) { 3472 propertiesNotFound.add(property); 3473 } else { 3474 addElement(propElem, TAG_CONTENTLANGUAGE); 3475 } 3476 } else if (property.equals(TAG_CONTENTLENGTH)) { 3477 if (item.isCollection()) { 3478 propertiesNotFound.add(property); 3479 } else { 3480 addElement(propElem, TAG_CONTENTLENGTH).addText((String.valueOf(item.getContentLength()))); 3481 } 3482 } else if (property.equals(TAG_CONTENTTYPE)) { 3483 if (item.isCollection()) { 3484 propertiesNotFound.add(property); 3485 } else { 3486 String contentType = item.getMimeType(); 3487 if (contentType == null) { 3488 contentType = getServletContext().getMimeType(item.getName()); 3489 } 3490 3491 if (contentType != null) { 3492 addElement(propElem, TAG_CONTENTTYPE).addText(contentType); 3493 } 3494 } 3495 } else if (property.equals(TAG_ETAG)) { 3496 if (item.isCollection()) { 3497 propertiesNotFound.add(property); 3498 } else { 3499 addElement(propElem, TAG_ETAG).addText(getETag(item)); 3500 } 3501 } else if (property.equals(TAG_LASTMODIFIED)) { 3502 addElement(propElem, TAG_LASTMODIFIED).addText( 3503 HTTP_DATE_FORMAT.format(new Date(item.getLastModifiedDate()))); 3504 } else if (property.equals(TAG_RESOURCETYPE)) { 3505 if (item.isCollection()) { 3506 addElement(addElement(propElem, TAG_RESOURCETYPE), TAG_COLLECTION); 3507 } else { 3508 addElement(propElem, TAG_RESOURCETYPE); 3509 } 3510 } else if (property.equals(TAG_SOURCE)) { 3511 addElement(propElem, TAG_SOURCE).addText(""); 3512 } else if (property.equals(TAG_SUPPORTEDLOCK)) { 3513 suppLockElem = addElement(propElem, TAG_SUPPORTEDLOCK); 3514 lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY); 3515 addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_EXCLUSIVE); 3516 addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE); 3517 lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY); 3518 addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_SHARED); 3519 addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE); 3520 } else if (property.equals(TAG_LOCKDISCOVERY)) { 3521 if (!generateLockDiscovery(path, propElem, req)) { 3522 addElement(propElem, TAG_LOCKDISCOVERY); 3523 } 3524 } else { 3525 propertiesNotFound.add(property); 3526 } 3527 } 3528 3529 addElement(propstatElem, TAG_STATUS).addText(status); 3530 3531 if (propertiesNotFound.size() > 0) { 3532 status = "HTTP/1.1 " 3533 + CmsWebdavStatus.SC_NOT_FOUND 3534 + " " 3535 + CmsWebdavStatus.getStatusText(CmsWebdavStatus.SC_NOT_FOUND); 3536 3537 propstatElem = addElement(responseElem, TAG_PROPSTAT); 3538 propElem = addElement(propstatElem, TAG_PROP); 3539 3540 Iterator<String> notFoundIter = propertiesNotFound.iterator(); 3541 while (notFoundIter.hasNext()) { 3542 addElement(propElem, notFoundIter.next()); 3543 } 3544 3545 addElement(propstatElem, TAG_STATUS).addText(status); 3546 } 3547 3548 break; 3549 3550 default: 3551 3552 if (LOG.isErrorEnabled()) { 3553 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PROPFIND_TYPE_0)); 3554 } 3555 break; 3556 } 3557 } 3558 3559 /** 3560 * Sends a response back to authenticate the user.<p> 3561 * 3562 * @param resp the servlet response we are processing 3563 * 3564 * @throws IOException if errors while writing to response occurs 3565 */ 3566 private void requestAuthorization(HttpServletResponse resp) throws IOException { 3567 3568 // Authorisation is required for the requested action. 3569 resp.setHeader(CmsRequestUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + BASIC_REALM + "\""); 3570 3571 resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); 3572 } 3573 3574 /** 3575 * Send a multistatus element containing a complete error report to the 3576 * client.<p> 3577 * 3578 * @param req the servlet request we are processing 3579 * @param resp the servlet response we are processing 3580 * @param errors the errors to be displayed 3581 * 3582 * @throws IOException if errors while writing to response occurs 3583 */ 3584 private void sendReport(HttpServletRequest req, HttpServletResponse resp, Map<String, Integer> errors) 3585 throws IOException { 3586 3587 resp.setStatus(CmsWebdavStatus.SC_MULTI_STATUS); 3588 3589 String absoluteUri = req.getRequestURI(); 3590 String relativePath = getRelativePath(req); 3591 3592 Document doc = DocumentHelper.createDocument(); 3593 Element multiStatusElem = doc.addElement(new QName(TAG_MULTISTATUS, Namespace.get(DEFAULT_NAMESPACE))); 3594 3595 Iterator<Entry<String, Integer>> it = errors.entrySet().iterator(); 3596 while (it.hasNext()) { 3597 Entry<String, Integer> e = it.next(); 3598 String errorPath = e.getKey(); 3599 int errorCode = e.getValue().intValue(); 3600 3601 Element responseElem = addElement(multiStatusElem, TAG_RESPONSE); 3602 3603 String toAppend = errorPath.substring(relativePath.length()); 3604 if (!toAppend.startsWith("/")) { 3605 toAppend = "/" + toAppend; 3606 } 3607 addElement(responseElem, TAG_HREF).addText(absoluteUri + toAppend); 3608 addElement(responseElem, TAG_STATUS).addText( 3609 "HTTP/1.1 " + errorCode + " " + CmsWebdavStatus.getStatusText(errorCode)); 3610 } 3611 3612 Writer writer = resp.getWriter(); 3613 doc.write(writer); 3614 writer.close(); 3615 } 3616}