mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-14 17:58:59 +00:00
43386: ALF-13091: Prevent bean post processor propagation to child application contexts. Remove Jsr250BeanPostPorcessor from the CXF configuration, to prevent strange interaction with component scanning. 43402: Merged DEV to V4.1-BUG-FIX 43330: ALF-16073: Share forms renders is slow when using sets. Optimize performance for fieldsVisibleInAnyMode populating in getFields and FormField obtaining in getVisibleFieldNamesFor. 43412: ALF-16680 ImageMagick exit code 255 is not seen as an error 43420: ALF-16627 SOLR indexing does not provide TransformationOption when converting content to plain text for indexing 43452: Fix for ALF-16296 - On site customization page, when a page containing apostrophe (') is renamed, a backslash (\) is added to the name 43453: Fix for ALF-16105 - Disabled 'Follow' feature does not disable it for existing users 43462: ALF-16715 : Merged V3.4-BUG-FIX (3.4.12) to V4.1-BUG-FIX (4.1.3) 43461: ALF-16713 Cannot disable metadata extractors 43464: ALF-16715 : Merged V3.4-BUG-FIX (3.4.12) to V4.1-BUG-FIX (4.1.3) 43463: ALF-16713 Cannot disable metadata extractors - fix test failures - none set mimetypeService 43471: Fix for ALF-16542 - Web application context for the onlineEditUrl variable is hard-coded to alfresco: Merged BRANCHES/DEV/CLOUD1_SP to BRANCHES/DEV/V4.1-BUG-FIX 40565: CLOUD-23: core changes to allow overriding of "edit online" button's URL generation. 43475: Fix for ALF-15859 - UploadContentServlet servlet /alfresco/upload can update the content, mime type and encoding but not the locale 43476: Merged DEV to V4.1-BUG-FIX 43401 : ALF-16587 GreenMail IMAP server does not clean up session handlers - memory leak 43478: MNT-181: Now WebDAV will ALWAYS preserve the original metadata and versions of ANY node that is temporarily 'moved out' in ANY kind of 'shuffle' operation - To make the source node temporarily invisible to WebDAV the client specific HIDDEN aspect features are used - WebDAVHelper.isRenameShuffle() method introduced, to parallel ALF-3856 CIFS fix and using similar system.webdav.renameShufflePattern global property to detect the start of a shuffle - WebDAVHelper converted to use proper dependency injection - CopyMethod has become a simple subclass of MoveMethod as all the hidden aspect munging is done by it - DeleteMethod now preserves hidden nodes - PropFindMethod now ignores hidden nodes - Listing methods will hide hidden nodes from WebDAV 43483: MNT-181: Corrected typo 43498: Fix for ALF-16648 - Alfresco Enterprise artifacts in artifacts.alfresco.com do not provide POM files / dependencies declarations: Merged HEAD to V4.1-BUG-FIX (4.1.2) 43380: -- added site content for alfresco-platform-distribution POM 43379: -- added site documentation for alfresco-platform-distribution POM 43378: -- added site documentation for alfresco-platform-distribution POM -- deployed site for 4.2.b Community at https://artifacts.alfresco.com/nexus/content/repositories/alfresco-docs/alfresco-platform-distribution/latest/index.html -- created repository for Enterprise docs and added url in the appropriate edition properties 43273: Use property to define POI version 42966: ALF-14353 - Added platform distribution POM to standard maven-deploy procedure 42965: ALF-14353 - added alfresco-platform-distribution to provide a Maven release descriptor (dependencyManagement) per each Community / Enterprise release -- moved maven-ant-tasks not to be in the runtime lib -- added platform distribution pom in the SDK folder -- updated maven.xml to deploy filter and deploy the appropriate platform-distribution POM per each releae -- in maven.xml moved configure-release and configure-snapshot goals to maven-env-prerequisites -- updated sdk readme to explain the presence of alfresco-platform-distribution POM 42912: -- updated README header on the POM specifying it's NOT usable to build Alfresco -- make a clear reference to the POMs that get deployed by pom-experimental.xml being usable for development 42842: ALF-14353: Fix artifactId alfresco-jlan -> alfresco-jlan-embed 41883: ALF-14353 - fixed multiple Maven build issues. Now mvn clean install -f pom-experimental.xml works fine. Also deployed Spring Surf 1.2.0-SNAPSHOT so proper Surf version is retrieved 41882: added pre-requisites to build POMs successfully with mvn clean install -f pom-experimental.xml 43499: Upgrade version in POM files to 4.1.3-SNAPSHOT 43520: ALF-16694 POI becoming responsiveness and causing jvm to freeze for a while with certain XLS (fraction formats) - Patched POI to: - limit the length of fractions to four digits (more than that takes too long to process) e.g. "# #/#####" is too long and will be reduced to "# #/####" which still takes about a second! - handle the -ve fraction format part (i.e. n in p;n;z), so its length is not interpret as part of the fraction format of the +ve fraction part. - handle custom formats a bit better (strip them) rather than interpret the text length as part of the fraction format - handle -ve fractions (just did not work) - limitations: - custom text gets stripped (still better than before) - formats that have p (+ve) and n (-ve) parts must include a fraction format in each part. Otherwise +ve and -ve values are not formatted as a fraction (still better than before) 43523: MNT-181: Corrections - WebDAVLockService.unlock() made 'harmless' to call on already-unlocked nodes - Delete method hides rather than deletes versioned nodes and working copes in case it is called by OSX Finder during a 'replace' operation 43524: MNT-181: Correction - PutMethod now 'unhides' hidden nodes and behaves as though it created them 43541: Merged DEV to V4.1-BUG-FIX 43536: ALF-16200: WQS delete fails to publish to live projects onDeleteNode behavior The condition of "if" operator was modified. Now "if" operator evaluate to true if least one of the nodesToTransfer or nodesToRemoveOnTransfer sets is not empty. beforeDeleteNode() methods were updated for check for the ASPECT_PENDING_DELETE aspect on the node. enqueueRemovedNodes() call was moved from onDeleteNode() into beforeDeleteNode(). These fixes allow to automatically update the sites published to (i.e. live projects) to include the removal of the file and removal of file from any collection (asset list) when a content was deleted from an editorial project. 43542: Fix for ALF-16618 - ScriptableHashMap does not have hasOwnProperty method 43555: Fix for ALF-16494 - site collaborator has rights to modify comments made by another user. 43556: ALF-15859: Prevent NPE on upload without NodeRef 43558: ALF-16694 POI becoming irresponsive and causing jvm to freeze with XLS that includes fraction formats - Original jar did not get removed in previous commit << NO NEED TO MERGE TO CLOUD1-BUG-FIX as there was a tree conflict and the original jar was removed. >> 43570: MNT-181: More corrections researched by Valery - Don't treat all moves to temporary locations as copies - just those from non-temporary locations. Avoids initial upload leaving lots of hidden files around. - Only copy the content, not the whole node including aspects to avoid versioning temporary files! - Don't version on changes to sys:clientVisibilityMask - avoids 'double versioning' - Recognize Mac .TemporaryItems folder and ._ files as temporary 43575: Fix for ALF-9317 - Links: Delete Link button in Selected Items menu is available for Consumer, Contributor and Collaborator 43577: Upgrade POM files after following changes: 43401: ALF-16587 patch greenmail 43520: ALF-16694 Uprade POI and patch it 43584: Merged V4.1 to V4.1-BUG-FIX <<Record Only>> 43582: Merged V4.1-BUG-FIX to V4.1 (4.1.2) 43402: Merged DEV to V4.1-BUG-FIX 43330: ALF-16073: Share forms renders is slow when using sets. Optimize performance for fieldsVisibleInAnyMode populating in getFields and FormField obtaining in getVisibleFieldNamesFor. 43557: Merged V4.1-BUG-FIX to V4.1 43555: Fix for ALF-16494 - site collaborator has rights to modify comments made by another user. 43586: MNT-181: Final correction researched by Valery - Corrected system.webdav.renameShufflePattern so that it matches .TemporaryItems folder and ._ files as a full match 43591: ALF-16772: If the WebDAV path of a document exceeds 255 characters, documents opened in MSOffice cannot be saved back - Interpret null nodeLockToken as not locked. 43594: Merged DEV to V4.1-BUG-FIX 43540: ALF-12425: Can't launch activiti workflow console from Share when external / ntlm / kerberos authentication is used. New webscript that redirects to activiti admin console with URL holding current ticket. The webscript resides below wcs and allows to use alfresco connector in Share. 43562: ALF-12425: Can't launch activiti workflow console from Share when external / ntlm / kerberos authentication is used. Activiti admin console webscript that allows admin console to be invoked behind wcs authentication. 43595: Merged V4.1 to V4.1-BUG-FIX 43376: Merged DEV to V4.1 43339: ALF-16590 : java.lang.IllegalArgumentException while initiating In-Place import FilesystemContentDataFactory#contentIsInStore method was modified. Now files' absolute paths are compared. 43390: ALF-15856: Test org.alfresco.repo.node.NodeServiceTest does not finish on DB2 - Now inner-nested retrying transaction in testConcurrentArchive passes its exception straight through to the outer transaction, which unwraps and retries it if necessary 43397: ALF-16021: RuleServiceImplTest never ends on DB2 - Added endTransaction() call so that testDeleteSpaceWithExecuteScriptRule() doesn't hang indefinitely waiting for the outer transaction to complete 43398: ALF-15856: Test org.alfresco.repo.node.NodeServiceTest does not finish on DB2 - Now inner-nested retrying transaction runs in its own thread and we don't wait forever for it 43404: ALF-16666: IMAP subsystem startup causes Tomcat crash - Stopped IMAP subsystem from depending on itself by using private rather than public IMapService! 43408: Fixed latest DB2 hangs for Samuel (concurrent nested transactions) 43424: Fixed latest DB2 hang for Samuel (concurrent nested transactions) 43426: ALF-16692: Merged HEAD to V4.1 (with corrections) 43425: Fixes issue with YUI SWF files (see: IT-9441) 43450: Possible fix for workflow tests on DB2 - retrying txns where necessary 43484: ALF-16702: Restored missing index in DB2 schema reference 43596: Merged V4.1 to V4.1-BUG-FIX (RECORD ONLY) 43589: Merged V4.1-BUG-FIX to V4.1 43575: Fix for ALF-9317 - Links: Delete Link button in Selected Items menu is available for Consumer, Contributor and Collaborator git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@43601 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
1689 lines
59 KiB
Java
1689 lines
59 KiB
Java
/*
|
|
* Copyright (C) 2005-2012 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 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<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..".
|
|
eTag = conditions[j].substring(index + 1, conditions[j].indexOf("]"));
|
|
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;
|
|
|
|
lockInfo.getRWLock().readLock().lock();
|
|
try
|
|
{
|
|
owner = lockInfo.getOwner();
|
|
expiry = lockInfo.getExpires();
|
|
scope = lockInfo.getScope();
|
|
depth = lockInfo.getDepth();
|
|
}
|
|
finally
|
|
{
|
|
lockInfo.getRWLock().readLock().unlock();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
protected void setHidden(NodeRef nodeRef, boolean isHidden)
|
|
{
|
|
int mask = 0;
|
|
boolean allVisible = true;
|
|
Visibility webDavVisibility = isHidden ? Visibility.NotVisible : Visibility.Visible;
|
|
HiddenAspect hiddenAspect = m_davHelper.getHiddenAspect();
|
|
for (Client client : hiddenAspect.getClients())
|
|
{
|
|
Visibility clientVisibility = client == FileFilterMode.getClient() ? webDavVisibility : hiddenAspect
|
|
.getVisibility(client, nodeRef);
|
|
if (clientVisibility != Visibility.Visible)
|
|
{
|
|
allVisible = false;
|
|
}
|
|
mask |= hiddenAspect.getClientVisibilityMask(client, clientVisibility);
|
|
}
|
|
if (allVisible)
|
|
{
|
|
getNodeService().removeAspect(nodeRef, ContentModel.ASPECT_HIDDEN);
|
|
}
|
|
else
|
|
{
|
|
hiddenAspect.hideNode(nodeRef, mask);
|
|
}
|
|
}
|
|
|
|
protected boolean isHidden(NodeRef nodeRef)
|
|
{
|
|
return m_davHelper.getHiddenAspect().getVisibility(FileFilterMode.getClient(), nodeRef) != Visibility.Visible;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
nodeLockInfo.getRWLock().readLock().lock();
|
|
try
|
|
{
|
|
String nodeETag = getDAVHelper().makeQuotedETag(fileInfo);
|
|
NodeRef nodeRef = fileInfo.getNodeRef();
|
|
|
|
|
|
// 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 (getDAVHelper().isLockedOrReadOnly(nodeRef) || (!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. If the node is locked we need to check it's Lock token independently of conditions check result.
|
|
// For example "(<wrong token>) (Not <DAV:no-lock>)" if always true,
|
|
// but request must fail with 423 Locked response because node is locked.
|
|
// 2. Check if ANY of the conditions in If header true.
|
|
checkLockToken(nodeLockInfo, ignoreShared, lockMethod);
|
|
checkConditions(nodeLockInfo.getExclusiveLockToken(), nodeETag);
|
|
|
|
return nodeLockInfo;
|
|
}
|
|
finally
|
|
{
|
|
nodeLockInfo.getRWLock().readLock().unlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
lockInfo.getRWLock().readLock().lock();
|
|
try
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lockInfo.getRWLock().readLock().unlock();
|
|
}
|
|
|
|
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 || nodeLockToken == null)
|
|
{
|
|
// No conditions were provided with the "If" request header, or this node isn't locked and will not be in the conditions thus the check is 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 = condition.getLockTokensMatch().contains(nodeLockToken) ? true : false;
|
|
}
|
|
// Check lock tokens that shouldn't match
|
|
if (condition.getLockTokensNotMatch() != null)
|
|
{
|
|
fMatchLockToken = condition.getLockTokensNotMatch().contains(nodeLockToken) ? false : true;
|
|
}
|
|
|
|
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)
|
|
{
|
|
lockInfo.getRWLock().readLock().lock();
|
|
try
|
|
{
|
|
if (lockInfo.isLocked())
|
|
{
|
|
return lockInfo;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lockInfo.getRWLock().readLock().unlock();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
lock.getRWLock().readLock().lock();
|
|
try
|
|
{
|
|
if (lock.isLocked())
|
|
{
|
|
return lock;
|
|
}
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
lock.getRWLock().readLock().unlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
parentLock.getRWLock().readLock().lock();
|
|
try
|
|
{
|
|
// 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.
|
|
finally
|
|
{
|
|
parentLock.getRWLock().readLock().unlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, servletPath);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|