1 # -*- coding: utf-8 -*-
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
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.
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.
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/>.
23 XML and iCal requests manager.
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).
31 import xml.etree.ElementTree as ET
34 import dateutil.parser
43 from . import client, config, webdav, paths
45 __package__ = 'calypso.xmlutils'
48 "C": "urn:ietf:params:xml:ns:caldav",
49 "A": "urn:ietf:params:xml:ns:carddav",
51 "CS": "http://calendarserver.org/ns/"}
53 log = logging.getLogger(__name__)
55 def _tag(short_name, local):
56 """Get XML Clark notation {uri(``short_name``)}``local``."""
57 return "{%s}%s" % (NAMESPACES[short_name], local)
61 """Return full W3C names from HTTP status codes."""
62 return "HTTP/1.1 %i %s" % (code, client.responses[code])
64 def delete(path, collection, context):
65 """Read and answer DELETE requests.
67 Read rfc4918-9.6 for info.
71 collection.remove(paths.resource_from_path(path), context=context)
74 multistatus = ET.Element(_tag("D", "multistatus"))
75 response = ET.Element(_tag("D", "response"))
76 multistatus.append(response)
78 href = ET.Element(_tag("D", "href"))
82 status = ET.Element(_tag("D", "status"))
83 status.text = _response(200)
84 response.append(status)
86 return ET.tostring(multistatus, config.get("encoding", "request"))
89 def propfind(path, xml_request, collection, depth, context):
90 """Read and answer PROPFIND requests.
92 Read rfc4918-9.1 for info.
96 item_name = paths.resource_from_path(path)
97 collection_name = paths.collection_from_path(path)
101 root = ET.fromstring(xml_request)
103 prop_element = root.find(_tag("D", "prop"))
107 if prop_element is not None:
108 prop_list = prop_element.getchildren()
109 props = [prop.tag for prop in prop_list]
111 props = [_tag("D", "resourcetype"),
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")]
124 multistatus = ET.Element(_tag("D", "multistatus"))
128 item = collection.get_item(item_name)
129 print "item_name %s item %s" % (item_name, item)
138 # depth is 1, infinity or not specified
139 # we limit ourselves to depth == 1
140 items = [collection] + collection.items
145 is_collection = isinstance(item, webdav.Collection)
147 response = ET.Element(_tag("D", "response"))
148 multistatus.append(response)
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)
154 propstat = ET.Element(_tag("D", "propstat"))
155 response.append(propstat)
157 prop = ET.Element(_tag("D", "prop"))
158 propstat.append(prop)
161 element = ET.Element(tag)
162 if tag == _tag("D", "resourcetype") and is_collection:
163 tag = ET.Element(_tag("C", "calendar"))
165 tag = ET.Element(_tag("A", "addressbook"))
167 tag = ET.Element(_tag("D", "collection"))
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"
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"))
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"))
195 elif tag == _tag("C", "supported-calendar-component-set"):
196 comp = ET.Element(_tag("C", "comp"))
197 comp.set("name", "VTODO") # pylint: disable=W0511
199 comp = ET.Element(_tag("C", "comp"))
200 comp.set("name", "VEVENT")
202 elif tag == _tag("D", "supported-report-set"):
203 tag = ET.Element(_tag("C", "calendar-multiget"))
205 tag = ET.Element(_tag("C", "filter"))
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
221 elif tag in (_tag("A", "addressbook-description"),
222 _tag("C", "calendar-description")) and is_collection:
223 element.text = collection.get_description()
226 status = ET.Element(_tag("D", "status"))
227 status.text = _response(200)
228 propstat.append(status)
230 return ET.tostring(multistatus, config.get("encoding", "request"))
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)
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)
247 def match_filter_element(vobject, fe):
248 if fe.tag == _tag("C", "comp-filter"):
249 comp = fe.get("name")
251 if comp == vobject.name:
254 for fc in fe.getchildren():
255 if match_filter_element(vobject, fc):
258 for vc in vobject.getChildren():
260 if match_filter_element (vc, fc):
265 if not hassub or submatch:
268 elif fe.tag == _tag("C", "time-range"):
270 rruleset = vobject.rruleset
271 except AttributeError:
273 start = fe.get("start")
276 rruleset = dateutil.rrule.rruleset()
277 dtstart = vobject.dtstart.value
279 dtstart = datetime.datetime.combine(dtstart, datetime.time())
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())
292 if rruleset.between(start_datetime, end_datetime, True):
295 start_datetime = start_datetime.replace(tzinfo = None)
296 end_datetime = end_datetime.replace(tzinfo = None)
298 if rruleset.between(start_datetime, end_datetime, True):
305 def match_filter(item, filter):
308 if filter.tag != _tag("C", "filter"):
310 for fe in filter.getchildren():
311 if match_filter_element(item.object, fe):
314 def report(path, xml_request, collection):
315 """Read and answer REPORT requests.
317 Read rfc3253-3.6 for info.
321 root = ET.fromstring(xml_request)
323 prop_element = root.find(_tag("D", "prop"))
324 prop_list = prop_element.getchildren()
325 props = [prop.tag for prop in prop_list]
327 filter_element = root.find(_tag("C", "filter"))
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"))))
335 hreferences = (path,)
340 multistatus = ET.Element(_tag("D", "multistatus"))
342 for hreference in hreferences:
343 # Check if the reference is an item or a collection
344 name = paths.resource_from_path(hreference)
346 # Reference is an item
347 path = paths.collection_from_path(hreference) + "/"
348 items = (item for item in collection.items if item.name == name)
350 # Reference is a collection
352 items = collection.items
356 if not match_filter(item, filter_element):
359 response = ET.Element(_tag("D", "response"))
360 multistatus.append(response)
362 href = ET.Element(_tag("D", "href"))
363 href.text = path.rstrip('/') + '/' + item.name
364 response.append(href)
366 propstat = ET.Element(_tag("D", "propstat"))
367 response.append(propstat)
369 prop = ET.Element(_tag("D", "prop"))
370 propstat.append(prop)
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
380 status = ET.Element(_tag("D", "status"))
381 status.text = _response(200)
382 propstat.append(status)
384 reply = ET.tostring(multistatus, config.get("encoding", "request"))