Add support for addressbook-description and calendar-description attributes.
[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 import urllib
41 import os.path
42
43 from . import client, config, webdav, paths
44
45 __package__ = 'calypso.xmlutils'
46
47 NAMESPACES = {
48     "C": "urn:ietf:params:xml:ns:caldav",
49     "A": "urn:ietf:params:xml:ns:carddav",
50     "D": "DAV:",
51     "CS": "http://calendarserver.org/ns/"}
52
53 log = logging.getLogger(__name__)
54
55 def _tag(short_name, local):
56     """Get XML Clark notation {uri(``short_name``)}``local``."""
57     return "{%s}%s" % (NAMESPACES[short_name], local)
58
59
60 def _response(code):
61     """Return full W3C names from HTTP status codes."""
62     return "HTTP/1.1 %i %s" % (code, client.responses[code])
63
64 def delete(path, collection, context):
65     """Read and answer DELETE requests.
66
67     Read rfc4918-9.6 for info.
68
69     """
70     # Reading request
71     collection.remove(paths.resource_from_path(path), context=context)
72
73     # Writing answer
74     multistatus = ET.Element(_tag("D", "multistatus"))
75     response = ET.Element(_tag("D", "response"))
76     multistatus.append(response)
77
78     href = ET.Element(_tag("D", "href"))
79     href.text = path
80     response.append(href)
81
82     status = ET.Element(_tag("D", "status"))
83     status.text = _response(200)
84     response.append(status)
85
86     return ET.tostring(multistatus, config.get("encoding", "request"))
87
88
89 def propfind(path, xml_request, collection, depth, context):
90     """Read and answer PROPFIND requests.
91
92     Read rfc4918-9.1 for info.
93
94     """
95
96     item_name = paths.resource_from_path(path)
97     collection_name = paths.collection_from_path(path)
98
99     if xml_request:
100         # Reading request
101         root = ET.fromstring(xml_request)
102
103         prop_element = root.find(_tag("D", "prop"))
104     else:
105         prop_element = None
106
107     if prop_element is not None:
108         prop_list = prop_element.getchildren()
109         props = [prop.tag for prop in prop_list]
110     else:
111         props = [_tag("D", "resourcetype"),
112                  _tag("D", "owner"),
113                  _tag("D", "getcontenttype"),
114                  _tag("D", "getetag"),
115                  _tag("D", "principal-collection-set"),
116                  _tag("C", "supported-calendar-component-set"),
117                  _tag("D", "supported-report-set"),
118                  _tag("D", "current-user-privilege-set"),
119                  _tag("D", "getcontentlength"),
120                  _tag("D", "getlastmodified")]
121
122     
123     # Writing answer
124     multistatus = ET.Element(_tag("D", "multistatus"))
125
126     if collection:
127         if item_name:
128             item = collection.get_item(item_name)
129             print "item_name %s item %s" % (item_name, item)
130             if item:
131                 items = [item]
132             else:
133                 items = []
134         else:
135             if depth == "0":
136                 items = [collection]
137             else:
138                 # depth is 1, infinity or not specified
139                 # we limit ourselves to depth == 1
140                 items = [collection] + collection.items
141     else:
142         items = []
143
144     for item in items:
145         is_collection = isinstance(item, webdav.Collection)
146
147         response = ET.Element(_tag("D", "response"))
148         multistatus.append(response)
149
150         href = ET.Element(_tag("D", "href"))
151         href.text = collection_name if is_collection else "/".join([collection_name, item.name])
152         response.append(href)
153
154         propstat = ET.Element(_tag("D", "propstat"))
155         response.append(propstat)
156
157         prop = ET.Element(_tag("D", "prop"))
158         propstat.append(prop)
159
160         for tag in props:
161             element = ET.Element(tag)
162             if tag == _tag("D", "resourcetype") and is_collection:
163                 tag = ET.Element(_tag("C", "calendar"))
164                 element.append(tag)
165                 tag = ET.Element(_tag("A", "addressbook"))
166                 element.append(tag)
167                 tag = ET.Element(_tag("D", "collection"))
168                 element.append(tag)
169             elif tag == _tag("D", "owner"):
170                 element.text = collection.owner
171             elif tag == _tag("D", "getcontenttype"):
172                 if item.tag == 'VCARD':
173                     element.text = "text/vcard"
174                 else:
175                     element.text = "text/calendar"
176             elif tag == _tag("CS", "getctag") and is_collection:
177                 element.text = item.ctag
178             elif tag == _tag("D", "getetag"):
179                 element.text = item.etag
180             elif tag == _tag("D", "displayname") and is_collection:
181                 element.text = collection.name
182             elif tag == _tag("D", "principal-URL"):
183                 # TODO: use a real principal URL, read rfc3744-4.2 for info
184                 tag = ET.Element(_tag("D", "href"))
185                 tag.text = path
186                 element.append(tag)
187             elif tag in (
188                 _tag("D", "principal-collection-set"),
189                 _tag("C", "calendar-user-address-set"),
190                 _tag("C", "calendar-home-set"),
191                 _tag("A", "addressbook-home-set")):
192                 tag = ET.Element(_tag("D", "href"))
193                 tag.text = path
194                 element.append(tag)
195             elif tag == _tag("C", "supported-calendar-component-set"):
196                 comp = ET.Element(_tag("C", "comp"))
197                 comp.set("name", "VTODO") # pylint: disable=W0511
198                 element.append(comp)
199                 comp = ET.Element(_tag("C", "comp"))
200                 comp.set("name", "VEVENT")
201                 element.append(comp)
202             elif tag == _tag("D", "supported-report-set"):
203                 tag = ET.Element(_tag("C", "calendar-multiget"))
204                 element.append(tag)
205                 tag = ET.Element(_tag("C", "filter"))
206                 element.append(tag)
207             elif tag == _tag("D", "current-user-privilege-set"):
208                 privilege = ET.Element(_tag("D", "privilege"))
209                 privilege.append(ET.Element(_tag("D", "all")))
210                 element.append(privilege)
211             elif tag == _tag("D", "getcontentlength"):
212                 element.text = item.length
213             elif tag == _tag("D", "getlastmodified"):
214 #                element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
215 #                element.text = email.utils.formatdate(item.last_modified)
216                 element.text = email.utils.formatdate(time.mktime(item.last_modified))
217             elif tag == _tag("D", "current-user-principal"):
218                 tag = ET.Element(_tag("D", "href"))
219                 tag.text = config.get("server", "user_principal") % context
220                 element.append(tag)
221             elif tag in (_tag("A", "addressbook-description"),
222                          _tag("C", "calendar-description")) and is_collection:
223                 element.text = collection.get_description()
224             prop.append(element)
225
226         status = ET.Element(_tag("D", "status"))
227         status.text = _response(200)
228         propstat.append(status)
229
230     return ET.tostring(multistatus, config.get("encoding", "request"))
231
232
233 def put(path, webdav_request, collection, context):
234     """Read PUT requests."""
235     name = paths.resource_from_path(path)
236     log.debug('xmlutils put path %s name %s', path, name)
237     if name in (item.name for item in collection.items):
238         # PUT is modifying an existing item
239         log.debug('Replacing item named %s', name)
240         return collection.replace(name, webdav_request, context=context)
241     else:
242         # PUT is adding a new item
243         log.debug('Putting a new item, because name %s is not known', name)
244         return collection.append(name, webdav_request, context=context)
245
246
247 def match_filter_element(vobject, fe):
248     if fe.tag == _tag("C", "comp-filter"):
249         comp = fe.get("name")
250         if comp:
251             if comp == vobject.name:
252                 hassub = False
253                 submatch = False
254                 for fc in fe.getchildren():
255                     if match_filter_element(vobject, fc):
256                         submatch = True
257                         break
258                     for vc in vobject.getChildren():
259                         hassub = True
260                         if match_filter_element (vc, fc):
261                             submatch = True
262                             break
263                     if submatch:
264                         break
265                 if not hassub or submatch:
266                     return True
267         return False
268     elif fe.tag == _tag("C", "time-range"):
269         try:
270             rruleset = vobject.rruleset
271         except AttributeError:
272             return False
273         start = fe.get("start")
274         end = fe.get("end")
275         if rruleset is None:
276             rruleset = dateutil.rrule.rruleset()
277             dtstart = vobject.dtstart.value
278             try:
279                 dtstart = datetime.datetime.combine(dtstart, datetime.time())
280             except Exception:
281                 0
282             if dtstart.tzinfo is None:
283                 dtstart = dtstart.replace(tzinfo = dateutil.tz.tzlocal())
284             rruleset.rdate(dtstart)
285         start_datetime = dateutil.parser.parse(start)
286         if start_datetime.tzinfo is None:
287             start_datetime = start_datetime.replace(tzinfo = dateutil.tz.tzlocal())
288         end_datetime = dateutil.parser.parse(end)
289         if end_datetime.tzinfo is None:
290             end_datetime = end_datetime.replace(tzinfo = dateutil.tz.tzlocal())
291         try:
292             if rruleset.between(start_datetime, end_datetime, True):
293                 return True
294         except TypeError:
295             start_datetime = start_datetime.replace(tzinfo = None)
296             end_datetime = end_datetime.replace(tzinfo = None)
297             try:
298                 if rruleset.between(start_datetime, end_datetime, True):
299                     return True
300             except TypeError:
301                 return True
302         return False
303     return True
304
305 def match_filter(item, filter):
306     if filter is None:
307         return True
308     if filter.tag != _tag("C", "filter"):
309         return True
310     for fe in filter.getchildren():
311         if match_filter_element(item.object, fe):
312             return True
313
314 def report(path, xml_request, collection):
315     """Read and answer REPORT requests.
316
317     Read rfc3253-3.6 for info.
318
319     """
320     # Reading request
321     root = ET.fromstring(xml_request)
322
323     prop_element = root.find(_tag("D", "prop"))
324     prop_list = prop_element.getchildren()
325     props = [prop.tag for prop in prop_list]
326
327     filter_element = root.find(_tag("C", "filter"))
328
329     if collection:
330         if root.tag == _tag("C", "calendar-multiget"):
331             # Read rfc4791-7.9 for info
332             hreferences = set((href_element.text for href_element
333                                in root.findall(_tag("D", "href"))))
334         else:
335             hreferences = (path,)
336     else:
337         hreferences = ()
338
339     # Writing answer
340     multistatus = ET.Element(_tag("D", "multistatus"))
341
342     for hreference in hreferences:
343         # Check if the reference is an item or a collection
344         name = paths.resource_from_path(hreference)
345         if name:
346             # Reference is an item
347             path = paths.collection_from_path(hreference) + "/"
348             items = (item for item in collection.items if item.name == name)
349         else:
350             # Reference is a collection
351             path = hreference
352             items = collection.items
353
354         
355         for item in items:
356             if not match_filter(item, filter_element):
357                 continue
358
359             response = ET.Element(_tag("D", "response"))
360             multistatus.append(response)
361
362             href = ET.Element(_tag("D", "href"))
363             href.text = path.rstrip('/') + '/' + item.name
364             response.append(href)
365
366             propstat = ET.Element(_tag("D", "propstat"))
367             response.append(propstat)
368
369             prop = ET.Element(_tag("D", "prop"))
370             propstat.append(prop)
371
372             for tag in props:
373                 element = ET.Element(tag)
374                 if tag == _tag("D", "getetag"):
375                     element.text = item.etag
376                 elif tag == _tag("C", "calendar-data"):
377                     element.text = item.text
378                 prop.append(element)
379
380             status = ET.Element(_tag("D", "status"))
381             status.text = _response(200)
382             propstat.append(status)
383
384     reply = ET.tostring(multistatus, config.get("encoding", "request"))
385         
386     return reply