f59d6081079c7f6322d72005647dc8ffd1637cd8
[jelmer/calypso.git] / calypso / xmlutils.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Calypso - CalDAV/CardDAV/WebDAV Server
4 # Copyright © 2011 Keith Packard
5 # Copyright © 2008-2011 Guillaume Ayoub
6 # Copyright © 2008 Nicolas Kandel
7 # Copyright © 2008 Pascal Halter
8 #
9 # This library is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This library is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with Calypso.  If not, see <http://www.gnu.org/licenses/>.
21
22 """
23 XML and iCal requests manager.
24
25 Note that all these functions need to receive unicode objects for full
26 iCal requests (PUT) and string objects with charset correctly defined
27 in them for XML requests (all but PUT).
28
29 """
30
31 import xml.etree.ElementTree as ET
32 import time
33 import dateutil
34 import dateutil.parser
35 import dateutil.rrule
36 import dateutil.tz
37 import datetime
38 import email.utils
39 import logging
40
41 from . import client, config, webdav, paths, principal
42 from .xmlutils_generic import _tag
43
44 __package__ = 'calypso.xmlutils'
45
46 log = logging.getLogger(__name__)
47
48
49 def _response(code):
50     """Return full W3C names from HTTP status codes."""
51     return "HTTP/1.1 %i %s" % (code, client.responses[code])
52
53 def delete(path, collection, context):
54     """Read and answer DELETE requests.
55
56     Read rfc4918-9.6 for info.
57
58     """
59     # Reading request
60     collection.remove(paths.resource_from_path(path), context=context)
61
62     # Writing answer
63     multistatus = ET.Element(_tag("D", "multistatus"))
64     response = ET.Element(_tag("D", "response"))
65     multistatus.append(response)
66
67     href = ET.Element(_tag("D", "href"))
68     href.text = path
69     response.append(href)
70
71     status = ET.Element(_tag("D", "status"))
72     status.text = _response(200)
73     response.append(status)
74
75     return ET.tostring(multistatus, config.get("encoding", "request"))
76
77 def propfind(path, xml_request, collection, resource, depth, context):
78     """Read and answer PROPFIND requests.
79
80     Read rfc4918-9.1 for info.
81
82     """
83
84     item_name = paths.resource_from_path(path)
85
86     if xml_request:
87         # Reading request
88         root = ET.fromstring(xml_request)
89
90         prop_element = root.find(_tag("D", "prop"))
91     else:
92         prop_element = None
93
94     if prop_element is not None:
95         prop_list = prop_element.getchildren()
96         props = [prop.tag for prop in prop_list]
97     else:
98         props = [_tag("D", "resourcetype"),
99                  _tag("D", "owner"),
100                  _tag("D", "getcontenttype"),
101                  _tag("D", "getetag"),
102                  _tag("D", "principal-collection-set"),
103                  _tag("C", "supported-calendar-component-set"),
104                  _tag("D", "supported-report-set"),
105                  _tag("D", "current-user-privilege-set"),
106                  _tag("D", "getcontentlength"),
107                  _tag("D", "getlastmodified")]
108
109     
110     # Writing answer
111     multistatus = ET.Element(_tag("D", "multistatus"))
112
113     if resource is not None:
114         items = resource.propfind_children(depth, context)
115     elif collection:
116         if item_name:
117             item = collection.get_item(item_name)
118             print "item_name %s item %s" % (item_name, item)
119             if item:
120                 items = [item]
121             else:
122                 items = []
123         else:
124             if depth == "0":
125                 items = [collection]
126             else:
127                 # depth is 1, infinity or not specified
128                 # we limit ourselves to depth == 1
129                 items = [collection] + collection.items
130     else:
131         items = []
132
133     for item in items:
134         is_collection = isinstance(item, webdav.Collection)
135         is_resource = isinstance(item, principal.Resource)
136
137         response = ET.Element(_tag("D", "response"))
138         multistatus.append(response)
139
140         href = ET.Element(_tag("D", "href"))
141         if is_collection:
142             href.text = item.urlpath
143         elif is_resource:
144             href.text = item.urlpath
145         else:
146             href.text = collection.urlpath + item.name
147         response.append(href)
148
149         propstat = ET.Element(_tag("D", "propstat"))
150         response.append(propstat)
151
152         prop = ET.Element(_tag("D", "prop"))
153         propstat.append(prop)
154
155         for tag in props:
156             element = ET.Element(tag)
157             if tag == _tag("D", "resourcetype") and is_collection:
158                 if collection.is_calendar:
159                     tag = ET.Element(_tag("C", "calendar"))
160                     element.append(tag)
161                 if collection.is_addressbook:
162                     tag = ET.Element(_tag("A", "addressbook"))
163                     element.append(tag)
164                 tag = ET.Element(_tag("D", "collection"))
165                 element.append(tag)
166             elif tag == _tag("D", "owner"):
167                 element.text = collection.owner
168             elif tag == _tag("D", "getcontenttype"):
169                 if item.tag == 'VCARD':
170                     element.text = "text/vcard"
171                 else:
172                     element.text = "text/calendar"
173             elif tag == _tag("CS", "getctag") and is_collection:
174                 element.text = item.ctag
175             elif tag == _tag("D", "getetag"):
176                 element.text = item.etag
177             elif tag == _tag("D", "displayname") and is_collection:
178                 element.text = collection.name
179             elif tag == _tag("E", "calendar-color") and is_collection:
180                 element.text = collection.color
181             elif tag == _tag("D", "principal-URL"):
182                 # TODO: use a real principal URL, read rfc3744-4.2 for info
183                 tag = ET.Element(_tag("D", "href"))
184                 tag.text = path
185                 element.append(tag)
186             elif tag in (
187                 _tag("D", "principal-collection-set"),
188                 _tag("C", "calendar-user-address-set"),
189                 ):
190                 # not meaningfully implemented yet
191                 tag = ET.Element(_tag("D", "href"))
192                 tag.text = path
193                 element.append(tag)
194             elif tag == _tag("C", "supported-calendar-component-set"):
195                 comp = ET.Element(_tag("C", "comp"))
196                 comp.set("name", "VTODO") # pylint: disable=W0511
197                 element.append(comp)
198                 comp = ET.Element(_tag("C", "comp"))
199                 comp.set("name", "VEVENT")
200                 element.append(comp)
201             elif tag == _tag("D", "supported-report-set"):
202                 tag = ET.Element(_tag("C", "calendar-multiget"))
203                 element.append(tag)
204                 tag = ET.Element(_tag("C", "filter"))
205                 element.append(tag)
206             elif tag == _tag("D", "current-user-privilege-set"):
207                 privilege = ET.Element(_tag("D", "privilege"))
208                 privilege.append(ET.Element(_tag("D", "all")))
209                 element.append(privilege)
210             elif tag == _tag("D", "getcontentlength"):
211                 element.text = item.length
212             elif tag == _tag("D", "getlastmodified"):
213 #                element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
214 #                element.text = email.utils.formatdate(item.last_modified)
215                 element.text = email.utils.formatdate(time.mktime(item.last_modified))
216             elif tag == _tag("D", "current-user-principal"):
217                 tag = ET.Element(_tag("D", "href"))
218                 tag.text = config.get("server", "user_principal") % context
219                 element.append(tag)
220             elif tag in (_tag("A", "addressbook-description"),
221                          _tag("C", "calendar-description")) and is_collection:
222                 element.text = collection.get_description()
223             prop.append(element)
224
225         status = ET.Element(_tag("D", "status"))
226         status.text = _response(200)
227         propstat.append(status)
228
229     return ET.tostring(multistatus, config.get("encoding", "request"))
230
231
232 def put(path, webdav_request, collection, context):
233     """Read PUT requests."""
234     name = paths.resource_from_path(path)
235     log.debug('xmlutils put path %s name %s', path, name)
236     if name in (item.name for item in collection.items):
237         # PUT is modifying an existing item
238         log.debug('Replacing item named %s', name)
239         return collection.replace(name, webdav_request, context=context)
240     else:
241         # PUT is adding a new item
242         log.debug('Putting a new item, because name %s is not known', name)
243         return collection.append(name, webdav_request, context=context)
244
245
246 def match_filter_element(vobject, fe):
247     if fe.tag == _tag("C", "comp-filter"):
248         comp = fe.get("name")
249         if comp:
250             if comp == vobject.name:
251                 hassub = False
252                 submatch = False
253                 for fc in fe.getchildren():
254                     if match_filter_element(vobject, fc):
255                         submatch = True
256                         break
257                     for vc in vobject.getChildren():
258                         hassub = True
259                         if match_filter_element (vc, fc):
260                             submatch = True
261                             break
262                     if submatch:
263                         break
264                 if not hassub or submatch:
265                     return True
266         return False
267     elif fe.tag == _tag("C", "time-range"):
268         try:
269             rruleset = vobject.rruleset
270         except AttributeError:
271             return False
272         start = fe.get("start")
273         end = fe.get("end")
274         # According to RFC 4791, one of start and stop must be set,
275         # but the other can be empty.  If both are empty, the
276         # specification is violated.
277         if start is None and end is None:
278             msg = "time-range missing both start and stop attribute (required by RFC 4791)"
279             log.error(msg)
280             raise ValueError(msg)
281         # RFC 4791 state if start is missing, assume it is -infinity
282         if start is None:
283             start = "00010101T000000Z"  # start of year one
284         # RFC 4791 state if end is missing, assume it is +infinity
285         if end is None:
286             end = "99991231T235959Z"  # last date with four digit year
287         if rruleset is None:
288             rruleset = dateutil.rrule.rruleset()
289             dtstart = vobject.dtstart.value
290             try:
291                 dtstart = datetime.datetime.combine(dtstart, datetime.time())
292             except Exception:
293                 0
294             if dtstart.tzinfo is None:
295                 dtstart = dtstart.replace(tzinfo = dateutil.tz.tzlocal())
296             rruleset.rdate(dtstart)
297         start_datetime = dateutil.parser.parse(start)
298         if start_datetime.tzinfo is None:
299             start_datetime = start_datetime.replace(tzinfo = dateutil.tz.tzlocal())
300         end_datetime = dateutil.parser.parse(end)
301         if end_datetime.tzinfo is None:
302             end_datetime = end_datetime.replace(tzinfo = dateutil.tz.tzlocal())
303         try:
304             if rruleset.between(start_datetime, end_datetime, True):
305                 return True
306         except TypeError:
307             start_datetime = start_datetime.replace(tzinfo = None)
308             end_datetime = end_datetime.replace(tzinfo = None)
309             try:
310                 if rruleset.between(start_datetime, end_datetime, True):
311                     return True
312             except TypeError:
313                 return True
314         return False
315     return True
316
317 def match_filter(item, filter):
318     if filter is None:
319         return True
320     if filter.tag != _tag("C", "filter"):
321         return True
322     for fe in filter.getchildren():
323         if match_filter_element(item.object, fe):
324             return True
325     return False
326
327 def report(path, xml_request, collection):
328     """Read and answer REPORT requests.
329
330     Read rfc3253-3.6 for info.
331
332     """
333     # Reading request
334     root = ET.fromstring(xml_request)
335
336     prop_element = root.find(_tag("D", "prop"))
337     prop_list = prop_element.getchildren()
338     props = [prop.tag for prop in prop_list]
339
340     filter_element = root.find(_tag("C", "filter"))
341
342     if collection:
343         if root.tag == _tag("C", "calendar-multiget") or root.tag == _tag('A', 'addressbook-multiget'):
344             # Read rfc4791-7.9 for info
345             hreferences = set((href_element.text for href_element
346                                in root.findall(_tag("D", "href"))))
347         else:
348             hreferences = (path,)
349     else:
350         hreferences = ()
351
352     # Writing answer
353     multistatus = ET.Element(_tag("D", "multistatus"))
354
355     for hreference in hreferences:
356         # Check if the reference is an item or a collection
357         name = paths.resource_from_path(hreference)
358         if name:
359             # Reference is an item
360             path = paths.collection_from_path(hreference) + "/"
361             items = (item for item in collection.items if item.name == name)
362         else:
363             # Reference is a collection
364             path = hreference
365             items = collection.items
366
367         
368         for item in items:
369             if not match_filter(item, filter_element):
370                 continue
371
372             response = ET.Element(_tag("D", "response"))
373             multistatus.append(response)
374
375             href = ET.Element(_tag("D", "href"))
376             href.text = path.rstrip('/') + '/' + item.name
377             response.append(href)
378
379             propstat = ET.Element(_tag("D", "propstat"))
380             response.append(propstat)
381
382             prop = ET.Element(_tag("D", "prop"))
383             propstat.append(prop)
384
385             for tag in props:
386                 element = ET.Element(tag)
387                 if tag == _tag("D", "getetag"):
388                     element.text = item.etag
389                 elif tag == _tag("C", "calendar-data"):
390                     element.text = item.text
391                 elif tag == _tag("A", "address-data"):
392                     element.text = item.text
393                 prop.append(element)
394
395             status = ET.Element(_tag("D", "status"))
396             status.text = _response(200)
397             propstat.append(status)
398
399     reply = ET.tostring(multistatus, config.get("encoding", "request"))
400         
401     return reply