/*
* Copyright (C) 2005-2013 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.webdav;
import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.SocketException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.model.filefolder.HiddenAspect;
import org.alfresco.repo.model.filefolder.HiddenAspect.Visibility;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.FileFilterMode;
import org.alfresco.util.FileFilterMode.Client;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.DocumentHelper;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.springframework.util.FileCopyUtils;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* Abstract base class for all the WebDAV method handling classes
*
* @author gavinc
*/
public abstract class WebDAVMethod
{
// Log output
private static final String VERSION_NUM_PATTERN = "\\d+\\.\\d+(\\.\\d+)?";
protected static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol");
// Output formatted XML in the response
private static final boolean XMLPrettyPrint = true;
// Mapping of User-Agent pattern to response status code
// used to determine which status code should be returned for AccessDeniedException
private static final Map accessDeniedStatusCodes = new LinkedHashMap();
static
{
accessDeniedStatusCodes.put("^WebDAVLib/" + VERSION_NUM_PATTERN + "$",
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
accessDeniedStatusCodes.put("^WebDAVFS/" + VERSION_NUM_PATTERN + " \\(\\d+\\)\\s+Darwin/" +
VERSION_NUM_PATTERN + "\\s+\\(.*\\)$",
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
accessDeniedStatusCodes.put(".*", HttpServletResponse.SC_FORBIDDEN);
}
// Servlet request/response
protected HttpServletRequest m_request;
protected HttpServletResponse m_response;
private File m_requestBody;
private ServletInputStream m_inputStream;
private CharArrayWriter m_xmlWriter;
private BufferedReader m_reader;
// WebDAV helper
protected WebDAVHelper m_davHelper;
// Root node
protected NodeRef m_rootNodeRef;
// Repository path
protected String m_strPath = null;
// User Agent
protected String m_userAgent = null;
// If header conditions
protected LinkedList m_conditions = null;
// If header resource-tag
protected String m_resourceTag = null;
// Depth header
protected int m_depth = WebDAV.DEPTH_INFINITY;
// request scope
protected Map m_childToParent = new HashMap();
protected Map m_parentLockInfo = new HashMap();
private String siteId;
private String tenantDomain;
/**
* Default constructor
*/
public WebDAVMethod()
{
}
/**
* Set the request/response details
*
* @param req
* HttpServletRequest
* @param resp
* HttpServletResponse
* @param registry
* ServiceRegistry
* @param rootNode
* NodeRef
*/
public void setDetails(final HttpServletRequest req, HttpServletResponse resp, WebDAVHelper davHelper,
NodeRef rootNode)
{
// Wrap the request so that it is 'retryable'. Calls to getInputStream() and getReader() will result in the
// request body being read into an intermediate file.
this.m_request = new HttpServletRequestWrapper(req)
{
@Override
public ServletInputStream getInputStream() throws IOException
{
if (WebDAVMethod.this.m_reader != null)
{
throw new IllegalStateException("Reader in use");
}
if (WebDAVMethod.this.m_inputStream == null)
{
final FileInputStream in = new FileInputStream(getRequestBodyAsFile(req));
WebDAVMethod.this.m_inputStream = new ServletInputStream()
{
@Override
public int read() throws IOException
{
return in.read();
}
@Override
public int read(byte b[]) throws IOException
{
return in.read(b);
}
@Override
public int read(byte b[], int off, int len) throws IOException
{
return in.read(b, off, len);
}
@Override
public long skip(long n) throws IOException
{
return in.skip(n);
}
@Override
public int available() throws IOException
{
return in.available();
}
@Override
public void close() throws IOException
{
in.close();
}
@Override
public void mark(int readlimit)
{
in.mark(readlimit);
}
@Override
public void reset() throws IOException
{
in.reset();
}
@Override
public boolean markSupported()
{
return in.markSupported();
}
};
}
return WebDAVMethod.this.m_inputStream;
}
@Override
public BufferedReader getReader() throws IOException
{
if (WebDAVMethod.this.m_inputStream != null)
{
throw new IllegalStateException("Input Stream in use");
}
if (WebDAVMethod.this.m_reader == null)
{
String encoding = req.getCharacterEncoding();
WebDAVMethod.this.m_reader = new BufferedReader(new InputStreamReader(new FileInputStream(
getRequestBodyAsFile(req)), encoding == null ? "ISO-8859-1" : encoding));
}
return WebDAVMethod.this.m_reader;
}
};
this.m_response = resp;
this.m_davHelper = davHelper;
this.m_rootNodeRef = rootNode;
this.m_strPath = m_davHelper.getRepositoryPath(m_request);
}
private File getRequestBodyAsFile(HttpServletRequest req) throws IOException
{
if (this.m_requestBody == null)
{
this.m_requestBody = TempFileProvider.createTempFile("webdav_" + req.getMethod() + "_", ".bin");
OutputStream out = new FileOutputStream(this.m_requestBody);
int bytesRead = FileCopyUtils.copy(req.getInputStream(), out);
// ALF-7377: check for corrupt request
int contentLength = req.getIntHeader(WebDAV.HEADER_CONTENT_LENGTH);
if (contentLength >= 0 && contentLength != bytesRead)
{
throw new IOException("Request body does not have specified Content Length");
}
}
return this.m_requestBody;
}
/**
* Override and return true if the method is a query method only. The default implementation
* returns false.
*
* @return Returns true if the method transaction may be read-only
*/
protected boolean isReadOnly()
{
return false;
}
/**
* Return the property find depth
*
* @return int
*/
public final int getDepth()
{
return m_depth;
}
/**
* Executes the method, wrapping the call to {@link #executeImpl()} in an appropriate transaction
* and handling the error conditions.
* @throws IOException
*/
public void execute() throws WebDAVServerException
{
// Parse the HTTP headers
parseRequestHeaders();
// Parse the HTTP body
try
{
parseRequestBody();
}
catch (WebDAVServerException e)
{
if (e.getCause() != null && e.getCause() instanceof SAXParseException)
{
SAXParseException saxParseEx = (SAXParseException) e.getCause();
if (logger.isTraceEnabled())
{
// Include stack trace.
logger.trace("Malformed request body", saxParseEx);
}
else if (logger.isDebugEnabled())
{
// Log message only.
logger.debug("Malformed request body: " + saxParseEx.getMessage());
}
try
{
m_response.sendError(e.getHttpStatusCode());
}
catch (IOException ioe)
{
if (logger.isDebugEnabled())
{
logger.debug("Unable to send status code", ioe);
}
}
// Halt processing.
return;
}
else
{
// Rethrow the exception, as we haven't dealt with it here.
throw e;
}
}
m_userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT);
RetryingTransactionCallback