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 &gt;= 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}