You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/08/08 11:46:47 UTC

(maven-wagon) branch wagon-3.x updated (45293e11 -> bb048652)

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a change to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git


    from 45293e11 Port the site documentation from APT to Markdown
     new 726ddf4d Remove the vendored copies of two Jackrabbit classes
     new 70a156e2 Replace the jackrabbit-webdav dependency with the requests actually used
     new f010b718 Address review feedback on the WebDAV request handling
     new 07a6a197 Correct two claims the new WebDAV code made about its own behaviour
     new 1d1b4882 Refuse to parse rather than fall back to an unsecured XML parser
     new bb048652 Reject malformed multistatus responses instead of reading past them

The 6 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:
 pom.xml                                            |   2 +-
 wagon-providers/wagon-webdav-jackrabbit/pom.xml    |  12 -
 .../org/apache/jackrabbit/webdav/MultiStatus.java  | 189 ---------------
 .../webdav/client/methods/XmlRequestEntity.java    |  90 --------
 .../maven/wagon/providers/webdav/DavMethods.java   |  99 ++++++++
 .../maven/wagon/providers/webdav/MultiStatus.java  | 256 +++++++++++++++++++++
 .../maven/wagon/providers/webdav/WebDavWagon.java  |  83 ++++---
 .../wagon/providers/webdav/MultiStatusTest.java    | 246 ++++++++++++++++++++
 8 files changed, 641 insertions(+), 336 deletions(-)
 delete mode 100644 wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java
 delete mode 100644 wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlRequestEntity.java
 create mode 100644 wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java
 create mode 100644 wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
 create mode 100644 wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java


(maven-wagon) 02/06: Replace the jackrabbit-webdav dependency with the requests actually used

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit 70a156e25fd15a6d9cfd1f008a2f2a4cdc4e2991
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Aug 7 22:25:07 2026 +0200

    Replace the jackrabbit-webdav dependency with the requests actually used
    
    This Wagon speaks a very small subset of WebDAV: MKCOL to create a
    collection, PROPFIND Depth:0 on resourcetype to tell a collection from a
    plain resource, and PROPFIND Depth:1 to list a collection. The Depth:1
    request nominally asks for displayname but never reads it, deriving
    every entry from the response href instead.
    
    jackrabbit-webdav models all of WebDAV -- locking, observation,
    ordering, versioning, search, transactions -- so those three requests
    were pulling in a dependency whose scope far exceeds what is needed. The
    requests and the multistatus parsing now live in this module, in Wagon's
    own package rather than squatting Jackrabbit's.
    
    The parser keeps the behaviour the callers rely on: responses stay in
    document order, because getFileList expects the requested collection
    first per RFC 4918 section 9.1, and hrefs are deduplicated the way
    Jackrabbit's href-keyed map did. Only propstat elements reporting 200
    are consulted for resourcetype. Since a multistatus body is remote
    input, the parser also refuses DOCTYPE declarations.
    
    Behaviour changes in two spots, both in the safe direction: a PROPFIND
    answering something other than 207 Multi-Status is no longer parsed for
    a body, and an empty multistatus no longer raises
    ArrayIndexOutOfBoundsException from isDirectory.
    
    The artifactId keeps the jackrabbit name so that consumers do not break.
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 wagon-providers/wagon-webdav-jackrabbit/pom.xml    |  12 --
 .../maven/wagon/providers/webdav/DavMethods.java   |  99 ++++++++++
 .../maven/wagon/providers/webdav/MultiStatus.java  | 207 +++++++++++++++++++++
 .../maven/wagon/providers/webdav/WebDavWagon.java  |  74 ++++----
 .../wagon/providers/webdav/MultiStatusTest.java    | 182 ++++++++++++++++++
 5 files changed, 520 insertions(+), 54 deletions(-)

diff --git a/wagon-providers/wagon-webdav-jackrabbit/pom.xml b/wagon-providers/wagon-webdav-jackrabbit/pom.xml
index 1cafcf92..a77c8058 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/pom.xml
+++ b/wagon-providers/wagon-webdav-jackrabbit/pom.xml
@@ -48,18 +48,6 @@ under the License.
       <artifactId>wagon-http-shared</artifactId>
       <version>${project.version}</version>
     </dependency>
