Merge branch 'carddav', and update copyright dates
Conflicts: radicale/__init__.py radicale/ical.py radicale/xmlutils.py
This commit is contained in:
commit
9c4a85ef1f
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
211
radicale/ical.py
211
radicale/ical.py
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
2
setup.py
2
setup.py
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user