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