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 "E": "http://apple.com/ns/ical/",
52 "CS": "http://calendarserver.org/ns/"}
54 log = logging.getLogger(__name__)
56 def _tag(short_name, local):
57 """Get XML Clark notation {uri(``short_name``)}``local``."""
58 return "{%s}%s" % (NAMESPACES[short_name], local)
62 """Return full W3C names from HTTP status codes."""
63 return "HTTP/1.1 %i %s" % (code, client.responses[code])
65 def delete(path, collection, context):
66 """Read and answer DELETE requests.
68 Read rfc4918-9.6 for info.
72 collection.remove(paths.resource_from_path(path), context=context)
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)
87 return ET.tostring(multistatus, config.get("encoding", "request"))
90 def propfind(path, xml_request, collection, depth, context):
91 """Read and answer PROPFIND requests.
93 Read rfc4918-9.1 for info.
97 item_name = paths.resource_from_path(path)
98 collection_name = paths.collection_from_path(path)
102 root = ET.fromstring(xml_request)
104 prop_element = root.find(_tag("D", "prop"))
108 if prop_element is not None:
109 prop_list = prop_element.getchildren()
110 props = [prop.tag for prop in prop_list]
112 props = [_tag("D", "resourcetype"),
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")]
125 multistatus = ET.Element(_tag("D", "multistatus"))
129 item = collection.get_item(item_name)
130 print "item_name %s item %s" % (item_name, item)
139 # depth is 1, infinity or not specified
140 # we limit ourselves to depth == 1
141 items = [collection] + collection.items
146 is_collection = isinstance(item, webdav.Collection)
148 response = ET.Element(_tag("D", "response"))
149 multistatus.append(response)
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)
155 propstat = ET.Element(_tag("D", "propstat"))
156 response.append(propstat)
158 prop = ET.Element(_tag("D", "prop"))
159 propstat.append(prop)
162 element = ET.Element(tag)
163 if tag == _tag("D", "resourcetype") and is_collection:
164 tag = ET.Element(_tag("C", "calendar"))
166 tag = ET.Element(_tag("A", "addressbook"))
168 tag = ET.Element(_tag("D", "collection"))
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"
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"))
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"))
198 elif tag == _tag("C", "supported-calendar-component-set"):
199 comp = ET.Element(_tag("C", "comp"))
200 comp.set("name", "VTODO") # pylint: disable=W0511
202 comp = ET.Element(_tag("C", "comp"))
203 comp.set("name", "VEVENT")
205 elif tag == _tag("D", "supported-report-set"):
206 tag = ET.Element(_tag("C", "calendar-multiget"))
208 tag = ET.Element(_tag("C", "filter"))
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
224 elif tag in (_tag("A", "addressbook-description"),
225 _tag("C", "calendar-description")) and is_collection:
226 element.text = collection.get_description()
229 status = ET.Element(_tag("D", "status"))
230 status.text = _response(200)
231 propstat.append(status)
233 return ET.tostring(multistatus, config.get("encoding", "request"))
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)
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)
250 def match_filter_element(vobject, fe):
251 if fe.tag == _tag("C", "comp-filter"):
252 comp = fe.get("name")
254 if comp == vobject.name:
257 for fc in fe.getchildren():
258 if match_filter_element(vobject, fc):
261 for vc in vobject.getChildren():
263 if match_filter_element (vc, fc):
268 if not hassub or submatch:
271 elif fe.tag == _tag("C", "time-range"):
273 rruleset = vobject.rruleset
274 except AttributeError:
276 start = fe.get("start")
279 rruleset = dateutil.rrule.rruleset()
280 dtstart = vobject.dtstart.value
282 dtstart = datetime.datetime.combine(dtstart, datetime.time())
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())
295 if rruleset.between(start_datetime, end_datetime, True):
298 start_datetime = start_datetime.replace(tzinfo = None)
299 end_datetime = end_datetime.replace(tzinfo = None)
301 if rruleset.between(start_datetime, end_datetime, True):
308 def match_filter(item, filter):
311 if filter.tag != _tag("C", "filter"):
313 for fe in filter.getchildren():
314 if match_filter_element(item.object, fe):
317 def report(path, xml_request, collection):
318 """Read and answer REPORT requests.
320 Read rfc3253-3.6 for info.
324 root = ET.fromstring(xml_request)
326 prop_element = root.find(_tag("D", "prop"))
327 prop_list = prop_element.getchildren()
328 props = [prop.tag for prop in prop_list]
330 filter_element = root.find(_tag("C", "filter"))
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"))))
338 hreferences = (path,)
343 multistatus = ET.Element(_tag("D", "multistatus"))
345 for hreference in hreferences:
346 # Check if the reference is an item or a collection
347 name = paths.resource_from_path(hreference)
349 # Reference is an item
350 path = paths.collection_from_path(hreference) + "/"
351 items = (item for item in collection.items if item.name == name)
353 # Reference is a collection
355 items = collection.items
359 if not match_filter(item, filter_element):
362 response = ET.Element(_tag("D", "response"))
363 multistatus.append(response)
365 href = ET.Element(_tag("D", "href"))
366 href.text = path.rstrip('/') + '/' + item.name
367 response.append(href)
369 propstat = ET.Element(_tag("D", "propstat"))
370 response.append(propstat)
372 prop = ET.Element(_tag("D", "prop"))
373 propstat.append(prop)
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
383 status = ET.Element(_tag("D", "status"))
384 status.text = _response(200)
385 propstat.append(status)
387 reply = ET.tostring(multistatus, config.get("encoding", "request"))