Add a (not tested) CardDAV support

This commit is contained in:
Guillaume Ayoub 2011-12-31 13:31:22 +01:00
parent f2d491ea61
commit 8a4be02075
3 changed files with 266 additions and 200 deletions

View File

@ -107,7 +107,7 @@ class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
class Application(object): class Application(object):
"""WSGI application managing calendars.""" """WSGI application managing collections."""
def __init__(self): def __init__(self):
"""Initialize application.""" """Initialize application."""
super(Application, self).__init__() super(Application, self).__init__()
@ -180,8 +180,8 @@ class Application(object):
else: else:
content = None content = None
# Find calendar(s) # Find collection(s)
items = ical.Calendar.from_path( items = ical.Collection.from_path(
environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0")) environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0"))
# Get function corresponding to method # Get function corresponding to method
@ -189,7 +189,7 @@ class Application(object):
# Check rights # Check rights
if not items or not self.acl: if not items or not self.acl:
# No calendar or no acl, don't check rights # No collection or no acl, don't check rights
status, headers, answer = function(environ, items, content, None) status, headers, answer = function(environ, items, content, None)
else: else:
# Ask authentication backend to check rights # Ask authentication backend to check rights
@ -203,37 +203,37 @@ class Application(object):
user = password = None user = password = None
last_allowed = None last_allowed = None
calendars = [] collections = []
for calendar in items: for collection in items:
if not isinstance(calendar, ical.Calendar): if not isinstance(collection, ical.Collection):
if last_allowed: if last_allowed:
calendars.append(calendar) collections.append(collection)
continue continue
if calendar.owner in acl.PUBLIC_USERS: if collection.owner in acl.PUBLIC_USERS:
log.LOGGER.info("Public calendar") log.LOGGER.info("Public collection")
calendars.append(calendar) collections.append(collection)
last_allowed = True last_allowed = True
else: else:
log.LOGGER.info( log.LOGGER.info(
"Checking rights for calendar owned by %s" % ( "Checking rights for collection owned by %s" % (
calendar.owner or "nobody")) collection.owner or "nobody"))
if self.acl.has_right(calendar.owner, user, password): if self.acl.has_right(collection.owner, user, password):
log.LOGGER.info( log.LOGGER.info(
"%s allowed" % (user or "Anonymous user")) "%s allowed" % (user or "Anonymous user"))
calendars.append(calendar) collections.append(collection)
last_allowed = True last_allowed = True
else: else:
log.LOGGER.info( log.LOGGER.info(
"%s refused" % (user or "Anonymous user")) "%s refused" % (user or "Anonymous user"))
last_allowed = False last_allowed = False
if calendars: if collections:
# Calendars found # Collections found
status, headers, answer = function( status, headers, answer = function(
environ, calendars, content, user) environ, collections, content, user)
elif user and last_allowed is None: elif user and last_allowed is None:
# Good user and no calendars found, redirect user to home # Good user and no collections found, redirect user to home
location = "/%s/" % str(quote(user)) location = "/%s/" % str(quote(user))
log.LOGGER.info("redirecting to %s" % location) log.LOGGER.info("redirecting to %s" % location)
status = client.FOUND status = client.FOUND
@ -264,21 +264,21 @@ class Application(object):
# All these functions must have the same parameters, some are useless # All these functions must have the same parameters, some are useless
# pylint: disable=W0612,W0613,R0201 # pylint: disable=W0612,W0613,R0201
def delete(self, environ, calendars, content, user): def delete(self, environ, collections, content, user):
"""Manage DELETE request.""" """Manage DELETE request."""
calendar = calendars[0] collection = collections[0]
if calendar.local_path == environ["PATH_INFO"].strip("/"): if collection.local_path == environ["PATH_INFO"].strip("/"):
# Path matching the calendar, the item to delete is the calendar # Path matching the collection, the collection must be deleted
item = calendar item = collection
else: else:
# Try to get an item matching the path # Try to get an item matching the path
item = calendar.get_item( item = collection.get_item(
xmlutils.name_from_path(environ["PATH_INFO"], calendar)) xmlutils.name_from_path(environ["PATH_INFO"], collection))
if item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag: if item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag:
# No ETag precondition or precondition verified, delete item # No ETag precondition or precondition verified, delete item
answer = xmlutils.delete(environ["PATH_INFO"], calendar) answer = xmlutils.delete(environ["PATH_INFO"], collection)
status = client.NO_CONTENT status = client.NO_CONTENT
else: else:
# No item or ETag precondition not verified, do not delete item # No item or ETag precondition not verified, do not delete item
@ -286,7 +286,7 @@ class Application(object):
status = client.PRECONDITION_FAILED status = client.PRECONDITION_FAILED
return status, {}, answer return status, {}, answer
def get(self, environ, calendars, content, user): def get(self, environ, collections, content, user):
"""Manage GET request.""" """Manage GET request."""
# Display a "Radicale works!" message if the root URL is requested # Display a "Radicale works!" message if the root URL is requested
if environ["PATH_INFO"] == "/": if environ["PATH_INFO"] == "/":
@ -294,67 +294,77 @@ class Application(object):
answer = "<!DOCTYPE html>\n<title>Radicale</title>Radicale works!" answer = "<!DOCTYPE html>\n<title>Radicale</title>Radicale works!"
return client.OK, headers, answer return client.OK, headers, answer
calendar = calendars[0] collection = collections[0]
item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar) item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
if item_name: if item_name:
# Get calendar item # Get collection item
item = calendar.get_item(item_name) item = collection.get_item(item_name)
if item: if item:
items = calendar.timezones items = collection.timezones
items.append(item) items.append(item)
answer_text = ical.serialize( answer_text = ical.serialize(
headers=calendar.headers, items=items) collection.tag, collection.headers, items)
etag = item.etag etag = item.etag
else: else:
return client.GONE, {}, None return client.GONE, {}, None
else: else:
# Get whole calendar # Get whole collection
answer_text = calendar.text answer_text = collection.text
etag = calendar.etag etag = collection.etag
headers = { headers = {
"Content-Type": "text/calendar", "Content-Type": collection.mimetype,
"Last-Modified": calendar.last_modified, "Last-Modified": collection.last_modified,
"ETag": etag} "ETag": etag}
answer = answer_text.encode(self.encoding) answer = answer_text.encode(self.encoding)
return client.OK, headers, answer return client.OK, headers, answer
def head(self, environ, calendars, content, user): def head(self, environ, collections, content, user):
"""Manage HEAD request.""" """Manage HEAD request."""
status, headers, answer = self.get(environ, calendars, content, user) status, headers, answer = self.get(environ, collections, content, user)
return status, headers, None return status, headers, None
def mkcalendar(self, environ, calendars, content, user): def mkcalendar(self, environ, collections, content, user):
"""Manage MKCALENDAR request.""" """Manage MKCALENDAR request."""
calendar = calendars[0] collection = collections[0]
props = xmlutils.props_from_request(content) props = xmlutils.props_from_request(content)
timezone = props.get('C:calendar-timezone') timezone = props.get('C:calendar-timezone')
if timezone: if timezone:
calendar.replace('', timezone) collection.replace('', timezone)
del props['C:calendar-timezone'] del props['C:calendar-timezone']
with calendar.props as calendar_props: with collection.props as collection_props:
for key, value in props.items(): for key, value in props.items():
calendar_props[key] = value collection_props[key] = value
calendar.write() collection.write()
return client.CREATED, {}, None return client.CREATED, {}, None
def move(self, environ, calendars, content, user): def mkcol(self, environ, collections, content, user):
"""Manage MKCOL request."""
collection = collections[0]
props = xmlutils.props_from_request(content)
with collection.props as collection_props:
for key, value in props.items():
collection_props[key] = value
collection.write()
return client.CREATED, {}, None
def move(self, environ, collections, content, user):
"""Manage MOVE request.""" """Manage MOVE request."""
from_calendar = calendars[0] from_collection = collections[0]
from_name = xmlutils.name_from_path( from_name = xmlutils.name_from_path(
environ["PATH_INFO"], from_calendar) environ["PATH_INFO"], from_collection)
if from_name: if from_name:
item = from_calendar.get_item(from_name) item = from_collection.get_item(from_name)
if item: if item:
# Move the item # Move the item
to_url_parts = urlparse(environ["HTTP_DESTINATION"]) to_url_parts = urlparse(environ["HTTP_DESTINATION"])
if to_url_parts.netloc == environ["HTTP_HOST"]: if to_url_parts.netloc == environ["HTTP_HOST"]:
to_url = to_url_parts.path to_url = to_url_parts.path
to_path, to_name = to_url.rstrip("/").rsplit("/", 1) to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
to_calendar = ical.Calendar.from_path( to_collection = ical.Collection.from_path(
to_path, depth="0")[0] to_path, depth="0")[0]
to_calendar.append(to_name, item.text) to_collection.append(to_name, item.text)
from_calendar.remove(from_name) from_collection.remove(from_name)
return client.CREATED, {}, None return client.CREATED, {}, None
else: else:
# Remote destination server, not supported # Remote destination server, not supported
@ -363,60 +373,60 @@ class Application(object):
# No item found # No item found
return client.GONE, {}, None return client.GONE, {}, None
else: else:
# Moving calendars, not supported # Moving collections, not supported
return client.FORBIDDEN, {}, None return client.FORBIDDEN, {}, None
def options(self, environ, calendars, content, user): def options(self, environ, collections, content, user):
"""Manage OPTIONS request.""" """Manage OPTIONS request."""
headers = { headers = {
"Allow": "DELETE, HEAD, GET, MKCALENDAR, MOVE, " \ "Allow": "DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " \
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT", "OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
"DAV": "1, calendar-access"} "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
return client.OK, headers, None return client.OK, headers, None
def propfind(self, environ, calendars, content, user): def propfind(self, environ, collections, content, user):
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
headers = { headers = {
"DAV": "1, calendar-access", "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
"Content-Type": "text/xml"} "Content-Type": "text/xml"}
answer = xmlutils.propfind( answer = xmlutils.propfind(
environ["PATH_INFO"], content, calendars, user) environ["PATH_INFO"], content, collections, user)
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
def proppatch(self, environ, calendars, content, user): def proppatch(self, environ, collections, content, user):
"""Manage PROPPATCH request.""" """Manage PROPPATCH request."""
calendar = calendars[0] collection = collections[0]
answer = xmlutils.proppatch(environ["PATH_INFO"], content, calendar) answer = xmlutils.proppatch(environ["PATH_INFO"], content, collection)
headers = { headers = {
"DAV": "1, calendar-access", "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
"Content-Type": "text/xml"} "Content-Type": "text/xml"}
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
def put(self, environ, calendars, content, user): def put(self, environ, collections, content, user):
"""Manage PUT request.""" """Manage PUT request."""
calendar = calendars[0] collection = collections[0]
headers = {} headers = {}
item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar) item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
item = calendar.get_item(item_name) item = collection.get_item(item_name)
if (not item and not environ.get("HTTP_IF_MATCH")) or ( if (not item and not environ.get("HTTP_IF_MATCH")) or (
item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag): item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag):
# PUT allowed in 3 cases # PUT allowed in 3 cases
# Case 1: No item and no ETag precondition: Add new item # Case 1: No item and no ETag precondition: Add new item
# Case 2: Item and ETag precondition verified: Modify item # Case 2: Item and ETag precondition verified: Modify item
# Case 3: Item and no Etag precondition: Force modifying item # Case 3: Item and no Etag precondition: Force modifying item
xmlutils.put(environ["PATH_INFO"], content, calendar) xmlutils.put(environ["PATH_INFO"], content, collection)
status = client.CREATED status = client.CREATED
headers["ETag"] = calendar.get_item(item_name).etag headers["ETag"] = collection.get_item(item_name).etag
else: else:
# PUT rejected in all other cases # PUT rejected in all other cases
status = client.PRECONDITION_FAILED status = client.PRECONDITION_FAILED
return status, headers, None return status, headers, None
def report(self, environ, calendars, content, user): def report(self, environ, collections, content, user):
"""Manage REPORT request.""" """Manage REPORT request."""
calendar = calendars[0] collection = collections[0]
headers = {'Content-Type': 'text/xml'} headers = {'Content-Type': 'text/xml'}
answer = xmlutils.report(environ["PATH_INFO"], content, calendar) answer = xmlutils.report(environ["PATH_INFO"], content, collection)
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
# pylint: enable=W0612,W0613,R0201 # pylint: enable=W0612,W0613,R0201

