Copilot commented on code in PR #898: URL: https://github.com/apache/maven-wagon/pull/898#discussion_r3738788661
########## wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java: ########## @@ -0,0 +1,255 @@ +/* + * 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 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; +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} 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 + */ +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) { + 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 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 = builder.parse(in); + } catch (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; 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; + for (Element hrefElement : childElements(response, "href")) { + href = hrefElement.getTextContent(); + break; + } + if (href != null) { + href = href.trim(); + responses.put(href, new Response(href, isCollection(response))); + } Review Comment: A `DAV:response` requires a non-empty `DAV:href`; silently dropping a malformed response changes the prior Jackrabbit behavior, which rejected it. This can also shift a later child into position 0, making `isDirectory` inspect the wrong resource and making `getFileList` skip the wrong entry. Reject missing or blank hrefs as parse errors. ########## wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java: ########## @@ -209,21 +191,17 @@ 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)) { + List<MultiStatus.Response> responses = readMultiStatus(closeableHttpResponse); + if (responses != null) { 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(); + for (int i = 0; i < responses.size(); i++) { + String entryUrl = responses.get(i).getHref(); Review Comment: The loop index is still used below to skip response 0 as the collection itself, but RFC 4918 does not assign significance to `response` order. A compliant Depth-1 response can put a child first, causing that child directory to be dropped and the requested collection to be returned as an entry. Identify and skip the response whose normalized `href` matches the requested URI instead of relying on `i == 0`. ########## wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/MultiStatus.java: ########## @@ -0,0 +1,255 @@ +/* + * 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 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; +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} 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 + */ +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) { + 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 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 = builder.parse(in); + } catch (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; 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; + 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}. + * <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"); + if (statusElements.isEmpty()) { + return true; Review Comment: Treating a missing mandatory `status` as success contradicts the stated fidelity rule that only propstats reporting 200 are consulted. Jackrabbit ignored a propstat unless both `status` and `prop` were present; this fallback can therefore classify a collection from a property whose success was never reported. Return false when status is absent and update the corresponding test. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
