Java tutorial
/* * 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 <http://www.gnu.org/licenses/>. */ package jp.aegif.alfresco.online_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.repo.webdav.LockInfo; import org.alfresco.repo.webdav.LockInfoImpl; import org.alfresco.repo.webdav.WebDAV; import org.alfresco.repo.webdav.WebDAVLockService; import org.alfresco.repo.webdav.WebDAVServerException; 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<String, Integer> accessDeniedStatusCodes = new LinkedHashMap<String, Integer>(); 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<Condition> 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<NodeRef, NodeRef> m_childToParent = new HashMap<NodeRef, NodeRef>(); protected Map<NodeRef, LockInfo> m_parentLockInfo = new HashMap<NodeRef, LockInfo>(); 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 <tt>true</tt> if the method is a query method only. The default implementation * returns <tt>false</tt>. * * @return Returns <tt>true</tt> 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<Object> executeImplCallback = new RetryingTransactionCallback<Object>() { public Object execute() throws Exception { // Reset the request input stream / reader state WebDAVMethod.this.m_inputStream = null; WebDAVMethod.this.m_reader = null; // cache current session getDAVHelper().getLockService().setCurrentSession(m_request.getSession()); executeImpl(); return null; } }; try { boolean isReadOnly = isReadOnly(); // Execute the method getTransactionService().getRetryingTransactionHelper().doInTransaction(executeImplCallback, isReadOnly); generateResponseImpl(); } catch (AccessDeniedException e) { // Return a forbidden status throw new WebDAVServerException(getStatusForAccessDeniedException(), e); } catch (Throwable e) { if (e instanceof WebDAVServerException) { throw (WebDAVServerException) e; } else if (e.getCause() instanceof WebDAVServerException) { throw (WebDAVServerException) e.getCause(); } else { boolean logOnly = false; Throwable t = e; while ((t = t.getCause()) != null) { if (t instanceof SocketException) { logOnly = true; // The client aborted the connection - we can't do much about this, except log it. if (logger.isTraceEnabled() || logger.isDebugEnabled()) { String message = "Client dropped connection [uri=" + m_request.getRequestURI() + "]"; if (logger.isTraceEnabled()) { // Include a stack trace when trace is enabled. logger.trace(message, e); } else if (logger.isDebugEnabled()) { // Just a message for debug-level output. logger.debug(message); } } break; } } // Convert error to a server error if (!logOnly) { throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } } finally { cleanUp(); } } /** * Clean up resources if about to finish processing the request. */ private void cleanUp() { // Remove temporary file if created if (this.m_requestBody != null) { try { this.m_requestBody.delete(); this.m_requestBody = null; } catch (Throwable t) { WebDAVMethod.logger.error("Failed to delete temp file", t); } } } /** * Access the content repository to satisfy the request and generates the appropriate WebDAV * response. * * @throws WebDAVServerException a general server exception * @throws Exception any unhandled exception */ protected abstract void executeImpl() throws WebDAVServerException, Exception; /** * Does nothing unless overridden - for reasons of backwards compatibility. Subclasses * implementing this method should separate the WebDAV method execution logic from * response generation logic. Execution logic should be contained in the {@link #executeImpl} method * and should NOT contain any code that writes to the response. Conversely response generation logic * should NOT contain any code relating to the desired effect of the WebDAV method (e.g. setting properties * on a node) and should be contained purely within this method. * <p> * Older methods, until refactored will not override this method, relying only on {@link #executeImpl()}. */ protected void generateResponseImpl() throws Exception { } /** * Parses the given request body represented as an XML document and sets any necessary context * ready for execution. */ protected abstract void parseRequestBody() throws WebDAVServerException; /** * Parses the HTTP headers of the request and sets any necessary context ready for execution. */ protected abstract void parseRequestHeaders() throws WebDAVServerException; /** * Retrieves the request body as an XML document * * @return The body of the request as an XML document or null if there isn't a body */ protected Document getRequestBodyAsDocument() throws WebDAVServerException { Document body = null; if (m_request.getContentLength() > 0) { // TODO: Do we need to do anything for chunking support? try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); if (m_request.getCharacterEncoding() == null) { // Let the XML parser work out the encoding if it is not explicitly declared in the HTTP header body = builder.parse(new InputSource(m_request.getInputStream())); } else { body = builder.parse(new InputSource(m_request.getReader())); } } catch (ParserConfigurationException e) { throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST, e); } catch (SAXException e) { throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST, e); } catch (IOException e) { throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST, e); } } return body; } /** * Parses "Depth" request header * * @throws WebDAVServerException */ protected void parseDepthHeader() throws WebDAVServerException { // Store the Depth header as this is used by several WebDAV methods String strDepth = m_request.getHeader(WebDAV.HEADER_DEPTH); if (strDepth != null && strDepth.length() > 0) { if (strDepth.equals(WebDAV.ZERO)) { m_depth = WebDAV.DEPTH_0; } else if (strDepth.equals(WebDAV.ONE)) { m_depth = WebDAV.DEPTH_1; } else { m_depth = WebDAV.DEPTH_INFINITY; } } } /** * Parses "If" header of the request. * Stores conditions that should be checked. * Parses both No-tag-list and Tagged-list formats * See "10.4.2 Syntax" paragraph of the WebDAV specification for "If" header format. * */ protected void parseIfHeader() throws WebDAVServerException { //String strLockToken = null; String strIf = m_request.getHeader(WebDAV.HEADER_IF); if (logger.isDebugEnabled()) logger.debug("Parsing If header: " + strIf); if (strIf != null && strIf.length() > 0) { if (strIf.startsWith("<")) { m_resourceTag = strIf.substring(1, strIf.indexOf(">")); strIf = strIf.substring(m_resourceTag.length() + 3); } m_conditions = new LinkedList<Condition>(); String[] parts = strIf.split("\\) \\("); for (int i = 0; i < parts.length; i++) { String partString = parts[i].replaceAll("\\(", "").replaceAll("\\)", ""); Condition c = new Condition(); String[] conditions = partString.split(" "); for (int j = 0; j < conditions.length; j++) { boolean fNot = false; String eTag = null; String lockToken = null; if (WebDAV.HEADER_KEY_NOT.equals(conditions[j])) { // Check if Not keyword followed by State-token or entity-tag if (j == (conditions.length - 1)) { throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); } fNot = true; j++; } // read State-token int index = conditions[j].indexOf('<'); if (index != -1) { try { String s = conditions[j].substring(index + 1, conditions[j].indexOf(">")); if (!s.startsWith(WebDAV.OPAQUE_LOCK_TOKEN)) { if (!fNot) { throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); } } else { lockToken = s; c.addLockTocken(lockToken, fNot); } } catch (IndexOutOfBoundsException e) { throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); } } // read entity-tag index = conditions[j].indexOf("[\""); if (index != -1) { // TODO: implement parsing of weak ETags: W/"123..". int index2 = conditions[j].indexOf("]"); if (index2 == -1) { logger.warn("No closing ']': " + conditions[j]); index2 = conditions[j].length(); } eTag = conditions[j].substring(index + 1, index2); c.addETag(eTag, fNot); } } m_conditions.add(c); } } } /** * Return the WebDAV protocol helper * * @return WebDAVHelper */ protected final WebDAVHelper getDAVHelper() { return m_davHelper; } /** * Return the service registry * * @return ServiceRegistry */ protected final ServiceRegistry getServiceRegistry() { return m_davHelper.getServiceRegistry(); } /** * Convenience method to return the transaction service * * @return TransactionService */ protected final TransactionService getTransactionService() { return m_davHelper.getServiceRegistry().getTransactionService(); } /** * Convenience method to return the node service * * @return NodeService */ protected final NodeService getNodeService() { return m_davHelper.getNodeService(); } /** * Convenience method to return the search service * * @return SearchService */ protected final SearchService getSearchService() { return m_davHelper.getSearchService(); } /** * Convenience method to return the namespace service * * @return NamespaceService */ protected final NamespaceService getNamespaceService() { return m_davHelper.getNamespaceService(); } /** * @return Returns the general file/folder manipulation service */ protected final FileFolderService getFileFolderService() { return m_davHelper.getFileFolderService(); } /** * Convenience method to return the content service * * @return ContentService */ protected final ContentService getContentService() { return m_davHelper.getServiceRegistry().getContentService(); } /** * Convenience method to return the mimetype service * * @return MimetypeService */ protected final MimetypeService getMimetypeService() { return m_davHelper.getMimetypeService(); } /** * Retrieve the (WebDAV protocol-level) locking service. * * @return WebDAVLockService */ protected final WebDAVLockService getDAVLockService() { return m_davHelper.getLockService(); } /** * Convenience method to return the action service * * @return ActionService */ protected final ActionService getActionService() { return m_davHelper.getActionService(); } /** * Convenience method to return the permission service * * @return PermissionService */ protected final PermissionService getPermissionService() { return m_davHelper.getPermissionService(); } /** * Convenience method to return the authentication service * * @return AuthenticationService */ protected final AuthenticationService getAuthenticationService() { return m_davHelper.getAuthenticationService(); } /** * @return Returns the path of the servlet, e.g. /webdav */ protected final String getServletPath() { return m_request.getServletPath(); } /** * @return Returns the context path of the servlet, e.g. /alfresco */ protected final String getContextPath() { return m_request.getContextPath(); } /** * Return the root node * * @return NodeRef */ protected final NodeRef getRootNodeRef() { return m_rootNodeRef; } /** * Return the relative path * * @return String */ public String getPath() { return m_strPath; } /** * Returns the format required for an XML response. This may vary per method. */ protected OutputFormat getXMLOutputFormat() { // Check if debug output or XML pretty printing is enabled return (XMLPrettyPrint || logger.isDebugEnabled()) ? OutputFormat.createPrettyPrint() : OutputFormat.createCompactFormat(); } /** * Create an XML writer for the response * * @return XMLWriter * @exception IOException */ protected final XMLWriter createXMLWriter() throws IOException { // Buffer the XML response, in case we have to reset mid-transaction m_xmlWriter = new CharArrayWriter(1024); return new XMLWriter(m_xmlWriter, getXMLOutputFormat()); } /** * Generates the lock discovery XML response * * @param xml XMLWriter * @param lockNode NodeRef * @param lockInfo */ protected void generateLockDiscoveryXML(XMLWriter xml, FileInfo lockNodeInfo, LockInfo lockInfo) throws Exception { String owner, scope, depth; Date expiry; owner = lockInfo.getOwner(); expiry = lockInfo.getExpires(); scope = lockInfo.getScope(); depth = lockInfo.getDepth(); generateLockDiscoveryXML(xml, lockNodeInfo, false, scope, depth, WebDAV.makeLockToken(lockNodeInfo.getNodeRef(), owner), owner, expiry); } /** * Generates the lock discovery XML response * * @param xml XMLWriter * @param lockNode NodeRef * @param emptyNamespace boolean True if namespace should be empty. Used to avoid bugs in WebDAV clients. * @param scope String lock scope * @param depth String lock depth * @param lToken String locktoken * @param owner String lock owner * @param expiryDate the date/time the lock should expire */ protected void generateLockDiscoveryXML(XMLWriter xml, FileInfo lockNodeInfo, boolean emptyNamespace, String scope, String depth, String lToken, String owner, Date expiryDate) throws Exception { Attributes nullAttr = getDAVHelper().getNullAttributes(); String ns = emptyNamespace ? "" : WebDAV.DAV_NS; if (lockNodeInfo != null) { // Output the XML response xml.startElement(ns, WebDAV.XML_LOCK_DISCOVERY, emptyNamespace ? WebDAV.XML_LOCK_DISCOVERY : WebDAV.XML_NS_LOCK_DISCOVERY, nullAttr); xml.startElement(ns, WebDAV.XML_ACTIVE_LOCK, emptyNamespace ? WebDAV.XML_ACTIVE_LOCK : WebDAV.XML_NS_ACTIVE_LOCK, nullAttr); xml.startElement(ns, WebDAV.XML_LOCK_TYPE, emptyNamespace ? WebDAV.XML_LOCK_TYPE : WebDAV.XML_NS_LOCK_TYPE, nullAttr); xml.write(DocumentHelper.createElement(emptyNamespace ? WebDAV.XML_WRITE : WebDAV.XML_NS_WRITE)); xml.endElement(ns, WebDAV.XML_LOCK_TYPE, emptyNamespace ? WebDAV.XML_LOCK_TYPE : WebDAV.XML_NS_LOCK_TYPE); xml.startElement(ns, WebDAV.XML_LOCK_SCOPE, emptyNamespace ? WebDAV.XML_LOCK_SCOPE : WebDAV.XML_NS_LOCK_SCOPE, nullAttr); xml.write(DocumentHelper.createElement(emptyNamespace ? scope : WebDAV.DAV_NS_PREFIX + scope)); xml.endElement(ns, WebDAV.XML_LOCK_SCOPE, emptyNamespace ? WebDAV.XML_LOCK_SCOPE : WebDAV.XML_NS_LOCK_SCOPE); // NOTE: We only support one level of lock at the moment xml.startElement(ns, WebDAV.XML_DEPTH, emptyNamespace ? WebDAV.XML_DEPTH : WebDAV.XML_NS_DEPTH, nullAttr); xml.write(depth); xml.endElement(ns, WebDAV.XML_DEPTH, emptyNamespace ? WebDAV.XML_DEPTH : WebDAV.XML_NS_DEPTH); xml.startElement(ns, WebDAV.XML_OWNER, emptyNamespace ? WebDAV.XML_OWNER : WebDAV.XML_NS_OWNER, nullAttr); xml.write(owner); xml.endElement(ns, WebDAV.XML_OWNER, emptyNamespace ? WebDAV.XML_OWNER : WebDAV.XML_NS_OWNER); xml.startElement(ns, WebDAV.XML_TIMEOUT, emptyNamespace ? WebDAV.XML_TIMEOUT : WebDAV.XML_NS_TIMEOUT, nullAttr); // Output the expiry time String strTimeout = WebDAV.INFINITE; if (expiryDate != null) { long timeoutRemaining = (expiryDate.getTime() - System.currentTimeMillis()) / 1000L; strTimeout = WebDAV.SECOND + timeoutRemaining; } xml.write(strTimeout); xml.endElement(ns, WebDAV.XML_TIMEOUT, emptyNamespace ? WebDAV.XML_TIMEOUT : WebDAV.XML_NS_TIMEOUT); xml.startElement(ns, WebDAV.XML_LOCK_TOKEN, emptyNamespace ? WebDAV.XML_LOCK_TOKEN : WebDAV.XML_NS_LOCK_TOKEN, nullAttr); xml.startElement(ns, WebDAV.XML_HREF, emptyNamespace ? WebDAV.XML_HREF : WebDAV.XML_NS_HREF, nullAttr); xml.write(lToken); xml.endElement(ns, WebDAV.XML_HREF, emptyNamespace ? WebDAV.XML_HREF : WebDAV.XML_NS_HREF); xml.endElement(ns, WebDAV.XML_LOCK_TOKEN, emptyNamespace ? WebDAV.XML_LOCK_TOKEN : WebDAV.XML_NS_LOCK_TOKEN); xml.endElement(ns, WebDAV.XML_ACTIVE_LOCK, emptyNamespace ? WebDAV.XML_ACTIVE_LOCK : WebDAV.XML_NS_ACTIVE_LOCK); xml.endElement(ns, WebDAV.XML_LOCK_DISCOVERY, emptyNamespace ? WebDAV.XML_LOCK_DISCOVERY : WebDAV.XML_NS_LOCK_DISCOVERY); } } /** * Generates a list of namespace declarations for the response */ protected String generateNamespaceDeclarations(HashMap<String, String> nameSpaces) { StringBuilder ns = new StringBuilder(); ns.append(" "); ns.append(WebDAV.XML_NS); ns.append(":"); ns.append(WebDAV.DAV_NS); ns.append("=\""); ns.append(WebDAV.DEFAULT_NAMESPACE_URI); ns.append("\""); // Add additional namespaces if (nameSpaces != null) { Iterator<String> namespaceList = nameSpaces.keySet().iterator(); while (namespaceList.hasNext()) { String strNamespaceUri = namespaceList.next(); String strNamespaceName = nameSpaces.get(strNamespaceUri); ns.append(" ").append(WebDAV.XML_NS).append(":").append(strNamespaceName).append("=\""); ns.append(strNamespaceUri == null ? "" : strNamespaceUri).append("\" "); } } return ns.toString(); } /** * Checks if write operation can be performed on node. * * @param fileInfo - node's file info * @param ignoreShared - if true ignores shared locks * @param lockMethod - must be true if used from lock method * @return node's lock info * @throws WebDAVServerException if node has shared or exclusive lock * or If header preconditions failed */ protected LockInfo checkNode(FileInfo fileInfo, boolean ignoreShared, boolean lockMethod) throws WebDAVServerException { LockInfo nodeLockInfo = getNodeLockInfo(fileInfo); NodeRef nodeRef = fileInfo.getNodeRef(); // Regardless of WebDAV locks, if we can't write to this node, then it's locked! if (getDAVHelper().isLockedAndReadOnly(nodeRef)) { throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); } String nodeETag = getDAVHelper().makeQuotedETag(fileInfo); // Handle the case where there are no conditions and no lock token stored on the node. Node just needs to be writable with no shared locks if (m_conditions == null) { // ALF-3681 fix. WebDrive 10 client doesn't send If header when locked resource is updated so check the node by lockOwner. if (!nodeLockInfo.isExclusive() || (m_userAgent != null && m_userAgent.equals(WebDAV.AGENT_MICROSOFT_DATA_ACCESS_INTERNET_PUBLISHING_PROVIDER_DAV))) { if (!ignoreShared && nodeLockInfo.isShared() && !nodeLockInfo.getSharedLockTokens().isEmpty()) { throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); } return nodeLockInfo; } } // Checking of the If tag consists of two checks: // 1. Check the appropriate lock token for the node has been supplied (if the node is locked) // 2. If there are conditions, check at least one condition (corresponding to this node) is satisfied. checkLockToken(nodeLockInfo, ignoreShared, lockMethod); checkConditions(nodeLockInfo.getExclusiveLockToken(), nodeETag); return nodeLockInfo; } /** * Checks if write operation can be performed on node. * * @param fileInfo * @return * @throws WebDAVServerException if node has shared or exclusive lock * or If header preconditions failed */ protected LockInfo checkNode(FileInfo fileInfo) throws WebDAVServerException { return checkNode(fileInfo, false, true); } /** * Checks if node can be accessed with WebDAV operation * * @param lockInfo - node's lock info * @param ignoreShared - if true - ignores shared lock tokens * @param lockMethod - must be true if used from lock method * @throws WebDAVServerException if node has no appropriate lock token */ private void checkLockToken(LockInfo lockInfo, boolean ignoreShared, boolean lockMethod) throws WebDAVServerException { String nodeLockToken = lockInfo.getExclusiveLockToken(); Set<String> sharedLockTokens = lockInfo.getSharedLockTokens(); if (m_conditions != null) { // Request has conditions to check if (lockInfo.isShared()) { // Node has shared lock. Check if conditions contains lock token of the node. // If not throw exception if (!sharedLockTokens.isEmpty()) { if (!ignoreShared) { for (Condition condition : m_conditions) { for (String sharedLockToken : sharedLockTokens) { if (condition.getLockTokensMatch().contains(sharedLockToken)) { return; } } } throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); } return; } } else { // This particular node is not locked so it will not be part of the conditions if (nodeLockToken == null) { return; } // Node has exclusive lock. Check if conditions contains lock token of the node // If not throw exception for (Condition condition : m_conditions) { if (condition.getLockTokensMatch().contains(nodeLockToken)) { return; } } throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); } } else { // Request has no conditions if (lockInfo.isShared()) { // If lock is shared and check was called not from LOCK method return if (!lockMethod) { return; } // Throw exception - we can't set lock on node with shared lock throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); } } throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); } /** * Checks If header conditions. Throws WebDAVServerException with 412(Precondition failed) * if none of the conditions success. * * @param nodeLockToken - node's lock token * @param nodeETag - node's ETag * @throws WebDAVServerException if conditions fail */ private void checkConditions(String nodeLockToken, String nodeETag) throws WebDAVServerException { // Checks If header conditions. // Each condition can contain check of ETag and check of Lock token. if (m_conditions == null) { // No conditions were provided with "If" request header, so check successful return; } // Check the list of "If" header's conditions. // If any condition conforms then check is successful for (Condition condition : m_conditions) { // Flag for ETag conditions boolean fMatchETag = true; // Flag for Lock token conditions boolean fMatchLockToken = true; // Check ETags that should match if (condition.getETagsMatch() != null) { fMatchETag = condition.getETagsMatch().contains(nodeETag) ? true : false; } // Check ETags that shouldn't match if (condition.getETagsNotMatch() != null) { fMatchETag = condition.getETagsNotMatch().contains(nodeETag) ? false : true; } // Check lock tokens that should match if (condition.getLockTokensMatch() != null) { fMatchLockToken = nodeLockToken == null || condition.getLockTokensMatch().contains(nodeLockToken); } // Check lock tokens that shouldn't match if (condition.getLockTokensNotMatch() != null) { fMatchLockToken = nodeLockToken == null || !condition.getLockTokensNotMatch().contains(nodeLockToken); } if (fMatchETag && fMatchLockToken) { // Condition conforms return; } } // None of the conditions successful throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); } /** * Returns node Lock token in consideration of WebDav lock depth. * * @param fileInfo node * @return String Lock token */ protected LockInfo getNodeLockInfo(final FileInfo nodeInfo) { // perf optimisation - effectively run against unprotected nodeService (to bypass repeated permission checks) return AuthenticationUtil.runAs(new RunAsWork<LockInfo>() { public LockInfo doWork() throws Exception { return getNodeLockInfoImpl(nodeInfo); } }, AuthenticationUtil.getSystemUserName()); } private LockInfo getNodeLockInfoImpl(final FileInfo nodeInfo) { // Check if node is locked directly. LockInfo lockInfo = getNodeLockInfoDirect(nodeInfo); if (lockInfo != null) { return lockInfo; } // Node isn't locked directly, try to search for an indirect lock. // ALF-13472: In accordance with http://www.webdav.org/specs/rfc2518.html#rfc.section.8.10.4 lock of collection causes locking each resource within it. // It should be possible to receive information about direct or indirect lock because it is one of the states of requested resource. return AuthenticationUtil.runAsSystem(new RunAsWork<LockInfo>() { @Override public LockInfo doWork() throws Exception { NodeService nodeService = getNodeService(); NodeRef node = nodeInfo.getNodeRef(); while (true) { NodeRef parent = m_childToParent.get(node); if ((parent == null) && (!m_childToParent.containsKey(node))) { ChildAssociationRef childAssocRef = nodeService.getPrimaryParent(node); parent = childAssocRef.getParentRef(); if (!childAssocRef.getTypeQName().equals(ContentModel.ASSOC_CONTAINS)) { parent = null; } // temporarily cache - for this request m_childToParent.put(node, parent); } if (parent == null) { // Node has no lock and Lock token return new LockInfoImpl(); } LockInfo lockInfo = m_parentLockInfo.get(parent); if (lockInfo != null) { if (lockInfo.isLocked()) { return lockInfo; } } if (lockInfo == null) { try { lockInfo = getNodeLockInfoIndirect(parent); if (lockInfo != null) { return lockInfo; } } finally { if (lockInfo == null) { lockInfo = new LockInfoImpl(); } // temporarily cache - for this request m_parentLockInfo.put(parent, lockInfo); } } node = parent; } // end while } }); } /** * Checks if a node is directly locked. A direct lock is one associated with the node itself * rather than associated with some ancestor. * * @param nodeInfo * @return The LockInfo if the node is <strong>locked</strong>, or null otherwise. */ private LockInfo getNodeLockInfoDirect(FileInfo nodeInfo) { LockInfo lock = getDAVLockService().getLockInfo(nodeInfo.getNodeRef()); if (lock == null) { return null; } if (lock.isLocked()) { return lock; } return null; } /** * Checks whether a parent node has a lock that is valid for all its descendants. * * @param parent * @return The LockInfo if the node is <strong>locked</strong>, or null otherwise. */ private LockInfo getNodeLockInfoIndirect(NodeRef parent) { LockInfo parentLock = getDAVLockService().getLockInfo(parent); if (parentLock == null) { return null; } // In this case node is locked indirectly. if (parentLock.isLocked() && WebDAV.INFINITY.equals(parentLock.getDepth())) { // In this case node is locked indirectly. //Get lock scope // Get shared lock tokens // Store lock information to the lockInfo object // Get lock token of the locked node - this is indirect lock token. return parentLock; } return null; // No has no exclusive lock but can be locked with shared lock // Check node lock depth. // If depth is WebDAV.INFINITY then return this node's Lock token. // In this case node is locked indirectly. //Get lock scope // Node has it's own Lock token. } /** * Get the file info for the given paths * * @param rootNodeRef the acting webdav root * @param path the path to search for * @param servletPath the base servlet path, which may be null or empty * @return Return the file info for the path * @throws FileNotFoundException if the path doesn't refer to a valid node */ protected FileInfo getNodeForPath(NodeRef rootNodeRef, String path, String servletPath) throws FileNotFoundException { return getDAVHelper().getNodeForPath(rootNodeRef, path); } /** * Returns a URL that could be used to access the given path. * * @param request HttpServletRequest * @param path the path to search for * @param isFolder indicates file or folder is requested * @return URL that could be used to access the given path */ protected String getURLForPath(HttpServletRequest request, String path, boolean isFolder) { return getDAVHelper().getURLForPath(request, path, isFolder, m_userAgent); } /** * Determines whether the XMLWriter should be flushed when XML is flushed. For some reason this is method specific. * @return <code>true</code> if the XMLWriter should be flushed when XML is flushed */ protected boolean shouldFlushXMLWriter() { return true; } /** * Flushes all XML written so far to the response * * @param xml XMLWriter that should be flushed */ protected final void flushXML(XMLWriter writer) throws IOException { if (shouldFlushXMLWriter()) { writer.flush(); } m_response.getWriter().write(m_xmlWriter.toCharArray()); m_xmlWriter.reset(); } /** * Returns a working copy of node for current user. * * @param nodeRef node reference * @return Returns the working copy's file information */ protected FileInfo getWorkingCopy(NodeRef nodeRef) { FileInfo result = null; NodeRef workingCopy = getServiceRegistry().getCheckOutCheckInService().getWorkingCopy(nodeRef); if (workingCopy != null) { String workingCopyOwner = getNodeService() .getProperty(workingCopy, ContentModel.PROP_WORKING_COPY_OWNER).toString(); if (workingCopyOwner.equals(getAuthenticationService().getCurrentUserName())) { result = getFileFolderService().getFileInfo(workingCopy); } } return result; } /** * Determines status code for AccessDeniedException based on client's HTTP headers. * * @return Returns status code */ protected int getStatusForAccessDeniedException() { if (m_request != null && m_request.getHeader(WebDAV.HEADER_USER_AGENT) != null) { String userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT); for (Entry<String, Integer> entry : accessDeniedStatusCodes.entrySet()) { if (Pattern.compile(entry.getKey()).matcher(userAgent).find()) { return entry.getValue(); } } } return HttpServletResponse.SC_UNAUTHORIZED; } /** * Class used for storing conditions which comes with "If" header of the request * * @author ivanry */ protected class Condition { // These tokens will be checked on equivalence against node's lock token private LinkedList<String> lockTokensMatch = new LinkedList<String>(); // These tokens will be checked on non-equivalence against node's lock token private LinkedList<String> lockTokensNotMatch = new LinkedList<String>(); // These ETags will be checked on equivalence against node's ETags private LinkedList<String> eTagsMatch; // These ETags will be checked on non-equivalence against node's ETags private LinkedList<String> eTagsNotMatch; /** * Default constructor * */ public Condition() { } /** * Returns the list of lock tokens that should be checked against node's lock token on equivalence. * * @return lock tokens */ public LinkedList<String> getLockTokensMatch() { return this.lockTokensMatch; } /** * Returns the list of lock tokens that should be checked against node's lock token on non-equivalence. * * @return lock tokens */ public LinkedList<String> getLockTokensNotMatch() { return this.lockTokensNotMatch; } /** * Returns the list of ETags that should be checked against node's ETag on equivalence. * * @return ETags list */ public LinkedList<String> getETagsMatch() { return this.eTagsMatch; } /** * Returns the list of ETags that should be checked against node's ETag on non-equivalence. * * @return ETags list */ public LinkedList<String> getETagsNotMatch() { return this.eTagsNotMatch; } /** * Adds lock token to check * * @param lockToken String * @param notMatch true is lock token should be added to the list matched tokens. * false if should be added to the list of non-matches. */ public void addLockTocken(String lockToken, boolean notMatch) { if (notMatch) { this.lockTokensNotMatch.add(lockToken); } else { this.lockTokensMatch.add(lockToken); } } /** * Add ETag to check * * @param eTag String * @param notMatch true is ETag should be added to the list matched ETags. * false if should be added to the list of non-matches. */ public void addETag(String eTag, boolean notMatch) { if (notMatch) { if (eTagsNotMatch == null) { eTagsNotMatch = new LinkedList<String>(); } this.eTagsNotMatch.add(eTag); } else { if (eTagsMatch == null) { eTagsMatch = new LinkedList<String>(); } this.eTagsMatch.add(eTag); } } } public String toString() { StringBuffer sb = new StringBuffer(); if (m_request != null) { sb.append("WebDAV "); sb.append(m_request.getMethod()); sb.append(" request for "); sb.append(m_strPath); } else { sb.append("Inactive WebDAV request via "); String clz = getClass().getName(); sb.append(clz.substring(clz.lastIndexOf('.') + 1)); } return sb.toString(); } /** * Get the site ID (short-name) that the current request relates to. The site ID * will be {@link DEFAULT_SITE_ID} if not specifically set. * * @return The site ID */ protected String getSiteId() { if (siteId == null) { siteId = getDAVHelper().determineSiteId(this); } return siteId; } /** * Get the tenant domain for the current user and request. The tenant domain * will be {@link TenantService#DEFAULT_DOMAIN} if not specifically set. * * @return The tenant domain. */ protected String getTenantDomain() { if (tenantDomain == null) { tenantDomain = getDAVHelper().determineTenantDomain(this); } return tenantDomain; } }