View File

@ -47,13 +47,17 @@ def open(path, mode="r"):
# pylint: enable=W0622 # pylint: enable=W0622
def serialize(headers=(), items=()): def serialize(tag, headers=(), items=()):
"""Return an iCal text corresponding to given ``headers`` and ``items``.""" """Return a collection text corresponding to given ``tag``.
lines = ["BEGIN:VCALENDAR"]
The collection has the given ``headers`` and ``items``.
"""
lines = ["BEGIN:%s" % tag]
for part in (headers, items): for part in (headers, items):
if part: if part:
lines.append("\n".join(item.text for item in part)) lines.append("\n".join(item.text for item in part))
lines.append("END:VCALENDAR\n") lines.append("END:%s\n" % tag)
return "\n".join(lines) return "\n".join(lines)
@ -135,37 +139,45 @@ class Header(Item):
"""Internal header class.""" """Internal header class."""
class Event(Item):
"""Internal event class."""
tag = "VEVENT"
class Todo(Item):
"""Internal todo class."""
# This is not a TODO!
# pylint: disable=W0511
tag = "VTODO"
# pylint: enable=W0511
class Journal(Item):
"""Internal journal class."""
tag = "VJOURNAL"
class Timezone(Item): class Timezone(Item):
"""Internal timezone class.""" """Internal timezone class."""
tag = "VTIMEZONE" tag = "VTIMEZONE"
class Calendar(object): class Component(Item):
"""Internal calendar class.""" """Internal main component of a collection."""
tag = "VCALENDAR"
class Event(Component):
"""Internal event class."""
tag = "VEVENT"
mimetype = "text/calendar"
class Todo(Component):
"""Internal todo class."""
tag = "VTODO" # pylint: disable=W0511
mimetype = "text/calendar"
class Journal(Component):
"""Internal journal class."""
tag = "VJOURNAL"
mimetype = "text/calendar"
class Card(Component):
"""Internal card class."""
tag = "VCARD"
mimetype = "text/vcard"
class Collection(object):
"""Internal collection item."""
def __init__(self, path, principal=False): def __init__(self, path, principal=False):
"""Initialize the calendar. """Initialize the collection.
``path`` must be the normalized relative path of the calendar, using ``path`` must be the normalized relative path of the collection, using
the slash as the folder delimiter, with no leading nor trailing slash. the slash as the folder delimiter, with no leading nor trailing slash.
""" """
@ -174,7 +186,7 @@ class Calendar(object):
self.path = os.path.join(FOLDER, path.replace("/", os.sep)) self.path = os.path.join(FOLDER, path.replace("/", os.sep))
self.props_path = self.path + '.props' self.props_path = self.path + '.props'
if principal and split_path and os.path.isdir(self.path): if principal and split_path and os.path.isdir(self.path):
# Already existing principal calendar # Already existing principal collection
self.owner = split_path[0] self.owner = split_path[0]
elif len(split_path) > 1: elif len(split_path) > 1:
# URL with at least one folder # URL with at least one folder
@ -186,7 +198,7 @@ class Calendar(object):
@classmethod @classmethod
def from_path(cls, path, depth="infinite", include_container=True): def from_path(cls, path, depth="infinite", include_container=True):
"""Return a list of calendars and items under the given ``path``. """Return a list of collections and items under the given ``path``.
If ``depth`` is "0", only the actual object under ``path`` is If ``depth`` is "0", only the actual object under ``path`` is
returned. Otherwise, also sub-items are appended to the result. If returned. Otherwise, also sub-items are appended to the result. If
@ -218,7 +230,7 @@ class Calendar(object):
result.append(cls(path, principal)) result.append(cls(path, principal))
try: try:
for filename in next(os.walk(abs_path))[2]: for filename in next(os.walk(abs_path))[2]:
if cls.is_vcalendar(os.path.join(abs_path, filename)): if cls.is_collection(os.path.join(abs_path, filename)):
result.append(cls(os.path.join(path, filename))) result.append(cls(os.path.join(path, filename)))
except StopIteration: except StopIteration:
# Directory does not exist yet # Directory does not exist yet
@ -227,17 +239,52 @@ class Calendar(object):
if depth == "0": if depth == "0":
result.append(cls(path)) result.append(cls(path))
else: else:
calendar = cls(path, principal) collection = cls(path, principal)
if include_container: if include_container:
result.append(calendar) result.append(collection)
result.extend(calendar.components) result.extend(collection.components)
return result return result
@staticmethod def is_collection(self, path):
def is_vcalendar(path): """Return ``True`` if there is a collection file under ``path``."""
"""Return ``True`` if there is a VCALENDAR file under ``path``.""" beginning_string = 'BEGIN:%s' % self.tag
with open(path) as stream: with open(path) as stream:
return 'BEGIN:VCALENDAR' == stream.read(15) beginning_string = stream.read(len(beginning_string))
@property
def items(self):
"""Get list of all items in collection."""
return self._parse(self.text, (Card, Event, Todo, Journal, Timezone))
@property
def components(self):
"""Get list of all components in collection."""
return self._parse(self.text, (Card, Event, Todo, Journal))
@property
def events(self):
"""Get list of ``Event`` items in collection."""
return self._parse(self.text, (Event,))
@property
def cards(self):
"""Get list of all cards in collection."""
return self._parse(self.text, (Card,))
@property
def todos(self):
"""Get list of ``Todo`` items in collection."""
return self._parse(self.text, (Todo,))
@property
def journals(self):
"""Get list of ``Journal`` items in collection."""
return self._parse(self.text, (Journal,))
@property
def timezones(self):
"""Get list of ``Timezome`` items in collection."""
return self._parse(self.text, (Timezone,))
@staticmethod @staticmethod
def _parse(text, item_types, name=None): def _parse(text, item_types, name=None):
@ -329,7 +376,7 @@ class Calendar(object):
self._create_dirs(self.path) self._create_dirs(self.path)
text = serialize(headers, items) text = serialize(self.tag, headers, items)
return open(self.path, "w").write(text) return open(self.path, "w").write(text)
@staticmethod @staticmethod
@ -338,21 +385,48 @@ class Calendar(object):
if not os.path.exists(os.path.dirname(path)): if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path)) os.makedirs(os.path.dirname(path))
@property
def tag(self):
"""Type of the collection."""
with self.props as props:
if "tag" not in props:
try:
props["tag"] = open(self.path).readlines()[0][6:].rstrip()
except IOError:
props["tag"] = "VCALENDAR"
return props["tag"]
@property
def mimetype(self):
"""Mimetype of the collection."""
if self.tag == "VADDRESSBOOK":
return "text/vcard"
elif self.tag == "VCALENDAR":
return "text/calendar"
@property
def resource_type(self):
"""Resource type of the collection."""
if self.tag == "VADDRESSBOOK":
return "addressbook"
elif self.tag == "VCALENDAR":
return "calendar"
@property @property
def etag(self): def etag(self):
"""Etag from calendar.""" """Etag from collection."""
return '"%s"' % hash(self.text) return '"%s"' % hash(self.text)
@property @property
def name(self): def name(self):
"""Calendar name.""" """Collection name."""
with self.props as props: with self.props as props:
return props.get('D:displayname', return props.get('D:displayname',
self.path.split(os.path.sep)[-1]) self.path.split(os.path.sep)[-1])
@property @property
def text(self): def text(self):
"""Calendar as plain text.""" """Collection as plain text."""
try: try:
return open(self.path).read() return open(self.path).read()
except IOError: except IOError:
@ -360,7 +434,7 @@ class Calendar(object):
@property @property
def headers(self): def headers(self):
"""Find headers items in calendar.""" """Find headers items in collection."""
header_lines = [] header_lines = []
lines = unfold(self.text) lines = unfold(self.text)
@ -373,39 +447,9 @@ class Calendar(object):
return header_lines return header_lines
@property
def items(self):
"""Get list of all items in calendar."""
return self._parse(self.text, (Event, Todo, Journal, Timezone))
@property
def components(self):
"""Get list of all components in calendar."""
return self._parse(self.text, (Event, Todo, Journal))
@property
def events(self):
"""Get list of ``Event`` items in calendar."""
return self._parse(self.text, (Event,))
@property
def todos(self):
"""Get list of ``Todo`` items in calendar."""
return self._parse(self.text, (Todo,))
@property
def journals(self):
"""Get list of ``Journal`` items in calendar."""
return self._parse(self.text, (Journal,))
@property
def timezones(self):
"""Get list of ``Timezome`` items in calendar."""
return self._parse(self.text, (Timezone,))
@property @property
def last_modified(self): def last_modified(self):
"""Get the last time the calendar has been modified. """Get the last time the collection has been modified.
The date is formatted according to rfc1123-5.2.14. The date is formatted according to rfc1123-5.2.14.
@ -420,7 +464,7 @@ class Calendar(object):
@property @property
@contextmanager @contextmanager
def props(self): def props(self):
"""Get the calendar properties.""" """Get the collection properties."""
# On enter # On enter
properties = {} properties = {}
if os.path.exists(self.props_path): if os.path.exists(self.props_path):
@ -434,7 +478,7 @@ class Calendar(object):
@property @property
def owner_url(self): def owner_url(self):
"""Get the calendar URL according to its owner.""" """Get the collection URL according to its owner."""
if self.owner: if self.owner:
return "/%s/" % self.owner return "/%s/" % self.owner
else: else:
@ -442,5 +486,5 @@ class Calendar(object):
@property @property
def url(self): def url(self):
"""Get the standard calendar URL.""" """Get the standard collection URL."""
return "/%s/" % self.local_path return "/%s/" % self.local_path

