From b1591aea6f4de6ceb9ea96e636c36843ffe450c8 Mon Sep 17 00:00:00 2001 From: "(no author)" <(no author)@74e4794c-479d-4a33-9dda-c6c359d70f12> Date: Tue, 30 Dec 2008 16:25:42 +0000 Subject: [PATCH] Files added git-svn-id: http://svn.32rwr.info/radicale/trunk@2 74e4794c-479d-4a33-9dda-c6c359d70f12 --- main.py | 52 +++++++++ radicale/__init__.py | 146 +++++++++++++++++++++++++ radicale/acl/__init__.py | 23 ++++ radicale/acl/htpasswd.py | 25 +++++ radicale/calendar.py | 103 ++++++++++++++++++ radicale/config.py | 68 ++++++++++++ radicale/ical.py | 119 ++++++++++++++++++++ radicale/support/__init__.py | 26 +++++ radicale/support/plain.py | 104 ++++++++++++++++++ radicale/xmlutils.py | 206 +++++++++++++++++++++++++++++++++++ 10 files changed, 872 insertions(+) create mode 100755 main.py create mode 100644 radicale/__init__.py create mode 100644 radicale/acl/__init__.py create mode 100644 radicale/acl/htpasswd.py create mode 100644 radicale/calendar.py create mode 100644 radicale/config.py create mode 100644 radicale/ical.py create mode 100644 radicale/support/__init__.py create mode 100644 radicale/support/plain.py create mode 100644 radicale/xmlutils.py diff --git a/main.py b/main.py new file mode 100755 index 0000000..9df37ce --- /dev/null +++ b/main.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +# TODO: Manage depth and calendars/collections (see xmlutils) +# TODO: Manage smart and configurable logs +# TODO: Manage authentication + +# TODO: remove this hack +import sys +sys.path.append("/usr/local/lib/python2.5/site-packages") + +from OpenSSL import SSL +from twisted.web import server +from twisted.internet import reactor +from twisted.python import log + +import radicale + +class ServerContextFactory(object): + """ + SSL context factory + """ + def getContext(self): + """ + Get SSL context for the HTTP server + """ + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_certificate_file(radicale.config.get("server", "certificate")) + ctx.use_privatekey_file(radicale.config.get("server", "privatekey")) + return ctx + +log.startLogging(sys.stdout) +log.startLogging(open(radicale.config.get("server", "log"), "w")) +factory = server.Site(radicale.HttpResource()) +reactor.listenSSL(radicale.config.getint("server", "port"), factory, ServerContextFactory()) +reactor.run() diff --git a/radicale/__init__.py b/radicale/__init__.py new file mode 100644 index 0000000..6057721 --- /dev/null +++ b/radicale/__init__.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +# TODO: Manage errors (see xmlutils) + +from twisted.web.resource import Resource +from twisted.web import http +import posixpath + +import config + +import support +import acl + +import xmlutils +import calendar + +_users = acl.users() +_calendars = support.calendars() + +class CalendarResource(Resource): + """ + Twisted resource for requests at calendar depth (/user/calendar) + """ + isLeaf = True + + def __init__(self, user, cal): + """ + Initialize resource creating a calendar object corresponding + to the stocked calendar named user/cal + """ + Resource.__init__(self) + self.calendar = calendar.Calendar(user, cal) + + def render_DELETE(self, request): + """ + Manage DELETE requests + """ + obj = request.getHeader("if-match") + answer = xmlutils.delete(obj, self.calendar, str(request.URLPath())) + request.setResponseCode(http.NO_CONTENT) + return answer + + def render_OPTIONS(self, request): + """ + Manage OPTIONS requests + """ + request.setHeader("Allow", "DELETE, OPTIONS, PROPFIND, PUT, REPORT") + request.setHeader("DAV", "1, calendar-access") + request.setResponseCode(http.OK) + return "" + + def render_PROPFIND(self, request): + """ + Manage PROPFIND requests + """ + xmlRequest = request.content.read() + answer = xmlutils.propfind(xmlRequest, self.calendar, str(request.URLPath())) + request.setResponseCode(http.MULTI_STATUS) + return answer + + def render_PUT(self, request): + """ + Manage PUT requests + """ + # TODO: Improve charset detection + contentType = request.getHeader("content-type") + if contentType and "charset=" in contentType: + charset = contentType.split("charset=")[1].strip() + else: + charset = config.get("encoding", "request") + icalRequest = unicode(request.content.read(), charset) + obj = request.getHeader("if-match") + xmlutils.put(icalRequest, self.calendar, str(request.URLPath()), obj) + request.setResponseCode(http.CREATED) + return "" + + def render_REPORT(self, request): + """ + Manage REPORT requests + """ + xmlRequest = request.content.read() + answer = xmlutils.report(xmlRequest, self.calendar, str(request.URLPath())) + request.setResponseCode(http.MULTI_STATUS) + return answer + +class UserResource(Resource): + """ + Twisted resource for requests at user depth (/user) + """ + def __init__(self, user): + """ + Initialize resource by connecting children requests to + the user calendars resources + """ + Resource.__init__(self) + for cal in _calendars: + if cal.startswith("%s%s"%(user, posixpath.sep)): + calName = cal.split(posixpath.sep)[1] + self.putChild(calName, CalendarResource(user, cal)) + + def getChild(self, cal, request): + """ + Get calendar resource if user exists + """ + if cal in _calendars: + return Resource.getChild(self, cal, request) + else: + return self + +class HttpResource(Resource): + """ + Twisted resource for requests at root depth (/) + """ + def __init__(self): + """ + Initialize resource by connecting children requests to + the users resources + """ + Resource.__init__(self) + for user in _users: + self.putChild(user, UserResource(user)) + + def getChild(self, user, request): + """ + Get user resource if user exists + """ + if user in _users: + return Resource.getChild(self, user, request) + else: + return self diff --git a/radicale/acl/__init__.py b/radicale/acl/__init__.py new file mode 100644 index 0000000..f7a0168 --- /dev/null +++ b/radicale/acl/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +from .. import config + +_acl = __import__(config.get("acl", "type"), locals(), globals()) + +users = _acl.users diff --git a/radicale/acl/htpasswd.py b/radicale/acl/htpasswd.py new file mode 100644 index 0000000..2c5e835 --- /dev/null +++ b/radicale/acl/htpasswd.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +from .. import config + +def users(): + """ + Get the List of all Users + """ + return [line.split(":")[0] for line in open(config.get("acl", "filename")).readlines()] diff --git a/radicale/calendar.py b/radicale/calendar.py new file mode 100644 index 0000000..942fc58 --- /dev/null +++ b/radicale/calendar.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +# TODO: Manage inheritance for classes + +from time import time + +import support + +class Calendar(object): + """ + Internal Calendar Class + """ + def __init__(self, user, cal): + # TODO: Use properties from the calendar + self.encoding = "utf-8" + self.owner = "lize" + self.user = user + self.cal = cal + self.version = "2.0" + self.ctag = str(hash(self.vcalendar())) + + def append(self, vcalendar): + """ + Append vcalendar + """ + self.ctag = str(hash(self.vcalendar())) + support.append(self.cal, vcalendar) + + def remove(self, uid): + """ + Remove Object Named uid + """ + self.ctag = str(hash(self.vcalendar())) + support.remove(self.cal, uid) + + def replace(self, uid, vcalendar): + """ + Replace Objet Named uid by vcalendar + """ + self.ctag = str(hash(self.vcalendar())) + support.remove(self.cal, uid) + support.append(self.cal, vcalendar) + + def vcalendar(self): + return unicode(support.read(self.cal), self.encoding) + +class Event(object): + """ + Internal Event Class + """ + # TODO: Fix the behaviour if no UID is given + def __init__(self, vcalendar): + self.text = vcalendar + + def etag(self): + return str(hash(self.text)) + +class Header(object): + """ + Internal Headers Class + """ + def __init__(self, vcalendar): + self.text = vcalendar + +class Timezone(object): + """ + Internal Timezone Class + """ + def __init__(self, vcalendar): + lines = vcalendar.splitlines() + for line in lines: + if line.startswith("TZID:"): + self.tzid = line.lstrip("TZID:") + break + + self.text = vcalendar + +class Todo(object): + """ + Internal Todo Class + """ + # TODO: Fix the behaviour if no UID is given + def __init__(self, vcalendar): + self.text = vcalendar + + def etag(self): + return str(hash(self.text)) diff --git a/radicale/config.py b/radicale/config.py new file mode 100644 index 0000000..f6d4763 --- /dev/null +++ b/radicale/config.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +from ConfigParser import RawConfigParser as ConfigParser + +# Default functions +_config = ConfigParser() +get = _config.get +set = _config.set +getboolean = _config.getboolean +getint = _config.getint +getfloat = _config.getfloat +options = _config.options +items = _config.items + +# Default config +_initial = { + "server": { + "certificate": "/etc/apache2/ssl/server.crt", + "privatekey": "/etc/apache2/ssl/server.key", + "log": "/var/www/radicale/server.log", + "port": "1001", + }, + "encoding": { + "request": "utf-8", + "stock": "utf-8", + }, + "namespace": { + "C": "urn:ietf:params:xml:ns:caldav", + "D": "DAV:", + "CS": "http://calendarserver.org/ns/", + }, + "status": { + "200": "HTTP/1.1 200 OK", + }, + "acl": { + "type": "htpasswd", + "filename": "/etc/radicale/users", + }, + "support": { + "type": "plain", + "folder": "/var/local/radicale", + }, + } + +# Set the default config +for section, values in _initial.iteritems(): + _config.add_section(section) + for key, value in values.iteritems(): + _config.set(section, key, value) + +# Set the user config +_config.read("/etc/radicale/config") diff --git a/radicale/ical.py b/radicale/ical.py new file mode 100644 index 0000000..ea45411 --- /dev/null +++ b/radicale/ical.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +# TODO: Manage filters (see xmlutils) +# TODO: Factorize code + +import calendar + +def writeCalendar(headers=[], timezones=[], todos=[], events=[]): + """ + Create calendar from headers, timezones, todos, events + """ + # TODO: Manage encoding and EOL + return "\n".join(( + "BEGIN:VCALENDAR", + "\n".join([header.text for header in headers]), + "\n".join([timezone.text for timezone in timezones]), + "\n".join([todo.text for todo in todos]), + "\n".join([event.text for event in events]), + "END:VCALENDAR")) + +def events(vcalendar): + """ + Find VEVENT Items in vcalendar + """ + events = [] + + lines = vcalendar.splitlines() + inEvent = False + eventLines = [] + + for line in lines: + if line.startswith("BEGIN:VEVENT"): + inEvent = True + eventLines = [] + + if inEvent: + # TODO: Manage encoding + eventLines.append(line) + if line.startswith("END:VEVENT"): + events.append(calendar.Event("\n".join(eventLines))) + + return events + +def headers(vcalendar): + """ + Find Headers Items in vcalendar + """ + headers = [] + + lines = vcalendar.splitlines() + for line in lines: + if line.startswith("PRODID:"): + headers.append(calendar.Header(line)) + for line in lines: + if line.startswith("VERSION:"): + headers.append(calendar.Header(line)) + + return headers + +def timezones(vcalendar): + """ + Find VTIMEZONE Items in vcalendar + """ + timezones = [] + + lines = vcalendar.splitlines() + inTz = False + tzLines = [] + + for line in lines: + if line.startswith("BEGIN:VTIMEZONE"): + inTz = True + tzLines = [] + + if inTz: + tzLines.append(line) + if line.startswith("END:VTIMEZONE"): + timezones.append(calendar.Timezone("\n".join(tzLines))) + + return timezones + +def todos(vcalendar): + """ + Find VTODO Items in vcalendar + """ + todos = [] + + lines = vcalendar.splitlines() + inTodo = False + todoLines = [] + + for line in lines: + if line.startswith("BEGIN:VTODO"): + inTodo = True + todoLines = [] + + if inTodo: + # TODO: Manage encoding + todoLines.append(line) + if line.startswith("END:VTODO"): + todos.append(calendar.Todo("\n".join(todoLines))) + + return todos diff --git a/radicale/support/__init__.py b/radicale/support/__init__.py new file mode 100644 index 0000000..5351db2 --- /dev/null +++ b/radicale/support/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +from .. import config + +_support = __import__(config.get("support", "type"), locals(), globals()) + +append = _support.append +calendars =_support.calendars +read = _support.read +remove = _support.remove diff --git a/radicale/support/plain.py b/radicale/support/plain.py new file mode 100644 index 0000000..8a8cbaf --- /dev/null +++ b/radicale/support/plain.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +import os +import posixpath + +from .. import ical +from .. import config + +def calendars(): + """ + List Available Calendars Paths + """ + calendars = [] + + for folder in os.listdir(config.get("support", "folder")): + for cal in os.listdir(os.path.join(config.get("support", "folder"), folder)): + calendars.append(posixpath.join(folder, cal)) + + return calendars + +def read(cal): + """ + Read cal + """ + path = os.path.join(config.get("support", "folder"), cal.replace(posixpath.sep, os.path.sep)) + return open(path).read() + +def append(cal, vcalendar): + """ + Append vcalendar to cal + """ + oldCalendar = read(cal) + oldTzs = [tz.tzid for tz in ical.timezones(oldCalendar)] + path = os.path.join(config.get("support", "folder"), cal.replace(posixpath.sep, os.path.sep)) + + oldObjects = [] + oldObjects.extend([event.etag() for event in ical.events(oldCalendar)]) + oldObjects.extend([todo.etag() for todo in ical.todos(oldCalendar)]) + + objects = [] + objects.extend(ical.events(vcalendar)) + objects.extend(ical.todos(vcalendar)) + + for tz in ical.timezones(vcalendar): + if tz.tzid not in oldTzs: + # TODO: Manage position, encoding and EOL + fd = open(path) + lines = [line for line in fd.readlines() if line] + fd.close() + + for i,line in enumerate(tz.text.splitlines()): + lines.insert(2+i, line.encode("utf-8")+"\n") + + fd = open(path, "w") + fd.writelines(lines) + fd.close() + + for obj in objects: + if obj.etag() not in oldObjects: + # TODO: Manage position, encoding and EOL + fd = open(path) + lines = [line for line in fd.readlines() if line] + fd.close() + + for line in obj.text.splitlines(): + lines.insert(-1, line.encode("utf-8")+"\n") + + fd = open(path, "w") + fd.writelines(lines) + fd.close() + +def remove(cal, etag): + """ + Remove object named uid from cal + """ + path = os.path.join(config.get("support", "folder"), cal.replace(posixpath.sep, os.path.sep)) + + cal = read(cal) + + headers = ical.headers(cal) + timezones = ical.timezones(cal) + todos = [todo for todo in ical.todos(cal) if todo.etag() != etag] + events = [event for event in ical.events(cal) if event.etag() != etag] + + fd = open(path, "w") + fd.write(ical.writeCalendar(headers, timezones, todos, events)) + fd.close() + diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py new file mode 100644 index 0000000..6394960 --- /dev/null +++ b/radicale/xmlutils.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8; indent-tabs-mode: nil; -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2008 The Radicale Team +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +XML and iCal requests manager + +Note that all these functions need to receive unicode objects for full +iCal requests (PUT) and string objects with charset correctly defined +in them for XML requests (all but PUT). +""" + +# TODO: Manage errors (see __init__) +# TODO: Manage depth and calendars/collections (see main) + +import xml.etree.ElementTree as ET + +import config +import ical + +# TODO: This is a well-known and accepted hack for ET +for key,value in config.items("namespace"): + ET._namespace_map[value] = key + +def _tag(shortName, local): + """ + Get XML Clark notation {uri(shortName)}local + """ + return "{%s}%s"%(config.get("namespace", shortName), local) + +def delete(obj, calendar, url): + """ + Read and answer DELETE requests + """ + # Read rfc4918-9.6 for info + + # Reading request + calendar.remove(obj) + + # Writing answer + multistatus = ET.Element(_tag("D", "multistatus")) + response = ET.Element(_tag("D", "response")) + multistatus.append(response) + + href = ET.Element(_tag("D", "href")) + href.text = url + response.append(href) + + status = ET.Element(_tag("D", "status")) + status.text = config.get("status", "200") + response.append(status) + + return ET.tostring(multistatus, config.get("encoding", "request")) + +def propfind(xmlRequest, calendar, url): + """ + Read and answer PROPFIND requests + """ + # Read rfc4918-9.1 for info + + # Reading request + root = ET.fromstring(xmlRequest) + + propElement = root.find(_tag("D", "prop")) + propList = propElement.getchildren() + properties = [property.tag for property in propList] + + # Writing answer + multistatus = ET.Element(_tag("D", "multistatus")) + response = ET.Element(_tag("D", "response")) + multistatus.append(response) + + href = ET.Element(_tag("D", "href")) + href.text = url + response.append(href) + + propstat = ET.Element(_tag("D", "propstat")) + response.append(propstat) + + prop = ET.Element(_tag("D", "prop")) + propstat.append(prop) + + if _tag("D", "resourcetype") in properties: + resourcetype = ET.Element(_tag("D", "resourcetype")) + resourcetype.append(ET.Element(_tag("D", "collection"))) + resourcetype.append(ET.Element(_tag("C", "calendar"))) + prop.append(resourcetype) + + if _tag("D", "owner") in properties: + owner = ET.Element(_tag("D", "owner")) + owner.text = calendar.owner + prop.append(owner) + + if _tag("CS", "getctag") in properties: + getctag = ET.Element(_tag("CS", "getctag")) + getctag.text = calendar.ctag + prop.append(getctag) + + status = ET.Element(_tag("D", "status")) + status.text = config.get("status", "200") + propstat.append(status) + + return ET.tostring(multistatus, config.get("encoding", "request")) + +def put(icalRequest, calendar, url, obj): + """ + Read PUT requests + """ + if obj: + # PUT is modifying obj + calendar.replace(obj, icalRequest) + else: + # PUT is adding a new object + calendar.append(icalRequest) + +def report(xmlRequest, calendar, url): + """ + Read and answer REPORT requests + """ + # Read rfc3253-3.6 for info + + # Reading request + root = ET.fromstring(xmlRequest) + + propElement = root.find(_tag("D", "prop")) + propList = propElement.getchildren() + properties = [property.tag for property in propList] + + filters = {} + filterElement = root.find(_tag("C", "filter")) + filterList = propElement.getchildren() + # TODO: This should be recursive + # TODO: Really manage filters (see ical) + for filter in filterList: + sub = filters[filter.get("name")] = {} + for subfilter in filter.getchildren(): + sub[subfilter.get("name")] = {} + + if root.tag == _tag("C", "calendar-multiget"): + # Read rfc4791-7.9 for info + hreferences = [hrefElement.text for hrefElement in root.findall(_tag("D", "href"))] + else: + hreferences = [url] + + # Writing answer + multistatus = ET.Element(_tag("D", "multistatus")) + + # TODO: WTF, sunbird needs one response by object, + # is that really what is needed? + # Read rfc4791-9.[6|10] for info + for hreference in hreferences: + headers = ical.headers(calendar.vcalendar()) + # TODO: Define timezones by obj + timezones = ical.timezones(calendar.vcalendar()) + + objects = [] + objects.extend(ical.events(calendar.vcalendar())) + objects.extend(ical.todos(calendar.vcalendar())) + + for obj in objects: + # TODO: Use the hreference to read data and create href.text + # We assume here that hreference is url + response = ET.Element(_tag("D", "response")) + multistatus.append(response) + + href = ET.Element(_tag("D", "href")) + href.text = url + response.append(href) + + propstat = ET.Element(_tag("D", "propstat")) + response.append(propstat) + + prop = ET.Element(_tag("D", "prop")) + propstat.append(prop) + + if _tag("D", "getetag") in properties: + # TODO: Can UID and ETAG be the same? + getetag = ET.Element(_tag("D", "getetag")) + getetag.text = obj.etag() + prop.append(getetag) + + if _tag("C", "calendar-data") in properties: + cdata = ET.Element(_tag("C", "calendar-data")) + # TODO: Maybe assume that events and todos are not the same + cdata.text = ical.writeCalendar(headers, timezones, [obj]) + prop.append(cdata) + + status = ET.Element(_tag("D", "status")) + status.text = config.get("status", "200") + propstat.append(status) + + return ET.tostring(multistatus, config.get("encoding", "request"))