From 9a07ec71d3ca505e841ff3a0107aef220e13bd3e Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 10 Feb 2010 23:52:50 +0100 Subject: [PATCH] Merge ical/support/calendar modules. --- radicale/__init__.py | 35 +++-- radicale/calendar.py | 260 +++++++++++++++++++++++++---------- radicale/config.py | 32 ++--- radicale/ical.py | 98 ------------- radicale/support/__init__.py | 32 ----- radicale/support/plain.py | 128 ----------------- radicale/xmlutils.py | 8 +- setup.py | 2 +- 8 files changed, 229 insertions(+), 366 deletions(-) delete mode 100644 radicale/ical.py delete mode 100644 radicale/support/__init__.py delete mode 100644 radicale/support/plain.py diff --git a/radicale/__init__.py b/radicale/__init__.py index 7711fd8..be8aad9 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -33,15 +33,19 @@ should have been included in this package. """ +import os import base64 import socket +# Manage Python2/3 different modules +# pylint: disable-msg=F0401 try: from http import client, server except ImportError: import httplib as client import BaseHTTPServer as server +# pylint: enable-msg=F0401 -from radicale import acl, calendar, config, support, xmlutils +from radicale import acl, calendar, config, xmlutils def _check(request, function): @@ -77,7 +81,9 @@ class HTTPSServer(HTTPServer): def __init__(self, address, handler): """Create server by wrapping HTTP socket in an SSL socket.""" # Fails with Python 2.5, import if needed + # pylint: disable-msg=F0401 import ssl + # pylint: enable-msg=F0401 HTTPServer.__init__(self, address, handler) self.socket = ssl.wrap_socket( @@ -98,14 +104,15 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): check_rights = lambda function: lambda request: _check(request, function) @property - def calendar(self): + def _calendar(self): """The ``calendar.Calendar`` object corresponding to the given path.""" - path = self.path.strip("/").split("/") - if len(path) >= 2: - cal = "%s/%s" % (path[0], path[1]) - return calendar.Calendar("radicale", cal) + # ``normpath`` should clean malformed and malicious request paths + attributes = os.path.normpath(self.path.strip("/")).split("/") + if len(attributes) >= 2: + path = "%s/%s" % (attributes[0], attributes[1]) + return calendar.Calendar(path) - def decode(self, text): + def _decode(self, text): """Try to decode text according to various parameters.""" # List of charsets to try charsets = [] @@ -134,7 +141,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): @check_rights def do_GET(self): """Manage GET request.""" - answer = self.calendar.vcalendar.encode(self._encoding) + answer = self._calendar.read().encode(self._encoding) self.send_response(client.OK) self.send_header("Content-Length", len(answer)) @@ -145,7 +152,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): def do_DELETE(self): """Manage DELETE request.""" obj = self.headers.get("If-Match", None) - answer = xmlutils.delete(obj, self.calendar, self.path) + answer = xmlutils.delete(obj, self._calendar, self.path) self.send_response(client.NO_CONTENT) self.send_header("Content-Length", len(answer)) @@ -162,7 +169,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): def do_PROPFIND(self): """Manage PROPFIND request.""" xml_request = self.rfile.read(int(self.headers["Content-Length"])) - answer = xmlutils.propfind(xml_request, self.calendar, self.path) + answer = xmlutils.propfind(xml_request, self._calendar, self.path) self.send_response(client.MULTI_STATUS) self.send_header("DAV", "1, calendar-access") @@ -173,10 +180,10 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): @check_rights def do_PUT(self): """Manage PUT request.""" - ical_request = self.decode( + ical_request = self._decode( self.rfile.read(int(self.headers["Content-Length"]))) obj = self.headers.get("If-Match", None) - xmlutils.put(ical_request, self.calendar, self.path, obj) + xmlutils.put(ical_request, self._calendar, self.path, obj) self.send_response(client.CREATED) @@ -184,9 +191,11 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): def do_REPORT(self): """Manage REPORT request.""" xml_request = self.rfile.read(int(self.headers["Content-Length"])) - answer = xmlutils.report(xml_request, self.calendar, self.path) + answer = xmlutils.report(xml_request, self._calendar, self.path) self.send_response(client.MULTI_STATUS) self.send_header("Content-Length", len(answer)) self.end_headers() self.wfile.write(answer) + + # pylint: enable-msg=C0103 diff --git a/radicale/calendar.py b/radicale/calendar.py index 1db24e5..e121851 100644 --- a/radicale/calendar.py +++ b/radicale/calendar.py @@ -25,93 +25,209 @@ Define the main classes of a calendar as seen from the server. """ -from radicale import support +import os +import codecs + +from radicale import config -def hash_tag(vcalendar): - """Hash an vcalendar string.""" - return str(hash(vcalendar)) +FOLDER = os.path.expanduser(config.get("storage", "folder")) + - -class Calendar(object): - """Internal calendar class.""" - def __init__(self, user, cal): - """Initialize the calendar with ``cal`` and ``user`` parameters.""" - # TODO: Use properties from the calendar configuration - self.support = support.load() - self.encoding = "utf-8" - self.owner = "radicale" - self.user = user - self.cal = cal - self.version = "2.0" - self.ctag = hash_tag(self.vcalendar) - - def append(self, vcalendar): - """Append vcalendar to the calendar.""" - self.ctag = hash_tag(self.vcalendar) - self.support.append(self.cal, vcalendar) - - def remove(self, uid): - """Remove object named ``uid`` from the calendar.""" - self.ctag = hash_tag(self.vcalendar) - self.support.remove(self.cal, uid) - - def replace(self, uid, vcalendar): - """Replace objet named ``uid`` by ``vcalendar`` in the calendar.""" - self.ctag = hash_tag(self.vcalendar) - self.support.remove(self.cal, uid) - self.support.append(self.cal, vcalendar) - - @property - def vcalendar(self): - """Unicode calendar from the calendar.""" - return self.support.read(self.cal) - - @property - def etag(self): - """Etag from calendar.""" - return '"%s"' % hash_tag(self.vcalendar) - - -class Event(object): - """Internal event class.""" - def __init__(self, vcalendar): - """Initialize event from ``vcalendar``.""" - self.text = vcalendar - - @property - def etag(self): - """Etag from event.""" - return '"%s"' % hash_tag(self.text) +# This function overrides the builtin ``open`` function for this module +# pylint: disable-msg=W0622 +def open(path, mode="r"): + """Open file at ``path`` with ``mode``, automagically managing encoding.""" + return codecs.open(path, mode, config.get("encoding", "stock")) +# pylint: enable-msg=W0622 class Header(object): """Internal header class.""" - def __init__(self, vcalendar): - """Initialize header from ``vcalendar``.""" - self.text = vcalendar + def __init__(self, text): + """Initialize header from ``text``.""" + self.text = text -class Timezone(object): - """Internal timezone class.""" - def __init__(self, vcalendar): - """Initialize timezone from ``vcalendar``.""" - lines = vcalendar.splitlines() - for line in lines: - if line.startswith("TZID:"): - self.id = line.lstrip("TZID:") - break +class Event(object): + """Internal event class.""" + tag = "VEVENT" - self.text = vcalendar + def __init__(self, text): + """Initialize event from ``text``.""" + self.text = text + + @property + def etag(self): + """Etag from event.""" + return '"%s"' % hash(self.text) class Todo(object): """Internal todo class.""" - def __init__(self, vcalendar): - """Initialize todo from ``vcalendar``.""" - self.text = vcalendar + # This is not a TODO! + # pylint: disable-msg=W0511 + tag = "VTODO" + # pylint: enable-msg=W0511 + + def __init__(self, text): + """Initialize todo from ``text``.""" + self.text = text @property def etag(self): """Etag from todo.""" - return hash_tag(self.text) + return '"%s"' % hash(self.text) + + +class Timezone(object): + """Internal timezone class.""" + tag = "VTIMEZONE" + + def __init__(self, text): + """Initialize timezone from ``text``.""" + lines = text.splitlines() + for line in lines: + if line.startswith("TZID:"): + self.name = line.replace("TZID:", "") + break + + self.text = text + + +class Calendar(object): + """Internal calendar class.""" + 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)) + self.ctag = self.etag + + @staticmethod + def _parse(text, obj): + """Find ``obj.tag`` items in ``text`` text. + + Return a list of items of type ``obj``. + + """ + items = [] + + lines = text.splitlines() + in_item = False + item_lines = [] + + for line in lines: + if line.startswith("BEGIN:%s" % obj.tag): + in_item = True + item_lines = [] + + if in_item: + item_lines.append(line) + if line.startswith("END:%s" % obj.tag): + items.append(obj("\n".join(item_lines))) + + return items + + def append(self, text): + """Append ``text`` to calendar.""" + self.ctag = self.etag + + timezones = self.timezones + events = self.events + todos = self.todos + + for new_timezone in self._parse(text, Timezone): + if new_timezone.name not in [timezone.name + for timezone in timezones]: + timezones.append(new_timezone) + + for new_event in self._parse(text, Event): + if new_event.etag not in [event.etag for event in events]: + events.append(new_event) + + for new_todo in self._parse(text, Todo): + if new_todo.etag not in [todo.etag for todo in todos]: + todos.append(new_todo) + + self.write(timezones=timezones, events=events, todos=todos) + + def remove(self, etag): + """Remove object named ``etag`` from the calendar.""" + self.ctag = self.etag + todos = [todo for todo in self.todos if todo.etag != etag] + events = [event for event in self.events if event.etag != etag] + + self.write(todos=todos, events=events) + + def replace(self, etag, text): + """Replace objet named ``etag`` by ``text`` in the calendar.""" + self.ctag = self.etag + self.remove(etag) + self.append(text) + + def write(self, headers=None, timezones=None, events=None, todos=None): + """Write calendar with given parameters.""" + headers = headers or self.headers or ( + Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), + Header("VERSION:2.0")) + timezones = timezones or self.timezones + events = events or self.events + todos = todos or self.todos + + # Create folder if absent + if not os.path.exists(os.path.dirname(self.path)): + os.makedirs(os.path.dirname(self.path)) + + text = "\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")) + return open(self.path, "w").write(text) + + @property + def etag(self): + """Etag from calendar.""" + return '"%s"' % hash(self.text) + + @property + def text(self): + """Calendar as plain text.""" + try: + return open(self.path).read() + except IOError: + return "" + + @property + def headers(self): + """Find headers items in calendar.""" + header_lines = [] + + lines = self.text.splitlines() + for line in lines: + if line.startswith("PRODID:"): + header_lines.append(Header(line)) + for line in lines: + if line.startswith("VERSION:"): + header_lines.append(Header(line)) + + return header_lines + + @property + def events(self): + """Get list of ``Event`` items in calendar.""" + return self._parse(self.text, Event) + + @property + def todos(self): + """Get list of ``Todo`` items in calendar.""" + return self._parse(self.text, Todo) + + @property + def timezones(self): + """Get list of ``Timezome`` items in calendar.""" + return self._parse(self.text, Timezone) diff --git a/radicale/config.py b/radicale/config.py index e99b010..7a95a78 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -29,10 +29,13 @@ Give a configparser-like interface to read and write configuration. import os import sys +# Manage Python2/3 different modules +# pylint: disable-msg=F0401 try: from configparser import RawConfigParser as ConfigParser except ImportError: from ConfigParser import RawConfigParser as ConfigParser +# pylint: enable-msg=F0401 # Default configuration @@ -43,34 +46,27 @@ INITIAL_CONFIG = { "daemon": "False", "ssl": "False", "certificate": "/etc/apache2/ssl/server.crt", - "key": "/etc/apache2/ssl/server.key", - }, + "key": "/etc/apache2/ssl/server.key"}, "encoding": { "request": "utf-8", - "stock": "utf-8", - }, + "stock": "utf-8"}, "acl": { "type": "fake", "filename": "/etc/radicale/users", - "encryption": "crypt", - }, - "support": { - "type": "plain", - "folder": os.path.expanduser("~/.config/radicale"), - "calendar": "radicale/cal", - }, - } + "encryption": "crypt"}, + "storage": { + "folder": os.path.expanduser("~/.config/radicale/calendars")}} # Create a ConfigParser and configure it -_CONFIG = ConfigParser() +_CONFIG_PARSER = ConfigParser() for section, values in INITIAL_CONFIG.items(): - _CONFIG.add_section(section) + _CONFIG_PARSER.add_section(section) for key, value in values.items(): - _CONFIG.set(section, key, value) + _CONFIG_PARSER.set(section, key, value) -_CONFIG.read("/etc/radicale/config") -_CONFIG.read(os.path.expanduser("~/.config/radicale/config")) +_CONFIG_PARSER.read("/etc/radicale/config") +_CONFIG_PARSER.read(os.path.expanduser("~/.config/radicale/config")) # Wrap config module into ConfigParser instance -sys.modules[__name__] = _CONFIG +sys.modules[__name__] = _CONFIG_PARSER diff --git a/radicale/ical.py b/radicale/ical.py deleted file mode 100644 index e10b46d..0000000 --- a/radicale/ical.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub -# Copyright © 2008 Nicolas Kandel -# Copyright © 2008 Pascal Halter -# -# 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 . - -""" -iCal parsing functions. - -""" - -# TODO: Manage filters (see xmlutils) - -from radicale import calendar - - -def write_calendar(headers=( - calendar.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), - calendar.Header("VERSION:2.0")), - timezones=(), todos=(), events=()): - """Create calendar from given parameters.""" - cal = "\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")) - return "\n".join([line for line in cal.splitlines() if line]) - - -def _parse(vcalendar, tag, obj): - """Find ``tag`` items in ``vcalendar``. - - Return a list of items of type ``obj``. - - """ - items = [] - - lines = vcalendar.splitlines() - in_item = False - item_lines = [] - - for line in lines: - if line.startswith("BEGIN:%s" % tag): - in_item = True - item_lines = [] - - if in_item: - item_lines.append(line) - if line.startswith("END:%s" % tag): - items.append(obj("\n".join(item_lines))) - - return items - - -def headers(vcalendar): - """Find Headers items in ``vcalendar``.""" - header_lines = [] - - lines = vcalendar.splitlines() - for line in lines: - if line.startswith("PRODID:"): - header_lines.append(calendar.Header(line)) - for line in lines: - if line.startswith("VERSION:"): - header_lines.append(calendar.Header(line)) - - return header_lines - - -def events(vcalendar): - """Get list of ``Event`` from VEVENTS items in ``vcalendar``.""" - return _parse(vcalendar, "VEVENT", calendar.Event) - - -def todos(vcalendar): - """Get list of ``Todo`` from VTODO items in ``vcalendar``.""" - return _parse(vcalendar, "VTODO", calendar.Todo) - - -def timezones(vcalendar): - """Get list of ``Timezome`` from VTIMEZONE items in ``vcalendar``.""" - return _parse(vcalendar, "VTIMEZONE", calendar.Timezone) diff --git a/radicale/support/__init__.py b/radicale/support/__init__.py deleted file mode 100644 index ace80ef..0000000 --- a/radicale/support/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub -# Copyright © 2008 Nicolas Kandel -# Copyright © 2008 Pascal Halter -# -# 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 . - -""" -Calendar storage support configuration. - -""" - -from radicale import config - -def load(): - """Load list of available storage support managers.""" - module = __import__("radicale.support", globals(), locals(), - [config.get("support", "type")]) - return getattr(module, config.get("support", "type")) diff --git a/radicale/support/plain.py b/radicale/support/plain.py deleted file mode 100644 index 3dd58dd..0000000 --- a/radicale/support/plain.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2010 Guillaume Ayoub -# Copyright © 2008 Nicolas Kandel -# Copyright © 2008 Pascal Halter -# -# 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 . - -""" -Plain text storage. - -""" - -import os -import posixpath -import codecs - -from radicale import config, ical - -FOLDER = os.path.expanduser(config.get("support", "folder")) -DEFAULT_CALENDAR = config.get("support", "calendar") - - -def _open(path, mode="r"): - """Open file at ``path`` with ``mode``, automagically managing encoding.""" - return codecs.open(path, mode, config.get("encoding", "stock")) - - -def calendars(): - """List available calendars paths.""" - available_calendars = [] - - for filename in os.listdir(FOLDER): - if os.path.isdir(os.path.join(FOLDER, filename)): - for cal in os.listdir(os.path.join(FOLDER, filename)): - available_calendars.append(posixpath.join(filename, cal)) - - return available_calendars - - -def mkcalendar(name): - """Write a new calendar called ``name``.""" - user, cal = name.split(posixpath.sep) - if not os.path.exists(os.path.join(FOLDER, user)): - os.makedirs(os.path.join(FOLDER, user)) - descriptor = _open(os.path.join(FOLDER, user, cal), "w") - descriptor.write(ical.write_calendar()) - - -def read(cal): - """Read calendar ``cal``.""" - path = os.path.join(FOLDER, cal.replace(posixpath.sep, os.path.sep)) - return _open(path).read() - - -def append(cal, vcalendar): - """Append ``vcalendar`` to ``cal``.""" - old_calendar = read(cal) - old_timezones = [timezone.id for timezone in ical.timezones(old_calendar)] - path = os.path.join(FOLDER, cal.replace(posixpath.sep, os.path.sep)) - - old_objects = [] - old_objects.extend([event.etag for event in ical.events(old_calendar)]) - old_objects.extend([todo.etag for todo in ical.todos(old_calendar)]) - - objects = [] - objects.extend(ical.events(vcalendar)) - objects.extend(ical.todos(vcalendar)) - - for timezone in ical.timezones(vcalendar): - if timezone.id not in old_timezones: - descriptor = _open(path) - lines = [line for line in descriptor.readlines() if line] - descriptor.close() - - for i, line in enumerate(timezone.text.splitlines()): - lines.insert(2 + i, line + "\n") - - descriptor = _open(path, "w") - descriptor.writelines(lines) - descriptor.close() - - for obj in objects: - if obj.etag not in old_objects: - descriptor = _open(path) - lines = [line for line in descriptor.readlines() if line] - descriptor.close() - - for line in obj.text.splitlines(): - lines.insert(-1, line + "\n") - - descriptor = _open(path, "w") - descriptor.writelines(lines) - descriptor.close() - - -def remove(cal, etag): - """Remove object named ``etag`` from ``cal``.""" - path = os.path.join(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] - - descriptor = _open(path, "w") - descriptor.write(ical.write_calendar(headers, timezones, todos, events)) - descriptor.close() - - -# Create default calendar if not present -if DEFAULT_CALENDAR: - if DEFAULT_CALENDAR not in calendars(): - mkcalendar(DEFAULT_CALENDAR) diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index d8866ca..abb7497 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -140,6 +140,7 @@ def propfind(xml_request, calendar, url): def put(ical_request, calendar, url, obj): """Read PUT requests.""" + # TODO: use url to set hreference if obj: # PUT is modifying obj calendar.replace(obj, ical_request) @@ -174,11 +175,10 @@ def report(xml_request, calendar, url): # is that really what is needed? # Read rfc4791-9.[6|10] for info for hreference in hreferences: - headers = ical.headers(calendar.vcalendar) - timezones = ical.timezones(calendar.vcalendar) + headers = ical.headers(calendar.text) + timezones = ical.timezones(calendar.text) - objects = \ - ical.events(calendar.vcalendar) + ical.todos(calendar.vcalendar) + objects = ical.events(calendar.text) + ical.todos(calendar.text) if not objects: # TODO: Read rfc4791-9.[6|10] to find a right answer diff --git a/setup.py b/setup.py index 85358d1..0b9d00c 100755 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup( author_email="guillaume.ayoub@kozea.fr", url="http://www.radicale.org/", license="GNU GPL v3", - packages=["radicale", "radicale.acl", "radicale.support"], + packages=["radicale", "radicale.acl"], scripts=["radicale.py"], cmdclass={'clean': Clean, "build_scripts": BuildScripts})