You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2024/10/22 12:16:52 UTC
(tomcat) branch 9.0.x updated (df2f6a1f54 -> 614f263ab4)
This is an automated email from the ASF dual-hosted git repository.
remm pushed a change to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
from df2f6a1f54 Code clean-up - formatting. No functional change.
new 09a8845bfb Remove WebDAV lock null support for compliance with RFC 4918
new 257cf6185d Add extension points to support dead properties
new 5d9e6aa71f Rewrite implementation of WebDAV shared locks
new 8323639fd8 Implement WebDAV If header processing
new efd6e7d616 Fix javadoc
new b19617838e Refine the dead properties handling
new b100925b10 Improve lock test coverage a bit more
new 39cb18f1ec Fix directory listings
new c12333007b Fix missing Javadoc
new 07a9eeb98e Add missing @Override
new 4976520690 Remove unnecessary type specifications
new b5a6f0405d Add additional test. lock may be null here
new 564f9f15a9 Fix unboxing warnings
new 3c7aea3a55 Remove unnecessary code. lock cannot be null here
new 01c57ee0f7 Code clean-up - formatting. No functional change.
new c2ea0fe876 Avoid writing namespace prefix
new e88fa7c068 Add a bit more of XML validation
new 77c10f2ce0 Fix changelog
new 614f263ab4 jakarta -> javax
The 19 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../servlets/TransientPropertiesWebdavServlet.java | 186 +++
.../apache/catalina/servlets/WebdavServlet.java | 1405 +++++++++++++-------
java/org/apache/catalina/util/DOMWriter.java | 12 +-
java/org/apache/catalina/util/XMLWriter.java | 22 +
.../tomcat/util/http/LocalStrings.properties | 3 +
.../apache/tomcat/util/http/WebdavIfHeader.java | 946 +++++++++++++
.../catalina/servlets/TestWebdavServlet.java | 512 ++++++-
webapps/docs/changelog.xml | 24 +
8 files changed, 2593 insertions(+), 517 deletions(-)
create mode 100644 java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
create mode 100644 java/org/apache/tomcat/util/http/WebdavIfHeader.java
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 14/19: Remove unnecessary code. lock cannot be null here
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3c7aea3a55e0609af500d16248328f3dd5ed07b7
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 08:35:35 2024 +0100
Remove unnecessary code. lock cannot be null here
---
java/org/apache/catalina/servlets/WebdavServlet.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index 42fbc80d6c..43168da56d 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -1740,7 +1740,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (lock != null) {
if ((parentPath != path && lock.depth > 0) || parentPath == path) {
if (ifHeader.contains(":" + token + ">")
- && (lock == null || lock.principal == null || lock.principal.equals(principal))) {
+ && (lock.principal == null || lock.principal.equals(principal))) {
return false;
}
unmatchedSharedLock = true;
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 19/19: jakarta -> javax
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 614f263ab48bd501aa4ea9473b26c0a697ae0cbf
Author: remm <re...@apache.org>
AuthorDate: Tue Oct 22 14:16:39 2024 +0200
jakarta -> javax
---
.../apache/catalina/servlets/TransientPropertiesWebdavServlet.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
index b27f974ad4..4e3d73cbb5 100644
--- a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
+++ b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
@@ -21,9 +21,9 @@ import java.io.StringWriter;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.util.DOMWriter;
import org.apache.catalina.util.XMLWriter;
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 04/19: Implement WebDAV If header processing
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 8323639fd8e739531b1e1f25b74b2c85f23d1c93
Author: remm <re...@apache.org>
AuthorDate: Sun Oct 20 14:13:01 2024 +0200
Implement WebDAV If header processing
Uses code from Apache Jackrabbit.
Add strict flag for the if header processing since some edge situations
are annoying.
Fix shared locks entry in the main lock map, and prevent its expiration.
Fix non compliant lock renewal (needed a request on the locked
resource).
Fix a couple instance of missing lock depth checks and shared lock
matching issue.
Lockdiscovery is supposed to be there even if there are no locks.
---
.../apache/catalina/servlets/WebdavServlet.java | 344 ++++++--
.../tomcat/util/http/LocalStrings.properties | 3 +
.../apache/tomcat/util/http/WebdavIfHeader.java | 943 +++++++++++++++++++++
.../catalina/servlets/TestWebdavServlet.java | 161 +++-
webapps/docs/changelog.xml | 4 +
5 files changed, 1370 insertions(+), 85 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index e60b92a560..d27506764c 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -31,6 +31,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Deque;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -59,6 +60,7 @@ import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.http.ConcurrentDateFormat;
import org.apache.tomcat.util.http.FastHttpDateFormat;
import org.apache.tomcat.util.http.RequestUtil;
+import org.apache.tomcat.util.http.WebdavIfHeader;
import org.apache.tomcat.util.security.ConcurrentMessageDigest;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -203,6 +205,19 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
protected static final String DEFAULT_NAMESPACE = "DAV:";
+ /**
+ * Supported locks.
+ */
+ protected static final String SUPPORTED_LOCKS =
+ "<D:lockentry>" +
+ "<D:lockscope><D:exclusive/></D:lockscope>" +
+ "<D:locktype><D:write/></D:locktype>" +
+ "</D:lockentry>" +
+ "<D:lockentry>" +
+ "<D:lockscope><D:shared/></D:lockscope>" +
+ "<D:locktype><D:write/></D:locktype>" +
+ "</D:lockentry>";
+
/**
* Simple date format for the creation date ISO representation (partial).
*/
@@ -242,6 +257,12 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
private boolean allowSpecialPaths = false;
+ /**
+ * Is the if header processing strict.
+ */
+ private boolean strictIfProcessing = false;
+
+
// --------------------------------------------------------- Public Methods
@Override
@@ -271,6 +292,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (getServletConfig().getInitParameter("allowSpecialPaths") != null) {
allowSpecialPaths = Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths"));
}
+
+ if (getServletConfig().getInitParameter("strictIfProcessing") != null) {
+ strictIfProcessing = Boolean.parseBoolean(getServletConfig().getInitParameter("strictIfProcessing"));
+ }
}
@@ -300,7 +325,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
}
-
// ------------------------------------------------------ Protected Methods
/**
@@ -402,7 +426,97 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return false;
}
- // FIXME : Process the WebDAV If header
+ // Process the WebDAV If header using Apache Jackrabbit code
+ String ifHeaderValue = request.getHeader("If");
+ if (ifHeaderValue != null) {
+ WebdavIfHeader ifHeader = new WebdavIfHeader(getUriPrefix(request), ifHeaderValue);
+ if (!ifHeader.hasValue()) {
+ // Allow bad if syntax, will only be used for lock tokens
+ return !strictIfProcessing;
+ }
+ String path = getRelativePath(request);
+ // Get all hrefs from the if header
+ Iterator<String> hrefs = ifHeader.getResources();
+
+ String currentPath = null;
+ String currentHref = null;
+ WebResource currentWebResource = null;
+ if (hrefs.hasNext()) {
+ currentHref = hrefs.next();
+ currentPath = getPathFromHref(currentHref, request);
+ currentWebResource = resources.getResource(currentPath);
+ } else {
+ currentPath = path;
+ currentHref = getEncodedPath(path, resource, request);
+ currentWebResource = resource;
+ }
+
+ // Iterate over all resources
+ do {
+ boolean exists = currentWebResource != null && currentWebResource.exists();
+ String eTag = exists ? generateETag(currentWebResource) : "";
+
+ // Collect all locks active on resource
+ ArrayList<String> lockTokens = new ArrayList<>();
+ // No lock evaluation for non existing paths in strict mode
+ // Problem: when doing a put with a locked parent folder, need to submit a tagged production with
+ // the parent path and the token, simply submitting the token in the if would fail the precondition.
+ if (!strictIfProcessing || exists) {
+ String parentPath = currentPath;
+ do {
+ LockInfo parentLock = resourceLocks.get(parentPath);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ resourceLocks.remove(parentPath);
+ } else {
+ if ((parentPath != currentPath && parentLock.depth > 0) || parentPath == currentPath) {
+ if (parentLock.isExclusive()) {
+ lockTokens.add("opaquelocktoken:" + parentLock.token);
+ } else {
+ for (String token : parentLock.sharedTokens) {
+ if (sharedLocks.get(token) == null) {
+ parentLock.sharedTokens.remove(token);
+ }
+ }
+ if (parentLock.sharedTokens.isEmpty()) {
+ resourceLocks.remove(parentLock.path);
+ }
+ for (String token : parentLock.sharedTokens) {
+ LockInfo sharedLock = sharedLocks.get(token);
+ if (sharedLock != null) {
+ if ((parentPath != currentPath && sharedLock.depth > 0) || parentPath == currentPath) {
+ lockTokens.add("opaquelocktoken:" + token);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ int slash = parentPath.lastIndexOf('/');
+ if (slash < 0) {
+ break;
+ }
+ parentPath = parentPath.substring(0, slash);
+ } while (true);
+ }
+
+ // Evaluation
+ if (ifHeader.matches(currentHref, lockTokens, eTag)) {
+ return true;
+ }
+
+ if (hrefs.hasNext()) {
+ currentHref = hrefs.next();
+ currentPath = getPathFromHref(currentHref, request);
+ currentWebResource = resources.getResource(currentPath);
+ } else {
+ break;
+ }
+ } while (true);
+
+ return false;
+ }
return true;
}
@@ -466,6 +580,61 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return rewriteUrl(href);
}
+ private String getUriPrefix(HttpServletRequest request) {
+ return request.getScheme() + "://" + request.getServerName();
+ }
+
+ private String getPathFromHref(String href, HttpServletRequest req) {
+
+ if (href == null || href.isEmpty()) {
+ return null;
+ }
+
+ URI hrefUri;
+ try {
+ hrefUri = new URI(href);
+ } catch (URISyntaxException e) {
+ return null;
+ }
+
+ String hrefPath = hrefUri.getPath();
+
+ // Avoid path traversals
+ if (!hrefPath.equals(RequestUtil.normalize(hrefPath))) {
+ return null;
+ }
+
+ if (hrefUri.isAbsolute()) {
+ if (!req.getServerName().equals(hrefUri.getHost())) {
+ return null;
+ }
+ }
+
+ if (hrefPath.length() > 1 && hrefPath.endsWith("/")) {
+ hrefPath = hrefPath.substring(0, hrefPath.length() - 1);
+ }
+
+ // Verify context path
+ String reqContextPath = getPathPrefix(req);
+ if (!hrefPath.startsWith(reqContextPath + "/")) {
+ return null;
+ }
+
+ // Remove context path & servlet path
+ hrefPath = hrefPath.substring(reqContextPath.length());
+
+ if (debug > 0) {
+ log(href + " Href path: " + hrefPath);
+ }
+
+ // Protect special subdirectories
+ if (isSpecialPath(hrefPath)) {
+ return null;
+ }
+
+ return hrefPath;
+ }
+
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("DAV", "1,2");
@@ -572,6 +741,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}
@@ -669,6 +839,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}
if (!resource.exists()) {
@@ -881,6 +1052,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}
@@ -934,15 +1106,17 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
String path = getRelativePath(req);
- if (isLocked(path, req)) {
- resp.sendError(WebdavStatus.SC_LOCKED);
+ WebResource resource = resources.getResource(path);
+ if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}
- WebResource resource = resources.getResource(path);
- if (!checkIfHeaders(req, resp, resource)) {
+ if (isLocked(path, req)) {
+ resp.sendError(WebdavStatus.SC_LOCKED);
return;
}
+
if (resource.isDirectory()) {
sendNotAllowed(req, resp);
return;
@@ -1022,6 +1196,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}
@@ -1311,6 +1486,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) {
resp.sendError(WebdavStatus.SC_CONFLICT);
return;
+ } else {
+ resp.setStatus(HttpServletResponse.SC_CREATED);
}
}
@@ -1323,8 +1500,12 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
// Checking if there is already a shared lock on this path
LockInfo sharedLock = resourceLocks.get(path);
if (sharedLock == null) {
- resourceLocks.put(path, lock);
- sharedLock = lock;
+ sharedLock = new LockInfo(maxDepth);
+ sharedLock.scope = "shared";
+ sharedLock.path = path;
+ sharedLock.lockroot = lock.lockroot;
+ sharedLock.depth = maxDepth;
+ resourceLocks.put(path, sharedLock);
}
sharedLock.sharedTokens.add(lockToken);
sharedLocks.put(lockToken, lock);
@@ -1344,28 +1525,53 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return;
}
- // Checking resource locks
-
- LockInfo toRenew = resourceLocks.get(path);
-
- if (toRenew != null) {
- if (toRenew.isExclusive()) {
- if (ifHeader.contains(toRenew.token)) {
- toRenew.expiresAt = lock.expiresAt;
- lock = toRenew;
- }
- } else {
- for (String token : toRenew.sharedTokens) {
- if (ifHeader.contains(token)) {
- toRenew = sharedLocks.get(token);
- if (toRenew != null) {
- toRenew.expiresAt = lock.expiresAt;
- lock = toRenew;
+ LockInfo toRenew = null;
+ String parentPath = path;
+ do {
+ LockInfo parentLock = resourceLocks.get(parentPath);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ resourceLocks.remove(parentPath);
+ } else {
+ if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
+ if (parentLock.isExclusive()) {
+ if (ifHeader.contains(parentLock.token)
+ && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) {
+ toRenew = parentLock;
+ break;
+ }
+ } else {
+ for (String token : parentLock.sharedTokens) {
+ if (ifHeader.contains(token)) {
+ LockInfo sharedLock = sharedLocks.get(token);
+ if (sharedLock != null
+ && (sharedLock.principal == null || sharedLock.principal.equals(req.getRemoteUser()))) {
+ if ((parentPath != path && sharedLock.depth > 0) || parentPath == path) {
+ toRenew = sharedLock;
+ break;
+ }
+ }
+ }
+ }
}
}
}
}
+ int slash = parentPath.lastIndexOf('/');
+ if (slash < 0) {
+ break;
+ }
+ parentPath = parentPath.substring(0, slash);
+ } while (true);
+
+ if (toRenew != null) {
+ if (!toRenew.hasExpired()) {
+ toRenew.expiresAt = lock.expiresAt;
+ } else {
+ toRenew = null;
+ }
}
+ lock = toRenew;
}
// Set the status, then generate the XML response containing
@@ -1376,7 +1582,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
- lock.toXML(generatedXML);
+ if (lock != null) {
+ lock.toXML(generatedXML);
+ }
generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING);
@@ -1408,6 +1616,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}
@@ -1437,13 +1646,15 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (lockTokenHeader.contains(token)) {
LockInfo lock = sharedLocks.get(token);
if (lock == null || lock.principal == null || lock.principal.equals(req.getRemoteUser())) {
- parentLock.sharedTokens.remove(token);
- if (parentLock.sharedTokens.isEmpty()) {
- resourceLocks.remove(parentPath);
+ if ((parentPath != path && lock.depth > 0) || parentPath == path) {
+ parentLock.sharedTokens.remove(token);
+ if (parentLock.sharedTokens.isEmpty()) {
+ resourceLocks.remove(parentPath);
+ }
+ sharedLocks.remove(token);
+ unlocked = true;
+ break;
}
- sharedLocks.remove(token);
- unlocked = true;
- break;
}
}
}
@@ -1510,6 +1721,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
*/
private boolean isLocked(String path, String principal, String ifHeader) {
+ boolean unmatchedSharedLock = false;
// Check if the resource or a parent is already locked
String parentPath = path;
do {
@@ -1520,26 +1732,26 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} else {
if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
if (parentLock.isExclusive()) {
- if (ifHeader.contains(parentLock.token)
+ if (ifHeader.contains(":" + parentLock.token + ">")
&& (parentLock.principal == null || parentLock.principal.equals(principal))) {
return false;
}
+ return true;
} else {
for (String token : parentLock.sharedTokens) {
- if (ifHeader.contains(token)) {
- if (principal == null) {
- return false;
- } else {
- LockInfo lock = sharedLocks.get(token);
- if (lock == null || lock.principal == null || lock.principal.equals(principal)) {
+ LockInfo lock = sharedLocks.get(token);
+ if (lock != null) {
+ if ((parentPath != path && lock.depth > 0) || parentPath == path) {
+ if (ifHeader.contains(":" + token + ">")
+ && (lock == null || lock.principal == null || lock.principal.equals(principal))) {
return false;
}
+ unmatchedSharedLock = true;
}
}
}
}
}
- return true;
}
}
int slash = parentPath.lastIndexOf('/');
@@ -1549,7 +1761,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
parentPath = parentPath.substring(0, slash);
} while (true);
- return false;
+ return unmatchedSharedLock;
}
@@ -1573,6 +1785,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return false;
}
if (!checkIfHeaders(req, resp, source)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
@@ -1825,6 +2038,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp) throws IOException {
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
+ resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
return deleteResource(path, req, resp, true);
@@ -2110,12 +2324,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
propfindResource(path, null, false, generatedXML);
- String supportedLocks = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" +
- "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" +
- "<D:lockscope><D:shared/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" +
- "</D:lockentry>";
generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING);
- generatedXML.writeRaw(supportedLocks);
+ generatedXML.writeRaw(SUPPORTED_LOCKS);
generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING);
generateLockDiscovery(path, generatedXML);
@@ -2215,17 +2425,11 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING);
}
} else if (property.equals("supportedlock")) {
- supportedLocks = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" +
- "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" +
- "<D:lockscope><D:shared/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" +
- "</D:lockentry>";
generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING);
- generatedXML.writeRaw(supportedLocks);
+ generatedXML.writeRaw(SUPPORTED_LOCKS);
generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING);
} else if (property.equals("lockdiscovery")) {
- if (!generateLockDiscovery(path, generatedXML)) {
- propertiesNotFound.add(propertyNode);
- }
+ generateLockDiscovery(path, generatedXML);
} else {
propertiesNotFound.add(propertyNode);
}
@@ -2288,12 +2492,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
*
* @param path Path
* @param generatedXML XML data to which the locks info will be appended
- *
- * @return <code>true</code> if at least one lock was displayed
*/
- private boolean generateLockDiscovery(String path, XMLWriter generatedXML) {
+ private void generateLockDiscovery(String path, XMLWriter generatedXML) {
- boolean wroteStart = false;
+ generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
String parentPath = path;
do {
@@ -2304,23 +2506,17 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} else {
if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
if (parentLock.isExclusive()) {
- if (!wroteStart) {
- wroteStart = true;
- generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
- }
parentLock.toXML(generatedXML);
} else {
for (String lockToken : parentLock.sharedTokens) {
- parentLock = sharedLocks.get(lockToken);
- if (parentLock != null) {
- if (parentLock.hasExpired()) {
+ LockInfo sharedLock = sharedLocks.get(lockToken);
+ if (sharedLock != null) {
+ if (sharedLock.hasExpired()) {
sharedLocks.remove(lockToken);
} else {
- if (!wroteStart) {
- wroteStart = true;
- generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
+ if ((parentPath != path && sharedLock.depth > 0) || parentPath == path) {
+ sharedLock.toXML(generatedXML);
}
- parentLock.toXML(generatedXML);
}
}
}
@@ -2335,13 +2531,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
parentPath = parentPath.substring(0, slash);
} while (true);
- if (wroteStart) {
- generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING);
- } else {
- return false;
- }
-
- return true;
+ generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING);
}
@@ -2454,7 +2644,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* @return true if the lock has expired.
*/
public boolean hasExpired() {
- return System.currentTimeMillis() > expiresAt;
+ return sharedTokens.size() == 0 && System.currentTimeMillis() > expiresAt;
}
diff --git a/java/org/apache/tomcat/util/http/LocalStrings.properties b/java/org/apache/tomcat/util/http/LocalStrings.properties
index 477cfaf940..19471ba42a 100644
--- a/java/org/apache/tomcat/util/http/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/http/LocalStrings.properties
@@ -46,3 +46,6 @@ rfc6265CookieProcessor.expectedBytes=Parsing cookie as String, expected bytes
rfc6265CookieProcessor.invalidCharInValue=An invalid character [{0}] was present in the Cookie value
rfc6265CookieProcessor.invalidDomain=An invalid domain [{0}] was specified for this cookie
rfc6265CookieProcessor.invalidPath=An invalid path [{0}] was specified for this cookie
+
+webdavifheader.unexpectedCharacter=Unexpected character [{0}] in state [{1}], expected any of [{2}]
+webdavifheader.ioError=IO Problem catching up to any of [{0}]
diff --git a/java/org/apache/tomcat/util/http/WebdavIfHeader.java b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
new file mode 100644
index 0000000000..fbca45cdfc
--- /dev/null
+++ b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
@@ -0,0 +1,943 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.http;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * The <code>IfHeader</code> class represents the state lists defined
+ * through the HTTP <em>If</em> header, which is specified in RFC 2518 as
+ * follows :
+ * <pre>
+ * If = "If" ":" ( 1*No-tag-list | 1*Tagged-list)
+ * No-tag-list = List
+ * Tagged-list = Resource 1*List
+ * Resource = Coded-URL
+ * List = "(" 1*(["Not"](State-etag | "[" entity-tag "]")) ")"
+ * State-etag = Coded-URL
+ * Coded-URL = "<" absoluteURI ">"
+ * </pre>
+ * <p>
+ * Reformulating this specification into proper EBNF as specified by N. Wirth
+ * we get the following productions, which map to the parse METHODS of this
+ * class. Any whitespace is ignored except for white space surrounding and
+ * within words which is considered significant.
+ * <pre>
+ * If = "If:" ( Tagged | Untagged ).
+ * Tagged = { "<" Word ">" Untagged } .
+ * Untagged = { "(" IfList ")" } .
+ * IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
+ * Word = characters .
+ * </pre>
+ * <p>
+ * An <em>If</em> header either contains untagged <em>IfList</em> entries or
+ * tagged <em>IfList</em> entries but not a mixture of both. An <em>If</em>
+ * header containing tagged entries is said to be of <em>tagged</em> type while
+ * an <em>If</em> header containing untagged entries is said to be of
+ * <em>untagged</em> type.
+ * <p>
+ * An <em>IfList</em> is a list of tokens - words enclosed in <em>< ></em>
+ * - and etags - words enclosed in <em>[ ]</em>. An <em>IfList</em> matches a
+ * (token, etag) tuple if all entries in the list match. If an entry in the list
+ * is prefixed with the word <em>Not</em> (parsed case insensitively) the entry
+ * must not match the concrete token or etag.
+ * <p>
+ * Example: The <em>ifList</em> <code>(<token> [etag])</code> only matches
+ * if the concret token has the value <code>token</code> and the conrete etag
+ * has the value <code>etag</code>. On the other hand, the <em>ifList</em>
+ * <code>(Not <notoken>)</code> matches any token which is not
+ * <code>notoken</code> (in this case the concrete value of the etag is
+ * not taken into consideration).
+ *
+ * This class was contributed by Apache Jackrabbit
+ *
+ * @author Felix Meschberger
+ */
+public class WebdavIfHeader {
+
+ private static final Log log = LogFactory.getLog(WebdavIfHeader.class);
+ private static final StringManager sm =
+ StringManager.getManager(WebdavIfHeader.class.getPackage().getName());
+
+ /**
+ * The string representation of the header value
+ */
+ private final String headerValue;
+
+ /**
+ * The list of untagged state entries
+ */
+ private final IfHeaderInterface ifHeader;
+
+ /**
+ * The list of resources present in the If header.
+ */
+ private List<String> resources = new ArrayList<String>();
+
+ /**
+ * The list of all positive tokens present in the If header.
+ */
+ private List<String> allTokens = new ArrayList<String>();
+
+ /**
+ * The list of all NOT tokens present in the If header.
+ */
+ private List<String> allNotTokens = new ArrayList<String>();
+
+ private String uriPrefix;
+
+ /**
+ * Create a Untagged <code>IfHeader</code> if the given lock tokens.
+ *
+ * @param tokens the tokens
+ */
+ public WebdavIfHeader(String[] tokens) throws IOException {
+ allTokens.addAll(Arrays.asList(tokens));
+ StringBuffer b = new StringBuffer();
+ for (String token : tokens) {
+ b.append("(").append("<");
+ b.append(token);
+ b.append(">").append(")");
+ }
+ headerValue = b.toString();
+ ifHeader = parse();
+ }
+
+ /**
+ * Parses the <em>If</em> header and creates and internal representation
+ * which is easy to query.
+ *
+ * @param uriPrefix The uri prefix to use for the absolute href
+ * @param ifHeaderValue the if header
+ */
+ public WebdavIfHeader(String uriPrefix, String ifHeaderValue) throws IOException {
+ this.uriPrefix = uriPrefix;
+ headerValue = ifHeaderValue;
+ ifHeader = parse();
+ }
+
+ /**
+ * Return {@link DavConstants#HEADER_IF If}
+ *
+ * @return {@link DavConstants#HEADER_IF If}
+ * @see DavConstants#HEADER_IF
+ */
+ public String getHeaderName() {
+ return "If";
+ }
+
+ /**
+ * Return the String representation of the If header present on
+ * the given request or <code>null</code>.
+ *
+ * @return If header value as String or <code>null</code>.
+ */
+ public String getHeaderValue() {
+ return headerValue;
+ }
+
+ /**
+ * Returns true if an If header was present in the given request. False otherwise.
+ *
+ * @return true if an If header was present.
+ */
+ public boolean hasValue() {
+ return ifHeader != null;
+ }
+
+ /**
+ * Tries to match the contents of the <em>If</em> header with the given
+ * token and etag values with the restriction to only check for the tag.
+ * <p>
+ * If the <em>If</em> header is of untagged type, the untagged <em>IfList</em>
+ * is matched against the token and etag given: A match of the token and
+ * etag is found if at least one of the <em>IfList</em> entries match the
+ * token and etag tuple.
+ *
+ * @param tag The tag to identify the <em>IfList</em> to match the token
+ * and etag against.
+ * @param tokens The tokens to compare.
+ * @param etag The ETag value to compare.
+ *
+ * @return If the <em>If</em> header is of untagged type the result is
+ * <code>true</code> if any of the <em>IfList</em> entries matches
+ * the token and etag values. For tagged type <em>If</em> header the
+ * result is <code>true</code> if either no entry for the given tag
+ * exists in the <em>If</em> header or if the <em>IfList</em> for the
+ * given tag matches the token and etag given.
+ */
+ public boolean matches(String tag, List<String> tokens, String etag) {
+ if (ifHeader == null) {
+ if (log.isTraceEnabled()) {
+ log.trace("matches: No If header, assume match");
+ }
+ return true;
+ } else {
+ return ifHeader.matches(tag, tokens, etag);
+ }
+ }
+
+ /**
+ * @return an iterator over all resources present in the if header.
+ */
+ public Iterator<String> getResources() {
+ return resources.iterator();
+ }
+
+ /**
+ * @return an iterator over all tokens present in the if header, that were
+ * not denied by a leading NOT statement.
+ */
+ public Iterator<String> getAllTokens() {
+ return allTokens.iterator();
+ }
+
+ /**
+ * @return an iterator over all NOT tokens present in the if header, that
+ * were explicitly denied.
+ */
+ public Iterator<String> getAllNotTokens() {
+ return allNotTokens.iterator();
+ }
+
+ /**
+ * Parse the original header value and build the internal IfHeaderInterface
+ * object that is easy to query.
+ */
+ private IfHeaderInterface parse()
+ throws IOException {
+ IfHeaderInterface ifHeader;
+ if (headerValue != null && headerValue.length() > 0) {
+ StringReader reader = null;
+ int firstChar = 0;
+
+ try {
+ reader = new StringReader(headerValue);
+ // get the first character to decide - expect '(' or '<'
+ try {
+ reader.mark(1);
+ firstChar = readWhiteSpace(reader);
+ reader.reset();
+ } catch (IOException ignore) {
+ // may be thrown according to API but is only thrown by the
+ // StringReader class if the reader is already closed.
+ }
+
+ if (firstChar == '(') {
+ ifHeader = parseUntagged(reader);
+ } else if (firstChar == '<') {
+ ifHeader = parseTagged(reader);
+ } else {
+ logIllegalState("If", firstChar, "(<", null);
+ ifHeader = null;
+ }
+
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+
+ } else {
+ if (log.isTraceEnabled()) {
+ log.trace("IfHeader: No If header in request");
+ }
+ ifHeader = null;
+ }
+ return ifHeader;
+ }
+
+ //---------- internal IF header parser -------------------------------------
+ /**
+ * Parses a tagged type <em>If</em> header. This method implements the
+ * <em>Tagged</em> production given in the class comment :
+ * <pre>
+ * Tagged = { "<" Word ">" Untagged } .
+ * </pre>
+ *
+ * @param reader the reader
+ * @return the parsed map
+ */
+ private IfHeaderMap parseTagged(StringReader reader)
+ throws IOException {
+ IfHeaderMap map = new IfHeaderMap();
+ while (true) {
+ // read next non-white space
+ int c = readWhiteSpace(reader);
+ if (c < 0) {
+ // end of input, no more entries
+ break;
+ } else if (c == '<') {
+ // start a tag with an IfList
+ String resource = readWord(reader, '>');
+ if (resource != null) {
+ // go to untagged after reading the resource
+ map.put(resource, parseUntagged(reader));
+ resources.add(resource);
+ } else {
+ break;
+ }
+ } else {
+ // unexpected character
+ // catchup to end of input or start of a tag
+ logIllegalState("Tagged", c, "<", reader);
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Parses an untagged type <em>If</em> header. This method implements the
+ * <em>Untagged</em> production given in the class comment :
+ * <pre>
+ * Untagged = { "(" IfList ")" } .
+ * </pre>
+ *
+ * @param reader The <code>StringReader</code> to read from for parsing
+ *
+ * @return An <code>ArrayList</code> of {@link IfList} entries.
+ */
+ private IfHeaderList parseUntagged(StringReader reader)
+ throws IOException {
+ IfHeaderList list = new IfHeaderList();
+ while (true) {
+ // read next non white space
+ reader.mark(1);
+ int c = readWhiteSpace(reader);
+ if (c < 0) {
+ // end of input, no more IfLists
+ break;
+
+ } else if (c == '(') {
+ // start of an IfList, parse
+ list.add(parseIfList(reader));
+
+ } else if (c == '<') {
+ // start of a tag, return current list
+ reader.reset();
+ break;
+
+ } else {
+ // unexpected character
+ // catchup to end of input or start of an IfList
+ logIllegalState("Untagged", c, "(", reader);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Parses an <em>IfList</em> in the <em>If</em> header. This method
+ * implements the <em>Tagged</em> production given in the class comment :
+ * <pre>
+ * IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
+ * </pre>
+ *
+ * @param reader The <code>StringReader</code> to read from for parsing
+ *
+ * @return The {@link IfList} for the input <em>IfList</em>.
+ *
+ * @throws IOException if a problem occurs during reading.
+ */
+ private IfList parseIfList(StringReader reader) throws IOException {
+ IfList res = new IfList();
+ boolean positive = true;
+ String word;
+
+ ReadLoop:
+ while (true) {
+ int nextChar = readWhiteSpace(reader);
+ switch (nextChar) {
+ case 'N':
+ case 'n':
+ // read not
+
+ // check whether o or O
+ int not = reader.read();
+ if (not != 'o' && not != 'O') {
+ logIllegalState("IfList-Not", not, "o", null);
+ break;
+ }
+
+ // check whether t or T
+ not = reader.read();
+ if (not !='t' && not != 'T') {
+ logIllegalState("IfList-Not", not, "t", null);
+ break;
+ }
+
+ // read Not ok
+ positive = false;
+ break;
+
+ case '<':
+ // state token
+ word = readWord(reader, '>');
+ if (word != null) {
+ res.add(new IfListEntryToken(word, positive));
+ // also add the token to the list of all tokens
+ if (positive) {
+ allTokens.add(word);
+ } else {
+ allNotTokens.add(word);
+ }
+ positive = true;
+ }
+ break;
+
+ case '[':
+ // etag
+ word = readWord(reader, ']');
+ if (word != null) {
+ res.add(new IfListEntryEtag(word, positive));
+ positive = true;
+ }
+ break;
+
+ case ')':
+ // correct end of list, end the loop
+ if (log.isTraceEnabled()) {
+ log.trace("parseIfList: End of If list, terminating loop");
+ }
+ break ReadLoop;
+
+ default:
+ logIllegalState("IfList", nextChar, "nN<[)", reader);
+
+ // abort loop if EOF
+ if (nextChar < 0) {
+ break ReadLoop;
+ }
+
+ break;
+ }
+ }
+
+ // return the current list anyway
+ return res;
+ }
+
+ /**
+ * Returns the first non-whitespace character from the reader or -1 if
+ * the end of the reader is encountered.
+ *
+ * @param reader The <code>Reader</code> to read from
+ *
+ * @return The first non-whitespace character or -1 in case of EOF.
+ *
+ * @throws IOException if a problem occurs during reading.
+ */
+ private int readWhiteSpace(Reader reader) throws IOException {
+ int c = reader.read();
+ while (c >= 0 && Character.isWhitespace((char) c)) {
+ c = reader.read();
+ }
+ return c;
+ }
+
+ /**
+ * Reads from the input until the end character is encountered and returns
+ * the string up to but not including this end character. If the end of input
+ * is reached before reading the end character <code>null</code> is
+ * returned.
+ * <p>
+ * Note that this method does not support any escaping.
+ *
+ * @param reader The <code>Reader</code> to read from
+ * @param end The ending character limiting the word.
+ *
+ * @return The string read up to but not including the ending character or
+ * <code>null</code> if the end of input is reached before the ending
+ * character has been read.
+ *
+ * @throws IOException if a problem occurs during reading.
+ */
+ private String readWord(Reader reader, char end) throws IOException {
+ StringBuffer buf = new StringBuffer();
+
+ // read the word value
+ int c = reader.read();
+ for (; c >= 0 && c != end; c=reader.read()) {
+ buf.append((char) c);
+ }
+
+ // check whether we succeeded
+ if (c < 0) {
+ log.error("readWord: Unexpected end of input reading word");
+ return null;
+ }
+
+ // build the string and return it
+ return buf.toString();
+ }
+
+ /**
+ * Logs an unexpected character with the corresponding state and list of
+ * expected characters. If the reader parameter is not null, characters
+ * are read until either the end of the input is reached or any of the
+ * characters in the expChar string is read.
+ *
+ * @param state The name of the current parse state. This method logs this
+ * name in the message. The intended value would probably be the
+ * name of the EBNF production during which the error occurs.
+ * @param effChar The effective character read.
+ * @param expChar The list of characters acceptable in the current state.
+ * @param reader The reader to be caught up to any of the expected
+ * characters. If <code>null</code> the input is not caught up to
+ * any of the expected characters (of course ;-).
+ */
+ private void logIllegalState(String state, int effChar, String expChar,
+ StringReader reader) {
+
+ // format the effective character to be logged
+ String effString = (effChar < 0) ? "<EOF>" : String.valueOf((char) effChar);
+
+ // log the error
+ log.error(sm.getString("webdavifheader.unexpectedCharacter", effString, state, expChar));
+
+ // catch up if a reader is given
+ if (reader != null && effChar >= 0) {
+ try {
+ if (log.isTraceEnabled()) {
+ log.trace("logIllegalState: Catch up to any of "+expChar);
+ }
+ do {
+ reader.mark(1);
+ effChar = reader.read();
+ } while (effChar >= 0 && expChar.indexOf(effChar) < 0);
+ if (effChar >= 0) {
+ reader.reset();
+ }
+ } catch (IOException ioe) {
+ log.error(sm.getString("webdavifheader.ioError", expChar));
+ }
+ }
+ }
+
+ //---------- internal If header structure ----------------------------------
+
+ /**
+ * The <code>IfListEntry</code> abstract class is the base class for
+ * entries in an <em>IfList</em> production. This abstract base class
+ * provides common functionality to both types of entries, namely tokens
+ * enclosed in angle brackets (<code>< ></code>) and etags enclosed
+ * in square brackets (<code>[ ]</code>).
+ */
+ private abstract static class IfListEntry {
+
+ /**
+ * The entry string value - the semantics of this value depends on the
+ * implementing class.
+ */
+ protected final String value;
+
+ /** Flag to indicate, whether this is a positive match or not */
+ protected final boolean positive;
+
+ /** The cached result of the {@link #toString} method. */
+ protected String stringValue;
+
+ /**
+ * Sets up the final fields of this abstract class. The meaning of
+ * value parameter depends solely on the implementing class. From the
+ * point of view of this abstract class, it is simply a string value.
+ *
+ * @param value The string value of this instance
+ * @param positive <code>true</code> if matches are positive
+ */
+ protected IfListEntry(String value, boolean positive) {
+ this.value = value;
+ this.positive = positive;
+ }
+
+ /**
+ * Matches the value from the parameter to the internal string value.
+ * If the parameter and the {@link #value} field match, the method
+ * returns <code>true</code> for positive matches and <code>false</code>
+ * for negative matches.
+ * <p>
+ * This helper method can be called by implementations to evaluate the
+ * concrete match on the correct value parameter. See
+ * {@link #match(String, String)} for the external API method.
+ *
+ * @param value The string value to compare to the {@link #value}
+ * field.
+ *
+ * @return <code>true</code> if the value parameter and the
+ * {@link #value} field match and the {@link #positive} field is
+ * <code>true</code> or if the values do not match and the
+ * {@link #positive} field is <code>false</code>.
+ */
+ protected boolean match(String value) {
+ return positive == this.value.equals(value);
+ }
+
+ /**
+ * Matches the entry's value to the the token or etag. Depending on the
+ * concrete implementation, only one of the parameters may be evaluated
+ * while the other may be ignored.
+ * <p>
+ * Implementing METHODS may call the helper method {@link #match(String)}
+ * for the actual matching.
+ *
+ * @param token The token value to compare
+ * @param etag The etag value to compare
+ *
+ * @return <code>true</code> if the token/etag matches the <em>IfList</em>
+ * entry.
+ */
+ public abstract boolean match(String token, String etag);
+
+ /**
+ * Returns a short type name for the implementation. This method is
+ * used by the {@link #toString} method to build the string representation
+ * if the instance.
+ *
+ * @return The type name of the implementation.
+ */
+ protected abstract String getType();
+
+ /**
+ * @return the value of this entry
+ */
+ protected String getValue() {
+ return value;
+ }
+
+ /**
+ * Returns the String representation of this entry. This method uses the
+ * {@link #getType} to build the string representation.
+ *
+ * @return the String representation of this entry.
+ */
+ @Override
+ public String toString() {
+ if (stringValue == null) {
+ stringValue = getType() + ": " + (positive ? "" : "!") + getValue();
+ }
+ return stringValue;
+ }
+ }
+
+ /**
+ * The <code>IfListEntryToken</code> extends the {@link IfListEntry}
+ * abstract class to represent an entry for token matching.
+ */
+ private static class IfListEntryToken extends IfListEntry {
+
+ /**
+ * Creates a token matching entry.
+ *
+ * @param token The token value pertinent to this instance.
+ * @param positive <code>true</code> if this is a positive match entry.
+ */
+ IfListEntryToken(String token, boolean positive) {
+ super(token, positive);
+ }
+
+ /**
+ * Matches the token parameter to the stored token value and returns
+ * <code>true</code> if the values match and if the match is positive.
+ * <code>true</code> is also returned for negative matches if the values
+ * do not match.
+ *
+ * @param token The token value to compare
+ * @param etag The etag value to compare, which is ignored in this
+ * implementation.
+ *
+ * @return <code>true</code> if the token matches the <em>IfList</em>
+ * entry's token value.
+ */
+ @Override
+ public boolean match(String token, String etag) {
+ return token == null || super.match(token);
+ }
+
+ /**
+ * Returns the type name of this implementation, which is fixed to
+ * be <em>Token</em>.
+ *
+ * @return The fixed string <em>Token</em> as the type name.
+ */
+ @Override
+ protected String getType() {
+ return "Token";
+ }
+ }
+
+ /**
+ * The <code>IfListEntryToken</code> extends the {@link IfListEntry}
+ * abstract class to represent an entry for etag matching.
+ */
+ private static class IfListEntryEtag extends IfListEntry {
+
+ /**
+ * Creates an etag matching entry.
+ *
+ * @param etag The etag value pertinent to this instance.
+ * @param positive <code>true</code> if this is a positive match entry.
+ */
+ IfListEntryEtag(String etag, boolean positive) {
+ super(etag, positive);
+ }
+
+ /**
+ * Matches the etag parameter to the stored etag value and returns
+ * <code>true</code> if the values match and if the match is positive.
+ * <code>true</code> is also returned for negative matches if the values
+ * do not match.
+ *
+ * @param token The token value to compare, which is ignored in this
+ * implementation.
+ * @param etag The etag value to compare
+ *
+ * @return <code>true</code> if the etag matches the <em>IfList</em>
+ * entry's etag value.
+ */
+ @Override
+ public boolean match(String token, String etag) {
+ return super.match(etag);
+ }
+
+ /**
+ * Returns the type name of this implementation, which is fixed to
+ * be <em>ETag</em>.
+ *
+ * @return The fixed string <em>ETag</em> as the type name.
+ */
+ @Override
+ protected String getType() {
+ return "ETag";
+ }
+ }
+
+ /**
+ * The <code>IfList</code> class extends the <code>ArrayList</code> class
+ * with the limitation to only support adding {@link IfListEntry} objects
+ * and adding a {@link #match} method.
+ * <p>
+ * This class is a container for data contained in the <em>If</em>
+ * production <em>IfList</em>
+ * <pre>
+ * IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
+ * </pre>
+ * <p>
+ */
+ private static class IfList extends ArrayList<IfListEntry> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Adds the {@link IfListEntry} at the end of the list.
+ *
+ * @param entry The {@link IfListEntry} to add to the list
+ *
+ * @return <code>true</code> (as per the general contract of Collection.add).
+ */
+ @Override
+ public boolean add(IfListEntry entry) {
+ return super.add(entry);
+ }
+
+ /**
+ * Adds the {@link IfListEntry} at the indicated position of the list.
+ *
+ * @param index the index
+ * @param entry the entry
+ *
+ * @throws IndexOutOfBoundsException if index is out of range
+ * <code>(index < 0 || index > size())</code>.
+ */
+ @Override
+ public void add(int index, IfListEntry entry) {
+ super.add(index, entry);
+ }
+
+ /**
+ * Returns <code>true</code> if all {@link IfListEntry} objects in the
+ * list match the given token and etag. If the list is entry, it is
+ * considered to match the token and etag.
+ *
+ * @param tokens The token to compare.
+ * @param etag The etag to compare.
+ *
+ * @return <code>true</code> if all entries in the list match the
+ * given tag and token.
+ */
+ public boolean match(List<String> tokens, String etag) {
+ if (log.isTraceEnabled()) {
+ log.trace("match: Trying to match token=" + tokens + ", etag=" + etag);
+ }
+ for (int i=0; i < size(); i++) {
+ IfListEntry ile = get(i);
+ boolean match = false;
+ for (String token : tokens) {
+ if (ile.match(token, etag)) {
+ match = true;
+ }
+ }
+ if (!match) {
+ if (log.isTraceEnabled()) {
+ log.trace("match: Entry " + i + "-" + ile + " does not match");
+ }
+ return false;
+ }
+ }
+ // invariant: all entries matched
+
+ return true;
+ }
+ }
+
+ /**
+ * The <code>IfHeaderInterface</code> interface abstracts away the difference of
+ * tagged and untagged <em>If</em> header lists. The single method provided
+ * by this interface is to check whether a request may be applied to a
+ * resource with given token and etag.
+ */
+ private interface IfHeaderInterface {
+
+ /**
+ * Matches the resource, token, and etag against this
+ * <code>IfHeaderInterface</code> instance.
+ *
+ * @param resource The resource to match this instance against. This
+ * must be absolute URI of the resource as defined in Section 3
+ * (URI Syntactic Components) of RFC 2396 Uniform Resource
+ * Identifiers (URI): Generic Syntax.
+ * @param tokens The resource's lock token to match
+ * @param etag The resource's etag to match
+ *
+ * @return <code>true</code> if the header matches the resource with
+ * token and etag, which means that the request is applicable
+ * to the resource according to the <em>If</em> header.
+ */
+ boolean matches(String resource, List<String> tokens, String etag);
+ }
+
+ /**
+ * The <code>IfHeaderList</code> class implements the {@link IfHeaderInterface}
+ * interface to support untagged lists of {@link IfList}s. This class
+ * implements the data container for the production :
+ * <pre>
+ * Untagged = { "(" IfList ")" } .
+ * </pre>
+ */
+ private static class IfHeaderList extends ArrayList<IfList> implements IfHeaderInterface {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Matches a list of {@link IfList}s against the token and etag. If any of
+ * the {@link IfList}s matches, the method returns <code>true</code>.
+ * On the other hand <code>false</code> is only returned if non of the
+ * {@link IfList}s match.
+ *
+ * @param resource The resource to match, which is ignored by this
+ * implementation. A value of <code>null</code> is therefor
+ * acceptable.
+ * @param tokens The tokens to compare.
+ * @param etag The ETag value to compare.
+ *
+ * @return <code>True</code> if any of the {@link IfList}s matches the token
+ * and etag, else <code>false</code> is returned.
+ */
+ public boolean matches(String resource, List<String> tokens, String etag) {
+ if (log.isTraceEnabled()) {
+ log.trace("matches: Trying to match token=" + tokens + ", etag=" + etag);
+ }
+
+ for (IfList il : this) {
+ if (il.match(tokens, etag)) {
+ if (log.isTraceEnabled()) {
+ log.trace("matches: Found match with " + il);
+ }
+ return true;
+ }
+ }
+ // invariant: no match found
+
+ return false;
+ }
+ }
+
+ /**
+ * The <code>IfHeaderMap</code> class implements the {@link IfHeaderInterface}
+ * interface to support tagged lists of {@link IfList}s. This class
+ * implements the data container for the production :
+ * <pre>
+ * Tagged = { "<" Word ">" "(" IfList ")" } .
+ * </pre>
+ */
+ private class IfHeaderMap extends HashMap<String, IfHeaderList> implements IfHeaderInterface {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Matches the token and etag for the given resource. If the resource is
+ * not mentioned in the header, a match is assumed and <code>true</code>
+ * is returned in this case.
+ *
+ * @param resource The absolute URI of the resource for which to find
+ * a match.
+ * @param tokens The tokens to compare.
+ * @param etag The etag to compare.
+ *
+ * @return <code>true</code> if either no entry exists for the resource
+ * or if the entry for the resource matches the token and etag.
+ */
+ public boolean matches(String resource, List<String> tokens, String etag) {
+ if (log.isTraceEnabled()) {
+ log.trace("matches: Trying to match resource=" + resource + ", token=" + tokens + "," + etag);
+ }
+
+ String uri;
+ String path;
+ if (resource.startsWith("/")) {
+ path = resource;
+ uri = WebdavIfHeader.this.uriPrefix + resource;
+ } else {
+ path = resource.substring(WebdavIfHeader.this.uriPrefix.length());
+ uri = resource;
+ }
+ IfHeaderList list = get(path);
+ if (list == null) {
+ list = get(uri);
+ }
+ if (list == null) {
+ if (log.isTraceEnabled()) {
+ log.trace("matches: No entry for tag " + resource + ", assuming mismatch");
+ }
+ return false;
+ } else {
+ return list.matches(resource, tokens, etag);
+ }
+ }
+ }
+}
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 2c4b217290..5a0c07c852 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -17,6 +17,7 @@
package org.apache.catalina.servlets;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
@@ -275,7 +276,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
SimpleHttpClient.CRLF + LOCK_BODY });
client.connect();
client.processRequest(true);
- Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
@@ -418,7 +419,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "PUT /myfolder/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: 6" + SimpleHttpClient.CRLF +
- "If: " + lockToken + SimpleHttpClient.CRLF +
+ "If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF + CONTENT });
client.connect();
@@ -452,7 +453,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
SimpleHttpClient.CRLF + LOCK_BODY });
client.connect();
client.processRequest(true);
- Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
String lockTokenFile = null;
for (String header : client.getResponseHeaders()) {
@@ -462,6 +463,17 @@ public class TestWebdavServlet extends TomcatBaseTest {
}
Assert.assertNotNull(lockTokenFile);
+ client.setRequest(new String[] { "LOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("/myfolder/file5.txt"));
+ Assert.assertTrue(client.getResponseBody().contains("HTTP/1.1 423"));
+
client.setRequest(new String[] { "PUT /myfolder/file5.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: 6" + SimpleHttpClient.CRLF +
@@ -475,7 +487,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "PUT /myfolder/file5.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: 6" + SimpleHttpClient.CRLF +
- "If: " + lockTokenFile + SimpleHttpClient.CRLF +
+ "If: (" + lockTokenFile + ")" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF + CONTENT });
client.connect();
@@ -485,7 +497,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Verify that this also removes the lock by doing another PUT without the token
client.setRequest(new String[] { "DELETE /myfolder/file5.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "If: " + lockTokenFile + SimpleHttpClient.CRLF +
+ "If: (" + lockTokenFile + ")" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF });
client.connect();
@@ -529,10 +541,19 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode());
+ client.setRequest(new String[] { "COPY /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Destination: /myfolder2" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_CREATED, client.getStatusCode());
+
// Delete /myfolder/file4.txt
client.setRequest(new String[] { "DELETE /myfolder/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "If: " + lockToken + SimpleHttpClient.CRLF +
+ "If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF });
client.connect();
@@ -579,6 +600,14 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode());
+ client.setRequest(new String[] { "DELETE /myfolder2 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode());
+
client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
@@ -696,7 +725,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "If: " + lockToken + SimpleHttpClient.CRLF +
+ "If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
"Content-Length: " + LOCK_BODY.length() + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF + LOCK_BODY });
@@ -705,6 +734,16 @@ public class TestWebdavServlet extends TomcatBaseTest {
// This should conflict, submitting a token does not help
Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode());
+ // Lock refresh /myfolder
+ client.setRequest(new String[] { "LOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+
client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF +
@@ -760,7 +799,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "If: " + lockToken2 + SimpleHttpClient.CRLF +
+ "If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
"Content-Length: 6" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF + CONTENT });
@@ -825,6 +864,112 @@ public class TestWebdavServlet extends TomcatBaseTest {
}
+ @Test
+ public void testIfHeader() throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+
+ // Create a temp webapp that can be safely written to
+ File tempWebapp = new File(getTemporaryDirectory(), "webdav-if");
+ File folder = new File(tempWebapp, "/myfolder/myfolder2/myfolder4/myfolder5");
+ Assert.assertTrue(folder.mkdirs());
+ File file = new File(folder, "myfile.txt");
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(CONTENT.getBytes());
+ }
+ folder = new File(tempWebapp, "/myfolder/myfolder3/myfolder6");
+ Assert.assertTrue(folder.mkdirs());
+ folder = new File(tempWebapp, "/myfolder/myfolder7/myfolder8/myfolder9");
+ Assert.assertTrue(folder.mkdirs());
+ Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
+ Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
+ webdavServlet.addInitParameter("listings", "true");
+ webdavServlet.addInitParameter("secret", "foo");
+ webdavServlet.addInitParameter("readonly", "false");
+ ctxt.addServletMappingDecoded("/*", "webdav");
+ tomcat.start();
+
+ Client client = new Client();
+ client.setPort(getPort());
+
+ client.setRequest(new String[] { "LOCK /myfolder/myfolder3 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ String lockToken = null;
+ for (String header : client.getResponseHeaders()) {
+ if (header.startsWith("Lock-Token: ")) {
+ lockToken = header.substring("Lock-Token: ".length());
+ }
+ }
+ Assert.assertNotNull(lockToken);
+
+ client.setRequest(new String[] { "LOCK /myfolder/myfolder7 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_SHARED_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ String lockToken2 = null;
+ for (String header : client.getResponseHeaders()) {
+ if (header.startsWith("Lock-Token: ")) {
+ lockToken2 = header.substring("Lock-Token: ".length());
+ }
+ }
+ Assert.assertNotNull(lockToken2);
+
+ client.setRequest(new String[] { "LOCK /myfolder/myfolder7/myfolder8 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_SHARED_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ String lockToken3 = null;
+ for (String header : client.getResponseHeaders()) {
+ if (header.startsWith("Lock-Token: ")) {
+ lockToken3 = header.substring("Lock-Token: ".length());
+ }
+ }
+ Assert.assertNotNull(lockToken3);
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "If: </myfolder/myfolder3/myfolder6> (<opaquelocktoken:5e1e2275b1cd9c17845e7e08>)" + // Obvious wrong token
+ " </myfolder/myfolder7/myfolder8/myfolder9> (" + lockToken + " " + lockToken2 + " " + lockToken3 + ")" + // lockToken is not there
+ " </myfolder/myfolder2/myfolder4> (<opaquelocktoken:7329872398754923752> [W/\"4-1729375899470\"])" + // Not locked
+ " </myfolder/myfolder7/myfolder8> (" + lockToken + ")" + SimpleHttpClient.CRLF + // lockToken is not there
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_PRECONDITION_FAILED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "If: </myfolder/myfolder3/myfolder6> (<opaquelocktoken:5e1e2275b1cd9c17845e7e08>)" + // Obvious wrong token
+ " </myfolder/myfolder2/myfolder4> (<opaquelocktoken:7329872398754923752> [W/\"4-1729375899470\"])" + // Not locked
+ " </myfolder/myfolder7/myfolder8> (" + lockToken + ")" + // lockToken is not there
+ " </myfolder/myfolder7/myfolder8/myfolder9> (" + lockToken2 + " " + lockToken3 + ")" + SimpleHttpClient.CRLF + // Correct
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_CREATED, client.getStatusCode());
+
+ }
+
private static final class Client extends SimpleHttpClient {
@Override
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index b325f810b6..edcaff8886 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -126,6 +126,10 @@
Rewrite implementation of WebDAV shared locks to comply with RFC 4918.
(remm)
</update>
+ <update>
+ Implement WebDAV <code>If</code> header using code from the Apache
+ Jackrabbit project. (remm)
+ </update>
<!-- Entries for backport and removal before 12.0.0-M1 below this line -->
>>>>>>> ac76965a76 (Rewrite implementation of WebDAV shared locks)
<update>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 17/19: Add a bit more of XML validation
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit e88fa7c0681a3779c4cd563ee09310f9a62891e1
Author: remm <re...@apache.org>
AuthorDate: Tue Oct 22 11:52:50 2024 +0200
Add a bit more of XML validation
---
test/org/apache/catalina/servlets/TestWebdavServlet.java | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index f8c3546c58..4b46a53b91 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -295,6 +295,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("<T:othercustomprop"));
+ validateXml(client.getResponseBody());
client.setRequest(new String[] { "PROPFIND /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
@@ -338,6 +339,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype>"));
Assert.assertFalse(client.getResponseBody().contains("<D:getlastmodified>"));
Assert.assertTrue(client.getResponseBody().contains("<myvalue xmlns=\"http://tomcat.apache.org/testsuite\">"));
+ validateXml(client.getResponseBody());
}
@@ -649,6 +651,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertFalse(client.getResponseBody().contains("/myfolder"));
+ validateXml(client.getResponseBody());
}
@@ -812,6 +815,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
}
}
Assert.assertNotNull(lockToken3);
+ validateXml(client.getResponseBody());
client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
@@ -825,6 +829,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
String timeoutValue = client.getResponseBody().substring(client.getResponseBody().indexOf("Second-"));
timeoutValue = timeoutValue.substring("Second-".length(), timeoutValue.indexOf('<'));
Assert.assertTrue(Integer.valueOf(timeoutValue).intValue() > 100000);
+ validateXml(client.getResponseBody());
client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
@@ -919,6 +924,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
// Verify all the shared locks are cleared
Assert.assertFalse(client.getResponseBody().contains("opaquelocktoken:"));
+ validateXml(client.getResponseBody());
}
@@ -1028,6 +1034,10 @@ public class TestWebdavServlet extends TomcatBaseTest {
}
+ private void validateXml(String xmlContent) throws Exception {
+ SAXParserFactory.newInstance().newSAXParser().getXMLReader().parse(new InputSource(new StringReader(xmlContent)));
+ }
+
private static final class Client extends SimpleHttpClient {
@Override
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 01/19: Remove WebDAV lock null support for compliance with RFC 4918
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 09a8845bfbd3dd377b7f0ce8a994bce70b49e4f1
Author: remm <re...@apache.org>
AuthorDate: Thu Oct 17 10:40:52 2024 +0200
Remove WebDAV lock null support for compliance with RFC 4918
Section 7.3 and annex D deprecate lock nulls and define a new behavior.
http://www.webdav.org/specs/rfc4918.html#lock-null
Instead a lock on a non existing resource will create an empty file
locked with a regular lock, which is of course far simpler. When
removing the lock, the corresponding file will not go away.
---
.../apache/catalina/servlets/WebdavServlet.java | 146 ++-------------------
.../catalina/servlets/TestWebdavServlet.java | 2 +-
webapps/docs/changelog.xml | 5 +
3 files changed, 16 insertions(+), 137 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index 8fc29b30c7..e9209629fa 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -16,6 +16,7 @@
*/
package org.apache.catalina.servlets;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
@@ -221,16 +222,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
private final ConcurrentHashMap<String,LockInfo> resourceLocks = new ConcurrentHashMap<>();
- /**
- * Repository of the lock-null resources.
- * <p>
- * Key : path of the collection containing the lock-null resource<br>
- * Value : List of lock-null resource which are members of the collection. Each element of the List is the path
- * associated with the lock-null resource.
- */
- private final ConcurrentHashMap<String,CopyOnWriteArrayList<String>> lockNullResources = new ConcurrentHashMap<>();
-
-
/**
* List of the inheritable collection locks.
*/
@@ -293,7 +284,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
for (LockInfo currentLock : resourceLocks.values()) {
if (currentLock.hasExpired()) {
resourceLocks.remove(currentLock.path);
- removeLockNull(currentLock.path);
}
}
Iterator<LockInfo> collectionLocksIterator = collectionLocks.iterator();
@@ -301,7 +291,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
LockInfo currentLock = collectionLocksIterator.next();
if (currentLock.hasExpired()) {
collectionLocksIterator.remove();
- removeLockNull(currentLock.path);
}
}
}
@@ -566,30 +555,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return;
}
- if (!resource.exists()) {
- int slash = path.lastIndexOf('/');
- if (slash != -1) {
- String parentPath = path.substring(0, slash);
- List<String> currentLockNullResources = lockNullResources.get(parentPath);
- if (currentLockNullResources != null) {
- for (String lockNullPath : currentLockNullResources) {
- if (lockNullPath.equals(path)) {
- resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
- resp.setContentType("text/xml; charset=UTF-8");
- // Create multistatus object
- XMLWriter generatedXML = new XMLWriter(resp.getWriter());
- generatedXML.writeXMLHeader();
- generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING);
- parseLockNullProperties(req, generatedXML, lockNullPath, type, properties);
- generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING);
- generatedXML.sendData();
- return;
- }
- }
- }
- }
- }
-
if (!resource.exists()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -634,18 +599,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
stackBelow.addFirst(newPath);
}
- // Displaying the lock-null resources present in that
- // collection
- String lockPath = currentPath;
- if (lockPath.endsWith("/")) {
- lockPath = lockPath.substring(0, lockPath.length() - 1);
- }
- List<String> currentLockNullResources = lockNullResources.get(lockPath);
- if (currentLockNullResources != null) {
- for (String lockNullPath : currentLockNullResources) {
- parseLockNullProperties(req, generatedXML, lockNullPath, type, properties);
- }
- }
}
if (stack.isEmpty()) {
@@ -741,8 +694,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (resources.mkdir(path)) {
resp.setStatus(WebdavStatus.SC_CREATED);
- // Removing any lock-null resource which would be present
- removeLockNull(path);
} else {
resp.sendError(WebdavStatus.SC_CONFLICT);
}
@@ -784,8 +735,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
super.doPut(req, resp);
- // Removing any lock-null resource which would be present
- removeLockNull(path);
}
@@ -1189,12 +1138,11 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
// Checking if a resource exists at this path
if (!resource.exists()) {
-
- // "Creating" a lock-null resource
- int slash = lock.path.lastIndexOf('/');
- String parentPath = lock.path.substring(0, slash);
-
- lockNullResources.computeIfAbsent(parentPath, k -> new CopyOnWriteArrayList<>()).add(lock.path);
+ // RFC 4918 removes lock null, instead an empty file is created
+ if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) {
+ resp.sendError(WebdavStatus.SC_CONFLICT);
+ return;
+ }
}
lock.tokens.add(lockToken);
@@ -1306,8 +1254,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (lock.tokens.isEmpty()) {
resourceLocks.remove(path);
- // Removing any lock-null resource which would be present
- removeLockNull(path);
}
}
@@ -1327,8 +1273,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
if (lock.tokens.isEmpty()) {
collectionLocks.remove(lock);
- // Removing any lock-null resource which would be present
- removeLockNull(path);
break;
}
}
@@ -1597,10 +1541,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
resp.setStatus(WebdavStatus.SC_CREATED);
}
- // Removing any lock-null resource which would be present at
- // the destination path
- removeLockNull(destinationPath);
-
return true;
}
@@ -1945,52 +1885,14 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
String rewrittenUrl = rewriteUrl(href);
- generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, resource.isFile(), false,
+ generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, resource.isFile(),
resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
getServletContext().getMimeType(resource.getName()), generateETag(resource));
}
- /**
- * Propfind helper method. Displays the properties of a lock-null resource.
- *
- * @param req The servlet request
- * @param generatedXML XML response to the Propfind request
- * @param path Path of the current resource
- * @param type Propfind type
- * @param properties If the propfind type is find properties by name, then this List contains those properties
- */
- private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type,
- List<String> properties) {
-
- // Exclude any resource in the /WEB-INF and /META-INF subdirectories
- if (isSpecialPath(path)) {
- return;
- }
-
- // Retrieving the lock associated with the lock-null resource
- LockInfo lock = resourceLocks.get(path);
-
- if (lock == null) {
- return;
- }
-
- String absoluteUri = req.getRequestURI();
- String relativePath = getRelativePath(req);
- String toAppend = path.substring(relativePath.length());
- if (!toAppend.startsWith("/")) {
- toAppend = "/" + toAppend;
- }
-
- String rewrittenUrl = rewriteUrl(RequestUtil.normalize(absoluteUri + toAppend));
-
- generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, true, true,
- lock.creationDate.getTime(), lock.creationDate.getTime(), 0, "", "");
- }
-
-
private void generatePropFindResponse(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType,
- List<String> properties, boolean isFile, boolean isLockNull, long created, long lastModified,
+ List<String> properties, boolean isFile, long created, long lastModified,
long contentLength, String contentType, String eTag) {
generatedXML.writeElement("D", "response", XMLWriter.OPENING);
@@ -2025,13 +1927,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeProperty("D", "getcontenttype", contentType);
}
generatedXML.writeProperty("D", "getetag", eTag);
- if (isLockNull) {
- generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING);
- generatedXML.writeElement("D", "lock-null", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING);
- } else {
- generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT);
- }
+ generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT);
} else {
generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified));
generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING);
@@ -2134,13 +2030,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
} else if (property.equals("resourcetype")) {
if (isFile) {
- if (isLockNull) {
- generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING);
- generatedXML.writeElement("D", "lock-null", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING);
- } else {
- generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT);
- }
+ generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT);
} else {
generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING);
generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT);
@@ -2281,21 +2171,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
- private void removeLockNull(String path) {
- int slash = path.lastIndexOf('/');
- if (slash >= 0) {
- String parentPath = path.substring(0, slash);
- List<String> paths = lockNullResources.get(parentPath);
- if (paths != null) {
- paths.remove(path);
- if (paths.isEmpty()) {
- lockNullResources.remove(parentPath);
- }
- }
- }
- }
-
-
private String getDAVNode(Node node) {
if (node.getNamespaceURI().equals(DEFAULT_NAMESPACE)) {
return node.getLocalName();
@@ -2329,7 +2204,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
String owner = "";
List<String> tokens = Collections.synchronizedList(new ArrayList<>());
long expiresAt = 0;
- Date creationDate = new Date();
// ----------------------------------------------------- Public Methods
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 8fe84abb85..a1df181f12 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -441,7 +441,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
SimpleHttpClient.CRLF + CONTENT });
client.connect();
client.processRequest(true);
- Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+ Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode());
// Verify that this also removes the lock by doing another PUT without the token
client.setRequest(new String[] { "DELETE /myfolder/file5.txt HTTP/1.1" + SimpleHttpClient.CRLF +
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 67ffcee4ed..1210f018da 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -160,6 +160,11 @@
WebDAV Delete should remove any existing lock on successfully deleted
resources. (remm)
</fix>
+ <update>
+ Remove WebDAV lock null support in accordance with RFC 4918 section 7.3
+ and annex D. Instead a lock on a non existing resource will create an
+ empty file locked with a regular lock. (remm)
+ </update>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 07/19: Improve lock test coverage a bit more
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit b100925b108d8b5688e2778b3f7733151ee38c29
Author: remm <re...@apache.org>
AuthorDate: Mon Oct 21 12:09:43 2024 +0200
Improve lock test coverage a bit more
Timeout header was likely an ancient style, now it is simpler.
Enable strict If header processing by default (for consistency and ease
of testing) since it is simple to disable.
---
.../apache/catalina/servlets/WebdavServlet.java | 27 ++++++-------
.../catalina/servlets/TestWebdavServlet.java | 44 +++++++++++++++++++++-
2 files changed, 53 insertions(+), 18 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index 8392be3161..33b26bb676 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -260,7 +260,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
* Is the if header processing strict.
*/
- private boolean strictIfProcessing = false;
+ private boolean strictIfProcessing = true;
// --------------------------------------------------------- Public Methods
@@ -1224,23 +1224,14 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
int lockDuration = DEFAULT_TIMEOUT;
String lockDurationStr = req.getHeader("Timeout");
if (lockDurationStr != null) {
- int commaPos = lockDurationStr.indexOf(',');
- // If multiple timeouts, just use the first
- if (commaPos != -1) {
- lockDurationStr = lockDurationStr.substring(0, commaPos);
- }
if (lockDurationStr.startsWith("Second-")) {
- lockDuration = Integer.parseInt(lockDurationStr.substring(7));
- } else {
- if (lockDurationStr.equalsIgnoreCase("infinity")) {
- lockDuration = MAX_TIMEOUT;
- } else {
- try {
- lockDuration = Integer.parseInt(lockDurationStr);
- } catch (NumberFormatException e) {
- lockDuration = MAX_TIMEOUT;
- }
+ try {
+ lockDuration = Integer.parseInt(lockDurationStr.substring("Second-".length()));
+ } catch (NumberFormatException e) {
+ // Ignore
}
+ } else if (lockDurationStr.equals("Infinite")) {
+ lockDuration = MAX_TIMEOUT;
}
if (lockDuration == 0) {
lockDuration = DEFAULT_TIMEOUT;
@@ -1640,6 +1631,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
resourceLocks.remove(parentPath);
unlocked = true;
break;
+ } else {
+ // No parent exclusive lock will be found
+ unlocked = false;
+ break;
}
} else {
for (String token : parentLock.sharedTokens) {
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 4997fa4378..234ad5dbfe 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -437,7 +437,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.setRequest(new String[] { "PUT /myfolder/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: 6" + SimpleHttpClient.CRLF +
- "If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
+ "If: </myfolder/> (" + lockToken + ")" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF + CONTENT });
client.connect();
@@ -454,8 +454,18 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode());
+ client.setRequest(new String[] { "UNLOCK /myfolder/file5.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Lock-Token: <my:locktoken>" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CONFLICT, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("<D:href>/myfolder</D:href>"));
+
// Unlock /myfolder
- client.setRequest(new String[] { "UNLOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ client.setRequest(new String[] { "UNLOCK /myfolder/file5.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Lock-Token: " + lockToken + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
@@ -534,6 +544,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Lock /myfolder again
client.setRequest(new String[] { "LOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Timeout: Second-20" + SimpleHttpClient.CRLF +
"Content-Length: " + LOCK_BODY.length() + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF + LOCK_BODY });
@@ -598,6 +609,10 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertFalse(client.getResponseBody().contains("/myfolder/file4.txt"));
Assert.assertTrue(client.getResponseBody().contains("/file7.txt"));
+ Assert.assertTrue(client.getResponseBody().contains("Second-"));
+ String timeoutValue = client.getResponseBody().substring(client.getResponseBody().indexOf("Second-"));
+ timeoutValue = timeoutValue.substring("Second-".length(), timeoutValue.indexOf('<'));
+ Assert.assertTrue(Integer.valueOf(timeoutValue) <= 20);
// Unlock /myfolder again
client.setRequest(new String[] { "UNLOCK /myfolder/ HTTP/1.1" + SimpleHttpClient.CRLF +
@@ -755,6 +770,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Lock refresh /myfolder
client.setRequest(new String[] { "LOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Timeout: Infinite" + SimpleHttpClient.CRLF +
"If: (" + lockToken + ")" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
SimpleHttpClient.CRLF });
@@ -805,6 +821,10 @@ public class TestWebdavServlet extends TomcatBaseTest {
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ Assert.assertTrue(client.getResponseBody().contains("Second-"));
+ String timeoutValue = client.getResponseBody().substring(client.getResponseBody().indexOf("Second-"));
+ timeoutValue = timeoutValue.substring("Second-".length(), timeoutValue.indexOf('<'));
+ Assert.assertTrue(Integer.valueOf(timeoutValue) > 100000);
client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
@@ -823,8 +843,28 @@ public class TestWebdavServlet extends TomcatBaseTest {
SimpleHttpClient.CRLF + CONTENT });
client.connect();
client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_PRECONDITION_FAILED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "If: </myfolder/myfolder2/myfolder4/myfolder5> (" + lockToken + ")" + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+ client.setRequest(new String[] { "UNLOCK /myfolder/myfolder3 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Lock-Token: " + lockToken2 + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CONFLICT, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("<D:lock-token-matches-request-uri/>"));
+
client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Lock-Token: " + lockToken2 + SimpleHttpClient.CRLF +
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 11/19: Remove unnecessary type specifications
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 497652069070a5708659e64e098cc43f85a67778
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 08:33:57 2024 +0100
Remove unnecessary type specifications
---
.../apache/catalina/servlets/TransientPropertiesWebdavServlet.java | 4 ++--
java/org/apache/tomcat/util/http/WebdavIfHeader.java | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
index 80c979c190..0ab6310ed9 100644
--- a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
+++ b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
@@ -64,7 +64,7 @@ public class TransientPropertiesWebdavServlet extends WebdavServlet {
} else {
ArrayList<Node> properties = deadProperties.get(path);
if (properties == null) {
- properties = new ArrayList<Node>();
+ properties = new ArrayList<>();
deadProperties.put(path, properties);
}
synchronized (properties) {
@@ -145,7 +145,7 @@ public class TransientPropertiesWebdavServlet extends WebdavServlet {
ArrayList<Node> propertiesDest = deadProperties.get(dest);
if (properties != null) {
if (propertiesDest == null) {
- propertiesDest = new ArrayList<Node>();
+ propertiesDest = new ArrayList<>();
deadProperties.put(dest, propertiesDest);
}
synchronized (properties) {
diff --git a/java/org/apache/tomcat/util/http/WebdavIfHeader.java b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
index c66bce3d0d..39f64051ea 100644
--- a/java/org/apache/tomcat/util/http/WebdavIfHeader.java
+++ b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
@@ -97,17 +97,17 @@ public class WebdavIfHeader {
/**
* The list of resources present in the If header.
*/
- private List<String> resources = new ArrayList<String>();
+ private List<String> resources = new ArrayList<>();
/**
* The list of all positive tokens present in the If header.
*/
- private List<String> allTokens = new ArrayList<String>();
+ private List<String> allTokens = new ArrayList<>();
/**
* The list of all NOT tokens present in the If header.
*/
- private List<String> allNotTokens = new ArrayList<String>();
+ private List<String> allNotTokens = new ArrayList<>();
private String uriPrefix;
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 13/19: Fix unboxing warnings
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 564f9f15a9ea7ebd205ed6564fb1e1c54482ad5d
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 08:34:52 2024 +0100
Fix unboxing warnings
---
test/org/apache/catalina/servlets/TestWebdavServlet.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 234ad5dbfe..f8c3546c58 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -612,7 +612,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertTrue(client.getResponseBody().contains("Second-"));
String timeoutValue = client.getResponseBody().substring(client.getResponseBody().indexOf("Second-"));
timeoutValue = timeoutValue.substring("Second-".length(), timeoutValue.indexOf('<'));
- Assert.assertTrue(Integer.valueOf(timeoutValue) <= 20);
+ Assert.assertTrue(Integer.valueOf(timeoutValue).intValue() <= 20);
// Unlock /myfolder again
client.setRequest(new String[] { "UNLOCK /myfolder/ HTTP/1.1" + SimpleHttpClient.CRLF +
@@ -824,7 +824,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertTrue(client.getResponseBody().contains("Second-"));
String timeoutValue = client.getResponseBody().substring(client.getResponseBody().indexOf("Second-"));
timeoutValue = timeoutValue.substring("Second-".length(), timeoutValue.indexOf('<'));
- Assert.assertTrue(Integer.valueOf(timeoutValue) > 100000);
+ Assert.assertTrue(Integer.valueOf(timeoutValue).intValue() > 100000);
client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 06/19: Refine the dead properties handling
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit b19617838e41699965338791c9658b4a70e3b2fb
Author: remm <re...@apache.org>
AuthorDate: Sun Oct 20 23:46:05 2024 +0200
Refine the dead properties handling
Add a proof of concept otherwise it is not possible to validate it.
Add namespaces on elments to the DOMWriter.
---
.../servlets/TransientPropertiesWebdavServlet.java | 186 +++++++++++++++++++++
.../apache/catalina/servlets/WebdavServlet.java | 71 ++++++--
java/org/apache/catalina/util/DOMWriter.java | 12 +-
.../catalina/servlets/TestWebdavServlet.java | 84 ++++------
4 files changed, 286 insertions(+), 67 deletions(-)
diff --git a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
new file mode 100644
index 0000000000..80c979c190
--- /dev/null
+++ b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.servlets;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.concurrent.ConcurrentHashMap;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.apache.catalina.util.DOMWriter;
+import org.apache.catalina.util.XMLWriter;
+import org.w3c.dom.Node;
+
+/**
+ * Extended WebDAV Servlet that implements dead properties using storage in memory.
+ */
+public class TransientPropertiesWebdavServlet extends WebdavServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private final ConcurrentHashMap<String,ArrayList<Node>> deadProperties = new ConcurrentHashMap<>();
+
+ @Override
+ protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.addHeader("DAV", "1,2,3");
+ resp.addHeader("Allow", determineMethodsAllowed(req));
+ resp.addHeader("MS-Author-Via", "DAV");
+ }
+
+ @Override
+ protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) {
+ boolean protectedProperty = false;
+ // Check for the protected properties
+ for (ProppatchOperation operation : operations) {
+ if (operation.getProtectedProperty()) {
+ protectedProperty = true;
+ operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN);
+ }
+ }
+ if (protectedProperty) {
+ for (ProppatchOperation operation : operations) {
+ if (!operation.getProtectedProperty()) {
+ operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY);
+ }
+ }
+ } else {
+ ArrayList<Node> properties = deadProperties.get(path);
+ if (properties == null) {
+ properties = new ArrayList<Node>();
+ deadProperties.put(path, properties);
+ }
+ synchronized (properties) {
+ for (ProppatchOperation operation : operations) {
+ if (operation.getUpdateType() == PropertyUpdateType.SET) {
+ Node node = operation.getPropertyNode().cloneNode(true);
+ boolean found = false;
+ for (int i = 0; i < properties.size(); i++) {
+ Node propertyNode = properties.get(i);
+ if (propertyEquals(node, propertyNode)) {
+ found = true;
+ properties.set(i, node);
+ break;
+ }
+ }
+ if (!found) {
+ properties.add(node);
+ }
+ } if (operation.getUpdateType() == PropertyUpdateType.REMOVE) {
+ Node node = operation.getPropertyNode();
+ for (int i = 0; i < properties.size(); i++) {
+ Node propertyNode = properties.get(i);
+ if (propertyEquals(node, propertyNode)) {
+ properties.remove(i);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected boolean propfindResource(String path, Node property, boolean nameOnly, XMLWriter generatedXML) {
+ ArrayList<Node> properties = deadProperties.get(path);
+ if (properties != null) {
+ synchronized (properties) {
+ if (nameOnly) {
+ // Add the names of all properties
+ for (Node node : properties) {
+ generatedXML.writeElement(node.getPrefix(),
+ node.getNamespaceURI(),
+ node.getLocalName(), XMLWriter.NO_CONTENT);
+ }
+ } else if (property != null) {
+ // Add a single property
+ Node foundNode = null;
+ for (Node node : properties) {
+ if (propertyEquals(node, property)) {
+ foundNode = node;
+ }
+ }
+ if (foundNode != null) {
+ StringWriter strWriter = new StringWriter();
+ DOMWriter domWriter = new DOMWriter(strWriter);
+ domWriter.print(foundNode);
+ generatedXML.writeRaw(strWriter.toString());
+ return true;
+ }
+ } else {
+ StringWriter strWriter = new StringWriter();
+ DOMWriter domWriter = new DOMWriter(strWriter);
+ // Add all properties
+ for (Node node : properties) {
+ domWriter.print(node);
+ }
+ generatedXML.writeRaw(strWriter.toString());
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void copyResource(String source, String dest) {
+ ArrayList<Node> properties = deadProperties.get(source);
+ ArrayList<Node> propertiesDest = deadProperties.get(dest);
+ if (properties != null) {
+ if (propertiesDest == null) {
+ propertiesDest = new ArrayList<Node>();
+ deadProperties.put(dest, propertiesDest);
+ }
+ synchronized (properties) {
+ synchronized (propertiesDest) {
+ for (Node node : properties) {
+ node = node.cloneNode(true);
+ boolean found = false;
+ for (int i = 0; i < propertiesDest.size(); i++) {
+ Node propertyNode = propertiesDest.get(i);
+ if (propertyEquals(node, propertyNode)) {
+ found = true;
+ propertiesDest.set(i, node);
+ break;
+ }
+ }
+ if (!found) {
+ propertiesDest.add(node);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void deleteResource(String path) {
+ deadProperties.remove(path);
+ }
+
+ private boolean propertyEquals(Node node1, Node node2) {
+ if (node1.getLocalName().equals(node2.getLocalName())
+ && ((node1.getNamespaceURI() == null && node2.getNamespaceURI() == null)
+ || (node1.getNamespaceURI() != null && node1.getNamespaceURI().equals(node2.getNamespaceURI())))) {
+ return true;
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index d27506764c..8392be3161 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -996,7 +996,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
- * Apply proppatch to the specified path. This should be overriden by subclasses to provide
+ * Apply proppatch to the specified path. This should be overridden by subclasses to provide
* useful behavior. The default implementation prevents setting protected properties
* (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
*
@@ -1970,6 +1970,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
errorList.put(dest, Integer.valueOf(WebdavStatus.SC_CONFLICT));
return false;
}
+ } else {
+ copyResource(source, dest);
}
if (infiniteCopy) {
@@ -2012,6 +2014,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (!resources.write(dest, is, false)) {
errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
return false;
+ } else {
+ copyResource(source, dest);
}
} catch (IOException e) {
log(sm.getString("webdavservlet.inputstreamclosefail", source), e);
@@ -2023,6 +2027,16 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return true;
}
+ /**
+ * Copy resource. This should be overridden by subclasses to provide
+ * useful behavior. The default implementation prevents setting protected properties
+ * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ *
+ * @param source the copy source path
+ * @param dest the copy destination path
+ */
+ protected void copyResource(String source, String dest) {
+ }
/**
* Delete a resource.
@@ -2077,7 +2091,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
sendNotAllowed(req, resp);
return false;
}
- unlockResource(path, null);
+ deleteResource(path);
} else {
Map<String,Integer> errorList = new LinkedHashMap<>();
@@ -2100,7 +2114,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
errorList.put(path, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED));
}
} else {
- unlockResource(path, null);
+ deleteResource(path);
}
if (!errorList.isEmpty()) {
@@ -2114,6 +2128,17 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return true;
}
+ /**
+ * Delete specified resource. This should be overridden by subclasses to provide
+ * useful behavior. The default implementation prevents setting protected properties
+ * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ *
+ * @param path the path of the resource to delete
+ */
+ protected void deleteResource(String path) {
+ unlockResource(path, null);
+ }
+
private void unlockResource(String path, String lockToken) {
LockInfo lock = resourceLocks.get(path);
if (lock != null) {
@@ -2201,7 +2226,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
errorList.put(childName, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED));
}
} else {
- unlockResource(childName, null);
+ deleteResource(childName);
}
}
}
@@ -2305,9 +2330,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "prop", XMLWriter.OPENING);
generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created));
- generatedXML.writeElement("D", "displayname", XMLWriter.OPENING);
- generatedXML.writeData(resourceName);
- generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING);
if (isFile) {
generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified));
generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength));
@@ -2344,7 +2366,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "prop", XMLWriter.OPENING);
generatedXML.writeElement("D", "creationdate", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("D", "displayname", XMLWriter.NO_CONTENT);
if (isFile) {
generatedXML.writeElement("D", "getcontentlanguage", XMLWriter.NO_CONTENT);
generatedXML.writeElement("D", "getcontentlength", XMLWriter.NO_CONTENT);
@@ -2474,7 +2495,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
- * Generate propfind XML fragments for dead properties.
+ * Generate propfind XML fragments for dead properties. This should be overridden by subclasses to provide
+ * useful behavior. The default implementation prevents setting protected properties
+ * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
*
* @param path the resource path
* @param property the dead property, if null then all dead properties must be written
@@ -2483,6 +2506,30 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* @return true if property was specified and a corresponding dead property was found on the resource, false otherwise
*/
protected boolean propfindResource(String path, Node property, boolean nameOnly, XMLWriter generatedXML) {
+ if (nameOnly) {
+ generatedXML.writeElement("D", "displayname", XMLWriter.NO_CONTENT);
+ } else if (property == null) {
+ String resourceName = path;
+ int lastSlash = path.lastIndexOf('/');
+ if (lastSlash != -1) {
+ resourceName = resourceName.substring(lastSlash + 1);
+ }
+ generatedXML.writeElement("D", "displayname", XMLWriter.OPENING);
+ generatedXML.writeData(resourceName);
+ generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING);
+ } else {
+ String davName = getDAVNode(property);
+ if ("displayname".equals(davName)) {
+ String resourceName = path;
+ int lastSlash = path.lastIndexOf('/');
+ if (lastSlash != -1) {
+ resourceName = resourceName.substring(lastSlash + 1);
+ }
+ generatedXML.writeElement("D", "displayname", XMLWriter.OPENING);
+ generatedXML.writeData(resourceName);
+ generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING);
+ }
+ }
return false;
}
@@ -2581,7 +2628,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
private static String getDAVNode(Node node) {
- if (node.getNamespaceURI().equals(DEFAULT_NAMESPACE)) {
+ if (DEFAULT_NAMESPACE.equals(node.getNamespaceURI())) {
return node.getLocalName();
}
return null;
@@ -2737,7 +2784,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
public ProppatchOperation(PropertyUpdateType updateType, Node propertyNode) {
this.updateType = updateType;
this.propertyNode = propertyNode;
- protectedProperty = getDAVNode(propertyNode) != null;
+ String davName = getDAVNode(propertyNode);
+ protectedProperty = davName != null
+ && (!(davName.equals("displayname") || davName.equals("getcontentlanguage")));
}
/**
* @return the updateType
diff --git a/java/org/apache/catalina/util/DOMWriter.java b/java/org/apache/catalina/util/DOMWriter.java
index 0cd3532526..7ef95b1ffa 100644
--- a/java/org/apache/catalina/util/DOMWriter.java
+++ b/java/org/apache/catalina/util/DOMWriter.java
@@ -64,14 +64,22 @@ public class DOMWriter {
out.print('<');
out.print(node.getLocalName());
Attr attrs[] = sortAttributes(node.getAttributes());
+ boolean xmlns = false;
for (Attr attr : attrs) {
out.print(' ');
out.print(attr.getLocalName());
-
+ if ("xmlns".equals(attr.getLocalName())) {
+ xmlns = true;
+ }
out.print("=\"");
out.print(Escape.xml("", true, attr.getNodeValue()));
out.print('"');
}
+ if (!xmlns && node.getNamespaceURI() != null) {
+ out.print(" xmlns=\"");
+ out.print(Escape.xml(node.getNamespaceURI()));
+ out.print('"');
+ }
out.print('>');
printChildren(node);
break;
@@ -88,7 +96,7 @@ public class DOMWriter {
// print text
case Node.TEXT_NODE:
- out.print(Escape.xml("", true, node.getNodeValue()));
+ out.print(Escape.xml("", false, node.getNodeValue()));
break;
// print processing instruction
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 5a0c07c852..4997fa4378 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -20,7 +20,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -35,7 +34,6 @@ import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
-import org.apache.catalina.util.XMLWriter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.websocket.server.WsContextListener;
import org.xml.sax.InputSource;
@@ -229,7 +227,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
"<D:propertyupdate xmlns:D=\"DAV:\" xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" +
" <D:set>\n" +
" <D:prop>\n" +
- " <T:customprop xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" +
+ " <T:customprop>\n" +
" <T:myvalue/>\n" +
" </T:customprop>\n" +
" </D:prop>\n" +
@@ -247,7 +245,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
File tempWebapp = new File(getTemporaryDirectory(), "webdav-properties");
Assert.assertTrue(tempWebapp.mkdirs());
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
- Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new CustomWebdavServlet());
+ Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new TransientPropertiesWebdavServlet());
webdavServlet.addInitParameter("listings", "true");
webdavServlet.addInitParameter("secret", "foo");
webdavServlet.addInitParameter("readonly", "false");
@@ -288,7 +286,17 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
- client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
+ client.setRequest(new String[] { "PROPPATCH /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + PROPPATCH_PROPNAME.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + PROPPATCH_PROPNAME });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("<T:othercustomprop"));
+
+ client.setRequest(new String[] { "PROPFIND /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: " + PROPFIND_PROPNAME.length() + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
@@ -298,7 +306,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype/>"));
- client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
+ client.setRequest(new String[] { "PROPFIND /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Content-Length: " + PROPFIND_PROP.length() + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
@@ -308,18 +316,28 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype>"));
Assert.assertFalse(client.getResponseBody().contains("<D:getlastmodified>"));
- Assert.assertTrue(client.getResponseBody().contains("<T:myvalue/>"));
+ Assert.assertTrue(client.getResponseBody().contains("<myvalue xmlns=\"http://tomcat.apache.org/testsuite\">"));
- client.setRequest(new String[] { "PROPPATCH /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ client.setRequest(new String[] { "MOVE /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
- "Content-Length: " + PROPPATCH_PROPNAME.length() + SimpleHttpClient.CRLF +
+ "Destination: /file3.txt" + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
- SimpleHttpClient.CRLF + PROPPATCH_PROPNAME });
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PROPFIND /file3.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + PROPFIND_PROP.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + PROPFIND_PROP });
client.connect();
client.processRequest(true);
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
- Assert.assertTrue(proppatchSuccess);
- Assert.assertTrue(client.getResponseBody().contains("<T:othercustomprop"));
+ Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype>"));
+ Assert.assertFalse(client.getResponseBody().contains("<D:getlastmodified>"));
+ Assert.assertTrue(client.getResponseBody().contains("<myvalue xmlns=\"http://tomcat.apache.org/testsuite\">"));
}
@@ -978,46 +996,4 @@ public class TestWebdavServlet extends TomcatBaseTest {
}
}
- private static boolean proppatchSuccess = false;
-
- private class CustomWebdavServlet extends WebdavServlet {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) {
- for (ProppatchOperation operation : operations) {
- if (operation.getUpdateType().equals(PropertyUpdateType.SET)
- && operation.getPropertyNode().getLocalName().equals("customprop")) {
- proppatchSuccess = true;
- }
- operation.setStatusCode(HttpServletResponse.SC_OK);
- }
- }
-
- @Override
- protected boolean propfindResource(String path, org.w3c.dom.Node property, boolean nameOnly, XMLWriter generatedXML) {
- if (nameOnly) {
- generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "othercustomprop", XMLWriter.NO_CONTENT);
- } else if (property == null) {
- generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.OPENING);
- generatedXML.writeElement("T", "myvalue", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("T", "customprop", XMLWriter.CLOSING);
- generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "othercustomprop", XMLWriter.OPENING);
- generatedXML.writeElement("T", "myothervalue", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("T", "othercustomprop", XMLWriter.CLOSING);
- } else if (property.getLocalName().equals("customprop")) {
- generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.OPENING);
- generatedXML.writeElement("T", "myvalue", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("T", "customprop", XMLWriter.CLOSING);
- return true;
- } else if (property.getLocalName().equals("othercustomprop")) {
- return false;
- }
- return false;
- }
-
- }
-
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 16/19: Avoid writing namespace prefix
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c2ea0fe876b8ced8ac89cd7b7bfe7c2166a7f24a
Author: remm <re...@apache.org>
AuthorDate: Tue Oct 22 11:44:04 2024 +0200
Avoid writing namespace prefix
---
java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
index efa02d23cb..b27f974ad4 100644
--- a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
+++ b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
@@ -107,7 +107,7 @@ public class TransientPropertiesWebdavServlet extends WebdavServlet {
if (nameOnly) {
// Add the names of all properties
for (Node node : properties) {
- generatedXML.writeElement(node.getPrefix(), node.getNamespaceURI(), node.getLocalName(),
+ generatedXML.writeElement(null, node.getNamespaceURI(), node.getLocalName(),
XMLWriter.NO_CONTENT);
}
} else if (property != null) {
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 02/19: Add extension points to support dead properties
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 257cf6185de7eabfccf3cb165104df0680d1124c
Author: remm <re...@apache.org>
AuthorDate: Fri Oct 18 09:21:41 2024 +0200
Add extension points to support dead properties
With a base PROPPATCH implementation.
Add a custom impl to the test to verify.
Remove D:source property (removed from RFC 4918).
---
.../apache/catalina/servlets/WebdavServlet.java | 400 +++++++++++++++++----
java/org/apache/catalina/util/XMLWriter.java | 22 ++
.../catalina/servlets/TestWebdavServlet.java | 76 +++-
webapps/docs/changelog.xml | 6 +
4 files changed, 428 insertions(+), 76 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index e9209629fa..033a02ff9c 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -133,7 +133,8 @@ import org.xml.sax.SAXException;
* access will be able to edit content available via http://host:port/context/content using
* http://host:port/context/webdavedit/content
* <p>
- * There are some known limitations of this Servlet due to it not implementing the PROPPATCH method. Details of these
+ * There are some known limitations of this Servlet due to it not implementing PROPPATCH and PROPFIND methods support
+ * for dead properties. The Servlet does provide extension points to add support for some as required by user. Details of these
* limitations and progress towards addressing them are being tracked under
* <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=69046">bug 69046</a>.
* </p>
@@ -444,6 +445,20 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
+ private String getEncodedPath(String path, WebResource resource, HttpServletRequest request) {
+ String href = getPathPrefix(request);
+ if ((href.endsWith("/")) && (path.startsWith("/"))) {
+ href += path.substring(1);
+ } else {
+ href += path;
+ }
+ if (resource.isDirectory() && (!href.endsWith("/"))) {
+ href += "/";
+ }
+
+ return rewriteUrl(href);
+ }
+
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("DAV", "1,2");
@@ -473,8 +488,14 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
path = path.substring(0, path.length() - 1);
}
+ // Exclude any resource in the /WEB-INF and /META-INF subdirectories
+ if (isSpecialPath(path)) {
+ resp.sendError(WebdavStatus.SC_FORBIDDEN);
+ return;
+ }
+
// Properties which are to be displayed.
- List<String> properties = new ArrayList<>();
+ List<Node> properties = new ArrayList<>();
// Propfind depth
int depth = maxDepth;
// Propfind type
@@ -524,12 +545,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
case Node.TEXT_NODE:
break;
case Node.ELEMENT_NODE:
- // href is a live property which is handled differently
- String propertyName = getDAVNode(currentNode2);
- // No support for non DAV: properties
- if (propertyName != null) {
- properties.add(propertyName);
- }
+ properties.add(currentNode2);
break;
}
}
@@ -571,7 +587,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING);
if (depth == 0) {
- parseProperties(req, generatedXML, path, type, properties);
+ propfindResource(generatedXML, getEncodedPath(path, resource, req),
+ path, type, properties, resource.isFile(),
+ resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
+ getServletContext().getMimeType(resource.getName()), generateETag(resource));
} else {
// The stack always contains the object of the current level
Deque<String> stack = new ArrayDeque<>();
@@ -583,9 +602,21 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
while ((!stack.isEmpty()) && (depth >= 0)) {
String currentPath = stack.remove();
- parseProperties(req, generatedXML, currentPath, type, properties);
+
+ // Exclude any resource in the /WEB-INF and /META-INF subdirectories
+ if (isSpecialPath(currentPath)) {
+ continue;
+ }
resource = resources.getResource(currentPath);
+ // File is in directory listing but doesn't appear to exist
+ // Broken symlink or odd permission settings?
+ if (resource.exists()) {
+ propfindResource(generatedXML, getEncodedPath(currentPath, resource, req),
+ currentPath, type, properties, resource.isFile(),
+ resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
+ getServletContext().getMimeType(resource.getName()), generateETag(resource));
+ }
if (resource.isDirectory() && (depth > 0)) {
@@ -625,16 +656,24 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* @param req The Servlet request
* @param resp The Servlet response
*
+ * @throws ServletException If an error occurs
* @throws IOException If an IO error occurs
*/
- protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = getRelativePath(req);
+ if (path.length() > 1 && path.endsWith("/")) {
+ path = path.substring(0, path.length() - 1);
+ }
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
return;
}
+ if (!resource.exists()) {
+ resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
if (readOnly) {
resp.sendError(WebdavStatus.SC_FORBIDDEN);
@@ -646,8 +685,177 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return;
}
- // FIXME: Compliant PROPPATCH processing
- resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ DocumentBuilder documentBuilder = getDocumentBuilder();
+ ArrayList<ProppatchOperation> operations = new ArrayList<>();
+
+ try {
+ Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
+
+ // Get the root element of the document
+ Element rootElement = document.getDocumentElement();
+ if (!"propertyupdate".equals(getDAVNode(rootElement))) {
+ resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+ return;
+ }
+ NodeList childList = rootElement.getChildNodes();
+
+ for (int i = 0; i < childList.getLength(); i++) {
+ Node currentNode = childList.item(i);
+ switch (currentNode.getNodeType()) {
+ case Node.TEXT_NODE:
+ break;
+ case Node.ELEMENT_NODE:
+ String nodeName = getDAVNode(currentNode);
+ if ("set".equals(nodeName)) {
+ NodeList setChildList = currentNode.getChildNodes();
+ for (int j = 0; j < setChildList.getLength(); j++) {
+ Node currentNode2 = setChildList.item(j);
+ switch (currentNode2.getNodeType()) {
+ case Node.TEXT_NODE:
+ break;
+ case Node.ELEMENT_NODE:
+ if ("prop".equals(getDAVNode(currentNode2))) {
+ NodeList propChildList = currentNode2.getChildNodes();
+ Node property = null;
+ for (int k = 0; k < propChildList.getLength(); k++) {
+ Node currentNode3 = propChildList.item(k);
+ switch (currentNode3.getNodeType()) {
+ case Node.TEXT_NODE:
+ break;
+ case Node.ELEMENT_NODE:
+ property = currentNode3;
+ break;
+ }
+ }
+ if (property != null) {
+ operations.add(new ProppatchOperation(PropertyUpdateType.SET, property));
+ } else {
+ resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+ return;
+ }
+ }
+ break;
+ }
+ }
+ }
+ if ("remove".equals(nodeName)) {
+ NodeList removeChildList = currentNode.getChildNodes();
+ for (int j = 0; j < removeChildList.getLength(); j++) {
+ Node currentNode2 = removeChildList.item(j);
+ switch (currentNode2.getNodeType()) {
+ case Node.TEXT_NODE:
+ break;
+ case Node.ELEMENT_NODE:
+ if ("prop".equals(getDAVNode(currentNode2))) {
+ NodeList propChildList = currentNode2.getChildNodes();
+ Node property = null;
+ for (int k = 0; k < propChildList.getLength(); k++) {
+ Node currentNode3 = propChildList.item(k);
+ switch (currentNode3.getNodeType()) {
+ case Node.TEXT_NODE:
+ break;
+ case Node.ELEMENT_NODE:
+ property = currentNode3;
+ break;
+ }
+ }
+ if (property != null) {
+ operations.add(new ProppatchOperation(PropertyUpdateType.REMOVE, property));
+ } else {
+ resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+ return;
+ }
+ }
+ break;
+ }
+ }
+ }
+ break;
+ }
+ }
+ } catch (SAXException | IOException e) {
+ // Something went wrong - bad request
+ resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+ return;
+ }
+
+ proppatchResource(path, operations);
+
+ resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
+
+ resp.setContentType("text/xml; charset=UTF-8");
+
+ // Create multistatus object
+ XMLWriter generatedXML = new XMLWriter(resp.getWriter());
+ generatedXML.writeXMLHeader();
+
+ generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING);
+ generatedXML.writeElement("D", "response", XMLWriter.OPENING);
+
+ // Generating href element
+ generatedXML.writeElement("D", "href", XMLWriter.OPENING);
+ generatedXML.writeText(getEncodedPath(path, resource, req));
+ generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
+
+ for (ProppatchOperation operation : operations) {
+ generatedXML.writeElement("D", "propstat", XMLWriter.OPENING);
+ generatedXML.writeElement("D", "prop", XMLWriter.OPENING);
+ generatedXML.writeElement(operation.propertyNode.getPrefix(),
+ operation.propertyNode.getNamespaceURI(),
+ operation.propertyNode.getLocalName(), XMLWriter.NO_CONTENT);
+ generatedXML.writeElement("D", "prop", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", "status", XMLWriter.OPENING);
+ generatedXML.writeText("HTTP/1.1 " + String.valueOf(operation.getStatusCode()) + " ");
+ generatedXML.writeElement("D", "status", XMLWriter.CLOSING);
+ if (operation.getProtectedProperty() && operation.getStatusCode() == HttpServletResponse.SC_FORBIDDEN) {
+ generatedXML.writeElement("D", "error", XMLWriter.OPENING);
+ generatedXML.writeElement("D", "cannot-modify-protected-property", XMLWriter.NO_CONTENT);
+ generatedXML.writeElement("D", "error", XMLWriter.CLOSING);
+ }
+ generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING);
+ }
+
+ generatedXML.writeElement("D", "response", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING);
+
+ generatedXML.sendData();
+
+ }
+
+
+ /**
+ * Apply proppatch to the specified path. This should be overriden by subclasses to provide
+ * useful behavior. The default implementation prevents setting protected properties
+ * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ *
+ * @param path the resource path on which to apply the proppatch
+ * @param operations the set and remove to apply, the final status codes of the result should be set on each operation
+ */
+ protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) {
+ boolean setProperty = false;
+ boolean protectedProperty = false;
+ // Check for the protected properties
+ for (ProppatchOperation operation : operations) {
+ if (operation.getUpdateType() == PropertyUpdateType.SET) {
+ setProperty = true;
+ }
+ if (operation.getProtectedProperty()) {
+ protectedProperty = true;
+ operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN);
+ }
+ }
+ if (protectedProperty) {
+ for (ProppatchOperation operation : operations) {
+ if (!operation.getProtectedProperty()) {
+ operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY);
+ }
+ }
+ } else if (setProperty) {
+ // No dead property support
+ for (ProppatchOperation operation : operations) {
+ operation.setStatusCode(WebdavStatus.SC_INSUFFICIENT_STORAGE);
+ }
+ }
}
@@ -664,6 +872,12 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
String path = getRelativePath(req);
+ // Exclude any resource in the /WEB-INF and /META-INF subdirectories
+ if (isSpecialPath(path)) {
+ resp.sendError(WebdavStatus.SC_FORBIDDEN);
+ return;
+ }
+
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
return;
@@ -1849,50 +2063,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
- /**
- * Propfind helper method.
- *
- * @param req The servlet request
- * @param generatedXML XML response to the Propfind request
- * @param path Path of the current resource
- * @param type Propfind type
- * @param properties If the propfind type is find properties by name, then this List contains those properties
- */
- private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type,
- List<String> properties) {
-
- // Exclude any resource in the /WEB-INF and /META-INF subdirectories
- if (isSpecialPath(path)) {
- return;
- }
-
- WebResource resource = resources.getResource(path);
- if (!resource.exists()) {
- // File is in directory listing but doesn't appear to exist
- // Broken symlink or odd permission settings?
- return;
- }
-
- String href = getPathPrefix(req);
- if ((href.endsWith("/")) && (path.startsWith("/"))) {
- href += path.substring(1);
- } else {
- href += path;
- }
- if (resource.isDirectory() && (!href.endsWith("/"))) {
- href += "/";
- }
-
- String rewrittenUrl = rewriteUrl(href);
-
- generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, resource.isFile(),
- resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
- getServletContext().getMimeType(resource.getName()), generateETag(resource));
- }
-
-
- private void generatePropFindResponse(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType,
- List<String> properties, boolean isFile, long created, long lastModified,
+ private void propfindResource(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType,
+ List<Node> properties, boolean isFile, long created, long lastModified,
long contentLength, String contentType, String eTag) {
generatedXML.writeElement("D", "response", XMLWriter.OPENING);
@@ -1934,8 +2106,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT);
generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING);
}
-
- generatedXML.writeProperty("D", "source", "");
+ propfindResource(path, null, false, generatedXML);
String supportedLocks = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" +
"<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" +
@@ -1970,8 +2141,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "getlastmodified", XMLWriter.NO_CONTENT);
}
generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT);
- generatedXML.writeElement("D", "source", XMLWriter.NO_CONTENT);
generatedXML.writeElement("D", "lockdiscovery", XMLWriter.NO_CONTENT);
+ propfindResource(path, null, true, generatedXML);
generatedXML.writeElement("D", "prop", XMLWriter.CLOSING);
generatedXML.writeElement("D", "status", XMLWriter.OPENING);
@@ -1983,15 +2154,20 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
case FIND_BY_PROPERTY:
- List<String> propertiesNotFound = new ArrayList<>();
+ List<Node> propertiesNotFound = new ArrayList<>();
// Parse the list of properties
generatedXML.writeElement("D", "propstat", XMLWriter.OPENING);
generatedXML.writeElement("D", "prop", XMLWriter.OPENING);
- for (String property : properties) {
- if (property.equals("creationdate")) {
+ for (Node propertyNode : properties) {
+ String property = getDAVNode(propertyNode);
+ if (property == null) {
+ if (!propfindResource(path, propertyNode, false, generatedXML)) {
+ propertiesNotFound.add(propertyNode);
+ }
+ } else if (property.equals("creationdate")) {
generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created));
} else if (property.equals("displayname")) {
generatedXML.writeElement("D", "displayname", XMLWriter.OPENING);
@@ -2001,32 +2177,32 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (isFile) {
generatedXML.writeElement("D", "getcontentlanguage", XMLWriter.NO_CONTENT);
} else {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
} else if (property.equals("getcontentlength")) {
if (isFile) {
generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength));
} else {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
} else if (property.equals("getcontenttype")) {
if (isFile) {
generatedXML.writeProperty("D", "getcontenttype", contentType);
} else {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
} else if (property.equals("getetag")) {
if (isFile) {
generatedXML.writeProperty("D", "getetag", eTag);
} else {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
} else if (property.equals("getlastmodified")) {
if (isFile) {
generatedXML.writeProperty("D", "getlastmodified",
FastHttpDateFormat.formatDate(lastModified));
} else {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
} else if (property.equals("resourcetype")) {
if (isFile) {
@@ -2036,8 +2212,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT);
generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING);
}
- } else if (property.equals("source")) {
- generatedXML.writeProperty("D", "source", "");
} else if (property.equals("supportedlock")) {
supportedLocks = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" +
"<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" +
@@ -2048,10 +2222,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING);
} else if (property.equals("lockdiscovery")) {
if (!generateLockDiscovery(path, generatedXML)) {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
} else {
- propertiesNotFound.add(property);
+ propertiesNotFound.add(propertyNode);
}
}
@@ -2068,8 +2242,15 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "propstat", XMLWriter.OPENING);
generatedXML.writeElement("D", "prop", XMLWriter.OPENING);
- for (String propertyNotFound : propertiesNotFound) {
- generatedXML.writeElement("D", propertyNotFound, XMLWriter.NO_CONTENT);
+ for (Node propertyNotFoundNode : propertiesNotFound) {
+ String propertyNotFound = getDAVNode(propertyNotFoundNode);
+ if (propertyNotFound != null) {
+ generatedXML.writeElement("D", propertyNotFound, XMLWriter.NO_CONTENT);
+ } else {
+ generatedXML.writeElement(propertyNotFoundNode.getPrefix(),
+ propertyNotFoundNode.getNamespaceURI(),
+ propertyNotFoundNode.getLocalName(), XMLWriter.NO_CONTENT);
+ }
}
generatedXML.writeElement("D", "prop", XMLWriter.CLOSING);
@@ -2086,6 +2267,20 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
+ /**
+ * Generate propfind XML fragments for dead properties.
+ *
+ * @param path the resource path
+ * @param property the dead property, if null then all dead properties must be written
+ * @param nameOnly true if only the property name element should be generated
+ * @param generatedXML the current generated XML for the PROPFIND response
+ * @return true if property was specified and a corresponding dead property was found on the resource, false otherwise
+ */
+ protected boolean propfindResource(String path, Node property, boolean nameOnly, XMLWriter generatedXML) {
+ return false;
+ }
+
+
/**
* Print the lock discovery information associated with a path.
*
@@ -2171,7 +2366,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
- private String getDAVNode(Node node) {
+ private static String getDAVNode(Node node) {
if (node.getNamespaceURI().equals(DEFAULT_NAMESPACE)) {
return node.getLocalName();
}
@@ -2312,6 +2507,51 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return new InputSource(new StringReader("Ignored external entity"));
}
}
+
+ enum PropertyUpdateType { SET, REMOVE }
+
+ protected static class ProppatchOperation {
+ private final PropertyUpdateType updateType;
+ private final Node propertyNode;
+ private final boolean protectedProperty;
+ private int statusCode = HttpServletResponse.SC_OK;
+ public ProppatchOperation(PropertyUpdateType updateType, Node propertyNode) {
+ this.updateType = updateType;
+ this.propertyNode = propertyNode;
+ protectedProperty = getDAVNode(propertyNode) != null;
+ }
+ /**
+ * @return the updateType
+ */
+ public PropertyUpdateType getUpdateType() {
+ return this.updateType;
+ }
+ /**
+ * @return the propertyNode
+ */
+ public Node getPropertyNode() {
+ return this.propertyNode;
+ }
+ /**
+ * @return the statusCode
+ */
+ public int getStatusCode() {
+ return this.statusCode;
+ }
+ /**
+ * @param statusCode the statusCode to set
+ */
+ public void setStatusCode(int statusCode) {
+ this.statusCode = statusCode;
+ }
+ /**
+ * @return the protectedProperty
+ */
+ public boolean getProtectedProperty() {
+ return this.protectedProperty;
+ }
+ }
+
}
@@ -2503,4 +2743,14 @@ class WebdavStatus {
* contain a valid Lock-Info header, or the Lock-Info header identifies a lock held by another principal.
*/
public static final int SC_LOCKED = 423;
+
+ /**
+ * Status code (424) indicating that another dependent operation failed.
+ */
+ public static final int SC_FAILED_DEPENDENCY = 424;
+
+ /**
+ * Status code (507) indicating that the server does not have enough storage to complete the operation.
+ */
+ public static final int SC_INSUFFICIENT_STORAGE = 507;
}
diff --git a/java/org/apache/catalina/util/XMLWriter.java b/java/org/apache/catalina/util/XMLWriter.java
index a8789bbe84..01c2e2c35f 100644
--- a/java/org/apache/catalina/util/XMLWriter.java
+++ b/java/org/apache/catalina/util/XMLWriter.java
@@ -165,6 +165,28 @@ public class XMLWriter {
lastWriteWasOpen = false;
break;
}
+ } else if ((namespaceInfo != null) && (namespaceInfo.length() > 0)) {
+ switch (type) {
+ case OPENING:
+ if (lastWriteWasOpen) {
+ buffer.append('\n');
+ }
+ buffer.append("<" + name + " xmlns=\"" + namespaceInfo + "\">");
+ lastWriteWasOpen = true;
+ break;
+ case CLOSING:
+ buffer.append("</" + name + ">\n");
+ lastWriteWasOpen = false;
+ break;
+ case NO_CONTENT:
+ default:
+ if (lastWriteWasOpen) {
+ buffer.append('\n');
+ }
+ buffer.append("<" + name + " xmlns=\"" + namespaceInfo + "\"/>\n");
+ lastWriteWasOpen = false;
+ break;
+ }
} else {
switch (type) {
case OPENING:
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index a1df181f12..7cf7418c69 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -19,6 +19,7 @@ package org.apache.catalina.servlets;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -33,6 +34,7 @@ import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.catalina.util.XMLWriter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.websocket.server.WsContextListener;
import org.xml.sax.InputSource;
@@ -201,6 +203,8 @@ public class TestWebdavServlet extends TomcatBaseTest {
"<D:propfind xmlns:D=\"DAV:\">\n" +
" <D:prop>\n" +
" <D:getcontenttype/>\n" +
+ " <T:customprop xmlns:T=\"http://tomcat.apache.org/testsuite\"/>\n" +
+ " <T:othercustomprop xmlns:T=\"http://tomcat.apache.org/testsuite\"/>\n" +
" <D:getcontentlength/>\n" +
" </D:prop>\n" +
"</D:propfind>";
@@ -211,6 +215,21 @@ public class TestWebdavServlet extends TomcatBaseTest {
" <D:propname/>\n" +
"</D:propfind>";
+ private static final String PROPPATCH_PROPNAME =
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<D:propertyupdate xmlns:D=\"DAV:\" xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" +
+ " <D:set>\n" +
+ " <D:prop>\n" +
+ " <T:customprop xmlns:T=\"http://tomcat.apache.org/testsuite\">\n" +
+ " <T:myvalue/>\n" +
+ " </T:customprop>\n" +
+ " </D:prop>\n" +
+ " </D:set>\n" +
+ " <D:remove>\n" +
+ " <D:prop><T:othercustomprop/></D:prop>\n" +
+ " </D:remove>\n" +
+ "</D:propertyupdate>";
+
@Test
public void testBasicProperties() throws Exception {
Tomcat tomcat = getTomcatInstance();
@@ -219,7 +238,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
File tempWebapp = new File(getTemporaryDirectory(), "webdav-properties");
Assert.assertTrue(tempWebapp.mkdirs());
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
- Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
+ Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new CustomWebdavServlet());
webdavServlet.addInitParameter("listings", "true");
webdavServlet.addInitParameter("secret", "foo");
webdavServlet.addInitParameter("readonly", "false");
@@ -280,6 +299,18 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
Assert.assertTrue(client.getResponseBody().contains("<D:getcontenttype>"));
Assert.assertFalse(client.getResponseBody().contains("<D:getlastmodified>"));
+ Assert.assertTrue(client.getResponseBody().contains("<T:myvalue/>"));
+
+ client.setRequest(new String[] { "PROPPATCH /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + PROPPATCH_PROPNAME.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + PROPPATCH_PROPNAME });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
+ Assert.assertTrue(proppatchSuccess);
+ Assert.assertTrue(client.getResponseBody().contains("<T:othercustomprop"));
}
@@ -558,4 +589,47 @@ public class TestWebdavServlet extends TomcatBaseTest {
return true;
}
}
+
+ private static boolean proppatchSuccess = false;
+
+ private class CustomWebdavServlet extends WebdavServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) {
+ for (ProppatchOperation operation : operations) {
+ if (operation.getUpdateType().equals(PropertyUpdateType.SET)
+ && operation.getPropertyNode().getLocalName().equals("customprop")) {
+ proppatchSuccess = true;
+ }
+ operation.setStatusCode(HttpServletResponse.SC_OK);
+ }
+ }
+
+ @Override
+ protected boolean propfindResource(String path, org.w3c.dom.Node property, boolean nameOnly, XMLWriter generatedXML) {
+ if (nameOnly) {
+ generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.NO_CONTENT);
+ generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "othercustomprop", XMLWriter.NO_CONTENT);
+ } else if (property == null) {
+ generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.OPENING);
+ generatedXML.writeElement("T", "myvalue", XMLWriter.NO_CONTENT);
+ generatedXML.writeElement("T", "customprop", XMLWriter.CLOSING);
+ generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "othercustomprop", XMLWriter.OPENING);
+ generatedXML.writeElement("T", "myothervalue", XMLWriter.NO_CONTENT);
+ generatedXML.writeElement("T", "othercustomprop", XMLWriter.CLOSING);
+ } else if (property.getLocalName().equals("customprop")) {
+ generatedXML.writeElement("T", "http://tomcat.apache.org/testsuite", "customprop", XMLWriter.OPENING);
+ generatedXML.writeElement("T", "myvalue", XMLWriter.NO_CONTENT);
+ generatedXML.writeElement("T", "customprop", XMLWriter.CLOSING);
+ return true;
+ } else if (property.getLocalName().equals("othercustomprop")) {
+ return false;
+ }
+ return false;
+ }
+
+ }
+
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 1210f018da..3af636641c 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -165,6 +165,12 @@
and annex D. Instead a lock on a non existing resource will create an
empty file locked with a regular lock. (remm)
</update>
+ <update>
+ Add base WebDAV <code>PROPPATCH</code> implementation, to be extended
+ by overriding the <code>proppatchResource</code> and
+ <code>propfindResource</code> methods of the WebDAV Servlet to
+ provide the desired level of support for dead properties. (remm)
+ </update>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 09/19: Fix missing Javadoc
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c12333007ba0d2c4e41cd5211083d629dcd6e098
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 08:31:20 2024 +0100
Fix missing Javadoc
---
java/org/apache/tomcat/util/http/WebdavIfHeader.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/java/org/apache/tomcat/util/http/WebdavIfHeader.java b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
index 77ab27ec13..2e0dd06ffc 100644
--- a/java/org/apache/tomcat/util/http/WebdavIfHeader.java
+++ b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
@@ -115,6 +115,8 @@ public class WebdavIfHeader {
* Create a Untagged <code>IfHeader</code> if the given lock tokens.
*
* @param tokens the tokens
+ *
+ * @throws IOException If the parsing of the <code>IfHeader</code> fails
*/
public WebdavIfHeader(String[] tokens) throws IOException {
allTokens.addAll(Arrays.asList(tokens));
@@ -134,6 +136,8 @@ public class WebdavIfHeader {
*
* @param uriPrefix The uri prefix to use for the absolute href
* @param ifHeaderValue the if header
+ *
+ * @throws IOException If the parsing of the <code>IfHeader</code> fails
*/
public WebdavIfHeader(String uriPrefix, String ifHeaderValue) throws IOException {
this.uriPrefix = uriPrefix;
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 10/19: Add missing @Override
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 07a9eeb98e68ff8e54aa391b3b892c293fd52db0
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 08:32:51 2024 +0100
Add missing @Override
---
java/org/apache/tomcat/util/http/WebdavIfHeader.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/java/org/apache/tomcat/util/http/WebdavIfHeader.java b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
index 2e0dd06ffc..c66bce3d0d 100644
--- a/java/org/apache/tomcat/util/http/WebdavIfHeader.java
+++ b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
@@ -869,6 +869,7 @@ public class WebdavIfHeader {
* @return <code>True</code> if any of the {@link IfList}s matches the token
* and etag, else <code>false</code> is returned.
*/
+ @Override
public boolean matches(String resource, List<String> tokens, String etag) {
if (log.isTraceEnabled()) {
log.trace("matches: Trying to match token=" + tokens + ", etag=" + etag);
@@ -913,6 +914,7 @@ public class WebdavIfHeader {
* @return <code>true</code> if either no entry exists for the resource
* or if the entry for the resource matches the token and etag.
*/
+ @Override
public boolean matches(String resource, List<String> tokens, String etag) {
if (log.isTraceEnabled()) {
log.trace("matches: Trying to match resource=" + resource + ", token=" + tokens + "," + etag);
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 12/19: Add additional test. lock may be null here
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit b5a6f0405d4f5b491fb53bb7fa4daa4b5e21f7d1
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 08:36:22 2024 +0100
Add additional test. lock may be null here
---
java/org/apache/catalina/servlets/WebdavServlet.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index f6e77cf844..42fbc80d6c 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -1643,7 +1643,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (lockTokenHeader.contains(token)) {
LockInfo lock = sharedLocks.get(token);
if (lock == null || lock.principal == null || lock.principal.equals(req.getRemoteUser())) {
- if ((parentPath != path && lock.depth > 0) || parentPath == path) {
+ if ((parentPath != path && lock != null && lock.depth > 0) || parentPath == path) {
parentLock.sharedTokens.remove(token);
if (parentLock.sharedTokens.isEmpty()) {
resourceLocks.remove(parentPath);
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 05/19: Fix javadoc
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit efd6e7d616b87b771d2b1f570b37a371bc771b3c
Author: remm <re...@apache.org>
AuthorDate: Sun Oct 20 14:29:27 2024 +0200
Fix javadoc
---
java/org/apache/tomcat/util/http/WebdavIfHeader.java | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/java/org/apache/tomcat/util/http/WebdavIfHeader.java b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
index fbca45cdfc..77ab27ec13 100644
--- a/java/org/apache/tomcat/util/http/WebdavIfHeader.java
+++ b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
@@ -142,10 +142,7 @@ public class WebdavIfHeader {
}
/**
- * Return {@link DavConstants#HEADER_IF If}
- *
- * @return {@link DavConstants#HEADER_IF If}
- * @see DavConstants#HEADER_IF
+ * @return If String.
*/
public String getHeaderName() {
return "If";
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 03/19: Rewrite implementation of WebDAV shared locks
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 5d9e6aa71f9a073bb693412a648795e6abc53715
Author: remm <re...@apache.org>
AuthorDate: Fri Oct 18 16:48:30 2024 +0200
Rewrite implementation of WebDAV shared locks
During my review of RFC 4918, I found the implementation of shared locks
was non compliant due to wrong design (one lock with multiple tokens,
for example).
Exclusive locks are simpler and are mostly unchanged.
Collection locks now go into the same map as resource locks (and path
traversal is used to discover them) to avoid having to use four maps.
Implement new RFC 4918 behavior regarding auth principals and locks.
Implement the many new clarifications on unlock and lock discovery.
Add a shared locks test case with lots of edge cases.
---
.../apache/catalina/servlets/WebdavServlet.java | 529 +++++++++++----------
.../catalina/servlets/TestWebdavServlet.java | 245 +++++++++-
webapps/docs/changelog.xml | 21 +
3 files changed, 544 insertions(+), 251 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index 033a02ff9c..e60b92a560 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -29,10 +29,8 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.Date;
import java.util.Deque;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -215,18 +213,15 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
// ----------------------------------------------------- Instance Variables
/**
- * Repository of the locks put on single resources.
- * <p>
- * Key : path <br>
- * Value : LockInfo
+ * Repository of all locks, keyed by path.
*/
- private final ConcurrentHashMap<String,LockInfo> resourceLocks = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, LockInfo> resourceLocks = new ConcurrentHashMap<>();
/**
- * List of the inheritable collection locks.
+ * Map of all shared locks, keyed by lock token.
*/
- private final CopyOnWriteArrayList<LockInfo> collectionLocks = new CopyOnWriteArrayList<>();
+ private final ConcurrentHashMap<String, LockInfo> sharedLocks = new ConcurrentHashMap<>();
/**
@@ -282,16 +277,25 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
@Override
public void periodicEvent() {
// Check expiration of all locks
- for (LockInfo currentLock : resourceLocks.values()) {
+ for (LockInfo currentLock : sharedLocks.values()) {
if (currentLock.hasExpired()) {
- resourceLocks.remove(currentLock.path);
+ sharedLocks.remove(currentLock.path);
}
}
- Iterator<LockInfo> collectionLocksIterator = collectionLocks.iterator();
- while (collectionLocksIterator.hasNext()) {
- LockInfo currentLock = collectionLocksIterator.next();
- if (currentLock.hasExpired()) {
- collectionLocksIterator.remove();
+ for (LockInfo currentLock : resourceLocks.values()) {
+ if (currentLock.isExclusive()) {
+ if (currentLock.hasExpired()) {
+ resourceLocks.remove(currentLock.path);
+ }
+ } else {
+ for (String token : currentLock.sharedTokens) {
+ if (sharedLocks.get(token) == null) {
+ currentLock.sharedTokens.remove(token);
+ }
+ }
+ if (currentLock.sharedTokens.isEmpty()) {
+ resourceLocks.remove(currentLock.path);
+ }
}
}
}
@@ -429,8 +433,11 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (result.length() == 0) {
result.append('/');
}
-
- return result.toString();
+ String resultString = result.toString();
+ if (resultString.length() > 1 && resultString.endsWith("/")) {
+ resultString = resultString.substring(0, resultString.length() - 1);
+ }
+ return resultString;
}
@@ -452,7 +459,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} else {
href += path;
}
- if (resource.isDirectory() && (!href.endsWith("/"))) {
+ if (resource != null && resource.isDirectory() && (!href.endsWith("/"))) {
href += "/";
}
@@ -484,9 +491,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
String path = getRelativePath(req);
- if (path.length() > 1 && path.endsWith("/")) {
- path = path.substring(0, path.length() - 1);
- }
// Exclude any resource in the /WEB-INF and /META-INF subdirectories
if (isSpecialPath(path)) {
@@ -662,9 +666,6 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = getRelativePath(req);
- if (path.length() > 1 && path.endsWith("/")) {
- path = path.substring(0, path.length() - 1);
- }
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
@@ -1019,16 +1020,13 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
String path = getRelativePath(req);
- if (isLocked(path, req)) {
- resp.sendError(WebdavStatus.SC_LOCKED);
- return;
- }
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
return;
}
LockInfo lock = new LockInfo(maxDepth);
+ lock.principal = req.getRemoteUser();
// Parsing lock request
@@ -1214,27 +1212,33 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
if (lockRequestType == LOCK_CREATION) {
- // Check if a parent is already locked
- Iterator<LockInfo> collectionLocksIterator = collectionLocks.iterator();
- while (collectionLocksIterator.hasNext()) {
- LockInfo currentLock = collectionLocksIterator.next();
- if (currentLock.hasExpired()) {
- collectionLocksIterator.remove();
- continue;
+ // Check if the resource or a parent is already locked
+ String parentPath = path;
+ do {
+ LockInfo parentLock = resourceLocks.get(parentPath);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ resourceLocks.remove(parentPath);
+ } else if (parentLock.isExclusive() || lock.isExclusive()) {
+ // A parent collection of this collection is locked
+ resp.setStatus(WebdavStatus.SC_LOCKED);
+ return;
+ }
}
- if (lock.path.startsWith(currentLock.path + "/") && (currentLock.isExclusive() || lock.isExclusive())) {
- // A parent collection of this collection is locked
- resp.setStatus(WebdavStatus.SC_LOCKED);
- return;
+ int slash = parentPath.lastIndexOf('/');
+ if (slash < 0) {
+ break;
}
- }
+ parentPath = parentPath.substring(0, slash);
+ } while (true);
// Generating lock id
String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" +
- req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" +
+ req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.token + "-" +
lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret;
String lockToken = HexUtils
.toHexString(ConcurrentMessageDigest.digestMD5(lockTokenStr.getBytes(StandardCharsets.ISO_8859_1)));
+ lock.token = lockToken;
if (resource.isDirectory() && lock.depth == maxDepth) {
@@ -1242,29 +1246,18 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
// Checking if a child resource of this collection is
// already locked
+ // Note: it is likely faster in many cases to go over the full lock list than trying to go over all the
+ // children (recursively)
List<String> lockPaths = new ArrayList<>();
- collectionLocksIterator = collectionLocks.iterator();
- while (collectionLocksIterator.hasNext()) {
- LockInfo currentLock = collectionLocksIterator.next();
- if (currentLock.hasExpired()) {
- collectionLocksIterator.remove();
- continue;
- }
- if (currentLock.path.startsWith(lock.path + "/") &&
- (currentLock.isExclusive() || lock.isExclusive())) {
- // A child collection of this collection is locked
- lockPaths.add(currentLock.path);
- }
- }
for (LockInfo currentLock : resourceLocks.values()) {
if (currentLock.hasExpired()) {
resourceLocks.remove(currentLock.path);
continue;
}
- if (currentLock.path.startsWith(lock.path + "/") &&
- (currentLock.isExclusive() || lock.isExclusive())) {
+ if ((currentLock.isExclusive() || lock.isExclusive()) &&
+ currentLock.path.startsWith(lock.path + "/")) {
// A child resource of this collection is locked
- lockPaths.add(currentLock.path);
+ lockPaths.add(currentLock.lockroot);
}
}
@@ -1280,6 +1273,14 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING);
+ generatedXML.writeElement("D", "response", XMLWriter.OPENING);
+ generatedXML.writeElement("D", "href", XMLWriter.OPENING);
+ generatedXML.writeText(getEncodedPath(path, resource, req));
+ generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", "status", XMLWriter.OPENING);
+ generatedXML.writeText("HTTP/1.1 " + WebdavStatus.SC_FAILED_DEPENDENCY + " ");
+ generatedXML.writeElement("D", "status", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", "response", XMLWriter.CLOSING);
for (String lockPath : lockPaths) {
generatedXML.writeElement("D", "response", XMLWriter.OPENING);
generatedXML.writeElement("D", "href", XMLWriter.OPENING);
@@ -1300,73 +1301,38 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return;
}
- boolean addLock = true;
-
- // Checking if there is already a shared lock on this path
- for (LockInfo currentLock : collectionLocks) {
- if (currentLock.path.equals(lock.path)) {
-
- if (currentLock.isExclusive()) {
- resp.sendError(WebdavStatus.SC_LOCKED);
- return;
- } else {
- if (lock.isExclusive()) {
- resp.sendError(WebdavStatus.SC_LOCKED);
- return;
- }
- }
-
- currentLock.tokens.add(lockToken);
- lock = currentLock;
- addLock = false;
- }
- }
-
- if (addLock) {
- lock.tokens.add(lockToken);
- collectionLocks.add(lock);
- // Add the Lock-Token header as by RFC 2518 8.10.1
- // - only do this for newly created locks
- resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
- }
-
} else {
// Locking a single resource
- // Retrieving an already existing lock on that resource
- LockInfo presentLock = resourceLocks.get(lock.path);
- if (presentLock != null) {
-
- if ((presentLock.isExclusive()) || (lock.isExclusive())) {
- // If either lock is exclusive, the lock can't be
- // granted
- resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);
+ // Checking if a resource exists at this path
+ if (!resource.exists()) {
+ // RFC 4918 removes lock null, instead an empty file is created
+ if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) {
+ resp.sendError(WebdavStatus.SC_CONFLICT);
return;
- } else {
- presentLock.tokens.add(lockToken);
- lock = presentLock;
- }
-
- } else {
-
- // Checking if a resource exists at this path
- if (!resource.exists()) {
- // RFC 4918 removes lock null, instead an empty file is created
- if (!resources.write(path, new ByteArrayInputStream(new byte[0]), false)) {
- resp.sendError(WebdavStatus.SC_CONFLICT);
- return;
- }
}
+ }
- lock.tokens.add(lockToken);
- resourceLocks.put(lock.path, lock);
+ }
- // Add the Lock-Token header as by RFC 2518 8.10.1
- // - only do this for newly created locks
- resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
+ lock.lockroot = getEncodedPath(lock.path, resource, req);
+ if (lock.isExclusive()) {
+ resourceLocks.put(path, lock);
+ } else {
+ // Checking if there is already a shared lock on this path
+ LockInfo sharedLock = resourceLocks.get(path);
+ if (sharedLock == null) {
+ resourceLocks.put(path, lock);
+ sharedLock = lock;
}
+ sharedLock.sharedTokens.add(lockToken);
+ sharedLocks.put(lockToken, lock);
}
+
+ // Add the Lock-Token header as by RFC 2518 8.10.1
+ resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
+
}
if (lockRequestType == LOCK_REFRESH) {
@@ -1383,22 +1349,19 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
LockInfo toRenew = resourceLocks.get(path);
if (toRenew != null) {
- // At least one of the tokens of the locks must have been given
- for (String token : toRenew.tokens) {
- if (ifHeader.contains(token)) {
+ if (toRenew.isExclusive()) {
+ if (ifHeader.contains(toRenew.token)) {
toRenew.expiresAt = lock.expiresAt;
lock = toRenew;
}
- }
- }
-
- // Checking inheritable collection locks
- for (LockInfo collecionLock : collectionLocks) {
- if (path.equals(collecionLock.path)) {
- for (String token : collecionLock.tokens) {
+ } else {
+ for (String token : toRenew.sharedTokens) {
if (ifHeader.contains(token)) {
- collecionLock.expiresAt = lock.expiresAt;
- lock = collecionLock;
+ toRenew = sharedLocks.get(token);
+ if (toRenew != null) {
+ toRenew.expiresAt = lock.expiresAt;
+ lock = toRenew;
+ }
}
}
}
@@ -1442,6 +1405,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
String path = getRelativePath(req);
+
WebResource resource = resources.getResource(path);
if (!checkIfHeaders(req, resp, resource)) {
return;
@@ -1452,50 +1416,55 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
lockTokenHeader = "";
}
- // Checking resource locks
-
- LockInfo lock = resourceLocks.get(path);
- if (lock != null) {
-
- // At least one of the tokens of the locks must have been given
- Iterator<String> tokenList = lock.tokens.iterator();
- while (tokenList.hasNext()) {
- String token = tokenList.next();
- if (lockTokenHeader.contains(token)) {
- tokenList.remove();
+ boolean unlocked = false;
+ String parentPath = path;
+ do {
+ LockInfo parentLock = resourceLocks.get(parentPath);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ resourceLocks.remove(parentPath);
+ } else {
+ if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
+ if (parentLock.isExclusive()) {
+ if (lockTokenHeader.contains(parentLock.token)
+ && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) {
+ resourceLocks.remove(parentPath);
+ unlocked = true;
+ break;
+ }
+ } else {
+ for (String token : parentLock.sharedTokens) {
+ if (lockTokenHeader.contains(token)) {
+ LockInfo lock = sharedLocks.get(token);
+ if (lock == null || lock.principal == null || lock.principal.equals(req.getRemoteUser())) {
+ parentLock.sharedTokens.remove(token);
+ if (parentLock.sharedTokens.isEmpty()) {
+ resourceLocks.remove(parentPath);
+ }
+ sharedLocks.remove(token);
+ unlocked = true;
+ break;
+ }
+ }
+ }
+ }
+ }
}
}
-
- if (lock.tokens.isEmpty()) {
- resourceLocks.remove(path);
+ int slash = parentPath.lastIndexOf('/');
+ if (slash < 0) {
+ break;
}
+ parentPath = parentPath.substring(0, slash);
+ } while (true);
+ if (unlocked) {
+ resp.setStatus(WebdavStatus.SC_NO_CONTENT);
+ } else {
+ sendReport(req, resp, parentPath, WebdavStatus.SC_CONFLICT, "lock-token-matches-request-uri");
}
-
- // Checking inheritable collection locks
- Iterator<LockInfo> collectionLocksList = collectionLocks.iterator();
- while (collectionLocksList.hasNext()) {
- lock = collectionLocksList.next();
- if (path.equals(lock.path)) {
- Iterator<String> tokenList = lock.tokens.iterator();
- while (tokenList.hasNext()) {
- String token = tokenList.next();
- if (lockTokenHeader.contains(token)) {
- tokenList.remove();
- break;
- }
- }
- if (lock.tokens.isEmpty()) {
- collectionLocks.remove(lock);
- break;
- }
- }
- }
-
- resp.setStatus(WebdavStatus.SC_NO_CONTENT);
}
-
// -------------------------------------------------------- Private Methods
/**
@@ -1525,7 +1494,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
lockTokenHeader = "";
}
- return isLocked(path, ifHeader + lockTokenHeader);
+ return isLocked(path, req.getRemoteUser(), ifHeader + lockTokenHeader);
}
@@ -1533,53 +1502,52 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* Check to see if a resource is currently write locked.
*
* @param path Path of the resource
+ * @param principal The authenticated principal name
* @param ifHeader "If" HTTP header which was included in the request
*
* @return <code>true</code> if the resource is locked (and no appropriate lock token has been found for at least
* one of the non-shared locks which are present on the resource).
*/
- private boolean isLocked(String path, String ifHeader) {
-
- // Checking resource locks
-
- LockInfo lock = resourceLocks.get(path);
- if ((lock != null) && (lock.hasExpired())) {
- resourceLocks.remove(path);
- } else if (lock != null) {
-
- // At least one of the tokens of the locks must have been given
-
- boolean tokenMatch = false;
- for (String token : lock.tokens) {
- if (ifHeader.contains(token)) {
- tokenMatch = true;
- break;
- }
- }
- if (!tokenMatch) {
- return true;
- }
- }
+ private boolean isLocked(String path, String principal, String ifHeader) {
- // Checking inheritable collection locks
- Iterator<LockInfo> collectionLockList = collectionLocks.iterator();
- while (collectionLockList.hasNext()) {
- lock = collectionLockList.next();
- if (lock.hasExpired()) {
- collectionLockList.remove();
- } else if (path.startsWith(lock.path)) {
- boolean tokenMatch = false;
- for (String token : lock.tokens) {
- if (ifHeader.contains(token)) {
- tokenMatch = true;
- break;
+ // Check if the resource or a parent is already locked
+ String parentPath = path;
+ do {
+ LockInfo parentLock = resourceLocks.get(parentPath);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ resourceLocks.remove(parentPath);
+ } else {
+ if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
+ if (parentLock.isExclusive()) {
+ if (ifHeader.contains(parentLock.token)
+ && (parentLock.principal == null || parentLock.principal.equals(principal))) {
+ return false;
+ }
+ } else {
+ for (String token : parentLock.sharedTokens) {
+ if (ifHeader.contains(token)) {
+ if (principal == null) {
+ return false;
+ } else {
+ LockInfo lock = sharedLocks.get(token);
+ if (lock == null || lock.principal == null || lock.principal.equals(principal)) {
+ return false;
+ }
+ }
+ }
+ }
+ }
}
- }
- if (!tokenMatch) {
return true;
}
}
- }
+ int slash = parentPath.lastIndexOf('/');
+ if (slash < 0) {
+ break;
+ }
+ parentPath = parentPath.substring(0, slash);
+ } while (true);
return false;
}
@@ -1653,6 +1621,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
}
+ if (destinationPath.length() > 1 && destinationPath.endsWith("/")) {
+ destinationPath = destinationPath.substring(0, destinationPath.length() - 1);
+ }
+
// Cross-context operations aren't supported
String reqContextPath = getPathPrefix(req);
if (!destinationPath.startsWith(reqContextPath + "/")) {
@@ -1891,7 +1863,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
sendNotAllowed(req, resp);
return false;
}
- resourceLocks.remove(path);
+ unlockResource(path, null);
} else {
Map<String,Integer> errorList = new LinkedHashMap<>();
@@ -1914,15 +1886,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
errorList.put(path, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED));
}
} else {
- Iterator<LockInfo> collectionLocksList = collectionLocks.iterator();
- while (collectionLocksList.hasNext()) {
- LockInfo lock = collectionLocksList.next();
- if (path.equals(lock.path)) {
- collectionLocks.remove(lock);
- break;
- }
- }
- resourceLocks.remove(path);
+ unlockResource(path, null);
}
if (!errorList.isEmpty()) {
@@ -1936,6 +1900,27 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
return true;
}
+ private void unlockResource(String path, String lockToken) {
+ LockInfo lock = resourceLocks.get(path);
+ if (lock != null) {
+ if (lock.isExclusive()) {
+ if (lockToken == null || lockToken.contains(lock.token)) {
+ resourceLocks.remove(path);
+ }
+ } else {
+ for (String token : lock.sharedTokens) {
+ if (lockToken == null || lockToken.contains(token)) {
+ lock.sharedTokens.remove(token);
+ if (lock.sharedTokens.isEmpty()) {
+ resourceLocks.remove(lock.path);
+ }
+ sharedLocks.remove(token);
+ break;
+ }
+ }
+ }
+ }
+ }
/**
* Deletes a collection.
@@ -1975,7 +1960,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
childName += entry;
- if (isLocked(childName, lockHeader)) {
+ if (isLocked(childName, req.getRemoteUser(), lockHeader)) {
errorList.put(childName, Integer.valueOf(WebdavStatus.SC_LOCKED));
@@ -2002,17 +1987,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
errorList.put(childName, Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED));
}
} else {
- if (childResource.isDirectory()) {
- Iterator<LockInfo> collectionLocksList = collectionLocks.iterator();
- while (collectionLocksList.hasNext()) {
- LockInfo lock = collectionLocksList.next();
- if (childName.equals(lock.path)) {
- collectionLocks.remove(lock);
- break;
- }
- }
- }
- resourceLocks.remove(childName);
+ unlockResource(childName, null);
}
}
}
@@ -2045,7 +2020,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", "response", XMLWriter.OPENING);
generatedXML.writeElement("D", "href", XMLWriter.OPENING);
- generatedXML.writeText(getPathPrefix(req) + errorPath);
+ generatedXML.writeText(getEncodedPath(errorPath, null, req));
generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
generatedXML.writeElement("D", "status", XMLWriter.OPENING);
@@ -2063,6 +2038,33 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
+ private void sendReport(HttpServletRequest req, HttpServletResponse resp, String errorPath, int errorCode, String error)
+ throws IOException {
+ resp.setStatus(errorCode);
+
+ XMLWriter generatedXML = new XMLWriter();
+ generatedXML.writeXMLHeader();
+
+ generatedXML.writeElement("D", DEFAULT_NAMESPACE, "error", XMLWriter.OPENING);
+
+ if (errorPath != null && errorPath.length() > 0) {
+ generatedXML.writeElement("D", error, XMLWriter.OPENING);
+ generatedXML.writeElement("D", "href", XMLWriter.OPENING);
+ generatedXML.writeText(getEncodedPath(errorPath, null, req));
+ generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", error, XMLWriter.CLOSING);
+ } else {
+ generatedXML.writeElement("D", error, XMLWriter.NO_CONTENT);
+ }
+
+ generatedXML.writeElement("D", "error", XMLWriter.CLOSING);
+
+ Writer writer = resp.getWriter();
+ writer.write(generatedXML.toString());
+ writer.close();
+ }
+
+
private void propfindResource(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType,
List<Node> properties, boolean isFile, long created, long lastModified,
long contentLength, String contentType, String eTag) {
@@ -2291,25 +2293,47 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
*/
private boolean generateLockDiscovery(String path, XMLWriter generatedXML) {
- LockInfo resourceLock = resourceLocks.get(path);
-
boolean wroteStart = false;
- if (resourceLock != null) {
- wroteStart = true;
- generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
- resourceLock.toXML(generatedXML);
- }
-
- for (LockInfo currentLock : collectionLocks) {
- if (path.startsWith(currentLock.path)) {
- if (!wroteStart) {
- wroteStart = true;
- generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
+ String parentPath = path;
+ do {
+ LockInfo parentLock = resourceLocks.get(parentPath);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ resourceLocks.remove(parentPath);
+ } else {
+ if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
+ if (parentLock.isExclusive()) {
+ if (!wroteStart) {
+ wroteStart = true;
+ generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
+ }
+ parentLock.toXML(generatedXML);
+ } else {
+ for (String lockToken : parentLock.sharedTokens) {
+ parentLock = sharedLocks.get(lockToken);
+ if (parentLock != null) {
+ if (parentLock.hasExpired()) {
+ sharedLocks.remove(lockToken);
+ } else {
+ if (!wroteStart) {
+ wroteStart = true;
+ generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING);
+ }
+ parentLock.toXML(generatedXML);
+ }
+ }
+ }
+ }
+ }
}
- currentLock.toXML(generatedXML);
}
- }
+ int slash = parentPath.lastIndexOf('/');
+ if (slash < 0) {
+ break;
+ }
+ parentPath = parentPath.substring(0, slash);
+ } while (true);
if (wroteStart) {
generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING);
@@ -2393,12 +2417,15 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
private final int maxDepth;
String path = "/";
+ String lockroot = "/";
String type = "write";
String scope = "exclusive";
int depth = 0;
String owner = "";
- List<String> tokens = Collections.synchronizedList(new ArrayList<>());
+ String token = "";
+ List<String> sharedTokens = new CopyOnWriteArrayList<>();
long expiresAt = 0;
+ String principal = null;
// ----------------------------------------------------- Public Methods
@@ -2416,10 +2443,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
result.append(owner);
result.append("\nExpiration:");
result.append(FastHttpDateFormat.formatDate(expiresAt));
- for (String token : tokens) {
- result.append("\nToken:");
- result.append(token);
- }
+ result.append("\nToken:");
+ result.append(token);
result.append("\n");
return result.toString();
}
@@ -2475,12 +2500,16 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeText("Second-" + timeout);
generatedXML.writeElement("D", "timeout", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", "lockroot", XMLWriter.OPENING);
+ generatedXML.writeElement("D", "href", XMLWriter.OPENING);
+ generatedXML.writeText(lockroot);
+ generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
+ generatedXML.writeElement("D", "lockroot", XMLWriter.CLOSING);
+
generatedXML.writeElement("D", "locktoken", XMLWriter.OPENING);
- for (String token : tokens) {
- generatedXML.writeElement("D", "href", XMLWriter.OPENING);
- generatedXML.writeText("opaquelocktoken:" + token);
- generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
- }
+ generatedXML.writeElement("D", "href", XMLWriter.OPENING);
+ generatedXML.writeText("opaquelocktoken:" + token);
+ generatedXML.writeElement("D", "href", XMLWriter.CLOSING);
generatedXML.writeElement("D", "locktoken", XMLWriter.CLOSING);
generatedXML.writeElement("D", "activelock", XMLWriter.CLOSING);
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 7cf7418c69..2c4b217290 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -198,6 +198,14 @@ public class TestWebdavServlet extends TomcatBaseTest {
" <D:owner>someone</D:owner>\n" +
"</D:lockinfo>";
+ private static final String LOCK_SHARED_BODY =
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<D:lockinfo xmlns:D='DAV:'>\n" +
+ " <D:lockscope><D:shared/></D:lockscope>\n" +
+ " <D:locktype><D:write/></D:locktype>\n" +
+ " <D:owner>someone</D:owner>\n" +
+ "</D:lockinfo>";
+
private static final String PROPFIND_PROP =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?> \n" +
"<D:propfind xmlns:D=\"DAV:\">\n" +
@@ -553,7 +561,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
Assert.assertTrue(client.getResponseBody().contains("/file7.txt"));
// Unlock /myfolder again
- client.setRequest(new String[] { "UNLOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ client.setRequest(new String[] { "UNLOCK /myfolder/ HTTP/1.1" + SimpleHttpClient.CRLF +
"Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
"Lock-Token: " + lockToken + SimpleHttpClient.CRLF +
"Connection: Close" + SimpleHttpClient.CRLF +
@@ -582,6 +590,241 @@ public class TestWebdavServlet extends TomcatBaseTest {
}
+ @Test
+ public void testSharedLocks() throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+
+ // Create a temp webapp that can be safely written to
+ File tempWebapp = new File(getTemporaryDirectory(), "webdav-lock");
+ Assert.assertTrue(tempWebapp.mkdirs());
+ Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
+ Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
+ webdavServlet.addInitParameter("listings", "true");
+ webdavServlet.addInitParameter("secret", "foo");
+ webdavServlet.addInitParameter("readonly", "false");
+ ctxt.addServletMappingDecoded("/*", "webdav");
+ tomcat.start();
+
+ Client client = new Client();
+ client.setPort(getPort());
+
+ // Create a few folders and files
+ client.setRequest(new String[] { "MKCOL /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "MKCOL /myfolder/myfolder2/ HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "MKCOL /myfolder/myfolder3 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "MKCOL /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "MKCOL /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /file1.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder3/file2.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/file3.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ // Lock /myfolder
+ client.setRequest(new String[] { "LOCK /myfolder HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_SHARED_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ String lockToken = null;
+ for (String header : client.getResponseHeaders()) {
+ if (header.startsWith("Lock-Token: ")) {
+ lockToken = header.substring("Lock-Token: ".length());
+ }
+ }
+ Assert.assertNotNull(lockToken);
+
+ client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "If: " + lockToken + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_BODY });
+ client.connect();
+ client.processRequest(true);
+ // This should conflict, submitting a token does not help
+ Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode());
+
+ client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_SHARED_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ String lockToken2 = null;
+ for (String header : client.getResponseHeaders()) {
+ if (header.startsWith("Lock-Token: ")) {
+ lockToken2 = header.substring("Lock-Token: ".length());
+ }
+ }
+ Assert.assertNotNull(lockToken2);
+
+ // Take a second different lock on the same collection
+ client.setRequest(new String[] { "LOCK /myfolder/myfolder2/myfolder4 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: " + LOCK_SHARED_BODY.length() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + LOCK_SHARED_BODY });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+ String lockToken3 = null;
+ for (String header : client.getResponseHeaders()) {
+ if (header.startsWith("Lock-Token: ")) {
+ lockToken3 = header.substring("Lock-Token: ".length());
+ }
+ }
+ Assert.assertNotNull(lockToken3);
+
+ client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
+ Assert.assertTrue(client.getResponseBody().contains("opaquelocktoken:"));
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "If: " + lockToken2 + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+
+ client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5 HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Lock-Token: " + lockToken2 + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode());
+
+ client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Lock-Token: " + lockToken3 + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: 6" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_LOCKED, client.getStatusCode());
+
+ client.setRequest(new String[] { "UNLOCK /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Lock-Token: " + lockToken + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(HttpServletResponse.SC_NO_CONTENT, client.getStatusCode());
+
+ client.setRequest(new String[] { "PUT /myfolder/myfolder2/myfolder4/myfolder5/file4.txt HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Content-Length: 12" + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF + CONTENT + CONTENT});
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_NO_CONTENT, client.getStatusCode());
+
+ client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + SimpleHttpClient.CRLF +
+ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+ "Connection: Close" + SimpleHttpClient.CRLF +
+ SimpleHttpClient.CRLF });
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, client.getStatusCode());
+ // Verify all the shared locks are cleared
+ Assert.assertFalse(client.getResponseBody().contains("opaquelocktoken:"));
+
+ }
+
private static final class Client extends SimpleHttpClient {
@Override
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 3af636641c..b325f810b6 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -107,6 +107,27 @@
<section name="Tomcat 9.0.97 (remm)" rtext="in development">
<subsection name="Catalina">
<changelog>
+<<<<<<< HEAD
+=======
+ <add>
+ Add support for the new Servlet API method
+ <code>HttpServletResponse.sendEarlyHints()</code>. (markt)
+ </add>
+ <update>
+ Remove default value (was <code>catalina</code>) for the
+ <code>secret</code> init parameter of the WebDAV Servlet. (remm)
+ </update>
+ <update>
+ Remove WebDAV lock null support in accordance with RFC 4918 section 7.3
+ and annex D. Instead a lock on a non existing resource will create an
+ empty file locked with a regular lock. (remm)
+ </update>
+ <update>
+ Rewrite implementation of WebDAV shared locks to comply with RFC 4918.
+ (remm)
+ </update>
+ <!-- Entries for backport and removal before 12.0.0-M1 below this line -->
+>>>>>>> ac76965a76 (Rewrite implementation of WebDAV shared locks)
<update>
<bug>69374</bug>: Properly separate between table header and body
in <code>DefaultServlet</code>'s listing. (michaelo)
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 15/19: Code clean-up - formatting. No functional change.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 01c57ee0f7fa994e837cb796f47356d864ab2119
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Oct 22 09:27:13 2024 +0100
Code clean-up - formatting. No functional change.
---
.../servlets/TransientPropertiesWebdavServlet.java | 14 +-
.../apache/catalina/servlets/WebdavServlet.java | 150 +++++++++++----------
2 files changed, 87 insertions(+), 77 deletions(-)
diff --git a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
index 0ab6310ed9..efa02d23cb 100644
--- a/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
+++ b/java/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
@@ -83,7 +83,8 @@ public class TransientPropertiesWebdavServlet extends WebdavServlet {
if (!found) {
properties.add(node);
}
- } if (operation.getUpdateType() == PropertyUpdateType.REMOVE) {
+ }
+ if (operation.getUpdateType() == PropertyUpdateType.REMOVE) {
Node node = operation.getPropertyNode();
for (int i = 0; i < properties.size(); i++) {
Node propertyNode = properties.get(i);
@@ -106,9 +107,8 @@ public class TransientPropertiesWebdavServlet extends WebdavServlet {
if (nameOnly) {
// Add the names of all properties
for (Node node : properties) {
- generatedXML.writeElement(node.getPrefix(),
- node.getNamespaceURI(),
- node.getLocalName(), XMLWriter.NO_CONTENT);
+ generatedXML.writeElement(node.getPrefix(), node.getNamespaceURI(), node.getLocalName(),
+ XMLWriter.NO_CONTENT);
}
} else if (property != null) {
// Add a single property
@@ -176,9 +176,9 @@ public class TransientPropertiesWebdavServlet extends WebdavServlet {
}
private boolean propertyEquals(Node node1, Node node2) {
- if (node1.getLocalName().equals(node2.getLocalName())
- && ((node1.getNamespaceURI() == null && node2.getNamespaceURI() == null)
- || (node1.getNamespaceURI() != null && node1.getNamespaceURI().equals(node2.getNamespaceURI())))) {
+ if (node1.getLocalName().equals(node2.getLocalName()) &&
+ ((node1.getNamespaceURI() == null && node2.getNamespaceURI() == null) ||
+ (node1.getNamespaceURI() != null && node1.getNamespaceURI().equals(node2.getNamespaceURI())))) {
return true;
}
return false;
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index 43168da56d..1423ba764e 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -134,8 +134,8 @@ import org.xml.sax.SAXException;
* http://host:port/context/webdavedit/content
* <p>
* There are some known limitations of this Servlet due to it not implementing PROPPATCH and PROPFIND methods support
- * for dead properties. The Servlet does provide extension points to add support for some as required by user. Details of these
- * limitations and progress towards addressing them are being tracked under
+ * for dead properties. The Servlet does provide extension points to add support for some as required by user. Details
+ * of these limitations and progress towards addressing them are being tracked under
* <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=69046">bug 69046</a>.
* </p>
*
@@ -208,15 +208,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
* Supported locks.
*/
- protected static final String SUPPORTED_LOCKS =
- "<D:lockentry>" +
- "<D:lockscope><D:exclusive/></D:lockscope>" +
- "<D:locktype><D:write/></D:locktype>" +
- "</D:lockentry>" +
- "<D:lockentry>" +
- "<D:lockscope><D:shared/></D:lockscope>" +
- "<D:locktype><D:write/></D:locktype>" +
- "</D:lockentry>";
+ protected static final String SUPPORTED_LOCKS = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" +
+ "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" +
+ "<D:lockscope><D:shared/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>";
/**
* Simple date format for the creation date ISO representation (partial).
@@ -230,13 +224,13 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
* Repository of all locks, keyed by path.
*/
- private final ConcurrentHashMap<String, LockInfo> resourceLocks = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String,LockInfo> resourceLocks = new ConcurrentHashMap<>();
/**
* Map of all shared locks, keyed by lock token.
*/
- private final ConcurrentHashMap<String, LockInfo> sharedLocks = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String,LockInfo> sharedLocks = new ConcurrentHashMap<>();
/**
@@ -484,7 +478,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
for (String token : parentLock.sharedTokens) {
LockInfo sharedLock = sharedLocks.get(token);
if (sharedLock != null) {
- if ((parentPath != currentPath && sharedLock.depth > 0) || parentPath == currentPath) {
+ if ((parentPath != currentPath && sharedLock.depth > 0) ||
+ parentPath == currentPath) {
lockTokens.add("opaquelocktoken:" + token);
}
}
@@ -516,7 +511,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} while (true);
return false;
- }
+ }
return true;
}
@@ -527,8 +522,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* because the WebDAV servlet has been mapped to a url other than /* to configure editing at different url than
* normal viewing.
*
- * @param request The servlet request we are processing
+ * @param request The servlet request we are processing
* @param allowEmptyPath Used only to identify a call from DefaultServlet, to avoid removing the trailing slash
+ *
* @return the relative path
*/
@Override
@@ -763,9 +759,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING);
if (depth == 0) {
- propfindResource(generatedXML, getEncodedPath(path, resource, req),
- path, type, properties, resource.isFile(),
- resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
+ propfindResource(generatedXML, getEncodedPath(path, resource, req), path, type, properties,
+ resource.isFile(), resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
getServletContext().getMimeType(resource.getName()), generateETag(resource));
} else {
// The stack always contains the object of the current level
@@ -788,10 +783,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
// File is in directory listing but doesn't appear to exist
// Broken symlink or odd permission settings?
if (resource.exists()) {
- propfindResource(generatedXML, getEncodedPath(currentPath, resource, req),
- currentPath, type, properties, resource.isFile(),
- resource.getCreation(), resource.getLastModified(), resource.getContentLength(),
- getServletContext().getMimeType(resource.getName()), generateETag(resource));
+ propfindResource(generatedXML, getEncodedPath(currentPath, resource, req), currentPath, type,
+ properties, resource.isFile(), resource.getCreation(), resource.getLastModified(),
+ resource.getContentLength(), getServletContext().getMimeType(resource.getName()),
+ generateETag(resource));
}
if (resource.isDirectory() && (depth > 0)) {
@@ -833,7 +828,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* @param resp The Servlet response
*
* @throws ServletException If an error occurs
- * @throws IOException If an IO error occurs
+ * @throws IOException If an IO error occurs
*/
protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
@@ -902,7 +897,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
}
if (property != null) {
- operations.add(new ProppatchOperation(PropertyUpdateType.SET, property));
+ operations
+ .add(new ProppatchOperation(PropertyUpdateType.SET, property));
} else {
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
@@ -934,7 +930,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
}
if (property != null) {
- operations.add(new ProppatchOperation(PropertyUpdateType.REMOVE, property));
+ operations.add(
+ new ProppatchOperation(PropertyUpdateType.REMOVE, property));
} else {
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
@@ -974,8 +971,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
for (ProppatchOperation operation : operations) {
generatedXML.writeElement("D", "propstat", XMLWriter.OPENING);
generatedXML.writeElement("D", "prop", XMLWriter.OPENING);
- generatedXML.writeElement(operation.propertyNode.getPrefix(),
- operation.propertyNode.getNamespaceURI(),
+ generatedXML.writeElement(operation.propertyNode.getPrefix(), operation.propertyNode.getNamespaceURI(),
operation.propertyNode.getLocalName(), XMLWriter.NO_CONTENT);
generatedXML.writeElement("D", "prop", XMLWriter.CLOSING);
generatedXML.writeElement("D", "status", XMLWriter.OPENING);
@@ -998,12 +994,13 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
- * Apply proppatch to the specified path. This should be overridden by subclasses to provide
- * useful behavior. The default implementation prevents setting protected properties
- * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ * Apply proppatch to the specified path. This should be overridden by subclasses to provide useful behavior. The
+ * default implementation prevents setting protected properties (anything from the DAV: namespace), and sets 507 for
+ * a set attempt on dead properties.
*
- * @param path the resource path on which to apply the proppatch
- * @param operations the set and remove to apply, the final status codes of the result should be set on each operation
+ * @param path the resource path on which to apply the proppatch
+ * @param operations the set and remove to apply, the final status codes of the result should be set on each
+ * operation
*/
protected void proppatchResource(String path, ArrayList<ProppatchOperation> operations) {
boolean setProperty = false;
@@ -1528,8 +1525,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} else {
if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
if (parentLock.isExclusive()) {
- if (ifHeader.contains(parentLock.token)
- && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) {
+ if (ifHeader.contains(parentLock.token) && (parentLock.principal == null ||
+ parentLock.principal.equals(req.getRemoteUser()))) {
toRenew = parentLock;
break;
}
@@ -1537,8 +1534,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
for (String token : parentLock.sharedTokens) {
if (ifHeader.contains(token)) {
LockInfo sharedLock = sharedLocks.get(token);
- if (sharedLock != null
- && (sharedLock.principal == null || sharedLock.principal.equals(req.getRemoteUser()))) {
+ if (sharedLock != null && (sharedLock.principal == null ||
+ sharedLock.principal.equals(req.getRemoteUser()))) {
if ((parentPath != path && sharedLock.depth > 0) || parentPath == path) {
toRenew = sharedLock;
break;
@@ -1628,8 +1625,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} else {
if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
if (parentLock.isExclusive()) {
- if (lockTokenHeader.contains(parentLock.token)
- && (parentLock.principal == null || parentLock.principal.equals(req.getRemoteUser()))) {
+ if (lockTokenHeader.contains(parentLock.token) && (parentLock.principal == null ||
+ parentLock.principal.equals(req.getRemoteUser()))) {
resourceLocks.remove(parentPath);
unlocked = true;
break;
@@ -1642,8 +1639,10 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
for (String token : parentLock.sharedTokens) {
if (lockTokenHeader.contains(token)) {
LockInfo lock = sharedLocks.get(token);
- if (lock == null || lock.principal == null || lock.principal.equals(req.getRemoteUser())) {
- if ((parentPath != path && lock != null && lock.depth > 0) || parentPath == path) {
+ if (lock == null || lock.principal == null ||
+ lock.principal.equals(req.getRemoteUser())) {
+ if ((parentPath != path && lock != null && lock.depth > 0) ||
+ parentPath == path) {
parentLock.sharedTokens.remove(token);
if (parentLock.sharedTokens.isEmpty()) {
resourceLocks.remove(parentPath);
@@ -1709,9 +1708,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
* Check to see if a resource is currently write locked.
*
- * @param path Path of the resource
+ * @param path Path of the resource
* @param principal The authenticated principal name
- * @param ifHeader "If" HTTP header which was included in the request
+ * @param ifHeader "If" HTTP header which was included in the request
*
* @return <code>true</code> if the resource is locked (and no appropriate lock token has been found for at least
* one of the non-shared locks which are present on the resource).
@@ -1729,8 +1728,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
} else {
if ((parentPath != path && parentLock.depth > 0) || parentPath == path) {
if (parentLock.isExclusive()) {
- if (ifHeader.contains(":" + parentLock.token + ">")
- && (parentLock.principal == null || parentLock.principal.equals(principal))) {
+ if (ifHeader.contains(":" + parentLock.token + ">") &&
+ (parentLock.principal == null || parentLock.principal.equals(principal))) {
return false;
}
return true;
@@ -1739,8 +1738,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
LockInfo lock = sharedLocks.get(token);
if (lock != null) {
if ((parentPath != path && lock.depth > 0) || parentPath == path) {
- if (ifHeader.contains(":" + token + ">")
- && (lock.principal == null || lock.principal.equals(principal))) {
+ if (ifHeader.contains(":" + token + ">") &&
+ (lock.principal == null || lock.principal.equals(principal))) {
return false;
}
unmatchedSharedLock = true;
@@ -2025,12 +2024,12 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
/**
- * Copy resource. This should be overridden by subclasses to provide
- * useful behavior. The default implementation prevents setting protected properties
- * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ * Copy resource. This should be overridden by subclasses to provide useful behavior. The default implementation
+ * prevents setting protected properties (anything from the DAV: namespace), and sets 507 for a set attempt on dead
+ * properties.
*
* @param source the copy source path
- * @param dest the copy destination path
+ * @param dest the copy destination path
*/
protected void copyResource(String source, String dest) {
}
@@ -2126,9 +2125,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
/**
- * Delete specified resource. This should be overridden by subclasses to provide
- * useful behavior. The default implementation prevents setting protected properties
- * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ * Delete specified resource. This should be overridden by subclasses to provide useful behavior. The default
+ * implementation prevents setting protected properties (anything from the DAV: namespace), and sets 507 for a set
+ * attempt on dead properties.
*
* @param path the path of the resource to delete
*/
@@ -2274,8 +2273,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
- private void sendReport(HttpServletRequest req, HttpServletResponse resp, String errorPath, int errorCode, String error)
- throws IOException {
+ private void sendReport(HttpServletRequest req, HttpServletResponse resp, String errorPath, int errorCode,
+ String error) throws IOException {
resp.setStatus(errorCode);
XMLWriter generatedXML = new XMLWriter();
@@ -2302,8 +2301,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
private void propfindResource(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType,
- List<Node> properties, boolean isFile, long created, long lastModified,
- long contentLength, String contentType, String eTag) {
+ List<Node> properties, boolean isFile, long created, long lastModified, long contentLength,
+ String contentType, String eTag) {
generatedXML.writeElement("D", "response", XMLWriter.OPENING);
String status = "HTTP/1.1 " + WebdavStatus.SC_OK + " ";
@@ -2472,8 +2471,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
generatedXML.writeElement("D", propertyNotFound, XMLWriter.NO_CONTENT);
} else {
generatedXML.writeElement(propertyNotFoundNode.getPrefix(),
- propertyNotFoundNode.getNamespaceURI(),
- propertyNotFoundNode.getLocalName(), XMLWriter.NO_CONTENT);
+ propertyNotFoundNode.getNamespaceURI(), propertyNotFoundNode.getLocalName(),
+ XMLWriter.NO_CONTENT);
}
}
@@ -2492,15 +2491,17 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
/**
- * Generate propfind XML fragments for dead properties. This should be overridden by subclasses to provide
- * useful behavior. The default implementation prevents setting protected properties
- * (anything from the DAV: namespace), and sets 507 for a set attempt on dead properties.
+ * Generate propfind XML fragments for dead properties. This should be overridden by subclasses to provide useful
+ * behavior. The default implementation prevents setting protected properties (anything from the DAV: namespace),
+ * and sets 507 for a set attempt on dead properties.
*
- * @param path the resource path
- * @param property the dead property, if null then all dead properties must be written
- * @param nameOnly true if only the property name element should be generated
+ * @param path the resource path
+ * @param property the dead property, if null then all dead properties must be written
+ * @param nameOnly true if only the property name element should be generated
* @param generatedXML the current generated XML for the PROPFIND response
- * @return true if property was specified and a corresponding dead property was found on the resource, false otherwise
+ *
+ * @return true if property was specified and a corresponding dead property was found on the resource, false
+ * otherwise
*/
protected boolean propfindResource(String path, Node property, boolean nameOnly, XMLWriter generatedXML) {
if (nameOnly) {
@@ -2771,44 +2772,53 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
}
}
- enum PropertyUpdateType { SET, REMOVE }
+ enum PropertyUpdateType {
+ SET,
+ REMOVE
+ }
protected static class ProppatchOperation {
private final PropertyUpdateType updateType;
private final Node propertyNode;
private final boolean protectedProperty;
private int statusCode = HttpServletResponse.SC_OK;
+
public ProppatchOperation(PropertyUpdateType updateType, Node propertyNode) {
this.updateType = updateType;
this.propertyNode = propertyNode;
String davName = getDAVNode(propertyNode);
- protectedProperty = davName != null
- && (!(davName.equals("displayname") || davName.equals("getcontentlanguage")));
+ protectedProperty =
+ davName != null && (!(davName.equals("displayname") || davName.equals("getcontentlanguage")));
}
+
/**
* @return the updateType
*/
public PropertyUpdateType getUpdateType() {
return this.updateType;
}
+
/**
* @return the propertyNode
*/
public Node getPropertyNode() {
return this.propertyNode;
}
+
/**
* @return the statusCode
*/
public int getStatusCode() {
return this.statusCode;
}
+
/**
* @param statusCode the statusCode to set
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
+
/**
* @return the protectedProperty
*/
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 18/19: Fix changelog
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 77c10f2ce0a46dbce1a3b6ebf5e0a010be54eb95
Author: remm <re...@apache.org>
AuthorDate: Tue Oct 22 12:10:38 2024 +0200
Fix changelog
---
webapps/docs/changelog.xml | 34 +++++++++++-----------------------
1 file changed, 11 insertions(+), 23 deletions(-)
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index edcaff8886..f68aa7679e 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -107,31 +107,10 @@
<section name="Tomcat 9.0.97 (remm)" rtext="in development">
<subsection name="Catalina">
<changelog>
-<<<<<<< HEAD
-=======
<add>
Add support for the new Servlet API method
<code>HttpServletResponse.sendEarlyHints()</code>. (markt)
</add>
- <update>
- Remove default value (was <code>catalina</code>) for the
- <code>secret</code> init parameter of the WebDAV Servlet. (remm)
- </update>
- <update>
- Remove WebDAV lock null support in accordance with RFC 4918 section 7.3
- and annex D. Instead a lock on a non existing resource will create an
- empty file locked with a regular lock. (remm)
- </update>
- <update>
- Rewrite implementation of WebDAV shared locks to comply with RFC 4918.
- (remm)
- </update>
- <update>
- Implement WebDAV <code>If</code> header using code from the Apache
- Jackrabbit project. (remm)
- </update>
- <!-- Entries for backport and removal before 12.0.0-M1 below this line -->
->>>>>>> ac76965a76 (Rewrite implementation of WebDAV shared locks)
<update>
<bug>69374</bug>: Properly separate between table header and body
in <code>DefaultServlet</code>'s listing. (michaelo)
@@ -190,10 +169,19 @@
and annex D. Instead a lock on a non existing resource will create an
empty file locked with a regular lock. (remm)
</update>
+ <update>
+ Rewrite implementation of WebDAV shared locks to comply with RFC 4918.
+ (remm)
+ </update>
+ <update>
+ Implement WebDAV <code>If</code> header using code from the Apache
+ Jackrabbit project. (remm)
+ </update>
<update>
Add base WebDAV <code>PROPPATCH</code> implementation, to be extended
- by overriding the <code>proppatchResource</code> and
- <code>propfindResource</code> methods of the WebDAV Servlet to
+ by overriding the <code>proppatchResource</code>,
+ <code>propfindResource</code>, <code>copyResource</code> and
+ <code>deleteResource</code> methods of the WebDAV Servlet to
provide the desired level of support for dead properties. (remm)
</update>
</changelog>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 08/19: Fix directory listings
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 39cb18f1ec4831ee60e620b4eae0882d434cbd0d
Author: remm <re...@apache.org>
AuthorDate: Mon Oct 21 13:25:55 2024 +0200
Fix directory listings
---
java/org/apache/catalina/servlets/WebdavServlet.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index 33b26bb676..f6e77cf844 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -528,6 +528,8 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
* normal viewing.
*
* @param request The servlet request we are processing
+ * @param allowEmptyPath Used only to identify a call from DefaultServlet, to avoid removing the trailing slash
+ * @return the relative path
*/
@Override
protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
@@ -548,7 +550,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
result.append('/');
}
String resultString = result.toString();
- if (resultString.length() > 1 && resultString.endsWith("/")) {
+ if (!allowEmptyPath && resultString.length() > 1 && resultString.endsWith("/")) {
resultString = resultString.substring(0, resultString.length() - 1);
}
return resultString;
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org