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
41 from . import client, config, webdav, paths, principal
43 __package__ = 'calypso.xmlutils'
46 "C": "urn:ietf:params:xml:ns:caldav",
47 "A": "urn:ietf:params:xml:ns:carddav",
49 "E": "http://apple.com/ns/ical/",
50 "CS": "http://calendarserver.org/ns/"}
52 log = logging.getLogger(__name__)
54 def _tag(short_name, local):
55 """Get XML Clark notation {uri(``short_name``)}``local``."""
56 return "{%s}%s" % (NAMESPACES[short_name], local)
60 """Return full W3C names from HTTP status codes."""
61 return "HTTP/1.1 %i %s" % (code, client.responses[code])
63 def delete(path, collection, context):
64 """Read and answer DELETE requests.
66 Read rfc4918-9.6 for info.
70 collection.remove(paths.resource_from_path(path), context=context)
73 multistatus = ET.Element(_tag("D", "multistatus"))
74 response = ET.Element(_tag("D", "response"))
75 multistatus.append(response)
77 href = ET.Element(_tag("D", "href"))
81 status = ET.Element(_tag("D", "status"))
82 status.text = _response(200)
83 response.append(status)
85 return ET.tostring(multistatus, config.get("encoding", "request"))
87 def identify_resource(path):
88 """Return a Resource object corresponding to the path (this is used for
89 everything that is not a collection, like Principal and HomeSet objects)"""
92 left, right = config.get('server', 'user_principal').split('%(user)s')
94 raise ValueError("user_principal setting must contain %(user)s.")
96 if not path.startswith(left):
99 remainder = path[len(left):]
100 if right not in remainder:
103 username = remainder[:remainder.index(right)]
104 remainder = remainder[remainder.index(right)+len(right):]
106 if remainder == principal.AddressbookHomeSet.type_dependent_suffix + "/":
107 return principal.AddressbookHomeSet(username)
108 elif remainder == principal.CalendarHomeSet.type_dependent_suffix + "/":
109 return principal.CalendarHomeSet(username)
110 elif remainder == "":
111 return principal.Principal(username)
115 def propfind(path, xml_request, collection, depth, context):
116 """Read and answer PROPFIND requests.
118 Read rfc4918-9.1 for info.
122 item_name = paths.resource_from_path(path)
126 root = ET.fromstring(xml_request)
128 prop_element = root.find(_tag("D", "prop"))
132 if prop_element is not None:
133 prop_list = prop_element.getchildren()
134 props = [prop.tag for prop in prop_list]
136 props = [_tag("D", "resourcetype"),
138 _tag("D", "getcontenttype"),
139 _tag("D", "getetag"),
140 _tag("D", "principal-collection-set"),
141 _tag("C", "supported-calendar-component-set"),
142 _tag("D", "supported-report-set"),
143 _tag("D", "current-user-privilege-set"),
144 _tag("D", "getcontentlength"),
145 _tag("D", "getlastmodified")]
149 multistatus = ET.Element(_tag("D", "multistatus"))
151 resource = identify_resource(path)
153 if resource is not None:
154 items = resource.propfind_children(depth)
157 item = collection.get_item(item_name)
158 print "item_name %s item %s" % (item_name, item)
167 # depth is 1, infinity or not specified
168 # we limit ourselves to depth == 1
169 items = [collection] + collection.items
174 is_collection = isinstance(item, webdav.Collection)
175 is_resource = isinstance(item, principal.Resource)
178 # parentcollectionhack. this is not the way to do it, but much of
179 # the below code relies on items which are collection members to
180 # have their parent collection in the collection variable. get rid
181 # of the collection propfind-global variable, and this and all
182 # other occurrences of "parentcollectionhack" can be dropped.
185 response = ET.Element(_tag("D", "response"))
186 multistatus.append(response)
188 href = ET.Element(_tag("D", "href"))
190 href.text = item.urlpath
192 href.text = item.urlpath
194 href.text = collection.urlpath + item.name
195 response.append(href)
197 propstat = ET.Element(_tag("D", "propstat"))
198 response.append(propstat)
200 prop = ET.Element(_tag("D", "prop"))
201 propstat.append(prop)
204 element = ET.Element(tag)
205 if tag == _tag("D", "resourcetype") and is_collection:
206 if collection.is_calendar:
207 tag = ET.Element(_tag("C", "calendar"))
209 if collection.is_addressbook:
210 tag = ET.Element(_tag("A", "addressbook"))
212 tag = ET.Element(_tag("D", "collection"))
214 elif tag == _tag("D", "owner"):
215 element.text = collection.owner
216 elif tag == _tag("D", "getcontenttype"):
217 if item.tag == 'VCARD':
218 element.text = "text/vcard"
220 element.text = "text/calendar"
221 elif tag == _tag("CS", "getctag") and is_collection:
222 element.text = item.ctag
223 elif tag == _tag("D", "getetag"):
224 element.text = item.etag
225 elif tag == _tag("D", "displayname") and is_collection:
226 element.text = collection.name
227 elif tag == _tag("E", "calendar-color") and is_collection:
228 element.text = collection.color
229 elif tag == _tag("D", "principal-URL"):
230 # TODO: use a real principal URL, read rfc3744-4.2 for info
231 tag = ET.Element(_tag("D", "href"))
235 _tag("D", "principal-collection-set"),
236 _tag("C", "calendar-user-address-set"),
238 # not meaningfully implemented yet
239 tag = ET.Element(_tag("D", "href"))
242 elif tag == _tag("C", "supported-calendar-component-set"):
243 comp = ET.Element(_tag("C", "comp"))
244 comp.set("name", "VTODO") # pylint: disable=W0511
246 comp = ET.Element(_tag("C", "comp"))
247 comp.set("name", "VEVENT")
249 elif tag == _tag("D", "supported-report-set"):
250 tag = ET.Element(_tag("C", "calendar-multiget"))
252 tag = ET.Element(_tag("C", "filter"))
254 elif tag == _tag("D", "current-user-privilege-set"):
255 privilege = ET.Element(_tag("D", "privilege"))
256 privilege.append(ET.Element(_tag("D", "all")))
257 element.append(privilege)
258 elif tag == _tag("D", "getcontentlength"):
259 element.text = item.length
260 elif tag == _tag("D", "getlastmodified"):
261 # element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
262 # element.text = email.utils.formatdate(item.last_modified)
263 element.text = email.utils.formatdate(time.mktime(item.last_modified))
264 elif tag == _tag("D", "current-user-principal"):
265 tag = ET.Element(_tag("D", "href"))
266 tag.text = config.get("server", "user_principal") % context
268 elif tag in (_tag("A", "addressbook-description"),
269 _tag("C", "calendar-description")) and is_collection:
270 element.text = collection.get_description()
273 status = ET.Element(_tag("D", "status"))
274 status.text = _response(200)
275 propstat.append(status)
277 return ET.tostring(multistatus, config.get("encoding", "request"))
280 def put(path, webdav_request, collection, context):
281 """Read PUT requests."""
282 name = paths.resource_from_path(path)
283 log.debug('xmlutils put path %s name %s', path, name)
284 if name in (item.name for item in collection.items):
285 # PUT is modifying an existing item
286 log.debug('Replacing item named %s', name)
287 return collection.replace(name, webdav_request, context=context)
289 # PUT is adding a new item
290 log.debug('Putting a new item, because name %s is not known', name)
291 return collection.append(name, webdav_request, context=context)
294 def match_filter_element(vobject, fe):
295 if fe.tag == _tag("C", "comp-filter"):
296 comp = fe.get("name")
298 if comp == vobject.name:
301 for fc in fe.getchildren():
302 if match_filter_element(vobject, fc):
305 for vc in vobject.getChildren():
307 if match_filter_element (vc, fc):
312 if not hassub or submatch:
315 elif fe.tag == _tag("C", "time-range"):
317 rruleset = vobject.rruleset
318 except AttributeError:
320 start = fe.get("start")
322 # According to RFC 4791, one of start and stop must be set,
323 # but the other can be empty. If both are empty, the
324 # specification is violated.
325 if start is None and end is None:
326 msg = "time-range missing both start and stop attribute (required by RFC 4791)"
328 raise ValueError(msg)
329 # RFC 4791 state if start is missing, assume it is -infinity
331 start = "00010101T000000Z" # start of year one
332 # RFC 4791 state if end is missing, assume it is +infinity
334 end = "99991231T235959Z" # last date with four digit year
336 rruleset = dateutil.rrule.rruleset()
337 dtstart = vobject.dtstart.value
339 dtstart = datetime.datetime.combine(dtstart, datetime.time())
342 if dtstart.tzinfo is None:
343 dtstart = dtstart.replace(tzinfo = dateutil.tz.tzlocal())
344 rruleset.rdate(dtstart)
345 start_datetime = dateutil.parser.parse(start)
346 if start_datetime.tzinfo is None:
347 start_datetime = start_datetime.replace(tzinfo = dateutil.tz.tzlocal())
348 end_datetime = dateutil.parser.parse(end)
349 if end_datetime.tzinfo is None:
350 end_datetime = end_datetime.replace(tzinfo = dateutil.tz.tzlocal())
352 if rruleset.between(start_datetime, end_datetime, True):
355 start_datetime = start_datetime.replace(tzinfo = None)
356 end_datetime = end_datetime.replace(tzinfo = None)
358 if rruleset.between(start_datetime, end_datetime, True):
365 def match_filter(item, filter):
368 if filter.tag != _tag("C", "filter"):
370 for fe in filter.getchildren():
371 if match_filter_element(item.object, fe):
375 def report(path, xml_request, collection):
376 """Read and answer REPORT requests.
378 Read rfc3253-3.6 for info.
382 root = ET.fromstring(xml_request)
384 prop_element = root.find(_tag("D", "prop"))
385 prop_list = prop_element.getchildren()
386 props = [prop.tag for prop in prop_list]
388 filter_element = root.find(_tag("C", "filter"))
391 if root.tag == _tag("C", "calendar-multiget") or root.tag == _tag('A', 'addressbook-multiget'):
392 # Read rfc4791-7.9 for info
393 hreferences = set((href_element.text for href_element
394 in root.findall(_tag("D", "href"))))
396 hreferences = (path,)
401 multistatus = ET.Element(_tag("D", "multistatus"))
403 for hreference in hreferences:
404 # Check if the reference is an item or a collection
405 name = paths.resource_from_path(hreference)
407 # Reference is an item
408 path = paths.collection_from_path(hreference) + "/"
409 items = (item for item in collection.items if item.name == name)
411 # Reference is a collection
413 items = collection.items
417 if not match_filter(item, filter_element):
420 response = ET.Element(_tag("D", "response"))
421 multistatus.append(response)
423 href = ET.Element(_tag("D", "href"))
424 href.text = path.rstrip('/') + '/' + item.name
425 response.append(href)
427 propstat = ET.Element(_tag("D", "propstat"))
428 response.append(propstat)
430 prop = ET.Element(_tag("D", "prop"))
431 propstat.append(prop)
434 element = ET.Element(tag)
435 if tag == _tag("D", "getetag"):
436 element.text = item.etag
437 elif tag == _tag("C", "calendar-data"):
438 element.text = item.text
439 elif tag == _tag("A", "address-data"):
440 element.text = item.text
443 status = ET.Element(_tag("D", "status"))
444 status.text = _response(200)
445 propstat.append(status)
447 reply = ET.tostring(multistatus, config.get("encoding", "request"))