This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new fe3f74debf Add extension points to support dead properties fe3f74debf is described below commit fe3f74debf0ae971918771a4399c8ba4eb3b6c54 Author: remm <r...@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 9fec057e5a..193fb8d4e0 100644 --- a/java/org/apache/catalina/servlets/WebdavServlet.java +++ b/java/org/apache/catalina/servlets/WebdavServlet.java @@ -134,7 +134,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> @@ -446,6 +447,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"); @@ -475,8 +490,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 @@ -526,12 +547,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; } } @@ -573,7 +589,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<>(); @@ -585,9 +604,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)) { @@ -627,16 +658,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); @@ -648,8 +687,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); + } + } } @@ -666,6 +874,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; @@ -1843,50 +2057,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); @@ -1928,8 +2100,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>" + @@ -1964,8 +2135,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); @@ -1977,15 +2148,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); @@ -1995,32 +2171,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) { @@ -2030,8 +2206,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>" + @@ -2042,10 +2216,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); } } @@ -2062,8 +2236,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); @@ -2080,6 +2261,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. * @@ -2165,7 +2360,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(); } @@ -2306,6 +2501,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; + } + } + } @@ -2497,4 +2737,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 df5cb2832b..45ed9ce9db 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; @@ -34,6 +35,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; @@ -202,6 +204,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>"; @@ -212,6 +216,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(); @@ -220,7 +239,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"); @@ -281,6 +300,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")); } @@ -559,4 +590,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 7d4e18af96..6e276efb75 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -180,6 +180,12 @@ WebDAV Delete should remove any existing lock on successfully deleted resources. (remm) </fix> + <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-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org