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_ALLPROP = 1; 063 064 /** Default recursion depth for no recursion */ 065 public static final int DEFAULT_DEPTH_NO_RECURSION = 0; 066 067 private static final List<Pair<String, String>> __DEFAULT_PROPS_RESOURCE = Arrays.asList(Pair.of("DAV:", "creationdate"), 068 Pair.of("DAV:", "getlastmodified"), 069 Pair.of("DAV:", "getcontentlength"), 070 Pair.of("DAV:", "displayname"), 071 Pair.of("DAV:", "contenttype"), 072 Pair.of("DAV:", "resourcetype")); 073 074 private static final List<Pair<String, String>> __DEFAULT_PROPS_COLLECTION = Arrays.asList(Pair.of("DAV:", "displayname"), 075 Pair.of("DAV:", "resourcetype")); 076 077 private enum PropfindType 078 { 079 PROP, 080 ALLPROP, 081 PROPNAME 082 } 083 084 public void generate() throws IOException, SAXException, ProcessingException 085 { 086 Request request = ObjectModelHelper.getRequest(objectModel); 087 AmetysObject resource = (AmetysObject) request.getAttribute("resource"); 088 089 HttpServletRequest httpRequest = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT); 090 091 Document document = null; 092 try (InputStream is = httpRequest.getInputStream()) 093 { 094 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 095 documentBuilderFactory.setNamespaceAware(true); 096 document = documentBuilderFactory.newDocumentBuilder().parse(is); 097 } 098 catch (ParserConfigurationException e) 099 { 100 throw new ProcessingException(e); 101 } 102 103 Element propfindRoot = document.getDocumentElement(); 104 Element propfindRequest = _getFirstChildElement(propfindRoot); 105 106 String requestName = propfindRequest.getLocalName(); 107 PropfindType type = PropfindType.valueOf(requestName.toUpperCase()); 108 109 List<Pair<String, String>> props = null; 110 if (type == PropfindType.PROP) 111 { 112 props = _getProps(propfindRequest); 113 } 114 115 Response response = ObjectModelHelper.getResponse(objectModel); 116 response.setHeader("Content-Type", "application/xml; charset=utf-8"); 117 118 HttpServletResponse httpResponse = (HttpServletResponse) objectModel.get(HttpEnvironment.HTTP_RESPONSE_OBJECT); 119 httpResponse.setStatus(207); 120 121 contentHandler.startDocument(); 122 contentHandler.startPrefixMapping("d", WEBDAV_NAMESPACE); 123 XMLUtils.startElement(contentHandler, "d:multistatus"); 124 125 if (resource instanceof Resource) 126 { 127 _processResource((Resource) resource, type, props); 128 } 129 else if (resource instanceof ResourceCollection) 130 { 131 // If we request ALLPROP, by default we will require 1 recursion, else no recursion (by default, so this is overridable by Depth) 132 int maxDepth = type == PropfindType.ALLPROP ? DEFAULT_DEPTH_ALLPROP : DEFAULT_DEPTH_NO_RECURSION; 133 try 134 { 135 String depthHeader = httpRequest.getHeader("Depth"); 136 if (!StringUtils.isEmpty(depthHeader)) 137 { 138 maxDepth = Integer.parseInt(depthHeader); 139 } 140 } 141 catch (NumberFormatException e) 142 { 143 // Nothing, depth will be the default value 144 } 145 146 _processCollection((ResourceCollection) resource, 0, maxDepth, type, props); 147 } 148 149 XMLUtils.endElement(contentHandler, "d:multistatus"); 150 contentHandler.endDocument(); 151 } 152 153 private Element _getFirstChildElement(Element element) 154 { 155 NodeList childNodes = element.getChildNodes(); 156 157 for (int i = 0; i < childNodes.getLength(); i++) 158 { 159 Node node = childNodes.item(i); 160 161 if (node instanceof Element) 162 { 163 return (Element) node; 164 } 165 } 166 167 throw new IllegalArgumentException("Unrecognized propfind request"); 168 } 169 170 private void _processResource(Resource resource, PropfindType type, List<Pair<String, String>> props) throws SAXException 171 { 172 String href = ResolveURIComponent.resolve("webdav-project-resource", resource.getId(), false, true); 173 174 _startResponseNode(href); 175 _startPropstatNode(); 176 177 List<Pair<String, String>> actualProps = props; 178 179 if (type == PropfindType.PROPNAME) 180 { 181 XMLUtils.createElement(contentHandler, "d:creationdate"); 182 XMLUtils.createElement(contentHandler, "d:getlastmodified"); 183 XMLUtils.createElement(contentHandler, "d:getcontentlength"); 184 XMLUtils.createElement(contentHandler, "d:getcontenttype"); 185 XMLUtils.createElement(contentHandler, "d:resourcetype"); 186 XMLUtils.createElement(contentHandler, "d:supportedlock"); 187 188 _endPropstatNode("HTTP/1.1 200 OK"); 189 _endResponseNode(); 190 return; 191 } 192 193 if (type == PropfindType.ALLPROP) 194 { 195 actualProps = __DEFAULT_PROPS_RESOURCE; 196 } 197 198 List<Pair<String, String>> notFound = new ArrayList<>(); 199 200 // get all asked properties 201 for (Pair<String, String> prop : actualProps) 202 { 203 String namespace = prop.getLeft(); 204 String name = prop.getRight(); 205 boolean found = _processProperty(namespace, name, resource, href); 206 207 if (!found) 208 { 209 //add to not found 210 notFound.add(prop); 211 } 212 } 213 214 _endPropstatNode("HTTP/1.1 200 OK"); 215 _notFoundPropstatNode(notFound); 216 _endResponseNode(); 217 } 218 219 private boolean _processProperty(String namespace, String name, Resource resource, String href) throws SAXException 220 { 221 boolean found = false; 222 223 if (namespace.equals(WEBDAV_NAMESPACE)) 224 { 225 if (name.equals("creationdate")) 226 { 227 String lastModified = DateTimeFormatter.ISO_INSTANT.format(DateUtils.asInstant(resource.getLastModified())); 228 XMLUtils.createElement(contentHandler, "d:creationdate", lastModified); 229 found = true; 230 } 231 else if (name.equals("getlastmodified")) 232 { 233 String lastModified = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(TimeZone.getTimeZone("GMT").toZoneId()).format(DateUtils.asZonedDateTime(resource.getLastModified(), ZoneId.systemDefault())); 234 XMLUtils.createElement(contentHandler, "d:getlastmodified", lastModified); 235 found = true; 236 } 237 else if (name.equals("getcontentlength")) 238 { 239 XMLUtils.createElement(contentHandler, "d:getcontentlength", String.valueOf(resource.getLength())); 240 found = true; 241 } 242 else if (name.equals("displayname")) 243 { 244 XMLUtils.createElement(contentHandler, "d:displayname", resource.getName()); 245 found = true; 246 } 247 else if (name.equals("contenttype")) 248 { 249 XMLUtils.createElement(contentHandler, "d:contenttype", resource.getMimeType()); 250 found = true; 251 } 252 else if (name.equals("getcontenttype")) 253 { 254 XMLUtils.createElement(contentHandler, "d:getcontenttype", resource.getMimeType()); 255 found = true; 256 } 257 else if (name.equals("resourcetype")) 258 { 259 XMLUtils.createElement(contentHandler, "d:resourcetype"); 260 found = true; 261 } 262 else if (name.equals("supportedlock")) 263 { 264 XMLUtils.startElement(contentHandler, "d:supportedlock"); 265 XMLUtils.startElement(contentHandler, "d:lockentry"); 266 XMLUtils.startElement(contentHandler, "d:lockscope"); 267 XMLUtils.createElement(contentHandler, "d:exclusive"); 268 XMLUtils.endElement(contentHandler, "d:lockscope"); 269 XMLUtils.startElement(contentHandler, "d:locktype"); 270 XMLUtils.createElement(contentHandler, "d:write"); 271 XMLUtils.endElement(contentHandler, "d:locktype"); 272 XMLUtils.endElement(contentHandler, "d:lockentry"); 273 XMLUtils.endElement(contentHandler, "d:supportedlock"); 274 found = true; 275 } 276 } 277 else if (namespace.equals("http://ucb.openoffice.org/dav/props/")) 278 { 279 if (name.equals("IsReadOnly")) 280 { 281 contentHandler.startPrefixMapping("o", "http://ucb.openoffice.org/dav/props/"); 282 XMLUtils.createElement(contentHandler, "o:IsReadOnly", "false"); 283 contentHandler.endPrefixMapping("o"); 284 found = true; 285 } 286 else if (name.equals("BaseURI")) 287 { 288 contentHandler.startPrefixMapping("o", "http://ucb.openoffice.org/dav/props/"); 289 XMLUtils.createElement(contentHandler, "o:BaseURI", href); 290 contentHandler.endPrefixMapping("o"); 291 found = true; 292 } 293 else if (name.equals("ObjectId")) 294 { 295 contentHandler.startPrefixMapping("o", "http://ucb.openoffice.org/dav/props/"); 296 XMLUtils.createElement(contentHandler, "o:ObjectId", resource.getId()); 297 contentHandler.endPrefixMapping("o"); 298 found = true; 299 } 300 } 301 302 return found; 303 } 304 305 /** 306 * Add a folder in the XML response 307 * @param resource the resource to sax 308 * @param props the list of properties to return 309 * @param currentDepth current depth 310 * @param maxDepth max depth (if current depth >= max depth, no children nodes will be added, only the current folder) 311 * @param type the prop find type 312 * @throws SAXException an exception occurred 313 */ 314 private void _processCollection(ResourceCollection resource, int currentDepth, int maxDepth, PropfindType type, List<Pair<String, String>> props) throws SAXException 315 { 316 String href = ResolveURIComponent.resolve("webdav-project-resource", resource.getId(), false, true) + "/"; 317 318 _startResponseNode(href); 319 _startPropstatNode(); 320 321 List<Pair<String, String>> actualProps = props; 322 323 if (type == PropfindType.PROPNAME) 324 { 325 XMLUtils.createElement(contentHandler, "d:displayname"); 326 XMLUtils.createElement(contentHandler, "d:resourcetype"); 327 328 _endPropstatNode("HTTP/1.1 200 OK"); 329 _endResponseNode(); 330 return; 331 } 332 333 if (type == PropfindType.ALLPROP) 334 { 335 actualProps = __DEFAULT_PROPS_COLLECTION; 336 } 337 338 List<Pair<String, String>> notFound = new ArrayList<>(); 339 340 // get all asked properties 341 for (Pair<String, String> prop : actualProps) 342 { 343 String namespace = prop.getLeft(); 344 String name = prop.getRight(); 345 boolean found = false; 346 347 if (namespace.equals(WEBDAV_NAMESPACE)) 348 { 349 if (name.equals("displayname")) 350 { 351 XMLUtils.createElement(contentHandler, "d:displayname", resource.getName()); 352 found = true; 353 } 354 else if (name.equals("resourcetype")) 355 { 356 XMLUtils.startElement(contentHandler, "d:resourcetype"); 357 XMLUtils.createElement(contentHandler, "d:collection"); 358 XMLUtils.endElement(contentHandler, "d:resourcetype"); 359 found = true; 360 } 361 } 362 363 if (!found) 364 { 365 //add to not found 366 notFound.add(prop); 367 } 368 } 369 370 _endPropstatNode("HTTP/1.1 200 OK"); 371 _notFoundPropstatNode(notFound); 372 _endResponseNode(); 373 374 if (currentDepth < maxDepth) 375 { 376 for (AmetysObject child : resource.getChildren()) 377 { 378 if (child instanceof Resource) 379 { 380 _processResource((Resource) child, type, props); 381 } 382 else if (child instanceof ResourceCollection) 383 { 384 _processCollection((ResourceCollection) child, currentDepth + 1, maxDepth, type, props); 385 } 386 } 387 } 388 } 389 390 private void _startResponseNode(String href) throws SAXException 391 { 392 XMLUtils.startElement(contentHandler, "d:response"); 393 XMLUtils.createElement(contentHandler, "d:href", href); 394 } 395 396 private void _startPropstatNode() throws SAXException 397 { 398 XMLUtils.startElement(contentHandler, "d:propstat"); 399 XMLUtils.startElement(contentHandler, "d:prop"); 400 } 401 402 private void _endResponseNode() throws SAXException 403 { 404 XMLUtils.endElement(contentHandler, "d:response"); 405 } 406 407 private void _endPropstatNode(String status) throws SAXException 408 { 409 XMLUtils.endElement(contentHandler, "d:prop"); 410 XMLUtils.createElement(contentHandler, "d:status", status); 411 XMLUtils.endElement(contentHandler, "d:propstat"); 412 } 413 414 private void _notFoundPropstatNode(List<Pair<String, String>> notFound) throws SAXException 415 { 416 if (notFound != null && !notFound.isEmpty()) 417 { 418 _startPropstatNode(); 419 420 for (Pair<String, String> prop : notFound) 421 { 422 String namespace = prop.getLeft(); 423 String name = prop.getRight(); 424 425 contentHandler.startPrefixMapping("", namespace); 426 XMLUtils.createElement(contentHandler, name); 427 contentHandler.endPrefixMapping(""); 428 } 429 430 _endPropstatNode("HTTP/1.1 404 Not Found"); 431 } 432 } 433 434 private List<Pair<String, String>> _getProps(Element props) 435 { 436 List<Pair<String, String>> result = new ArrayList<>(); 437 NodeList propNames = props.getChildNodes(); 438 for (int j = 0; j < propNames.getLength(); j++) 439 { 440 Node propName = propNames.item(j); 441 442 if (propName.getNodeType() != Node.ELEMENT_NODE) 443 { 444 continue; 445 } 446 447 result.add(Pair.of(propName.getNamespaceURI(), propName.getLocalName())); 448 } 449 450 return result; 451 } 452}