]> git.samba.org - jelmer/calypso.git/blob - calypso/xmlutils.py
Fix up encoding issues.
[jelmer/calypso.git] / calypso / xmlutils.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Calypso Server - Calendar Server
4 # Copyright © 2008-2011 Guillaume Ayoub
5 # Copyright © 2008 Nicolas Kandel
6 # Copyright © 2008 Pascal Halter
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Calypso.  If not, see <http://www.gnu.org/licenses/>.
20
21 """
22 XML and iCal requests manager.
23
24 Note that all these functions need to receive unicode objects for full
25 iCal requests (PUT) and string objects with charset correctly defined
26 in them for XML requests (all but PUT).
27
28 """
29
30 import xml.etree.ElementTree as ET
31 import time
32 import dateutil
33 import dateutil.parser
34 import dateutil.rrule
35 import dateutil.tz
36 import datetime
37
38 import urllib
39
40 from calypso import client, config, ical
41
42
43 NAMESPACES = {
44     "C": "urn:ietf:params:xml:ns:caldav",
45     "D": "DAV:",
46     "CS": "http://calendarserver.org/ns/"}
47
48
49 def _tag(short_name, local):
50     """Get XML Clark notation {uri(``short_name``)}``local``."""
51     return "{%s}%s" % (NAMESPACES[short_name], local)
52
53
54 def _response(code):
55     """Return full W3C names from HTTP status codes."""
56     return "HTTP/1.1 %i %s" % (code, client.responses[code])
57
58
59 def name_from_path(path):
60     """Return Calypso item name from ``path``."""
61     path_parts = path.strip("/").split("/")
62     return urllib.unquote(path_parts[-1]) if len(path_parts) > 2 else None
63
64 def delete(path, calendar):
65     """Read and answer DELETE requests.
66
67     Read rfc4918-9.6 for info.
68
69     """
70     # Reading request
71     print "delete name %s" % name_from_path(path)
72     calendar.remove(name_from_path(path))
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     print "Deleted\n"
88     return ET.tostring(multistatus, config.get("encoding", "request"))
89
90
91 def propfind(path, xml_request, calendar, depth):
92     """Read and answer PROPFIND requests.
93
94     Read rfc4918-9.1 for info.
95
96     """
97     # Reading request
98     root = ET.fromstring(xml_request)
99
100     prop_element = root.find(_tag("D", "prop"))
101     prop_list = prop_element.getchildren()
102     props = [prop.tag for prop in prop_list]
103     
104     # Writing answer
105     multistatus = ET.Element(_tag("D", "multistatus"))
106
107     if calendar:
108         if depth == "0":
109             items = [calendar]
110         else:
111             # depth is 1, infinity or not specified
112             # we limit ourselves to depth == 1
113             items = [calendar] + calendar.items
114     else:
115         items = []
116
117     for item in items:
118         is_calendar = isinstance(item, ical.Calendar)
119
120         response = ET.Element(_tag("D", "response"))
121         multistatus.append(response)
122
123         href = ET.Element(_tag("D", "href"))
124         href.text = path if is_calendar else path + item.name
125         response.append(href)
126
127         propstat = ET.Element(_tag("D", "propstat"))
128         response.append(propstat)
129
130         prop = ET.Element(_tag("D", "prop"))
131         propstat.append(prop)
132
133         for tag in props:
134             element = ET.Element(tag)
135             if tag == _tag("D", "resourcetype"):
136                 tag = ET.Element(_tag("C", "calendar"))
137                 element.append(tag)
138                 tag = ET.Element(_tag("D", "collection"))
139                 element.append(tag)
140             elif tag == _tag("D", "owner"):
141                 element.text = calendar.owner
142             elif tag == _tag("D", "getcontenttype"):
143                 if item.tag == 'VCARD':
144                     element.text = "text/vcard"
145                 else:
146                     element.text = "text/calendar"
147             elif tag == _tag("CS", "getctag") and is_calendar:
148                 element.text = item.ctag
149             elif tag == _tag("D", "getetag"):
150                 element.text = item.etag
151             elif tag == _tag("D", "displayname"):
152                 element.text = calendar.name
153             elif tag == _tag("D", "principal-URL"):
154                 # TODO: use a real principal URL, read rfc3744-4.2 for info
155                 tag = ET.Element(_tag("D", "href"))
156                 tag.text = path
157                 element.append(tag)
158             elif tag in (
159                 _tag("D", "principal-collection-set"),
160                 _tag("C", "calendar-user-address-set"),
161                 _tag("C", "calendar-home-set")):
162                 tag = ET.Element(_tag("D", "href"))
163                 tag.text = path
164                 element.append(tag)
165             elif tag == _tag("C", "supported-calendar-component-set"):
166                 comp = ET.Element(_tag("C", "comp"))
167                 comp.set("name", "VTODO") # pylint: disable=W0511
168                 element.append(comp)
169                 comp = ET.Element(_tag("C", "comp"))
170                 comp.set("name", "VEVENT")
171                 element.append(comp)
172             elif tag == _tag("D", "current-user-privilege-set"):
173                 privilege = ET.Element(_tag("D", "privilege"))
174                 privilege.append(ET.Element(_tag("D", "all")))
175                 element.append(privilege)
176             elif tag == _tag("D", "getcontentlength"):
177                 element.text = item.length
178             elif tag == _tag("D", "getlastmodified"):
179                 element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
180             prop.append(element)
181
182         status = ET.Element(_tag("D", "status"))
183         status.text = _response(200)
184         propstat.append(status)
185
186     return ET.tostring(multistatus, config.get("encoding", "request"))
187
188
189 def put(path, ical_request, calendar):
190     """Read PUT requests."""
191     name = name_from_path(path)
192     if name in (item.name for item in calendar.items):
193         # PUT is modifying an existing item
194         calendar.replace(name, ical_request)
195     else:
196         # PUT is adding a new item
197         calendar.append(name, ical_request)
198
199
200 def match_filter_element(vobject, fe):
201     if fe.tag == _tag("C", "comp-filter"):
202         comp = fe.get("name")
203         if comp:
204             if comp == vobject.name:
205                 hassub = False
206                 submatch = False
207                 for fc in fe.getchildren():
208                     if match_filter_element(vobject, fc):
209                         submatch = True
210                         break
211                     for vc in vobject.getChildren():
212                         hassub = True
213                         if match_filter_element (vc, fc):
214                             submatch = True
215                             break
216                     if submatch:
217                         break
218                 if not hassub or submatch:
219                     return True
220         return False
221     elif fe.tag == _tag("C", "time-range"):
222         try:
223             rruleset = vobject.rruleset
224         except AttributeError:
225             return False
226         start = fe.get("start")
227         end = fe.get("end")
228         if rruleset is None:
229             rruleset = dateutil.rrule.rruleset()
230             dtstart = vobject.dtstart.value
231             try:
232                 dtstart = datetime.datetime.combine(dtstart, datetime.time())
233             except Exception:
234                 0
235             if dtstart.tzinfo is None:
236                 dtstart = dtstart.replace(tzinfo = dateutil.tz.tzlocal())
237             rruleset.rdate(dtstart)
238         start_datetime = dateutil.parser.parse(start)
239         if start_datetime.tzinfo is None:
240             start_datetime = start_datetime.replace(tzinfo = dateutil.tz.tzlocal())
241         end_datetime = dateutil.parser.parse(end)
242         if end_datetime.tzinfo is None:
243             end_datetime = end_datetime.replace(tzinfo = dateutil.tz.tzlocal())
244         try:
245             if rruleset.between(start_datetime, end_datetime, True):
246                 return True
247         except TypeError:
248             start_datetime = start_datetime.replace(tzinfo = None)
249             end_datetime = end_datetime.replace(tzinfo = None)
250             try:
251                 if rruleset.between(start_datetime, end_datetime, True):
252                     return True
253             except TypeError:
254                 return True
255         return False
256     return True
257
258 def match_filter(item, filter):
259     if filter is None:
260         return True
261     if filter.tag != _tag("C", "filter"):
262         return True
263     for fe in filter.getchildren():
264         if match_filter_element(item.object, fe):
265             return True
266
267 def report(path, xml_request, calendar):
268     """Read and answer REPORT requests.
269
270     Read rfc3253-3.6 for info.
271
272     """
273     # Reading request
274     root = ET.fromstring(xml_request)
275
276     prop_element = root.find(_tag("D", "prop"))
277     prop_list = prop_element.getchildren()
278     props = [prop.tag for prop in prop_list]
279
280     filter_element = root.find(_tag("C", "filter"))
281
282     if calendar:
283         if root.tag == _tag("C", "calendar-multiget"):
284             # Read rfc4791-7.9 for info
285             hreferences = set((href_element.text for href_element
286                                in root.findall(_tag("D", "href"))))
287         else:
288             hreferences = (path,)
289     else:
290         hreferences = ()
291
292     # Writing answer
293     multistatus = ET.Element(_tag("D", "multistatus"))
294
295     for hreference in hreferences:
296         # Check if the reference is an item or a calendar
297         name = name_from_path(hreference)
298         if name:
299             # Reference is an item
300             path = "/".join(hreference.split("/")[:-1]) + "/"
301             items = (item for item in calendar.items if item.name == name)
302         else:
303             # Reference is a calendar
304             path = hreference
305             items = calendar.items
306
307         
308         for item in items:
309             if not match_filter(item, filter_element):
310                 continue
311
312             response = ET.Element(_tag("D", "response"))
313             multistatus.append(response)
314
315             href = ET.Element(_tag("D", "href"))
316             href.text = path + item.name
317             response.append(href)
318
319             propstat = ET.Element(_tag("D", "propstat"))
320             response.append(propstat)
321
322             prop = ET.Element(_tag("D", "prop"))
323             propstat.append(prop)
324
325             for tag in props:
326                 element = ET.Element(tag)
327                 if tag == _tag("D", "getetag"):
328                     element.text = item.etag
329                 elif tag == _tag("C", "calendar-data"):
330                     element.text = item.text
331                 prop.append(element)
332
333             status = ET.Element(_tag("D", "status"))
334             status.text = _response(200)
335             propstat.append(status)
336
337     reply = ET.tostring(multistatus, config.get("encoding", "request"))
338         
339     return reply