-    <dependency>
-      <groupId>org.apache.jackrabbit</groupId>
-      <artifactId>jackrabbit-webdav</artifactId>
-      <!-- 2.20.x is the latest version that is compatible with Java 8 -->
-      <version>2.20.16</version>
-      <exclusions>
-        <exclusion>
-          <groupId>commons-logging</groupId>
-          <artifactId>commons-logging</artifactId>
-        </exclusion>
-      </exclusions>
-    </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-utils</artifactId>
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java
new file mode 100644
index 00000000..c9803e50
--- /dev/null
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java
@@ -0,0 +1,99 @@
+/*
+ * 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.maven.wagon.providers.webdav;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+
+/**
+ * The handful of WebDAV requests this Wagon issues.
+ * <p>
+ * Only MKCOL and a deliberately narrow flavour of PROPFIND are needed: creating collections when
+ * deploying, and discovering whether a resource is a collection and what it contains. See
+ * <a href="http://www.webdav.org/specs/rfc4918.html">RFC 4918</a>.
+ *
+ * @since 4.0.0
+ */
+final class DavMethods {
+
+    /** The WebDAV XML namespace. */
+    static final String DAV_NAMESPACE = "DAV:";
+
+    /** Name of the {@code resourcetype} property, used to tell collections from plain resources. */
+    static final String PROPERTY_RESOURCETYPE = "resourcetype";
+
+    /** Name of the {@code displayname} property. */
+    static final String PROPERTY_DISPLAYNAME = "displayname";
+
+    /** Child of {@code resourcetype} marking a resource as a collection. */
+    static final String XML_COLLECTION = "collection";
+
+    /** Apply the request to the resource itself only. */
+    static final int DEPTH_0 = 0;
+
+    /** Apply the request to the resource and its immediate children. */
+    static final int DEPTH_1 = 1;
+
+    private DavMethods() {
+        // utility class
+    }
+
+    /**
+     * MKCOL request, creating a single collection. The parent collection must already exist, so
+     * callers deploying a nested path have to walk it from the top down.
+     */
+    static final class HttpMkcol extends HttpRequestBase {
+        HttpMkcol(String uri) {
+            setURI(java.net.URI.create(uri));
+        }
+
+        @Override
+        public String getMethod() {
+            return "MKCOL";
+        }
+    }
+
+    /**
+     * PROPFIND request asking for a single named property in the {@code DAV:} namespace.
+     * <p>
+     * The response is a {@code 207 Multi-Status} document; hand it to
+     * {@link MultiStatus#parse} to read it.
+     */
+    static final class HttpPropfind extends HttpEntityEnclosingRequestBase {
+        HttpPropfind(String uri, String propertyName, int depth) {
+            setURI(java.net.URI.create(uri));
+            setHeader("Depth", String.valueOf(depth));
+            setEntity(new StringEntity(
+                    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                            + "<D:propfind xmlns:D=\"" + DAV_NAMESPACE + "\">"
+                            + "<D:prop><D:" + propertyName + "/></D:prop>"
+                            + "</D:propfind>",
+                    ContentType.create("text/xml", StandardCharsets.UTF_8)));
+        }
+
+        @Override
+        public String getMethod() {
+            return "PROPFIND";
+        }
+    }
+}
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
new file mode 100644
index 00000000..4a0006b8
--- /dev/null
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
@@ -0,0 +1,207 @@
+/*
+ * 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.maven.wagon.providers.webdav;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import static org.apache.maven.wagon.providers.webdav.DavMethods.DAV_NAMESPACE;
+import static org.apache.maven.wagon.providers.webdav.DavMethods.PROPERTY_RESOURCETYPE;
+import static org.apache.maven.wagon.providers.webdav.DavMethods.XML_COLLECTION;
+
+/**
+ * The {@code 207 Multi-Status} body of a PROPFIND response, reduced to what this Wagon needs: the
+ * href of each response, in document order, and whether that response describes a collection.
+ * <p>
+ * Responses keep their document order because {@code getFileList} relies on the first entry being
+ * the requested collection itself, as mandated by
+ * <a href="http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1">RFC 4918 section 9.1</a>.
+ *
+ * @since 4.0.0
+ */
+final class MultiStatus {
+
+    private final List<Response> responses;
+
+    private MultiStatus(List<Response> responses) {
+        this.responses = responses;
+    }
+
+    /**
+     * A single {@code DAV:response} element.
+     */
+    static final class Response {
+        private final String href;
+        private final boolean collection;
+
+        Response(String href, boolean collection) {
+            this.href = href;
+            this.collection = collection;
+        }
+
+        String getHref() {
+            return href;
+        }
+
+        /**
+         * Whether the {@code resourcetype} property carried a {@code collection} child. Only
+         * {@code propstat} elements with a {@code 200} status are considered; a resource whose
+         * {@code resourcetype} is absent or empty is not a collection.
+         */
+        boolean isCollection() {
+            return collection;
+        }
+    }
+
+    List<Response> getResponses() {
+        return responses;
+    }
+
+    /**
+     * Parses a multistatus document.
+     *
+     * @param in the response body, never {@code null}
+     * @return the parsed responses, possibly empty but never {@code null}
+     * @throws IOException if the body is not a well-formed multistatus document
+     */
+    static MultiStatus parse(InputStream in) throws IOException {
+        Document document;
+        try {
+            document = newDocumentBuilder().parse(in);
+        } catch (ParserConfigurationException | SAXException e) {
+            throw new IOException("Cannot parse multistatus response: " + e.getMessage(), e);
+        }
+
+        Element root = document.getDocumentElement();
+        if (root == null || !isDavElement(root, "multistatus")) {
+            throw new IOException("Expected a DAV:multistatus response body");
+        }
+
+        // an href must occur only once per RFC 4918, so later duplicates are dropped rather than
+        // reported twice; a LinkedHashMap keeps the document order the callers depend on
+        Map<String, Response> responses = new LinkedHashMap<>();
+        for (Element response : childElements(root, "response")) {
+            String href = null;
+            for (Element hrefElement : childElements(response, "href")) {
+                href = hrefElement.getTextContent();
+                break;
+            }
+            if (href != null) {
+                href = href.trim();
+                responses.put(href, new Response(href, isCollection(response)));
+            }
+        }
+        return new MultiStatus(Collections.unmodifiableList(new ArrayList<>(responses.values())));
+    }
+
+    /**
+     * Looks for {@code resourcetype/collection} inside any {@code propstat} that reported a
+     * {@code 200} status.
+     */
+    private static boolean isCollection(Element response) {
+        for (Element propstat : childElements(response, "propstat")) {
+            if (!isOkStatus(propstat)) {
+                continue;
+            }
+            for (Element prop : childElements(propstat, "prop")) {
+                for (Element resourceType : childElements(prop, PROPERTY_RESOURCETYPE)) {
+                    if (!childElements(resourceType, XML_COLLECTION).isEmpty()) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Reads the {@code status} child, whose text is a status line such as {@code HTTP/1.1 200 OK}.
+     * A {@code propstat} without a status element is treated as successful, matching how lenient
+     * servers are read elsewhere.
+     */
+    private static boolean isOkStatus(Element propstat) {
+        List<Element> statusElements = childElements(propstat, "status");
+        if (statusElements.isEmpty()) {
+            return true;
+        }
+        String[] tokens = statusElements.get(0).getTextContent().trim().split("\\s+");
+        for (String token : tokens) {
+            if (token.length() == 3 && token.chars().allMatch(Character::isDigit)) {
+                return "200".equals(token);
+            }
+        }
+        return false;
+    }
+
+    private static List<Element> childElements(Element parent, String localName) {
+        List<Element> elements = new ArrayList<>();
+        NodeList children = parent.getChildNodes();
+        for (int i = 0; i < children.getLength(); i++) {
+            Node child = children.item(i);
+            if (child.getNodeType() == Node.ELEMENT_NODE && isDavElement((Element) child, localName)) {
+                elements.add((Element) child);
+            }
+        }
+        return elements;
+    }
+
+    /**
+     * Matches on local name within the {@code DAV:} namespace. Servers that emit the WebDAV
+     * elements without a namespace are tolerated, since some do.
+     */
+    private static boolean isDavElement(Element element, String localName) {
+        String elementLocalName = element.getLocalName() != null ? element.getLocalName() : element.getNodeName();
+        int colon = elementLocalName.indexOf(':');
+        if (colon >= 0) {
+            elementLocalName = elementLocalName.substring(colon + 1);
+        }
+        if (!localName.equals(elementLocalName)) {
+            return false;
+        }
+        String namespace = element.getNamespaceURI();
+        return namespace == null || DAV_NAMESPACE.equals(namespace);
+    }
+
+    private static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        // multistatus bodies come from a remote server, so no external entity resolution
+        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+        factory.setXIncludeAware(false);
+        factory.setExpandEntityReferences(false);
+        factory.setNamespaceAware(true);
+        return factory.newDocumentBuilder();
+    }
+}
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
index 97a57912..5762fa03 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
@@ -28,16 +28,6 @@
 import org.apache.http.HttpStatus;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.util.EntityUtils;
-import org.apache.jackrabbit.webdav.DavConstants;
-import org.apache.jackrabbit.webdav.DavException;
-import org.apache.jackrabbit.webdav.MultiStatus;
-import org.apache.jackrabbit.webdav.MultiStatusResponse;
-import org.apache.jackrabbit.webdav.client.methods.HttpMkcol;
-import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
-import org.apache.jackrabbit.webdav.property.DavProperty;
-import org.apache.jackrabbit.webdav.property.DavPropertyName;
-import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
-import org.apache.jackrabbit.webdav.property.DavPropertySet;
 import org.apache.maven.wagon.PathUtils;
 import org.apache.maven.wagon.ResourceDoesNotExistException;
 import org.apache.maven.wagon.TransferFailedException;
@@ -46,8 +36,11 @@
 import org.apache.maven.wagon.repository.Repository;
 import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
 import org.codehaus.plexus.util.FileUtils;
-import org.w3c.dom.Node;
 
+import static org.apache.maven.wagon.providers.webdav.DavMethods.DEPTH_0;
+import static org.apache.maven.wagon.providers.webdav.DavMethods.DEPTH_1;
+import static org.apache.maven.wagon.providers.webdav.DavMethods.PROPERTY_DISPLAYNAME;
+import static org.apache.maven.wagon.providers.webdav.DavMethods.PROPERTY_RESOURCETYPE;
 import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
 
 /**
@@ -66,6 +59,9 @@
 public class WebDavWagon extends AbstractHttpClientWagon {
     protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure";
 
+    /** {@code 207 Multi-Status}, not defined by {@link HttpStatus}. */
+    private static final int SC_MULTI_STATUS = 207;
+
     private final boolean continueOnFailure = Boolean.getBoolean(CONTINUE_ON_FAILURE_PROPERTY);
 
     /**
@@ -138,15 +134,13 @@ protected void mkdirs(String dir) throws IOException {
     }
 
     private int doMkCol(String url) throws IOException {
-        HttpMkcol method = new HttpMkcol(url);
+        DavMethods.HttpMkcol method = new DavMethods.HttpMkcol(url);
         try (CloseableHttpResponse closeableHttpResponse = execute(method)) {
             return closeableHttpResponse.getStatusLine().getStatusCode();
         } catch (HttpException e) {
             throw new IOException(e.getMessage(), e);
         } finally {
-            if (method != null) {
-                method.releaseConnection();
-            }
+            method.releaseConnection();
         }
     }
 
@@ -172,25 +166,18 @@ public void putDirectory(File sourceDirectory, String destinationDirectory)
         }
     }
 
-    private boolean isDirectory(String url) throws IOException, DavException {
-        DavPropertyNameSet nameSet = new DavPropertyNameSet();
-        nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_RESOURCETYPE));
-
+    private boolean isDirectory(String url) throws IOException {
         CloseableHttpResponse closeableHttpResponse = null;
-        HttpPropfind method = null;
+        DavMethods.HttpPropfind method = null;
         try {
-            method = new HttpPropfind(url, nameSet, DavConstants.DEPTH_0);
+            method = new DavMethods.HttpPropfind(url, PROPERTY_RESOURCETYPE, DEPTH_0);
             closeableHttpResponse = execute(method);
 
-            if (method.succeeded(closeableHttpResponse)) {
-                MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(closeableHttpResponse);
-                MultiStatusResponse response = multiStatus.getResponses()[0];
-                DavPropertySet propertySet = response.getProperties(HttpStatus.SC_OK);
-                DavProperty<?> property = propertySet.get(DavConstants.PROPERTY_RESOURCETYPE);
-                if (property != null) {
-                    Node node = (Node) property.getValue();
-                    return node.getLocalName().equals(DavConstants.XML_COLLECTION);
-                }
+            if (isMultiStatus(closeableHttpResponse)) {
+                List<MultiStatus.Response> responses = MultiStatus.parse(
+                                closeableHttpResponse.getEntity().getContent())
+                        .getResponses();
+                return !responses.isEmpty() && responses.get(0).isCollection();
             }
             return false;
         } catch (HttpException e) {
@@ -211,21 +198,19 @@ public List<String> getFileList(String destinationDirectory)
         String repositoryUrl = repository.getUrl();
         String url = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + destinationDirectory;
 
-        HttpPropfind method = null;
+        DavMethods.HttpPropfind method = null;
         CloseableHttpResponse closeableHttpResponse = null;
         try {
             if (isDirectory(url)) {
-                DavPropertyNameSet nameSet = new DavPropertyNameSet();
-                nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_DISPLAYNAME));
-
-                method = new HttpPropfind(url, nameSet, DavConstants.DEPTH_1);
+                method = new DavMethods.HttpPropfind(url, PROPERTY_DISPLAYNAME, DEPTH_1);
                 closeableHttpResponse = execute(method);
-                if (method.succeeded(closeableHttpResponse)) {
+                if (isMultiStatus(closeableHttpResponse)) {
                     ArrayList<String> dirs = new ArrayList<>();
-                    MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(closeableHttpResponse);
-                    for (int i = 0; i < multiStatus.getResponses().length; i++) {
-                        MultiStatusResponse response = multiStatus.getResponses()[i];
-                        String entryUrl = response.getHref();
+                    List<MultiStatus.Response> responses = MultiStatus.parse(
+                                    closeableHttpResponse.getEntity().getContent())
+                            .getResponses();
+                    for (int i = 0; i < responses.size(); i++) {
+                        String entryUrl = responses.get(i).getHref();
                         String fileName = PathUtils.filename(URLDecoder.decode(entryUrl));
                         if (entryUrl.endsWith("/")) {
                             if (i == 0) {
@@ -256,8 +241,6 @@ public List<String> getFileList(String destinationDirectory)
             }
         } catch (HttpException e) {
             throw new TransferFailedException(e.getMessage(), e);
-        } catch (DavException e) {
-            throw new TransferFailedException(e.getMessage(), e);
         } catch (IOException e) {
             throw new TransferFailedException(e.getMessage(), e);
         } finally {
@@ -278,6 +261,13 @@ public List<String> getFileList(String destinationDirectory)
                 "Destination path exists but is not a " + "WebDAV collection (directory): " + url);
     }
 
+    /**
+     * A PROPFIND only carries a parseable body when the server answered {@code 207 Multi-Status}.
+     */
+    private static boolean isMultiStatus(CloseableHttpResponse response) {
+        return response.getStatusLine().getStatusCode() == SC_MULTI_STATUS && response.getEntity() != null;
+    }
+
     public String getURL(Repository repository) {
         String url = repository.getUrl();
 
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
new file mode 100644
index 00000000..26d8e6bb
--- /dev/null
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.maven.wagon.providers.webdav;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for parsing {@code 207 Multi-Status} PROPFIND responses.
+ */
+public class MultiStatusTest {
+
+    private static List<MultiStatus.Response> parse(String xml) throws IOException {
+        return MultiStatus.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))
+                .getResponses();
+    }
+
+    @Test
+    public void testCollectionIsRecognised() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<D:multistatus xmlns:D=\"DAV:\">"
+                + "  <D:response>"
+                + "    <D:href>/repo/dir/</D:href>"
+                + "    <D:propstat>"
+                + "      <D:prop><D:resourcetype><D:collection/></D:resourcetype></D:prop>"
+                + "      <D:status>HTTP/1.1 200 OK</D:status>"
+                + "    </D:propstat>"
+                + "  </D:response>"
+                + "</D:multistatus>");
+
+        assertEquals(1, responses.size());
+        assertEquals("/repo/dir/", responses.get(0).getHref());
+        assertTrue(responses.get(0).isCollection());
+    }
+
+    @Test
+    public void testPlainResourceIsNotACollection() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<D:multistatus xmlns:D=\"DAV:\">"
+                + "  <D:response>"
+                + "    <D:href>/repo/artifact.jar</D:href>"
+                + "    <D:propstat>"
+                + "      <D:prop><D:resourcetype/></D:prop>"
+                + "      <D:status>HTTP/1.1 200 OK</D:status>"
+                + "    </D:propstat>"
+                + "  </D:response>"
+                + "</D:multistatus>");
+
+        assertEquals(1, responses.size());
+        assertFalse(responses.get(0).isCollection());
+    }
+
+    /**
+     * A resourcetype reported under a non-200 propstat says nothing about the resource.
+     */
+    @Test
+    public void testResourceTypeUnderNonOkStatusIsIgnored() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<D:multistatus xmlns:D=\"DAV:\">"
+                + "  <D:response>"
+                + "    <D:href>/repo/thing</D:href>"
+                + "    <D:propstat>"
+                + "      <D:prop><D:resourcetype><D:collection/></D:resourcetype></D:prop>"
+                + "      <D:status>HTTP/1.1 404 Not Found</D:status>"
+                + "    </D:propstat>"
+                + "  </D:response>"
+                + "</D:multistatus>");
+
+        assertFalse(responses.get(0).isCollection());
+    }
+
+    /**
+     * {@code getFileList} relies on the requested collection arriving first, per RFC 4918 9.1.
+     */
+    @Test
+    public void testDocumentOrderIsPreserved() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<D:multistatus xmlns:D=\"DAV:\">"
+                + "  <D:response><D:href>/repo/dir/</D:href></D:response>"
+                + "  <D:response><D:href>/repo/dir/a.jar</D:href></D:response>"
+                + "  <D:response><D:href>/repo/dir/b.jar</D:href></D:response>"
+                + "</D:multistatus>");
+
+        assertEquals(3, responses.size());
+        assertEquals("/repo/dir/", responses.get(0).getHref());
+        assertEquals("/repo/dir/a.jar", responses.get(1).getHref());
+        assertEquals("/repo/dir/b.jar", responses.get(2).getHref());
+    }
+
+    @Test
+    public void testDuplicateHrefsCollapse() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<D:multistatus xmlns:D=\"DAV:\">"
+                + "  <D:response><D:href>/repo/dir/a.jar</D:href></D:response>"
+                + "  <D:response><D:href>/repo/dir/a.jar</D:href></D:response>"
+                + "</D:multistatus>");
+
+        assertEquals(1, responses.size());
+    }
+
+    /**
+     * Some servers emit the WebDAV elements with a default namespace or a different prefix.
+     */
+    @Test
+    public void testAlternatePrefixIsAccepted() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<multistatus xmlns=\"DAV:\">"
+                + "  <response>"
+                + "    <href>/repo/dir/</href>"
+                + "    <propstat>"
+                + "      <prop><resourcetype><collection/></resourcetype></prop>"
+                + "      <status>HTTP/1.1 200 OK</status>"
+                + "    </propstat>"
+                + "  </response>"
+                + "</multistatus>");
+
+        assertEquals(1, responses.size());
+        assertTrue(responses.get(0).isCollection());
+    }
+
+    @Test
+    public void testNonMultiStatusBodyIsRejected() {
+        try {
+            parse("<?xml version=\"1.0\"?><html><body>nope</body></html>");
+            fail("expected an IOException");
+        } catch (IOException expected) {
+            assertTrue(expected.getMessage().contains("multistatus"));
+        }
+    }
+
+    @Test
+    public void testMalformedBodyIsRejected() {
+        try {
+            parse("<?xml version=\"1.0\"?><D:multistatus xmlns:D=\"DAV:\">");
+            fail("expected an IOException");
+        } catch (IOException expected) {
+            // expected
+        }
+    }
+
+    /**
+     * The body is remote input, so a DOCTYPE must not be honoured.
+     */
+    @Test
+    public void testDoctypeIsRejected() {
+        try {
+            parse("<?xml version=\"1.0\"?>"
+                    + "<!DOCTYPE multistatus [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>"
+                    + "<D:multistatus xmlns:D=\"DAV:\">"
+                    + "  <D:response><D:href>&xxe;</D:href></D:response>"
+                    + "</D:multistatus>");
+            fail("expected an IOException");
+        } catch (IOException expected) {
+            // expected
+        }
+    }
+}


