Merge branch 'carddav', and update copyright dates

Conflicts:
	radicale/__init__.py
	radicale/ical.py
	radicale/xmlutils.py
This commit is contained in:
Guillaume Ayoub 2012-01-23 16:21:30 +01:00
commit 9c4a85ef1f
15 changed files with 309 additions and 227 deletions

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Guillaume Ayoub # Copyright © 2011-2012 Guillaume Ayoub
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Guillaume Ayoub # Copyright © 2011-2012 Guillaume Ayoub
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #
@ -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__()
@ -181,8 +181,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
@ -190,7 +190,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
@ -204,37 +204,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
@ -265,21 +265,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.path == environ["PATH_INFO"].strip("/"): if collection.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
@ -287,7 +287,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"] == "/":
@ -295,67 +295,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
@ -364,60 +374,61 @@ 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]
collection.set_mimetype(environ.get("CONTENT_TYPE"))
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

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Guillaume Ayoub # Copyright © 2011-2012 Guillaume Ayoub
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -2,7 +2,7 @@
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Corentin Le Bail # Copyright © 2011 Corentin Le Bail
# Copyright © 2011 Guillaume Ayoub # Copyright © 2011-2012 Guillaume Ayoub
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #
@ -19,9 +19,9 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
""" """
Radicale calendar classes. Radicale collection classes.
Define the main classes of a calendar as seen from the server. Define the main classes of a collection as seen from the server.
""" """
@ -31,13 +31,24 @@ import uuid
from contextlib import contextmanager from contextlib import contextmanager
def serialize(headers=(), items=()): def serialize(tag, headers=(), items=(), whole=False):
"""Return an iCal text corresponding to given ``headers`` and ``items``.""" """Return a text corresponding to given collection ``tag``.
lines = ["BEGIN:VCALENDAR"]
for part in (headers, items): The text may have the given ``headers`` and ``items`` added around the
if part: items if needed (ie. for calendars).
lines.append("\n".join(item.text for item in part))
lines.append("END:VCALENDAR\n") If ``whole`` is ``True``, the collection tags and headers are added, even
for address books.
"""
if tag == "VADDRESSBOOK" and not whole:
lines = [items[0].text]
else:
lines = ["BEGIN:%s" % tag]
for part in (headers, items):
if part:
lines.append("\n".join(item.text for item in part))
lines.append("END:%s\n" % tag)
return "\n".join(lines) return "\n".join(lines)
@ -92,11 +103,11 @@ class Item(object):
line, "X-RADICALE-NAME:%s" % self._name) line, "X-RADICALE-NAME:%s" % self._name)
else: else:
self.text = self.text.replace( self.text = self.text.replace(
"\nUID:", "\nX-RADICALE-NAME:%s\nUID:" % self._name) "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name)
else: else:
self._name = str(uuid.uuid4()) self._name = str(uuid.uuid4())
self.text = self.text.replace( self.text = self.text.replace(
"\nEND:", "\nUID:%s\nEND:" % self._name) "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name)
@property @property
def etag(self): def etag(self):
@ -121,41 +132,49 @@ 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."""
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.
This class must be overridden and replaced by a storage backend. This class must be overridden and replaced by a storage backend.
""" """
tag = "VCALENDAR"
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.
""" """
@ -163,7 +182,7 @@ class Calendar(object):
split_path = path.split("/") split_path = path.split("/")
self.path = path if path != '.' else '' self.path = path if path != '.' else ''
if principal and split_path and self.is_collection(self.path): if principal and split_path and self.is_collection(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
@ -174,7 +193,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
@ -208,58 +227,54 @@ 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
def save(self, text): def save(self, text):
"""Save the text into the calendar.""" """Save the text into the collection."""
raise NotImplemented raise NotImplementedError
def delete(self): def delete(self):
"""Delete the calendar.""" """Delete the collection."""
raise NotImplemented raise NotImplementedError
@property @property
def text(self): def text(self):
"""Calendar as plain text.""" """Collection as plain text."""
raise NotImplemented raise NotImplementedError
@classmethod @classmethod
def children(cls, path): def children(cls, path):
"""Yield the children of the collection at local ``path``.""" """Yield the children of the collection at local ``path``."""
raise NotImplemented raise NotImplementedError
@classmethod @classmethod
def is_collection(cls, path): def is_collection(cls, path):
"""Return ``True`` if relative ``path`` is a collection.""" """Return ``True`` if relative ``path`` is a collection."""
raise NotImplemented raise NotImplementedError
@classmethod @classmethod
def is_item(cls, path): def is_item(cls, path):
"""Return ``True`` if relative ``path`` is a collection item.""" """Return ``True`` if relative ``path`` is a collection item."""
raise NotImplemented raise NotImplementedError
@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.
""" """
raise NotImplemented raise NotImplementedError
@property @property
@contextmanager @contextmanager
def props(self): def props(self):
"""Get the calendar properties.""" """Get the collection properties."""
raise NotImplemented raise NotImplementedError
def is_vcalendar(self, path):
"""Return ``True`` if there is a VCALENDAR under relative ``path``."""
return self.text.startswith('BEGIN:VCALENDAR')
@staticmethod @staticmethod
def _parse(text, item_types, name=None): def _parse(text, item_types, name=None):
@ -303,13 +318,13 @@ class Calendar(object):
return list(items.values()) return list(items.values())
def get_item(self, name): def get_item(self, name):
"""Get calendar item called ``name``.""" """Get collection item called ``name``."""
for item in self.items: for item in self.items:
if item.name == name: if item.name == name:
return item return item
def append(self, name, text): def append(self, name, text):
"""Append items from ``text`` to calendar. """Append items from ``text`` to collection.
If ``name`` is given, give this name to new items in ``text``. If ``name`` is given, give this name to new items in ``text``.
@ -317,14 +332,14 @@ class Calendar(object):
items = self.items items = self.items
for new_item in self._parse( for new_item in self._parse(
text, (Timezone, Event, Todo, Journal), name): text, (Timezone, Event, Todo, Journal, Card), name):
if new_item.name not in (item.name for item in items): if new_item.name not in (item.name for item in items):
items.append(new_item) items.append(new_item)
self.write(items=items) self.write(items=items)
def remove(self, name): def remove(self, name):
"""Remove object named ``name`` from calendar.""" """Remove object named ``name`` from collection."""
components = [ components = [
component for component in self.components component for component in self.components
if component.name != name] if component.name != name]
@ -333,56 +348,82 @@ class Calendar(object):
self.write(items=items) self.write(items=items)
def replace(self, name, text): def replace(self, name, text):
"""Replace content by ``text`` in objet named ``name`` in calendar.""" """Replace content by ``text`` in collection objet called ``name``."""
self.remove(name) self.remove(name)
self.append(name, text) self.append(name, text)
def write(self, headers=None, items=None): def write(self, headers=None, items=None):
"""Write calendar with given parameters.""" """Write collection with given parameters."""
headers = headers or self.headers or ( headers = headers or self.headers or (
Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
Header("VERSION:2.0")) Header("VERSION:%s" % self.version))
items = items if items is not None else self.items items = items if items is not None else self.items
text = serialize(headers, items) text = serialize(headers, items)
self.save(text) self.save(text)
@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 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)
for line in lines: for header in ("PRODID", "VERSION"):
if line.startswith("PRODID:"): for line in lines:
header_lines.append(Header(line)) if line.startswith("%s:" % header):
for line in lines: header_lines.append(Header(line))
if line.startswith("VERSION:"): break
header_lines.append(Header(line))
return header_lines return header_lines
@property @property
def items(self): def items(self):
"""Get list of all items in calendar.""" """Get list of all items in collection."""
return self._parse(self.text, (Event, Todo, Journal, Timezone)) return self._parse(self.text, (Event, Todo, Journal, Card, Timezone))
@property @property
def components(self): def components(self):
"""Get list of all components in calendar.""" """Get list of all components in collection."""
return self._parse(self.text, (Event, Todo, Journal)) return self._parse(self.text, (Event, Todo, Journal, Card))
@property @property
def events(self): def events(self):
@ -404,9 +445,14 @@ class Calendar(object):
"""Get list of ``Timezome`` items in calendar.""" """Get list of ``Timezome`` items in calendar."""
return self._parse(self.text, (Timezone,)) return self._parse(self.text, (Timezone,))
@property
def cards(self):
"""Get list of ``Card`` items in address book."""
return self._parse(self.text, (Card,))
@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:
@ -414,5 +460,10 @@ class Calendar(object):
@property @property
def url(self): def url(self):
"""Get the standard calendar URL.""" """Get the standard collection URL."""
return "/%s/" % self.path return "/%s/" % self.path
@property
def version(self):
"""Get the version of the collection type."""
return "3.0" if self.tag == "VADDRESSBOOK" else "2.0"

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Guillaume Ayoub # Copyright © 2011-2012 Guillaume Ayoub
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

View File

@ -36,12 +36,14 @@ FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder"))
# This function overrides the builtin ``open`` function for this module # This function overrides the builtin ``open`` function for this module
# pylint: disable=W0622 # pylint: disable=W0622
def open(path, mode="r"): def open(path, mode="r"):
"""Open a file at ``path`` with encoding set in the configuration."""
abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) abs_path = os.path.join(FOLDER, path.replace("/", os.sep))
return codecs.open(abs_path, mode, config.get("encoding", "stock")) return codecs.open(abs_path, mode, config.get("encoding", "stock"))
# pylint: enable=W0622 # pylint: enable=W0622
class Calendar(ical.Calendar): class Collection(ical.Collection):
"""Collection stored in a flat ical file."""
@property @property
def _path(self): def _path(self):
"""Absolute path of the file at local ``path``.""" """Absolute path of the file at local ``path``."""
@ -49,11 +51,11 @@ class Calendar(ical.Calendar):
@property @property
def _props_path(self): def _props_path(self):
"""Absolute path of the file storing the calendar properties.""" """Absolute path of the file storing the collection properties."""
return self._path + ".props" return self._path + ".props"
def _create_dirs(self): def _create_dirs(self):
"""Create folder storing the calendar if absent.""" """Create folder storing the collection if absent."""
if not os.path.exists(os.path.dirname(self._path)): if not os.path.exists(os.path.dirname(self._path)):
os.makedirs(os.path.dirname(self._path)) os.makedirs(os.path.dirname(self._path))
@ -74,10 +76,12 @@ class Calendar(ical.Calendar):
@classmethod @classmethod
def children(cls, path): def children(cls, path):
abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) rel_path = path.replace("/", os.sep)
abs_path = os.path.join(FOLDER, rel_path)
for filename in next(os.walk(abs_path))[2]: for filename in next(os.walk(abs_path))[2]:
if cls.is_collection(path): rel_filename = os.path.join(rel_path, filename)
yield cls(path) if cls.is_collection(rel_filename):
yield cls(rel_filename)
@classmethod @classmethod
def is_collection(cls, path): def is_collection(cls, path):
@ -91,7 +95,7 @@ class Calendar(ical.Calendar):
@property @property
def last_modified(self): def last_modified(self):
# Create calendar if needed # Create collection if needed
if not os.path.exists(self._path): if not os.path.exists(self._path):
self.write() self.write()
@ -113,4 +117,4 @@ class Calendar(ical.Calendar):
json.dump(properties, prop_file) json.dump(properties, prop_file)
ical.Calendar = Calendar ical.Collection = Collection

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008-2012 Guillaume Ayoub
# Copyright © 2008 Nicolas Kandel # Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# #
@ -31,7 +31,7 @@ try:
from collections import OrderedDict from collections import OrderedDict
except ImportError: except ImportError:
# Python 2.6 has no OrderedDict, use a dict instead # Python 2.6 has no OrderedDict, use a dict instead
OrderedDict = dict # pylint: disable=C0103 OrderedDict = dict # pylint: disable=C0103
import re import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -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/",
@ -56,7 +57,7 @@ for short, url in NAMESPACES.items():
ET.register_namespace("" if short == "D" else short, url) ET.register_namespace("" if short == "D" else short, url)
else: else:
# ... and badly with Python 2.6 and 3.1 # ... and badly with Python 2.6 and 3.1
ET._namespace_map[url] = short # pylint: disable=W0212 ET._namespace_map[url] = short # pylint: disable=W0212
CLARK_TAG_REGEX = re.compile(r""" CLARK_TAG_REGEX = re.compile(r"""
@ -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.path.split("/") collection_parts = collection.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.path == path.strip("/"): if collection.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)
@ -234,6 +242,7 @@ def _propfind_response(path, item, props, user):
elif tag in ( elif tag in (
_tag("D", "principal-collection-set"), _tag("D", "principal-collection-set"),
_tag("C", "calendar-user-address-set"), _tag("C", "calendar-user-address-set"),
_tag("CR", "addressbook-home-set"),
_tag("C", "calendar-home-set")): _tag("C", "calendar-home-set")):
tag = ET.Element(_tag("D", "href")) tag = ET.Element(_tag("D", "href"))
tag.text = path tag.text = path
@ -263,15 +272,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 +289,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 +351,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 +372,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 +391,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,8 +414,10 @@ 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 in (
_tag("C", "calendar-multiget"),
_tag("CR", "addressbook-multiget")):
# Read rfc4791-7.9 for info # Read rfc4791-7.9 for info
hreferences = set( hreferences = set(
href_element.text for href_element href_element.text for href_element
@ -417,21 +430,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"))
@ -451,10 +465,12 @@ def report(path, xml_request, calendar):
element = ET.Element(tag) element = ET.Element(tag)
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 in (
if isinstance(item, (ical.Event, ical.Todo, ical.Journal)): _tag("C", "calendar-data"), _tag("CR", "address-data")):
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"))

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # This file is part of Radicale Server - Calendar Server
# Copyright © 2009-2011 Guillaume Ayoub # Copyright © 2009-2012 Guillaume Ayoub
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by