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