# -*- coding: utf-8 -*-
#
-# This file is part of Calypso Server - Calendar Server
+# This file is part of Calypso - CalDAV/CardDAV/WebDAV Server
# Copyright © 2011 Keith Packard
# Copyright © 2008-2011 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel
import dateutil.tz
import datetime
import email.utils
-import urllib
+import logging
-from . import client, config, ical
+from . import client, config, webdav, paths, principal
+__package__ = 'calypso.xmlutils'
NAMESPACES = {
"C": "urn:ietf:params:xml:ns:caldav",
+ "A": "urn:ietf:params:xml:ns:carddav",
"D": "DAV:",
+ "E": "http://apple.com/ns/ical/",
"CS": "http://calendarserver.org/ns/"}
+log = logging.getLogger(__name__)
def _tag(short_name, local):
"""Get XML Clark notation {uri(``short_name``)}``local``."""
"""Return full W3C names from HTTP status codes."""
return "HTTP/1.1 %i %s" % (code, client.responses[code])
-
-def name_from_path(path):
- """Return Calypso item name from ``path``."""
- path_parts = path.strip("/").split("/")
- return urllib.unquote(path_parts[-1]) if len(path_parts) > 2 else None
-
-def delete(path, calendar):
+def delete(path, collection, context):
"""Read and answer DELETE requests.
Read rfc4918-9.6 for info.
"""
# Reading request
- calendar.remove(name_from_path(path))
+ collection.remove(paths.resource_from_path(path), context=context)
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
return ET.tostring(multistatus, config.get("encoding", "request"))
+def identify_resource(path):
+ """Return a Resource object corresponding to the path (this is used for
+ everything that is not a collection, like Principal and HomeSet objects)"""
+
+ try:
+ left, right = config.get('server', 'user_principal').split('%(user)s')
+ except ValueError:
+ raise ValueError("user_principal setting must contain %(user)s.")
+
+ if not path.startswith(left):
+ return None
+
+ remainder = path[len(left):]
+ if right not in remainder:
+ return None
+
+ username = remainder[:remainder.index(right)]
+ remainder = remainder[remainder.index(right)+len(right):]
-def propfind(path, xml_request, calendar, depth):
+ if remainder == principal.AddressbookHomeSet.type_dependent_suffix + "/":
+ return principal.AddressbookHomeSet(username)
+ elif remainder == principal.CalendarHomeSet.type_dependent_suffix + "/":
+ return principal.CalendarHomeSet(username)
+ elif remainder == "":
+ return principal.Principal(username)
+ else:
+ return None
+
+def propfind(path, xml_request, collection, depth, context):
"""Read and answer PROPFIND requests.
Read rfc4918-9.1 for info.
"""
- # Reading request
- root = ET.fromstring(xml_request)
- prop_element = root.find(_tag("D", "prop"))
+ item_name = paths.resource_from_path(path)
+
+ if xml_request:
+ # Reading request
+ root = ET.fromstring(xml_request)
+
+ prop_element = root.find(_tag("D", "prop"))
+ else:
+ prop_element = None
+
if prop_element is not None:
prop_list = prop_element.getchildren()
props = [prop.tag for prop in prop_list]
else:
- props = None
+ props = [_tag("D", "resourcetype"),
+ _tag("D", "owner"),
+ _tag("D", "getcontenttype"),
+ _tag("D", "getetag"),
+ _tag("D", "principal-collection-set"),
+ _tag("C", "supported-calendar-component-set"),
+ _tag("D", "supported-report-set"),
+ _tag("D", "current-user-privilege-set"),
+ _tag("D", "getcontentlength"),
+ _tag("D", "getlastmodified")]
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
- if calendar:
- if depth == "0":
- items = [calendar]
+ resource = identify_resource(path)
+
+ if resource is not None:
+ items = resource.propfind_children(depth)
+ elif collection:
+ if item_name:
+ item = collection.get_item(item_name)
+ print "item_name %s item %s" % (item_name, item)
+ if item:
+ items = [item]
+ else:
+ items = []
else:
- # depth is 1, infinity or not specified
- # we limit ourselves to depth == 1
- items = [calendar] + calendar.items
-# items = [calendar]
+ if depth == "0":
+ items = [collection]
+ else:
+ # depth is 1, infinity or not specified
+ # we limit ourselves to depth == 1
+ items = [collection] + collection.items
else:
items = []
for item in items:
- is_calendar = isinstance(item, ical.Calendar)
+ is_collection = isinstance(item, webdav.Collection)
+ is_resource = isinstance(item, principal.Resource)
+
+ if is_collection:
+ # parentcollectionhack. this is not the way to do it, but much of
+ # the below code relies on items which are collection members to
+ # have their parent collection in the collection variable. get rid
+ # of the collection propfind-global variable, and this and all
+ # other occurrences of "parentcollectionhack" can be dropped.
+ collection = item
response = ET.Element(_tag("D", "response"))
multistatus.append(response)
href = ET.Element(_tag("D", "href"))
- href.text = path if is_calendar else path + item.name
+ if is_collection:
+ href.text = item.urlpath
+ elif is_resource:
+ href.text = item.urlpath
+ else:
+ href.text = collection.urlpath + item.name
response.append(href)
propstat = ET.Element(_tag("D", "propstat"))
for tag in props:
element = ET.Element(tag)
- if tag == _tag("D", "resourcetype") and is_calendar:
- tag = ET.Element(_tag("C", "calendar"))
- element.append(tag)
+ if tag == _tag("D", "resourcetype") and is_collection:
+ if collection.is_calendar:
+ tag = ET.Element(_tag("C", "calendar"))
+ element.append(tag)
+ if collection.is_addressbook:
+ tag = ET.Element(_tag("A", "addressbook"))
+ element.append(tag)
tag = ET.Element(_tag("D", "collection"))
element.append(tag)
elif tag == _tag("D", "owner"):
- element.text = calendar.owner
+ element.text = collection.owner
elif tag == _tag("D", "getcontenttype"):
if item.tag == 'VCARD':
element.text = "text/vcard"
else:
element.text = "text/calendar"
- elif tag == _tag("CS", "getctag") and is_calendar:
+ elif tag == _tag("CS", "getctag") and is_collection:
element.text = item.ctag
elif tag == _tag("D", "getetag"):
element.text = item.etag
- elif tag == _tag("D", "displayname") and is_calendar:
- element.text = calendar.name
+ elif tag == _tag("D", "displayname") and is_collection:
+ element.text = collection.name
+ elif tag == _tag("E", "calendar-color") and is_collection:
+ element.text = collection.color
elif tag == _tag("D", "principal-URL"):
# TODO: use a real principal URL, read rfc3744-4.2 for info
tag = ET.Element(_tag("D", "href"))
elif tag in (
_tag("D", "principal-collection-set"),
_tag("C", "calendar-user-address-set"),
- _tag("C", "calendar-home-set"),
- _tag("C", "addressbook-home-set")):
+ ):
+ # not meaningfully implemented yet
tag = ET.Element(_tag("D", "href"))
tag.text = path
element.append(tag)
# element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
# element.text = email.utils.formatdate(item.last_modified)
element.text = email.utils.formatdate(time.mktime(item.last_modified))
+ elif tag == _tag("D", "current-user-principal"):
+ tag = ET.Element(_tag("D", "href"))
+ tag.text = config.get("server", "user_principal") % context
+ element.append(tag)
+ elif tag in (_tag("A", "addressbook-description"),
+ _tag("C", "calendar-description")) and is_collection:
+ element.text = collection.get_description()
prop.append(element)
status = ET.Element(_tag("D", "status"))
return ET.tostring(multistatus, config.get("encoding", "request"))
-def put(path, ical_request, calendar):
+def put(path, webdav_request, collection, context):
"""Read PUT requests."""
- name = name_from_path(path)
- if name in (item.name for item in calendar.items):
+ name = paths.resource_from_path(path)
+ log.debug('xmlutils put path %s name %s', path, name)
+ if name in (item.name for item in collection.items):
# PUT is modifying an existing item
- calendar.replace(name, ical_request)
+ log.debug('Replacing item named %s', name)
+ return collection.replace(name, webdav_request, context=context)
else:
# PUT is adding a new item
- calendar.append(name, ical_request)
+ log.debug('Putting a new item, because name %s is not known', name)
+ return collection.append(name, webdav_request, context=context)
def match_filter_element(vobject, fe):
return False
start = fe.get("start")
end = fe.get("end")
+ # According to RFC 4791, one of start and stop must be set,
+ # but the other can be empty. If both are empty, the
+ # specification is violated.
+ if start is None and end is None:
+ msg = "time-range missing both start and stop attribute (required by RFC 4791)"
+ log.error(msg)
+ raise ValueError(msg)
+ # RFC 4791 state if start is missing, assume it is -infinity
+ if start is None:
+ start = "00010101T000000Z" # start of year one
+ # RFC 4791 state if end is missing, assume it is +infinity
+ if end is None:
+ end = "99991231T235959Z" # last date with four digit year
if rruleset is None:
rruleset = dateutil.rrule.rruleset()
dtstart = vobject.dtstart.value
for fe in filter.getchildren():
if match_filter_element(item.object, fe):
return True
+ return False
-def report(path, xml_request, calendar):
+def report(path, xml_request, collection):
"""Read and answer REPORT requests.
Read rfc3253-3.6 for info.
filter_element = root.find(_tag("C", "filter"))
- if calendar:
- if root.tag == _tag("C", "calendar-multiget"):
+ if collection:
+ if root.tag == _tag("C", "calendar-multiget") or root.tag == _tag('A', 'addressbook-multiget'):
# Read rfc4791-7.9 for info
hreferences = set((href_element.text for href_element
in root.findall(_tag("D", "href"))))
multistatus = ET.Element(_tag("D", "multistatus"))
for hreference in hreferences:
- # Check if the reference is an item or a calendar
- name = name_from_path(hreference)
+ # Check if the reference is an item or a collection
+ name = paths.resource_from_path(hreference)
if name:
# Reference is an item
- path = "/".join(hreference.split("/")[:-1]) + "/"
- items = (item for item in calendar.items if item.name == name)
+ path = paths.collection_from_path(hreference) + "/"
+ items = (item for item in collection.items if item.name == name)
else:
- # Reference is a calendar
+ # Reference is a collection
path = hreference
- items = calendar.items
+ items = collection.items
for item in items:
multistatus.append(response)
href = ET.Element(_tag("D", "href"))
- href.text = path + item.name
+ href.text = path.rstrip('/') + '/' + item.name
response.append(href)
propstat = ET.Element(_tag("D", "propstat"))
element.text = item.etag
elif tag == _tag("C", "calendar-data"):
element.text = item.text
+ elif tag == _tag("A", "address-data"):
+ element.text = item.text
prop.append(element)
status = ET.Element(_tag("D", "status"))