(maven-wagon) 01/06: Remove the vendored copies of two Jackrabbit classes

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit 726ddf4db52abd5ebb40b03574853aa858f08017
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Aug 7 22:22:44 2026 +0200

    Remove the vendored copies of two Jackrabbit classes
    
    MultiStatus was forked here so that responses would keep their document
    order, which getFileList depends on. Jackrabbit's own MultiStatus has
    since been changed to a LinkedHashMap, so the fork no longer differs
    from the class it shadows.
    
    Shadowing is the problem: the released jar carried
    org/apache/jackrabbit/webdav/MultiStatus.class, which also exists in
    jackrabbit-webdav, so which of the two won depended on classpath order.
    
    XmlRequestEntity is left over from Jackrabbit 2.14, which no longer
    ships or references it, and nothing in this module ever used it.
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 .../org/apache/jackrabbit/webdav/MultiStatus.java  | 189 ---------------------
 .../webdav/client/methods/XmlRequestEntity.java    |  90 ----------
 2 files changed, 279 deletions(-)

diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java
deleted file mode 100644
index 9013837a..00000000
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * 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.jackrabbit.webdav;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
-import org.apache.jackrabbit.webdav.xml.DomUtil;
-import org.apache.jackrabbit.webdav.xml.ElementIterator;
-import org.apache.jackrabbit.webdav.xml.XmlSerializable;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-/**
- * MultiStatus representing the content of a multistatus response body and
- * allows to retrieve the Xml representation.
- */
-public class MultiStatus implements DavConstants, XmlSerializable {
-
-    /**
-     * Map collecting the responses for this multistatus, where every href must
-     * only occur one single time.
-     */
-    private Map<String, MultiStatusResponse> responses = new LinkedHashMap<>();
-
-    /**
-     * A general response description at the multistatus top level is used to
-     * provide a general message describing the overarching nature of the response.
-     * If this value is available an application may use it instead of
-     * presenting the individual response descriptions contained within the
-     * responses.
-     */
-    private String responseDescription;
-
-    /**
-     * Add response(s) to this multistatus, in order to build a multistatus for
-     * responding to a PROPFIND request.
-     *
-     * @param resource The resource to add property from
-     * @param propNameSet The requested property names of the PROPFIND request
-     * @param propFindType
-     * @param depth
-     */
-    public void addResourceProperties(
-            DavResource resource, DavPropertyNameSet propNameSet, int propFindType, int depth) {
-        addResponse(new MultiStatusResponse(resource, propNameSet, propFindType));
-        if (depth > 0 && resource.isCollection()) {
-            DavResourceIterator iter = resource.getMembers();
-            while (iter.hasNext()) {
-                addResourceProperties(iter.nextResource(), propNameSet, propFindType, depth - 1);
-            }
-        }
-    }
-
-    /**
-     * Add response(s) to this multistatus, in order to build a multistatus e.g.
-     * in order to respond to a PROPFIND request. Please note, that in terms
-     * of PROPFIND, this method would correspond to a
-     * {@link DavConstants#PROPFIND_BY_PROPERTY} propfind type.
-     *
-     * @param resource The resource to add property from
-     * @param propNameSet The requested property names of the PROPFIND request
-     * @param depth
-     * @see #addResourceProperties(DavResource, DavPropertyNameSet, int, int) for
-     * the corresponding method that allows to specify the type explicitly.
-     */
-    public void addResourceProperties(DavResource resource, DavPropertyNameSet propNameSet, int depth) {
-        addResourceProperties(resource, propNameSet, PROPFIND_BY_PROPERTY, depth);
-    }
-
-    /**
-     * Add response(s) to this multistatus, in order to build a multistatus
-     * as returned for COPY, MOVE, LOCK or DELETE requests resulting in an error
-     * with a resource other than the resource identified in the Request-URI.
-     *
-     * @param resource
-     * @param status
-     * @param depth
-     */
-    public void addResourceStatus(DavResource resource, int status, int depth) {
-        addResponse(new MultiStatusResponse(resource.getHref(), status));
-        if (depth > 0 && resource.isCollection()) {
-            DavResourceIterator iter = resource.getMembers();
-            while (iter.hasNext()) {
-                addResourceStatus(iter.nextResource(), status, depth - 1);
-            }
-        }
-    }
-
-    /**
-     * Add a <code>MultiStatusResponse</code> element to this <code>MultiStatus</code>
-     *
-     * @param response
-     */
-    public void addResponse(MultiStatusResponse response) {
-        responses.put(response.getHref(), response);
-    }
-
-    /**
-     * Returns the multistatus responses present as array.
-     *
-     * @return array of all {@link MultiStatusResponse responses} present in this
-     * multistatus.
-     */
-    public MultiStatusResponse[] getResponses() {
-        return (MultiStatusResponse[]) responses.values().toArray(new MultiStatusResponse[responses.size()]);
-    }
-
-    /**
-     * Set the response description.
-     *
-     * @param responseDescription
-     */
-    public void setResponseDescription(String responseDescription) {
-        this.responseDescription = responseDescription;
-    }
-
-    /**
-     * Returns the response description.
-     *
-     * @return responseDescription
-     */
-    public String getResponseDescription() {
-        return responseDescription;
-    }
-
-    /**
-     * Return the Xml representation of this <code>MultiStatus</code>.
-     *
-     * @return Xml document
-     * @param document
-     */
-    public Element toXml(Document document) {
-        Element multistatus = DomUtil.createElement(document, XML_MULTISTATUS, NAMESPACE);
-        for (MultiStatusResponse response : responses.values()) {
-            multistatus.appendChild(response.toXml(document));
-        }
-        if (responseDescription != null) {
-            Element respDesc = DomUtil.createElement(document, XML_RESPONSEDESCRIPTION, NAMESPACE, responseDescription);
-            multistatus.appendChild(respDesc);
-        }
-        return multistatus;
-    }
-
-    /**
-     * Build a <code>MultiStatus</code> from the specified xml element.
-     *
-     * @param multistatusElement
-     * @return new <code>MultiStatus</code> instance.
-     * @throws IllegalArgumentException if the given document is <code>null</code>
-     * or does not provide the required element.
-     */
-    public static MultiStatus createFromXml(Element multistatusElement) {
-        if (!DomUtil.matches(multistatusElement, XML_MULTISTATUS, NAMESPACE)) {
-            throw new IllegalArgumentException("DAV:multistatus element expected.");
-        }
-
-        MultiStatus multistatus = new MultiStatus();
-
-        ElementIterator it = DomUtil.getChildren(multistatusElement, XML_RESPONSE, NAMESPACE);
-        while (it.hasNext()) {
-            Element respElem = it.nextElement();
-            MultiStatusResponse response = MultiStatusResponse.createFromXml(respElem);
-            multistatus.addResponse(response);
-        }
-
-        // optional response description on the multistatus element
-        multistatus.setResponseDescription(
-                DomUtil.getChildText(multistatusElement, XML_RESPONSEDESCRIPTION, NAMESPACE));
-        return multistatus;
-    }
-}
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlRequestEntity.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlRequestEntity.java
deleted file mode 100644
index 697c34b1..00000000
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlRequestEntity.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.jackrabbit.webdav.client.methods;
-
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.StringEntity;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-
-/**
- * <code>XmlRequestEntity</code>...
- * @deprecated is it really use???
- */
-public class XmlRequestEntity extends HttpEntityEnclosingRequestBase {
-
-    private static Logger log = LoggerFactory.getLogger(XmlRequestEntity.class);
-
-    private final StringEntity delegatee;
-
-    public XmlRequestEntity(Document xmlDocument) throws IOException {
-        super();
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-
-        try {
-            TransformerFactory factory = TransformerFactory.newInstance();
-            Transformer transformer = factory.newTransformer();
-            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
-            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
-            transformer.setOutputProperty(OutputKeys.INDENT, "no");
-            transformer.transform(new DOMSource(xmlDocument), new StreamResult(out));
-        } catch (TransformerException e) {
-            log.error("XML serialization failed", e);
-            IOException exception = new IOException("XML serialization failed");
-            exception.initCause(e);
-            throw exception;
-        }
-
-        delegatee = new StringEntity(out.toString(), "text/xml", "UTF-8");
-    }
-
-    public boolean isRepeatable() {
-        return delegatee.isRepeatable();
-    }
-
-    public String getContentType() {
-        return delegatee.getContentType().getValue();
-    }
-
-    public void writeRequest(OutputStream out) throws IOException {
-        delegatee.writeTo(out);
-    }
-
-    public long getContentLength() {
-        return delegatee.getContentLength();
-    }
-
-    @Override
-    public String getMethod() {
-        return HttpPost.METHOD_NAME;
-    }
-}