View File

@ -40,6 +40,7 @@ from radicale import client, config, ical
NAMESPACES = { NAMESPACES = {
"C": "urn:ietf:params:xml:ns:caldav", "C": "urn:ietf:params:xml:ns:caldav",
"CR": "urn:ietf:params:xml:ns:carddav",
"D": "DAV:", "D": "DAV:",
"CS": "http://calendarserver.org/ns/", "CS": "http://calendarserver.org/ns/",
"ICAL": "http://apple.com/ns/ical/", "ICAL": "http://apple.com/ns/ical/",
@ -118,11 +119,12 @@ def _response(code):
return "HTTP/1.1 %i %s" % (code, client.responses[code]) return "HTTP/1.1 %i %s" % (code, client.responses[code])
def name_from_path(path, calendar): def name_from_path(path, collection):
"""Return Radicale item name from ``path``.""" """Return Radicale item name from ``path``."""
calendar_parts = calendar.local_path.strip("/").split("/") collection_parts = collection.local_path.strip("/").split("/")
path_parts = path.strip("/").split("/") path_parts = path.strip("/").split("/")
return path_parts[-1] if (len(path_parts) - len(calendar_parts)) else None if (len(path_parts) - len(collection_parts)):
return path_parts[-1]
def props_from_request(root, actions=("set", "remove")): def props_from_request(root, actions=("set", "remove")):
@ -142,23 +144,29 @@ def props_from_request(root, actions=("set", "remove")):
if prop_element is not None: if prop_element is not None:
for prop in prop_element: for prop in prop_element:
result[_tag_from_clark(prop.tag)] = prop.text result[_tag_from_clark(prop.tag)] = prop.text
if prop.tag == "resourcetype":
for resource_type in prop:
if resource_type.tag in ("calendar", "addressbook"):
result["resourcetype"] = \
"V%s" % resource_type.tag.upper()
break
return result return result
def delete(path, calendar): def delete(path, collection):
"""Read and answer DELETE requests. """Read and answer DELETE requests.
Read rfc4918-9.6 for info. Read rfc4918-9.6 for info.
""" """
# Reading request # Reading request
if calendar.local_path == path.strip("/"): if collection.local_path == path.strip("/"):
# Delete the whole calendar # Delete the whole collection
calendar.delete() collection.delete()
else: else:
# Remove an item from the calendar # Remove an item from the collection
calendar.remove(name_from_path(path, calendar)) collection.remove(name_from_path(path, collection))
# Writing answer # Writing answer
multistatus = ET.Element(_tag("D", "multistatus")) multistatus = ET.Element(_tag("D", "multistatus"))
@ -176,7 +184,7 @@ def delete(path, calendar):
return _pretty_xml(multistatus) return _pretty_xml(multistatus)
def propfind(path, xml_request, calendars, user=None): def propfind(path, xml_request, collections, user=None):
"""Read and answer PROPFIND requests. """Read and answer PROPFIND requests.
Read rfc4918-9.1 for info. Read rfc4918-9.1 for info.
@ -191,8 +199,8 @@ def propfind(path, xml_request, calendars, user=None):
# Writing answer # Writing answer
multistatus = ET.Element(_tag("D", "multistatus")) multistatus = ET.Element(_tag("D", "multistatus"))
for calendar in calendars: for collection in collections:
response = _propfind_response(path, calendar, props, user) response = _propfind_response(path, collection, props, user)
multistatus.append(response) multistatus.append(response)
return _pretty_xml(multistatus) return _pretty_xml(multistatus)
@ -200,15 +208,15 @@ def propfind(path, xml_request, calendars, user=None):
def _propfind_response(path, item, props, user): def _propfind_response(path, item, props, user):
"""Build and return a PROPFIND response.""" """Build and return a PROPFIND response."""
is_calendar = isinstance(item, ical.Calendar) is_collection = isinstance(item, ical.Collection)
if is_calendar: if is_collection:
with item.props as cal_props: with item.props as properties:
calendar_props = cal_props collection_props = properties
response = ET.Element(_tag("D", "response")) response = ET.Element(_tag("D", "response"))
href = ET.Element(_tag("D", "href")) href = ET.Element(_tag("D", "href"))
uri = item.url if is_calendar else "%s/%s" % (path, item.name) uri = item.url if is_collection else "%s/%s" % (path, item.name)
href.text = uri.replace("//", "/") href.text = uri.replace("//", "/")
response.append(href) response.append(href)
@ -263,15 +271,15 @@ def _propfind_response(path, item, props, user):
report_tag.text = report_name report_tag.text = report_name
supported.append(report_tag) supported.append(report_tag)
element.append(supported) element.append(supported)
elif is_calendar: elif is_collection:
if tag == _tag("D", "getcontenttype"): if tag == _tag("D", "getcontenttype"):
element.text = "text/calendar" element.text = item.mimetype
elif tag == _tag("D", "resourcetype"): elif tag == _tag("D", "resourcetype"):
if item.is_principal: if item.is_principal:
tag = ET.Element(_tag("D", "principal")) tag = ET.Element(_tag("D", "principal"))
element.append(tag) element.append(tag)
else: else:
tag = ET.Element(_tag("C", "calendar")) tag = ET.Element(_tag("C", item.resource_type))
element.append(tag) element.append(tag)
tag = ET.Element(_tag("D", "collection")) tag = ET.Element(_tag("D", "collection"))
element.append(tag) element.append(tag)
@ -280,16 +288,18 @@ def _propfind_response(path, item, props, user):
elif tag == _tag("CS", "getctag"): elif tag == _tag("CS", "getctag"):
element.text = item.etag element.text = item.etag
elif tag == _tag("C", "calendar-timezone"): elif tag == _tag("C", "calendar-timezone"):
element.text = ical.serialize(item.headers, item.timezones) element.text = ical.serialize(
item.tag, item.headers, item.timezones)
else: else:
human_tag = _tag_from_clark(tag) human_tag = _tag_from_clark(tag)
if human_tag in calendar_props: if human_tag in collection_props:
element.text = calendar_props[human_tag] element.text = collection_props[human_tag]
else: else:
is404 = True is404 = True
# Not for calendars # Not for collections
elif tag == _tag("D", "getcontenttype"): elif tag == _tag("D", "getcontenttype"):
element.text = "text/calendar; component=%s" % item.tag.lower() element.text = "%s; component=%s" % (
item.mimetype, item.tag.lower())
elif tag == _tag("D", "resourcetype"): elif tag == _tag("D", "resourcetype"):
# resourcetype must be returned empty for non-collection elements # resourcetype must be returned empty for non-collection elements
pass pass
@ -340,7 +350,7 @@ def _add_propstat_to(element, tag, status_number):
propstat.append(status) propstat.append(status)
def proppatch(path, xml_request, calendar): def proppatch(path, xml_request, collection):
"""Read and answer PROPPATCH requests. """Read and answer PROPPATCH requests.
Read rfc4918-9.2 for info. Read rfc4918-9.2 for info.
@ -361,17 +371,17 @@ def proppatch(path, xml_request, calendar):
href.text = path href.text = path
response.append(href) response.append(href)
with calendar.props as calendar_props: with collection.props as collection_props:
for short_name, value in props_to_set.items(): for short_name, value in props_to_set.items():
if short_name == 'C:calendar-timezone': if short_name == 'C:calendar-timezone':
calendar.replace('', value) collection.replace('', value)
calendar.write() collection.write()
else: else:
calendar_props[short_name] = value collection_props[short_name] = value
_add_propstat_to(response, short_name, 200) _add_propstat_to(response, short_name, 200)
for short_name in props_to_remove: for short_name in props_to_remove:
try: try:
del calendar_props[short_name] del collection_props[short_name]
except KeyError: except KeyError:
_add_propstat_to(response, short_name, 412) _add_propstat_to(response, short_name, 412)
else: else:
@ -380,18 +390,18 @@ def proppatch(path, xml_request, calendar):
return _pretty_xml(multistatus) return _pretty_xml(multistatus)
def put(path, ical_request, calendar): def put(path, ical_request, collection):
"""Read PUT requests.""" """Read PUT requests."""
name = name_from_path(path, calendar) name = name_from_path(path, collection)
if name in (item.name for item in calendar.items): if name in (item.name for item in collection.items):
# PUT is modifying an existing item # PUT is modifying an existing item
calendar.replace(name, ical_request) collection.replace(name, ical_request)
else: else:
# PUT is adding a new item # PUT is adding a new item
calendar.append(name, ical_request) collection.append(name, ical_request)
def report(path, xml_request, calendar): def report(path, xml_request, collection):
"""Read and answer REPORT requests. """Read and answer REPORT requests.
Read rfc3253-3.6 for info. Read rfc3253-3.6 for info.
@ -403,7 +413,7 @@ def report(path, xml_request, calendar):
prop_element = root.find(_tag("D", "prop")) prop_element = root.find(_tag("D", "prop"))
props = [prop.tag for prop in prop_element] props = [prop.tag for prop in prop_element]
if calendar: if collection:
if root.tag == _tag("C", "calendar-multiget"): if root.tag == _tag("C", "calendar-multiget"):
# Read rfc4791-7.9 for info # Read rfc4791-7.9 for info
hreferences = set( hreferences = set(
@ -417,21 +427,22 @@ def report(path, xml_request, calendar):
# Writing answer # Writing answer
multistatus = ET.Element(_tag("D", "multistatus")) multistatus = ET.Element(_tag("D", "multistatus"))
calendar_items = calendar.items collection_tag = collection.tag
calendar_headers = calendar.headers collection_items = collection.items
calendar_timezones = calendar.timezones collection_headers = collection.headers
collection_timezones = collection.timezones
for hreference in hreferences: for hreference in hreferences:
# Check if the reference is an item or a calendar # Check if the reference is an item or a collection
name = name_from_path(hreference, calendar) name = name_from_path(hreference, collection)
if name: if name:
# Reference is an item # Reference is an item
path = "/".join(hreference.split("/")[:-1]) + "/" path = "/".join(hreference.split("/")[:-1]) + "/"
items = (item for item in calendar_items if item.name == name) items = (item for item in collection_items if item.name == name)
else: else:
# Reference is a calendar # Reference is a collection
path = hreference path = hreference
items = calendar.components items = collection.components
for item in items: for item in items:
response = ET.Element(_tag("D", "response")) response = ET.Element(_tag("D", "response"))
@ -452,9 +463,10 @@ def report(path, xml_request, calendar):
if tag == _tag("D", "getetag"): if tag == _tag("D", "getetag"):
element.text = item.etag element.text = item.etag
elif tag == _tag("C", "calendar-data"): elif tag == _tag("C", "calendar-data"):
if isinstance(item, (ical.Event, ical.Todo, ical.Journal)): if isinstance(item, ical.Component):
element.text = ical.serialize( element.text = ical.serialize(
calendar_headers, calendar_timezones + [item]) collection_tag, collection_headers,
collection_timezones + [item])
prop.append(element) prop.append(element)
status = ET.Element(_tag("D", "status")) status = ET.Element(_tag("D", "status"))