001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.workspaces.dav; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.time.ZoneId; 021import java.time.format.DateTimeFormatter; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.List; 025import java.util.TimeZone; 026 027import javax.servlet.http.HttpServletRequest; 028import javax.servlet.http.HttpServletResponse; 029import javax.xml.parsers.DocumentBuilderFactory; 030import javax.xml.parsers.ParserConfigurationException; 031 032import org.apache.cocoon.ProcessingException; 033import org.apache.cocoon.environment.ObjectModelHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.environment.Response; 036import org.apache.cocoon.environment.http.HttpEnvironment; 037import org.apache.cocoon.generation.AbstractGenerator; 038import org.apache.cocoon.xml.XMLUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.tuple.Pair; 041import org.w3c.dom.Document; 042import org.w3c.dom.Element; 043import org.w3c.dom.Node; 044import org.w3c.dom.NodeList; 045import org.xml.sax.SAXException; 046 047import org.ametys.cms.transformation.xslt.ResolveURIComponent; 048import org.ametys.core.util.DateUtils; 049import org.ametys.plugins.explorer.resources.Resource; 050import org.ametys.plugins.explorer.resources.ResourceCollection; 051import org.ametys.plugins.repository.AmetysObject; 052 053/** 054 * Reader for WebDAV PROFIND method 055 */ 056public class WebdavPropfindGenerator extends AbstractGenerator 057{ 058 /** The webdav namespace */ 059 public static final String WEBDAV_NAMESPACE = "DAV:"; 060 061 /** Default recursion depth for folders */ 062 public static final int DEFAULT_DEPTH = 1; 063 064 private static final List<Pair<String, String>> __DEFAULT_PROPS_RESOURCE = Arrays.asList(Pair.of("DAV:", "creationdate"), 065 Pair.of("DAV:", "getlastmodified"), 066 Pair.of("DAV:", "getcontentlength"), 067 Pair.of("DAV:", "displayname"), 068 Pair.of("DAV:", "contenttype"), 069 Pair.of("DAV:", "resourcetype")); 070 071 private static final List<Pair<String, String>> __DEFAULT_PROPS_COLLECTION = Arrays.asList(Pair.of("DAV:", "displayname"), 072 Pair.of("DAV:", "resourcetype")); 073 074 private enum PropfindType 075 { 076 PROP, 077 ALLPROP, 078 PROPNAME 079 } 080 081 public void generate() throws IOException, SAXException, ProcessingException 082 { 083 Request request = ObjectModelHelper.getRequest(objectModel); 084 AmetysObject resource = (AmetysObject) request.getAttribute("resource"); 085 086 HttpServletRequest httpRequest = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT); 087 088 Document document = null; 089 try (InputStream is = httpRequest.getInputStream()) 090 { 091 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 092 documentBuilderFactory.setNamespaceAware(true); 093 document = documentBuilderFactory.newDocumentBuilder().parse(is); 094 } 095 catch (ParserConfigurationException e) 096 { 097 throw new ProcessingException(e); 098 } 099 100 Element propfindRoot = document.getDocumentElement(); 101 Element propfindRequest = _getFirstChildElement(propfindRoot); 102 103 String requestName = propfindRequest.getLocalName(); 104 PropfindType type = PropfindType.valueOf(requestName.toUpperCase()); 105 106 List<Pair<String, String>> props = null; 107 if (type == PropfindType.PROP) 108 { 109 props = _getProps(propfindRequest); 110 } 111 112 Response response = ObjectModelHelper.getResponse(objectModel); 113 response.setHeader("Content-Type", "application/xml; charset=utf-8"); 114 115 HttpServletResponse httpResponse = (HttpServletResponse) objectModel.get(HttpEnvironment.HTTP_RESPONSE_OBJECT); 116 httpResponse.setStatus(207); 117 118 contentHandler.startDocument(); 119 contentHandler.startPrefixMapping("d", WEBDAV_NAMESPACE); 120 XMLUtils.startElement(contentHandler, "d:multistatus"); 121 122 if (resource instanceof Resource) 123 { 124 _processResource((Resource) resource, type, props); 125 } 126 else if (resource instanceof ResourceCollection) 127 { 128 int maxDepth = DEFAULT_DEPTH; 129 try 130 { 131 String depthHeader = httpRequest.getHeader("Depth"); 132 if (!StringUtils.isEmpty(depthHeader)) 133 { 134 maxDepth = Integer.parseInt(depthHeader); 135 } 136 } 137 catch (NumberFormatException e) 138 { 139 // Nothing, depth will be the default value 140 } 141 142 _processCollection((ResourceCollection) resource, 0, maxDepth, type, props); 143 } 144 145 XMLUtils.endElement(contentHandler, "d:multistatus"); 146 contentHandler.endDocument(); 147 } 148 149 private Element _getFirstChildElement(Element element) 150 { 151 NodeList childNodes = element.getChildNodes(); 152 153 for (int i = 0; i < childNodes.getLength(); i++) 154 { 155 Node node = childNodes.item(i); 156 157 if (node instanceof Element) 158 { 159 return (Element) node; 160 } 161 } 162 163 throw new IllegalArgumentException("Unrecognized propfind request"); 164 } 165 166 private void _processResource(Resource resource, PropfindType type, List<Pair<String, String>> props) throws SAXException 167 { 168 String href = ResolveURIComponent.resolve("webdav-project-resource", resource.getId(), false, true); 169 170 _startResponseNode(href); 171 _startPropstatNode(); 172 173 List<Pair<String, String>> actualProps = props; 174 175 if (type == PropfindType.PROPNAME) 176 { 177 XMLUtils.createElement(contentHandler, "d:creationdate"); 178 XMLUtils.createElement(contentHandler, "d:getlastmodified"); 179 XMLUtils.createElement(contentHandler, "d:getcontentlength"); 180 XMLUtils.createElement(contentHandler, "d:getcontenttype"); 181 XMLUtils.createElement(contentHandler, "d:resourcetype"); 182 XMLUtils.createElement(contentHandler, "d:supportedlock"); 183 184 _endPropstatNode("HTTP/1.1 200 OK"); 185 _endResponseNode(); 186 return; 187 } 188 189 if (type == PropfindType.ALLPROP) 190 { 191 actualProps = __DEFAULT_PROPS_RESOURCE; 192 } 193 194 List<Pair<String, String>> notFound = new ArrayList<>(); 195 196 // get all asked properties 197 for (Pair<String, String> prop : actualProps) 198 { 199 String namespace = prop.getLeft(); 200 String name = prop.getRight(); 201 boolean found = false; 202 203 if (namespace.equals(WEBDAV_NAMESPACE)) 204 { 205 if (name.equals("creationdate")) 206 { 207 String lastModified = DateTimeFormatter.ISO_INSTANT.format(DateUtils.asInstant(resource.getLastModified())); 208 XMLUtils.createElement(contentHandler, "d:creationdate", lastModified); 209 found = true; 210 } 211 else if (name.equals("getlastmodified")) 212 { 213 String lastModified = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(TimeZone.getTimeZone("GMT").toZoneId()).format(DateUtils.asZonedDateTime(resource.getLastModified(), ZoneId.systemDefault())); 214 XMLUtils.createElement(contentHandler, "d:getlastmodified", lastModified); 215 found = true; 216 } 217 else if (name.equals("getcontentlength")) 218 { 219 XMLUtils.createElement(contentHandler, "d:getcontentlength", String.valueOf(resource.getLength())); 220 found = true; 221 } 222 else if (name.equals("displayname")) 223 { 224 XMLUtils.createElement(contentHandler, "d:displayname", resource.getName()); 225 found = true; 226 } 227 else if (name.equals("contenttype")) 228 { 229 XMLUtils.createElement(contentHandler, "d:contenttype", resource.getMimeType()); 230 found = true; 231 } 232 else if (name.equals("resourcetype")) 233 { 234 XMLUtils.createElement(contentHandler, "d:resourcetype"); 235 found = true; 236 } 237 } 238 else if (namespace.equals("http://ucb.openoffice.org/dav/props/")) 239 { 240 if (name.equals("IsReadOnly")) 241 { 242 contentHandler.startPrefixMapping("o", "http://ucb.openoffice.org/dav/props/"); 243 XMLUtils.createElement(contentHandler, "o:IsReadOnly", "false"); 244 contentHandler.endPrefixMapping("o"); 245 found = true; 246 } 247 else if (name.equals("BaseURI")) 248 { 249 contentHandler.startPrefixMapping("o", "http://ucb.openoffice.org/dav/props/"); 250 XMLUtils.createElement(contentHandler, "o:BaseURI", href); 251 contentHandler.endPrefixMapping("o"); 252 found = true; 253 } 254 } 255 256 if (!found) 257 { 258 //add to not found 259 notFound.add(prop); 260 } 261 } 262 263 _endPropstatNode("HTTP/1.1 200 OK"); 264 _notFoundPropstatNode(notFound); 265 _endResponseNode(); 266 } 267 268 /** 269 * Add a folder in the XML response 270 * @param resource the resource to sax 271 * @param props the list of properties to return 272 * @param currentDepth current depth 273 * @param maxDepth max depth (if current depth >= max depth, no children nodes will be added, only the current folder) 274 * @param type the prop find type 275 * @throws SAXException an exception occurred 276 */ 277 private void _processCollection(ResourceCollection resource, int currentDepth, int maxDepth, PropfindType type, List<Pair<String, String>> props) throws SAXException 278 { 279 String href = ResolveURIComponent.resolve("webdav-project-resource", resource.getId(), false, true) + "/"; 280 281 _startResponseNode(href); 282 _startPropstatNode(); 283 284 List<Pair<String, String>> actualProps = props; 285 286 if (type == PropfindType.PROPNAME) 287 { 288 XMLUtils.createElement(contentHandler, "d:displayname"); 289 XMLUtils.createElement(contentHandler, "d:resourcetype"); 290 291 _endPropstatNode("HTTP/1.1 200 OK"); 292 _endResponseNode(); 293 return; 294 } 295 296 if (type == PropfindType.ALLPROP) 297 { 298 actualProps = __DEFAULT_PROPS_COLLECTION; 299 } 300 301 List<Pair<String, String>> notFound = new ArrayList<>(); 302 303 // get all asked properties 304 for (Pair<String, String> prop : actualProps) 305 { 306 String namespace = prop.getLeft(); 307 String name = prop.getRight(); 308 boolean found = false; 309 310 if (namespace.equals(WEBDAV_NAMESPACE)) 311 { 312 if (name.equals("displayname")) 313 { 314 XMLUtils.createElement(contentHandler, "d:displayname", resource.getName()); 315 found = true; 316 } 317 else if (name.equals("resourcetype")) 318 { 319 XMLUtils.startElement(contentHandler, "d:resourcetype"); 320 XMLUtils.createElement(contentHandler, "d:collection"); 321 XMLUtils.endElement(contentHandler, "d:resourcetype"); 322 found = true; 323 } 324 } 325 326 if (!found) 327 { 328 //add to not found 329 notFound.add(prop); 330 } 331 } 332 333 _endPropstatNode("HTTP/1.1 200 OK"); 334 _notFoundPropstatNode(notFound); 335 _endResponseNode(); 336 337 if (currentDepth < maxDepth) 338 { 339 for (AmetysObject child : resource.getChildren()) 340 { 341 if (child instanceof Resource) 342 { 343 _processResource((Resource) child, type, props); 344 } 345 else if (child instanceof ResourceCollection) 346 { 347 _processCollection((ResourceCollection) child, currentDepth + 1, maxDepth, type, props); 348 } 349 } 350 } 351 } 352 353 private void _startResponseNode(String href) throws SAXException 354 { 355 XMLUtils.startElement(contentHandler, "d:response"); 356 XMLUtils.createElement(contentHandler, "d:href", href); 357 } 358 359 private void _startPropstatNode() throws SAXException 360 { 361 XMLUtils.startElement(contentHandler, "d:propstat"); 362 XMLUtils.startElement(contentHandler, "d:prop"); 363 } 364 365 private void _endResponseNode() throws SAXException 366 { 367 XMLUtils.endElement(contentHandler, "d:response"); 368 } 369 370 private void _endPropstatNode(String status) throws SAXException 371 { 372 XMLUtils.endElement(contentHandler, "d:prop"); 373 XMLUtils.createElement(contentHandler, "d:status", status); 374 XMLUtils.endElement(contentHandler, "d:propstat"); 375 } 376 377 private void _notFoundPropstatNode(List<Pair<String, String>> notFound) throws SAXException 378 { 379 if (notFound != null && !notFound.isEmpty()) 380 { 381 _startPropstatNode(); 382 383 for (Pair<String, String> prop : notFound) 384 { 385 String namespace = prop.getLeft(); 386 String name = prop.getRight(); 387 388 contentHandler.startPrefixMapping("", namespace); 389 XMLUtils.createElement(contentHandler, name); 390 contentHandler.endPrefixMapping(""); 391 } 392 393 _endPropstatNode("HTTP/1.1 404 Not Found"); 394 } 395 } 396 397 private List<Pair<String, String>> _getProps(Element props) 398 { 399 List<Pair<String, String>> result = new ArrayList<>(); 400 NodeList propNames = props.getChildNodes(); 401 for (int j = 0; j < propNames.getLength(); j++) 402 { 403 Node propName = propNames.item(j); 404 405 if (propName.getNodeType() != Node.ELEMENT_NODE) 406 { 407 continue; 408 } 409 410 result.add(Pair.of(propName.getNamespaceURI(), propName.getLocalName())); 411 } 412 413 return result; 414 } 415}