(maven-wagon) 04/06: Correct two claims the new WebDAV code made about its own behaviour

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit 07a6a197a13d776a054ef6c1ea9d100e86427330
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Aug 7 22:58:55 2026 +0200

    Correct two claims the new WebDAV code made about its own behaviour
    
    The multistatus javadoc cited RFC 4918 section 9.1 as mandating that a
    server list the request URI first. It mandates no ordering at all. The
    assumption is an observed server behaviour and predates this class,
    which the comment it replaced described accurately; getFileList still
    skips the first entry as before, but the reason is now stated honestly.
    
    HttpStatus.SC_MULTI_STATUS does exist in HttpCore, so the local constant
    claiming otherwise is gone.
    
    Also drop the local-name fallback in isDavElement, which a namespace
    aware parser can never reach, and send the PROPFIND body as
    application/xml to match what Jackrabbit put on the wire. Tolerating an
    absent namespace is kept and now noted as a deliberate relaxation.
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 .../apache/maven/wagon/providers/webdav/DavMethods.java |  2 +-
 .../maven/wagon/providers/webdav/MultiStatus.java       | 17 +++++++----------
 .../maven/wagon/providers/webdav/WebDavWagon.java       |  5 +----
 3 files changed, 9 insertions(+), 15 deletions(-)

diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java
index c9803e50..43555247 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/DavMethods.java
@@ -88,7 +88,7 @@ static final class HttpPropfind extends HttpEntityEnclosingRequestBase {
                             + "<D:propfind xmlns:D=\"" + DAV_NAMESPACE + "\">"
                             + "<D:prop><D:" + propertyName + "/></D:prop>"
                             + "</D:propfind>",
-                    ContentType.create("text/xml", StandardCharsets.UTF_8)));
+                    ContentType.create("application/xml", StandardCharsets.UTF_8)));
         }
 
         @Override
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
index df8c8687..18976f7c 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
@@ -46,9 +46,9 @@
  * The {@code 207 Multi-Status} body of a PROPFIND response, reduced to what this Wagon needs: the
  * href of each response, in document order, and whether that response describes a collection.
  * <p>
- * Responses keep their document order because {@code getFileList} relies on the first entry being
- * the requested collection itself, as mandated by
- * <a href="http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1">RFC 4918 section 9.1</a>.
+ * Responses keep their document order because {@code getFileList} skips the first one, taking it to
+ * be the requested collection itself. RFC 4918 does not order responses; that a server lists the
+ * request URI first is an observed behaviour, and the assumption predates this class.
  *
  * @since 4.0.0
  */
@@ -199,15 +199,12 @@ private static List<Element> childElements(Element parent, String localName) {
 
     /**
      * Matches on local name within the {@code DAV:} namespace. Servers that emit the WebDAV
-     * elements without a namespace are tolerated, since some do.
+     * elements in no namespace at all are tolerated, since some do; Jackrabbit required
+     * {@code DAV:} exactly.
      */
     private static boolean isDavElement(Element element, String localName) {
-        String elementLocalName = element.getLocalName() != null ? element.getLocalName() : element.getNodeName();
-        int colon = elementLocalName.indexOf(':');
-        if (colon >= 0) {
-            elementLocalName = elementLocalName.substring(colon + 1);
-        }
-        if (!localName.equals(elementLocalName)) {
+        // the parser is namespace aware, so a parsed element always reports a local name
+        if (!localName.equals(element.getLocalName())) {
             return false;
         }
         String namespace = element.getNamespaceURI();
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
index c85f9794..5ca7a49f 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
@@ -60,9 +60,6 @@
 public class WebDavWagon extends AbstractHttpClientWagon {
     protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure";
 
-    /** {@code 207 Multi-Status}, not defined by {@link HttpStatus}. */
-    private static final int SC_MULTI_STATUS = 207;
-
     private final boolean continueOnFailure = Boolean.getBoolean(CONTINUE_ON_FAILURE_PROPERTY);
 
     /**
@@ -265,7 +262,7 @@ public List<String> getFileList(String destinationDirectory)
      *     cannot be parsed
      */
     private static List<MultiStatus.Response> readMultiStatus(CloseableHttpResponse response) throws IOException {
-        if (response.getStatusLine().getStatusCode() != SC_MULTI_STATUS) {
+        if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS) {
             return null;
         }
         HttpEntity entity = response.getEntity();


(maven-wagon) 03/06: Address review feedback on the WebDAV request handling

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit f010b71806a664f754497f3f6e8e3a26a64be0bd
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Aug 7 22:45:46 2026 +0200

    Address review feedback on the WebDAV request handling
    
    Do not let parser hardening break the transport. disallow-doctype-decl
    is a Xerces feature and setFeature rejects features a parser does not
    know, so a JAXP implementation other than the platform one would have
    made every PROPFIND fail. It is now applied only if supported, falling
    back to refusing external entities, and a configuration failure is
    reported as such instead of being blamed on the response body.
    
    Install an ErrorHandler on the DocumentBuilder. Without one the parser
    writes its own diagnostics to stderr before throwing, so a server
    answering 207 with a broken body would have littered the build output.
    Fatal errors still surface as an exception.
    
    Keep a 207 carrying no body a transport failure. It was being read as
    "not a collection", which surfaced as ResourceDoesNotExistException and
    told the resolver the artifact was simply absent.
    
    Also correct the comment about repeated hrefs, which described dropping
    later duplicates when the last one in fact wins, and record why a
    propstat without a status is read as successful.
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 pom.xml                                            |  2 +-
 .../maven/wagon/providers/webdav/MultiStatus.java  | 57 ++++++++++++++++++----
 .../maven/wagon/providers/webdav/WebDavWagon.java  | 36 ++++++++------
 .../wagon/providers/webdav/MultiStatusTest.java    | 42 ++++++++++++++--
 4 files changed, 108 insertions(+), 29 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9da6e561..dc6262d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -329,7 +329,7 @@ under the License.
                 </group>
                 <group>
                   <title>WebDAV Provider</title>
-                  <packages>org.apache.maven.wagon.providers.webdav*:org.apache.jackrabbit.webdav*</packages>
+                  <packages>org.apache.maven.wagon.providers.webdav*</packages>
                 </group>
                 <group>
                   <title>HTTP TCK</title>
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
index 4a0006b8..df8c8687 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
@@ -36,6 +36,7 @@
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
 
 import static org.apache.maven.wagon.providers.webdav.DavMethods.DAV_NAMESPACE;
 import static org.apache.maven.wagon.providers.webdav.DavMethods.PROPERTY_RESOURCETYPE;
@@ -53,6 +54,12 @@
  */
 final class MultiStatus {
 
+    private static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
+
+    private static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
+
+    private static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
+
     private final List<Response> responses;
 
     private MultiStatus(List<Response> responses) {
@@ -94,13 +101,23 @@ List<Response> getResponses() {
      *
      * @param in the response body, never {@code null}
      * @return the parsed responses, possibly empty but never {@code null}
-     * @throws IOException if the body is not a well-formed multistatus document
+     * @throws IOException if the parser cannot be configured, or if the body is not a well-formed
+     *     multistatus document
      */
     static MultiStatus parse(InputStream in) throws IOException {
+        DocumentBuilder builder;
+        try {
+            builder = newDocumentBuilder();
+        } catch (ParserConfigurationException e) {
+            // a configuration problem is not the server's fault, so say so rather than blaming the
+            // response body
+            throw new IOException("XML parser configuration error: " + e.getMessage(), e);
+        }
+
         Document document;
         try {
-            document = newDocumentBuilder().parse(in);
-        } catch (ParserConfigurationException | SAXException e) {
+            document = builder.parse(in);
+        } catch (SAXException e) {
             throw new IOException("Cannot parse multistatus response: " + e.getMessage(), e);
         }
 
@@ -109,8 +126,9 @@ static MultiStatus parse(InputStream in) throws IOException {
             throw new IOException("Expected a DAV:multistatus response body");
         }
 
-        // an href must occur only once per RFC 4918, so later duplicates are dropped rather than
-        // reported twice; a LinkedHashMap keeps the document order the callers depend on
+        // an href must occur only once per RFC 4918; should a server repeat one, the last wins and
+        // keeps the position of the first, which is what the Jackrabbit-backed code did. A
+        // LinkedHashMap also preserves the document order the callers depend on.
         Map<String, Response> responses = new LinkedHashMap<>();
         for (Element response : childElements(root, "response")) {
             String href = null;
@@ -148,8 +166,10 @@ private static boolean isCollection(Element response) {
 
     /**
      * Reads the {@code status} child, whose text is a status line such as {@code HTTP/1.1 200 OK}.
-     * A {@code propstat} without a status element is treated as successful, matching how lenient
-     * servers are read elsewhere.
+     * <p>
+     * RFC 4918 requires the element, and a {@code propstat} lacking one used to be skipped
+     * outright, which made every property of such a response invisible. It is read as successful
+     * here instead, so that a server omitting the status still gets its properties honoured.
      */
     private static boolean isOkStatus(Element propstat) {
         List<Element> statusElements = childElements(propstat, "status");
@@ -198,10 +218,29 @@ private static DocumentBuilder newDocumentBuilder() throws ParserConfigurationEx
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         // multistatus bodies come from a remote server, so no external entity resolution
         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
-        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+        // the parser in use is not necessarily Xerces, and setFeature rejects features it does not
+        // know, so fall back to refusing external entities where the doctype cannot be banned
+        if (!setFeatureIfSupported(factory, DISALLOW_DOCTYPE_DECL, true)) {
+            setFeatureIfSupported(factory, EXTERNAL_GENERAL_ENTITIES, false);
+            setFeatureIfSupported(factory, EXTERNAL_PARAMETER_ENTITIES, false);
+        }
         factory.setXIncludeAware(false);
         factory.setExpandEntityReferences(false);
         factory.setNamespaceAware(true);
-        return factory.newDocumentBuilder();
+
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        // without a handler the parser writes its own diagnostics to stderr; fatal errors still
+        // surface as an exception
+        builder.setErrorHandler(new DefaultHandler());
+        return builder;
+    }
+
+    private static boolean setFeatureIfSupported(DocumentBuilderFactory factory, String feature, boolean value) {
+        try {
+            factory.setFeature(feature, value);
+            return true;
+        } catch (ParserConfigurationException e) {
+            return false;
+        }
     }
 }
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
index 5762fa03..c85f9794 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java
@@ -24,6 +24,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.http.HttpEntity;
 import org.apache.http.HttpException;
 import org.apache.http.HttpStatus;
 import org.apache.http.client.methods.CloseableHttpResponse;
@@ -173,13 +174,8 @@ private boolean isDirectory(String url) throws IOException {
             method = new DavMethods.HttpPropfind(url, PROPERTY_RESOURCETYPE, DEPTH_0);
             closeableHttpResponse = execute(method);
 
-            if (isMultiStatus(closeableHttpResponse)) {
-                List<MultiStatus.Response> responses = MultiStatus.parse(
-                                closeableHttpResponse.getEntity().getContent())
-                        .getResponses();
-                return !responses.isEmpty() && responses.get(0).isCollection();
-            }
-            return false;
+            List<MultiStatus.Response> responses = readMultiStatus(closeableHttpResponse);
+            return responses != null && !responses.isEmpty() && responses.get(0).isCollection();
         } catch (HttpException e) {
             throw new IOException(e.getMessage(), e);
         } finally {
@@ -204,11 +200,9 @@ public List<String> getFileList(String destinationDirectory)
             if (isDirectory(url)) {
                 method = new DavMethods.HttpPropfind(url, PROPERTY_DISPLAYNAME, DEPTH_1);
                 closeableHttpResponse = execute(method);
-                if (isMultiStatus(closeableHttpResponse)) {
+                List<MultiStatus.Response> responses = readMultiStatus(closeableHttpResponse);
+                if (responses != null) {
                     ArrayList<String> dirs = new ArrayList<>();
-                    List<MultiStatus.Response> responses = MultiStatus.parse(
-                                    closeableHttpResponse.getEntity().getContent())
-                            .getResponses();
                     for (int i = 0; i < responses.size(); i++) {
                         String entryUrl = responses.get(i).getHref();
                         String fileName = PathUtils.filename(URLDecoder.decode(entryUrl));
@@ -262,10 +256,24 @@ public List<String> getFileList(String destinationDirectory)
     }
 
     /**
-     * A PROPFIND only carries a parseable body when the server answered {@code 207 Multi-Status}.
+     * Reads the body of a PROPFIND response, which is only present when the server answered
+     * {@code 207 Multi-Status}.
+     *
+     * @return the responses, or {@code null} if the status was not {@code 207} and the caller
+     *     should inspect it itself
+     * @throws IOException if the server answered {@code 207} without a body, or with one that
+     *     cannot be parsed
      */
-    private static boolean isMultiStatus(CloseableHttpResponse response) {
-        return response.getStatusLine().getStatusCode() == SC_MULTI_STATUS && response.getEntity() != null;
+    private static List<MultiStatus.Response> readMultiStatus(CloseableHttpResponse response) throws IOException {
+        if (response.getStatusLine().getStatusCode() != SC_MULTI_STATUS) {
+            return null;
+        }
+        HttpEntity entity = response.getEntity();
+        if (entity == null) {
+            // a transport fault rather than a missing resource, so it must not be reported as one
+            throw new IOException("Multi-Status response has no body");
+        }
+        return MultiStatus.parse(entity.getContent()).getResponses();
     }
 
     public String getURL(Repository repository) {
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
index 26d8e6bb..9a56ea3b 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
@@ -112,22 +112,54 @@ public void testDocumentOrderIsPreserved() throws Exception {
         assertEquals("/repo/dir/b.jar", responses.get(2).getHref());
     }
 
+    /**
+     * An href must occur only once; should a server repeat one, the last wins and keeps the
+     * position of the first, as the Jackrabbit-backed code did.
+     */
     @Test
     public void testDuplicateHrefsCollapse() throws Exception {
         List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
                 + "<D:multistatus xmlns:D=\"DAV:\">"
-                + "  <D:response><D:href>/repo/dir/a.jar</D:href></D:response>"
-                + "  <D:response><D:href>/repo/dir/a.jar</D:href></D:response>"
+                + "  <D:response><D:href>/repo/dir/a</D:href></D:response>"
+                + "  <D:response><D:href>/repo/dir/b</D:href></D:response>"
+                + "  <D:response>"
+                + "    <D:href>/repo/dir/a</D:href>"
+                + "    <D:propstat>"
+                + "      <D:prop><D:resourcetype><D:collection/></D:resourcetype></D:prop>"
+                + "      <D:status>HTTP/1.1 200 OK</D:status>"
+                + "    </D:propstat>"
+                + "  </D:response>"
                 + "</D:multistatus>");
 
-        assertEquals(1, responses.size());
+        assertEquals(2, responses.size());
+        assertEquals("/repo/dir/a", responses.get(0).getHref());
+        assertTrue("the later duplicate wins", responses.get(0).isCollection());
+        assertEquals("/repo/dir/b", responses.get(1).getHref());
+    }
+
+    /**
+     * RFC 4918 requires a status, but a server omitting one should still get its properties read.
+     */
+    @Test
+    public void testPropstatWithoutStatusIsRead() throws Exception {
+        List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
+                + "<D:multistatus xmlns:D=\"DAV:\">"
+                + "  <D:response>"
+                + "    <D:href>/repo/dir/</D:href>"
+                + "    <D:propstat>"
+                + "      <D:prop><D:resourcetype><D:collection/></D:resourcetype></D:prop>"
+                + "    </D:propstat>"
+                + "  </D:response>"
+                + "</D:multistatus>");
+
+        assertTrue(responses.get(0).isCollection());
     }
 
     /**
-     * Some servers emit the WebDAV elements with a default namespace or a different prefix.
+     * Some servers put the WebDAV elements in a default namespace rather than a prefixed one.
      */
     @Test
-    public void testAlternatePrefixIsAccepted() throws Exception {
+    public void testDefaultNamespaceIsAccepted() throws Exception {
         List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
                 + "<multistatus xmlns=\"DAV:\">"
                 + "  <response>"


(maven-wagon) 06/06: Reject malformed multistatus responses instead of reading past them

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit bb048652381cc272cd0abee6b83390931c3cf94c
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Aug 7 23:28:04 2026 +0200

    Reject malformed multistatus responses instead of reading past them
    
    A DAV:response carrying no href was being skipped. That moves the next
    response into its place, and both callers read meaning into position:
    isDirectory inspects the first entry, and getFileList takes the first
    entry to be the collection it asked about. A server omitting an href
    could therefore have had a child directory silently dropped from a
    listing and another one classified from the wrong resource. Such a
    response is now a parse error, as it was before.
    
    Stop reading a propstat that carries no status as successful, too. RFC
    4918 requires the element, so a propstat without one reports success for
    nothing, and treating it as 200 contradicted the rule stated on
    Response.isCollection that only propstats reporting 200 are consulted.
    It also went beyond porting the behaviour, on the strength of a lenient
    server nobody has actually met.
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 .../maven/wagon/providers/webdav/MultiStatus.java  | 17 +++++-----
 .../wagon/providers/webdav/MultiStatusTest.java    | 38 ++++++++++++++++++++--
 2 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
index 345377f5..64719d35 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
@@ -136,10 +136,13 @@ static MultiStatus parse(InputStream in) throws IOException {
                 href = hrefElement.getTextContent();
                 break;
             }
-            if (href != null) {
-                href = href.trim();
-                responses.put(href, new Response(href, isCollection(response)));
+            if (href == null || href.trim().isEmpty()) {
+                // dropping it would move the next response into its place, and the callers read
+                // meaning into that position
+                throw new IOException("Multi-Status response without a DAV:href");
             }
+            href = href.trim();
+            responses.put(href, new Response(href, isCollection(response)));
         }
         return new MultiStatus(Collections.unmodifiableList(new ArrayList<>(responses.values())));
     }
@@ -166,15 +169,13 @@ private static boolean isCollection(Element response) {
 
     /**
      * Reads the {@code status} child, whose text is a status line such as {@code HTTP/1.1 200 OK}.
-     * <p>
-     * RFC 4918 requires the element, and a {@code propstat} lacking one used to be skipped
-     * outright, which made every property of such a response invisible. It is read as successful
-     * here instead, so that a server omitting the status still gets its properties honoured.
+     * RFC 4918 requires the element, and a {@code propstat} lacking one reports no success for the
+     * properties it carries, so they are not read.
      */
     private static boolean isOkStatus(Element propstat) {
         List<Element> statusElements = childElements(propstat, "status");
         if (statusElements.isEmpty()) {
-            return true;
+            return false;
         }
         String[] tokens = statusElements.get(0).getTextContent().trim().split("\\s+");
         for (String token : tokens) {
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
index 9a56ea3b..98f6a247 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/MultiStatusTest.java
@@ -138,10 +138,11 @@ public void testDuplicateHrefsCollapse() throws Exception {
     }
 
     /**
-     * RFC 4918 requires a status, but a server omitting one should still get its properties read.
+     * RFC 4918 requires a status; a propstat lacking one reports no success, so its properties
+     * must not be read.
      */
     @Test
-    public void testPropstatWithoutStatusIsRead() throws Exception {
+    public void testPropstatWithoutStatusIsIgnored() throws Exception {
         List<MultiStatus.Response> responses = parse("<?xml version=\"1.0\"?>"
                 + "<D:multistatus xmlns:D=\"DAV:\">"
                 + "  <D:response>"
@@ -152,7 +153,38 @@ public void testPropstatWithoutStatusIsRead() throws Exception {
                 + "  </D:response>"
                 + "</D:multistatus>");
 
-        assertTrue(responses.get(0).isCollection());
+        assertFalse(responses.get(0).isCollection());
+    }
+
+    /**
+     * Skipping a response with no href would move the next one into its place, and both callers
+     * read meaning into that position.
+     */
+    @Test
+    public void testResponseWithoutHrefIsRejected() {
+        try {
+            parse("<?xml version=\"1.0\"?>"
+                    + "<D:multistatus xmlns:D=\"DAV:\">"
+                    + "  <D:response><D:propstat><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>"
+                    + "  <D:response><D:href>/repo/dir/child</D:href></D:response>"
+                    + "</D:multistatus>");
+            fail("expected an IOException");
+        } catch (IOException expected) {
+            assertTrue(expected.getMessage().contains("DAV:href"));
+        }
+    }
+
+    @Test
+    public void testResponseWithBlankHrefIsRejected() {
+        try {
+            parse("<?xml version=\"1.0\"?>"
+                    + "<D:multistatus xmlns:D=\"DAV:\">"
+                    + "  <D:response><D:href>   </D:href></D:response>"
+                    + "</D:multistatus>");
+            fail("expected an IOException");
+        } catch (IOException expected) {
+            assertTrue(expected.getMessage().contains("DAV:href"));
+        }
     }
 
     /**


(maven-wagon) 05/06: Refuse to parse rather than fall back to an unsecured XML parser

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git

commit 1d1b4882ba08a086801d378124f5c1819568da70
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Aug 7 23:02:31 2026 +0200

    Refuse to parse rather than fall back to an unsecured XML parser
    
    Where disallow-doctype-decl is unavailable the parser falls back to
    refusing external entities, but those two calls were allowed to fail as
    quietly as the first. A parser supporting none of the three would then
    have read remote input with only secure processing in force, which
    bounds resource use and says nothing about what the document may reach.
    The fallback now insists on both entity features and reports a parser it
    cannot secure as a configuration error.
    
    Also deny the external DTD subset, which the entity features do not
    govern, tolerating parsers that do not recognise the property.
    
    The branch only runs on a JAXP implementation other than the platform
    one, so it cannot be reached from a test; it was exercised by
    configuring a factory the same way by hand, where a DOCTYPE parses, as
    it did under Jackrabbit, and an external entity yields empty text.
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 .../maven/wagon/providers/webdav/MultiStatus.java      | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
index 18976f7c..345377f5 100644
--- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java
@@ -216,10 +216,22 @@ private static DocumentBuilder newDocumentBuilder() throws ParserConfigurationEx
         // multistatus bodies come from a remote server, so no external entity resolution
         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
         // the parser in use is not necessarily Xerces, and setFeature rejects features it does not
-        // know, so fall back to refusing external entities where the doctype cannot be banned
+        // know, so where the doctype cannot be banned outright, refuse external entities instead
         if (!setFeatureIfSupported(factory, DISALLOW_DOCTYPE_DECL, true)) {
-            setFeatureIfSupported(factory, EXTERNAL_GENERAL_ENTITIES, false);
-            setFeatureIfSupported(factory, EXTERNAL_PARAMETER_ENTITIES, false);
+            boolean generalEntities = setFeatureIfSupported(factory, EXTERNAL_GENERAL_ENTITIES, false);
+            boolean parameterEntities = setFeatureIfSupported(factory, EXTERNAL_PARAMETER_ENTITIES, false);
+            if (!generalEntities || !parameterEntities) {
+                // secure processing on its own bounds resource use, it does not stop a remote
+                // server reaching the entities it names, so refuse to parse at all
+                throw new ParserConfigurationException("XML parser supports neither " + DISALLOW_DOCTYPE_DECL
+                        + " nor the external entity features, and cannot be secured");
+            }
+            // neither of those governs the external DTD subset itself
+            try {
+                factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+            } catch (IllegalArgumentException e) {
+                // property unrecognised; the entity features above still hold
+            }
         }
         factory.setXIncludeAware(false);
         factory.setExpandEntityReferences(false);