1 # -*- coding: utf-8 -*-
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
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.
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.
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/>.
22 XML and iCal requests manager.
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).
30 import xml.etree.ElementTree as ET
33 import dateutil.parser
40 from calypso import client, config, ical
44 "C": "urn:ietf:params:xml:ns:caldav",
46 "CS": "http://calendarserver.org/ns/"}
49 def _tag(short_name, local):
50 """Get XML Clark notation {uri(``short_name``)}``local``."""
51 return "{%s}%s" % (NAMESPACES[short_name], local)
55 """Return full W3C names from HTTP status codes."""
56 return "HTTP/1.1 %i %s" % (code, client.responses[code])
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
64 def delete(path, calendar):
65 """Read and answer DELETE requests.
67 Read rfc4918-9.6 for info.
71 print "delete name %s" % name_from_path(path)
72 calendar.remove(name_from_path(path))
75 multistatus = ET.Element(_tag("D", "multistatus"))
76 response = ET.Element(_tag("D", "response"))
77 multistatus.append(response)
79 href = ET.Element(_tag("D", "href"))
83 status = ET.Element(_tag("D", "status"))
84 status.text = _response(200)
85 response.append(status)
88 return ET.tostring(multistatus, config.get("encoding", "request"))
91 def propfind(path, xml_request, calendar, depth):
92 """Read and answer PROPFIND requests.
94 Read rfc4918-9.1 for info.
98 root = ET.fromstring(xml_request)
100 prop_element = root.find(_tag("D", "prop"))
101 prop_list = prop_element.getchildren()
102 props = [prop.tag for prop in prop_list]
105 multistatus = ET.Element(_tag("D", "multistatus"))
111 # depth is 1, infinity or not specified
112 # we limit ourselves to depth == 1
113 items = [calendar] + calendar.items
118 is_calendar = isinstance(item, ical.Calendar)
120 response = ET.Element(_tag("D", "response"))
121 multistatus.append(response)
123 href = ET.Element(_tag("D", "href"))
124 href.text = path if is_calendar else path + item.name
125 response.append(href)
127 propstat = ET.Element(_tag("D", "propstat"))
128 response.append(propstat)
130 prop = ET.Element(_tag("D", "prop"))
131 propstat.append(prop)
134 element = ET.Element(tag)
135 if tag == _tag("D", "resourcetype"):
136 tag = ET.Element(_tag("C", "calendar"))
138 tag = ET.Element(_tag("D", "collection"))
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"
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"))
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"))
165 elif tag == _tag("C", "supported-calendar-component-set"):
166 comp = ET.Element(_tag("C", "comp"))
167 comp.set("name", "VTODO") # pylint: disable=W0511
169 comp = ET.Element(_tag("C", "comp"))
170 comp.set("name", "VEVENT")
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)
182 status = ET.Element(_tag("D", "status"))
183 status.text = _response(200)
184 propstat.append(status)
186 return ET.tostring(multistatus, config.get("encoding", "request"))
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)
196 # PUT is adding a new item
197 calendar.append(name, ical_request)
200 def match_filter_element(vobject, fe):
201 if fe.tag == _tag("C", "comp-filter"):
202 comp = fe.get("name")
204 if comp == vobject.name:
207 for fc in fe.getchildren():
208 if match_filter_element(vobject, fc):
211 for vc in vobject.getChildren():
213 if match_filter_element (vc, fc):
218 if not hassub or submatch:
221 elif fe.tag == _tag("C", "time-range"):
223 rruleset = vobject.rruleset
224 except AttributeError:
226 start = fe.get("start")
229 rruleset = dateutil.rrule.rruleset()
230 dtstart = vobject.dtstart.value
232 dtstart = datetime.datetime.combine(dtstart, datetime.time())
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())
245 if rruleset.between(start_datetime, end_datetime, True):
248 start_datetime = start_datetime.replace(tzinfo = None)
249 end_datetime = end_datetime.replace(tzinfo = None)
251 if rruleset.between(start_datetime, end_datetime, True):
258 def match_filter(item, filter):
261 if filter.tag != _tag("C", "filter"):
263 for fe in filter.getchildren():
264 if match_filter_element(item.object, fe):
267 def report(path, xml_request, calendar):
268 """Read and answer REPORT requests.
270 Read rfc3253-3.6 for info.
274 root = ET.fromstring(xml_request)
276 prop_element = root.find(_tag("D", "prop"))
277 prop_list = prop_element.getchildren()
278 props = [prop.tag for prop in prop_list]
280 filter_element = root.find(_tag("C", "filter"))
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"))))
288 hreferences = (path,)
293 multistatus = ET.Element(_tag("D", "multistatus"))
295 for hreference in hreferences:
296 # Check if the reference is an item or a calendar
297 name = name_from_path(hreference)
299 # Reference is an item
300 path = "/".join(hreference.split("/")[:-1]) + "/"
301 items = (item for item in calendar.items if item.name == name)
303 # Reference is a calendar
305 items = calendar.items
309 if not match_filter(item, filter_element):
312 response = ET.Element(_tag("D", "response"))
313 multistatus.append(response)
315 href = ET.Element(_tag("D", "href"))
316 href.text = path + item.name
317 response.append(href)
319 propstat = ET.Element(_tag("D", "propstat"))
320 response.append(propstat)
322 prop = ET.Element(_tag("D", "prop"))
323 propstat.append(prop)
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
333 status = ET.Element(_tag("D", "status"))
334 status.text = _response(200)
335 propstat.append(status)
337 reply = ET.tostring(multistatus, config.get("encoding", "request"))