diff --git a/NEWS b/NEWS index ea78e6d2..fa927ff1 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,15 @@ NEWS ------ +0.5 - *Not released yet* +======================== + +* Calendar depth +* MacOS and Windows support +* HEAD requests management +* htpasswd user from calendar path + + 0.4 - Hot Days Back =================== diff --git a/TODO b/TODO index e9f57b34..f394b8ab 100644 --- a/TODO +++ b/TODO @@ -9,9 +9,16 @@ 0.5 === -* Calendar collections -* Group calendars -* [IN PROGRESS] Windows and MacOS tested support +* iCal and iPhone support + + +0.6 +=== + +* [IN PROGRESS] Group calendars +* [IN PROGRESS] LDAP and databases auth support +* CalDAV rights +* Read-only access for foreign users 1.0 diff --git a/radicale.py b/radicale.py index e7d490c2..03f6a147 100755 --- a/radicale.py +++ b/radicale.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # @@ -28,7 +28,7 @@ """ Radicale Server entry point. -Launch the Radicale Serve according to configuration and command-line +Launch the Radicale Server according to configuration and command-line arguments. """ diff --git a/radicale/__init__.py b/radicale/__init__.py index aa7e0546..18b54266 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # @@ -56,6 +56,11 @@ def _check(request, function): log.log(10, "Check if user has sufficient rights for performing ``request``.") # ``_check`` decorator can access ``request`` protected functions # pylint: disable=W0212 + + # If we have no calendar, don't check rights + if not request._calendar: + return function(request) + authorization = request.headers.get("Authorization", None) if authorization: challenge = authorization.lstrip("Basic").strip().encode("ascii") @@ -94,6 +99,7 @@ class HTTPServer(server.HTTPServer): class HTTPSServer(HTTPServer): """HTTPS server.""" PROTOCOL = "https" + def __init__(self, address, handler): """Create server by wrapping HTTP socket in an SSL socket.""" log.log(10, "Create server by wrapping HTTP socket in an SSL socket.") @@ -163,7 +169,8 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): """Manage GET request.""" log.log(10, "Manage GET request.") self.do_HEAD() - self.wfile.write(self._answer) + if self._answer: + self.wfile.write(self._answer) @check_rights def do_HEAD(self): @@ -180,6 +187,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): headers=self._calendar.headers, items=items) etag = item.etag else: + self._answer = None self.send_response(client.GONE) return else: @@ -212,12 +220,19 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): # No item or ETag precondition not verified, do not delete item self.send_response(client.PRECONDITION_FAILED) + @check_rights + def do_MKCALENDAR(self): + """Manage MKCALENDAR request.""" + self.send_response(client.CREATED) + self.end_headers() + def do_OPTIONS(self): """Manage OPTIONS request.""" log.log(10, "Manage OPTIONS request.") self.send_response(client.OK) self.send_header( - "Allow", "DELETE, HEAD, GET, OPTIONS, PROPFIND, PUT, REPORT") + "Allow", "DELETE, HEAD, GET, MKCALENDAR, " + "OPTIONS, PROPFIND, PUT, REPORT") self.send_header("DAV", "1, calendar-access") self.end_headers() @@ -227,7 +242,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): xml_request = self.rfile.read(int(self.headers["Content-Length"])) self._answer = xmlutils.propfind( self.path, xml_request, self._calendar, - self.headers.get("depth", "infinity"), self) + self.headers.get("depth", "infinity")) self.send_response(client.MULTI_STATUS) self.send_header("DAV", "1, calendar-access") diff --git a/radicale/acl/__init__.py b/radicale/acl/__init__.py index 349b1dd0..d33e8788 100644 --- a/radicale/acl/__init__.py +++ b/radicale/acl/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # diff --git a/radicale/acl/fake.py b/radicale/acl/fake.py index 145f3d12..9ddd2246 100644 --- a/radicale/acl/fake.py +++ b/radicale/acl/fake.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # diff --git a/radicale/acl/htpasswd.py b/radicale/acl/htpasswd.py index bb2f26bc..250ec901 100644 --- a/radicale/acl/htpasswd.py +++ b/radicale/acl/htpasswd.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # @@ -49,7 +49,7 @@ def _sha1(hash_value, password): """Check if ``hash_value`` and ``password`` match using sha1 method.""" hash_value = hash_value.replace("{SHA}", "").encode("ascii") password = password.encode(config.get("encoding", "stock")) - sha1 = hashlib.sha1() + sha1 = hashlib.sha1() # pylint: disable=E1101 sha1.update(password) return sha1.digest() == base64.b64decode(hash_value) diff --git a/radicale/config.py b/radicale/config.py index ca4b0da8..74a315e2 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # @@ -25,8 +25,6 @@ Give a configparser-like interface to read and write configuration. """ -# TODO: Use abstract filenames for other platforms - import os import sys # Manage Python2/3 different modules diff --git a/radicale/ical.py b/radicale/ical.py index cff038f8..f8d51e4a 100644 --- a/radicale/ical.py +++ b/radicale/ical.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # @@ -135,7 +135,6 @@ class Calendar(object): def __init__(self, path): """Initialize the calendar with ``cal`` and ``user`` parameters.""" - # TODO: Use properties from the calendar configuration self.encoding = "utf-8" self.owner = path.split("/")[0] self.path = os.path.join(FOLDER, path.replace("/", os.path.sep)) diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index 9289e9d0..4bbee011 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub +# Copyright © 2008-2011 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # @@ -27,8 +27,6 @@ in them for XML requests (all but PUT). """ -# TODO: Manage depth and calendars/collections - import xml.etree.ElementTree as ET from radicale import client, config, ical, log @@ -54,7 +52,8 @@ def _response(code): def name_from_path(path): """Return Radicale item name from ``path``.""" log.log(10, "Return Radicale item name from ``path``.") - return path.split("/")[-1] + path_parts = path.strip("/").split("/") + return path_parts[-1] if len(path_parts) > 2 else None def delete(path, calendar): @@ -83,7 +82,7 @@ def delete(path, calendar): return ET.tostring(multistatus, config.get("encoding", "request")) -def propfind(path, xml_request, calendar, depth, request): +def propfind(path, xml_request, calendar, depth): """Read and answer PROPFIND requests. Read rfc4918-9.1 for info. @@ -100,23 +99,24 @@ def propfind(path, xml_request, calendar, depth, request): # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) - if depth == "0": - elements = [calendar] - elif depth == "1": - elements = [calendar] + calendar.events + calendar.todos + if calendar: + if depth == "0": + items = [calendar] + else: + # depth is 1, infinity or not specified + # we limit ourselves to depth == 1 + items = [calendar] + calendar.events + calendar.todos else: - # depth is infinity or not specified - # we limit ourselves to depth == 1 - elements = [calendar] + calendar.events + calendar.todos + items = [] - for element in elements: - is_calendar = isinstance(element, ical.Calendar) + for item in items: + is_calendar = isinstance(item, ical.Calendar) response = ET.Element(_tag("D", "response")) multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = path if is_calendar else "%s/%s" % (path, element.name) + href.text = path if is_calendar else path + item.name response.append(href) propstat = ET.Element(_tag("D", "propstat")) @@ -127,36 +127,44 @@ def propfind(path, xml_request, calendar, depth, request): for tag in props: element = ET.Element(tag) - if tag == _tag("D", "resourcetype"): - if is_calendar: - tag = ET.Element(_tag("C", "calendar")) - element.append(tag) - tag = ET.Element(_tag("D", "collection")) - element.append(tag) - else: - tag = ET.Element(_tag("C", "comp")) - tag.set("name", element.tag) - element.append(tag) + if tag == _tag("D", "resourcetype") and is_calendar: + tag = ET.Element(_tag("C", "calendar")) + element.append(tag) + tag = ET.Element(_tag("D", "collection")) + element.append(tag) elif tag == _tag("D", "owner"): element.text = calendar.owner elif tag == _tag("D", "getcontenttype"): element.text = "text/calendar" + elif tag == _tag("CS", "getctag") and is_calendar: + element.text = item.etag elif tag == _tag("D", "getetag"): - element.text = element.etag - elif tag == _tag("D", "displayname"): + element.text = item.etag + elif tag == _tag("D", "displayname") and is_calendar: element.text = calendar.name - elif tag == _tag("D", "supported-report-set"): - supported_report = ET.Element(_tag("D", "supported-report")) - report_set = ET.Element(_tag("D", "report")) - report_set.append(ET.Element(_tag("C", "calendar-multiget"))) - supported_report.append(report_set) - element.append(supported_report) elif tag == _tag("D", "principal-URL"): # TODO: use a real principal URL, read rfc3744-4.2 for info - element.text = "%s://%s%s" % ( - request.server.PROTOCOL, request.headers["Host"], - request.path) - + tag = ET.Element(_tag("D", "href")) + tag.text = path + element.append(tag) + elif tag in ( + _tag("D", "principal-collection-set"), + _tag("C", "calendar-user-address-set"), + _tag("C", "calendar-home-set")): + tag = ET.Element(_tag("D", "href")) + tag.text = path + element.append(tag) + elif tag == _tag("C", "supported-calendar-component-set"): + comp = ET.Element(_tag("C", "comp")) + comp.set("name", "VTODO") # pylint: disable=W0511 + element.append(comp) + comp = ET.Element(_tag("C", "comp")) + comp.set("name", "VEVENT") + element.append(comp) + elif tag == _tag("D", "current-user-privilege-set"): + privilege = ET.Element(_tag("D", "privilege")) + privilege.append(ET.Element(_tag("D", "all"))) + element.append(privilege) prop.append(element) status = ET.Element(_tag("D", "status")) @@ -192,12 +200,15 @@ def report(path, xml_request, calendar): prop_list = prop_element.getchildren() props = [prop.tag for prop in prop_list] - if root.tag == _tag("C", "calendar-multiget"): - # Read rfc4791-7.9 for info - hreferences = set((href_element.text for href_element - in root.findall(_tag("D", "href")))) + if calendar: + if root.tag == _tag("C", "calendar-multiget"): + # Read rfc4791-7.9 for info + hreferences = set((href_element.text for href_element + in root.findall(_tag("D", "href")))) + else: + hreferences = (path,) else: - hreferences = (path,) + hreferences = () # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) @@ -228,19 +239,14 @@ def report(path, xml_request, calendar): prop = ET.Element(_tag("D", "prop")) propstat.append(prop) - if _tag("D", "getetag") in props: - element = ET.Element(_tag("D", "getetag")) - element.text = item.etag - prop.append(element) - - if _tag("C", "calendar-data") in props: - element = ET.Element(_tag("C", "calendar-data")) - if isinstance(item, ical.Event): - element.text = ical.serialize( - calendar.headers, calendar.timezones + [item]) - elif isinstance(item, ical.Todo): - element.text = ical.serialize( - calendar.headers, calendar.timezones + [item]) + for tag in props: + element = ET.Element(tag) + if tag == _tag("D", "getetag"): + element.text = item.etag + elif tag == _tag("C", "calendar-data"): + if isinstance(item, (ical.Event, ical.Todo)): + element.text = ical.serialize( + calendar.headers, calendar.timezones + [item]) prop.append(element) status = ET.Element(_tag("D", "status")) diff --git a/setup.py b/setup.py index 70553b1e..2d9b09cf 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server -# Copyright © 2009-2010 Guillaume Ayoub +# Copyright © 2009-2011 Guillaume Ayoub # # 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