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
42 from .xmlutils_generic import _tag
44 __package__ = 'calypso.xmlutils'
46 log = logging.getLogger(__name__)
50 """Return full W3C names from HTTP status codes."""
51 return "HTTP/1.1 %i %s" % (code, client.responses[code])
53 def delete(path, collection, context):
54 """Read and answer DELETE requests.
56 Read rfc4918-9.6 for info.
60 collection.remove(paths.resource_from_path(path), context=context)
63 multistatus = ET.Element(_tag("D", "multistatus"))
64 response = ET.Element(_tag("D", "response"))
65 multistatus.append(response)
67 href = ET.Element(_tag("D", "href"))
71 status = ET.Element(_tag("D", "status"))
72 status.text = _response(200)
73 response.append(status)
75 return ET.tostring(multistatus, config.get("encoding", "request"))
77 def propfind(path, xml_request, collection, resource, depth, context):
78 """Read and answer PROPFIND requests.
80 Read rfc4918-9.1 for info.
84 item_name = paths.resource_from_path(path)
88 root = ET.fromstring(xml_request)
90 prop_element = root.find(_tag("D", "prop"))
94 if prop_element is not None:
95 prop_list = prop_element.getchildren()
96 props = [prop.tag for prop in prop_list]
98 props = [_tag("D", "resourcetype"),
100 _tag("D", "getcontenttype"),
101 _tag("D", "getetag"),
102 _tag("D", "principal-collection-set"),
103 _tag("C", "supported-calendar-component-set"),
104 _tag("D", "supported-report-set"),
105 _tag("D", "current-user-privilege-set"),
106 _tag("D", "getcontentlength"),
107 _tag("D", "getlastmodified")]
111 multistatus = ET.Element(_tag("D", "multistatus"))
113 if resource is not None:
114 items = resource.propfind_children(depth, context)
117 item = collection.get_item(item_name)
118 print "item_name %s item %s" % (item_name, item)
127 # depth is 1, infinity or not specified
128 # we limit ourselves to depth == 1
129 items = [collection] + collection.items
134 is_collection = isinstance(item, webdav.Collection)
135 is_resource = isinstance(item, principal.Resource)
137 response = ET.Element(_tag("D", "response"))
138 multistatus.append(response)
140 href = ET.Element(_tag("D", "href"))
142 href.text = item.urlpath
144 href.text = item.urlpath
146 href.text = collection.urlpath + item.name
147 response.append(href)
149 propstat = ET.Element(_tag("D", "propstat"))
150 response.append(propstat)
152 prop = ET.Element(_tag("D", "prop"))
153 propstat.append(prop)
156 element = ET.Element(tag)
157 if tag == _tag("D", "resourcetype") and is_collection:
158 if collection.is_calendar:
159 tag = ET.Element(_tag("C", "calendar"))
161 if collection.is_addressbook:
162 tag = ET.Element(_tag("A", "addressbook"))
164 tag = ET.Element(_tag("D", "collection"))
166 elif tag == _tag("D", "owner"):
167 element.text = collection.owner
168 elif tag == _tag("D", "getcontenttype"):
169 if item.tag == 'VCARD':
170 element.text = "text/vcard"
172 element.text = "text/calendar"
173 elif tag == _tag("CS", "getctag") and is_collection:
174 element.text = item.ctag
175 elif tag == _tag("D", "getetag"):
176 element.text = item.etag
177 elif tag == _tag("D", "displayname") and is_collection:
178 element.text = collection.name
179 elif tag == _tag("E", "calendar-color") and is_collection:
180 element.text = collection.color
181 elif tag == _tag("D", "principal-URL"):
182 # TODO: use a real principal URL, read rfc3744-4.2 for info
183 tag = ET.Element(_tag("D", "href"))
187 _tag("D", "principal-collection-set"),
188 _tag("C", "calendar-user-address-set"),
190 # not meaningfully implemented yet
191 tag = ET.Element(_tag("D", "href"))
194 elif tag == _tag("C", "supported-calendar-component-set"):
195 comp = ET.Element(_tag("C", "comp"))
196 comp.set("name", "VTODO") # pylint: disable=W0511
198 comp = ET.Element(_tag("C", "comp"))
199 comp.set("name", "VEVENT")
201 elif tag == _tag("D", "supported-report-set"):
202 tag = ET.Element(_tag("C", "calendar-multiget"))
204 tag = ET.Element(_tag("C", "filter"))
206 elif tag == _tag("D", "current-user-privilege-set"):
207 privilege = ET.Element(_tag("D", "privilege"))
208 privilege.append(ET.Element(_tag("D", "all")))
209 element.append(privilege)
210 elif tag == _tag("D", "getcontentlength"):
211 element.text = item.length
212 elif tag == _tag("D", "getlastmodified"):
213 # element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
214 # element.text = email.utils.formatdate(item.last_modified)
215 element.text = email.utils.formatdate(time.mktime(item.last_modified))
216 elif tag == _tag("D", "current-user-principal"):
217 tag = ET.Element(_tag("D", "href"))
218 tag.text = config.get("server", "user_principal") % context
220 elif tag in (_tag("A", "addressbook-description"),
221 _tag("C", "calendar-description")) and is_collection:
222 element.text = collection.get_description()
225 status = ET.Element(_tag("D", "status"))
226 status.text = _response(200)
227 propstat.append(status)
229 return ET.tostring(multistatus, config.get("encoding", "request"))
232 def put(path, webdav_request, collection, context):
233 """Read PUT requests."""
234 name = paths.resource_from_path(path)
235 log.debug('xmlutils put path %s name %s', path, name)
236 if name in (item.name for item in collection.items):
237 # PUT is modifying an existing item
238 log.debug('Replacing item named %s', name)
239 return collection.replace(name, webdav_request, context=context)
241 # PUT is adding a new item
242 log.debug('Putting a new item, because name %s is not known', name)
243 return collection.append(name, webdav_request, context=context)
246 def match_filter_element(vobject, fe):
247 if fe.tag == _tag("C", "comp-filter"):
248 comp = fe.get("name")
250 if comp == vobject.name:
253 for fc in fe.getchildren():
254 if match_filter_element(vobject, fc):
257 for vc in vobject.getChildren():
259 if match_filter_element (vc, fc):
264 if not hassub or submatch:
267 elif fe.tag == _tag("C", "time-range"):
269 rruleset = vobject.rruleset
270 except AttributeError:
272 start = fe.get("start")
274 # According to RFC 4791, one of start and stop must be set,
275 # but the other can be empty. If both are empty, the
276 # specification is violated.
277 if start is None and end is None:
278 msg = "time-range missing both start and stop attribute (required by RFC 4791)"
280 raise ValueError(msg)
281 # RFC 4791 state if start is missing, assume it is -infinity
283 start = "00010101T000000Z" # start of year one
284 # RFC 4791 state if end is missing, assume it is +infinity
286 end = "99991231T235959Z" # last date with four digit year
288 rruleset = dateutil.rrule.rruleset()
289 dtstart = vobject.dtstart.value
291 dtstart = datetime.datetime.combine(dtstart, datetime.time())
294 if dtstart.tzinfo is None:
295 dtstart = dtstart.replace(tzinfo = dateutil.tz.tzlocal())
296 rruleset.rdate(dtstart)
297 start_datetime = dateutil.parser.parse(start)
298 if start_datetime.tzinfo is None:
299 start_datetime = start_datetime.replace(tzinfo = dateutil.tz.tzlocal())
300 end_datetime = dateutil.parser.parse(end)
301 if end_datetime.tzinfo is None:
302 end_datetime = end_datetime.replace(tzinfo = dateutil.tz.tzlocal())
304 if rruleset.between(start_datetime, end_datetime, True):
307 start_datetime = start_datetime.replace(tzinfo = None)
308 end_datetime = end_datetime.replace(tzinfo = None)
310 if rruleset.between(start_datetime, end_datetime, True):
317 def match_filter(item, filter):
320 if filter.tag != _tag("C", "filter"):
322 for fe in filter.getchildren():
323 if match_filter_element(item.object, fe):
327 def report(path, xml_request, collection):
328 """Read and answer REPORT requests.
330 Read rfc3253-3.6 for info.
334 root = ET.fromstring(xml_request)
336 prop_element = root.find(_tag("D", "prop"))
337 prop_list = prop_element.getchildren()
338 props = [prop.tag for prop in prop_list]
340 filter_element = root.find(_tag("C", "filter"))
343 if root.tag == _tag("C", "calendar-multiget") or root.tag == _tag('A', 'addressbook-multiget'):
344 # Read rfc4791-7.9 for info
345 hreferences = set((href_element.text for href_element
346 in root.findall(_tag("D", "href"))))
348 hreferences = (path,)
353 multistatus = ET.Element(_tag("D", "multistatus"))
355 for hreference in hreferences:
356 # Check if the reference is an item or a collection
357 name = paths.resource_from_path(hreference)
359 # Reference is an item
360 path = paths.collection_from_path(hreference) + "/"
361 items = (item for item in collection.items if item.name == name)
363 # Reference is a collection
365 items = collection.items
369 if not match_filter(item, filter_element):
372 response = ET.Element(_tag("D", "response"))
373 multistatus.append(response)
375 href = ET.Element(_tag("D", "href"))
376 href.text = path.rstrip('/') + '/' + item.name
377 response.append(href)
379 propstat = ET.Element(_tag("D", "propstat"))
380 response.append(propstat)
382 prop = ET.Element(_tag("D", "prop"))
383 propstat.append(prop)
386 element = ET.Element(tag)
387 if tag == _tag("D", "getetag"):
388 element.text = item.etag
389 elif tag == _tag("C", "calendar-data"):
390 element.text = item.text
391 elif tag == _tag("A", "address-data"):
392 element.text = item.text
395 status = ET.Element(_tag("D", "status"))
396 status.text = _response(200)
397 propstat.append(status)
399 reply = ET.tostring(multistatus, config.get("encoding", "request"))