Merge ical/support/calendar modules.

This commit is contained in:
Guillaume Ayoub 2010-02-10 23:52:50 +01:00
parent 21a743fcde
commit 9a07ec71d3
8 changed files with 229 additions and 366 deletions

View File

@ -33,15 +33,19 @@ should have been included in this package.
""" """
import os
import base64 import base64
import socket import socket
# Manage Python2/3 different modules
# pylint: disable-msg=F0401
try: try:
from http import client, server from http import client, server
except ImportError: except ImportError:
import httplib as client import httplib as client
import BaseHTTPServer as server 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): def _check(request, function):
@ -77,7 +81,9 @@ class HTTPSServer(HTTPServer):
def __init__(self, address, handler): def __init__(self, address, handler):
"""Create server by wrapping HTTP socket in an SSL socket.""" """Create server by wrapping HTTP socket in an SSL socket."""
# Fails with Python 2.5, import if needed # Fails with Python 2.5, import if needed
# pylint: disable-msg=F0401
import ssl import ssl
# pylint: enable-msg=F0401
HTTPServer.__init__(self, address, handler) HTTPServer.__init__(self, address, handler)
self.socket = ssl.wrap_socket( self.socket = ssl.wrap_socket(
@ -98,14 +104,15 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
check_rights = lambda function: lambda request: _check(request, function) check_rights = lambda function: lambda request: _check(request, function)
@property @property
def calendar(self): def _calendar(self):
"""The ``calendar.Calendar`` object corresponding to the given path.""" """The ``calendar.Calendar`` object corresponding to the given path."""
path = self.path.strip("/").split("/") # ``normpath`` should clean malformed and malicious request paths
if len(path) >= 2: attributes = os.path.normpath(self.path.strip("/")).split("/")
cal = "%s/%s" % (path[0], path[1]) if len(attributes) >= 2:
return calendar.Calendar("radicale", cal) 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.""" """Try to decode text according to various parameters."""
# List of charsets to try # List of charsets to try
charsets = [] charsets = []
@ -134,7 +141,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
@check_rights @check_rights
def do_GET(self): def do_GET(self):
"""Manage GET request.""" """Manage GET request."""
answer = self.calendar.vcalendar.encode(self._encoding) answer = self._calendar.read().encode(self._encoding)
self.send_response(client.OK) self.send_response(client.OK)
self.send_header("Content-Length", len(answer)) self.send_header("Content-Length", len(answer))
@ -145,7 +152,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
def do_DELETE(self): def do_DELETE(self):
"""Manage DELETE request.""" """Manage DELETE request."""
obj = self.headers.get("If-Match", None) 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_response(client.NO_CONTENT)
self.send_header("Content-Length", len(answer)) self.send_header("Content-Length", len(answer))
@ -162,7 +169,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
def do_PROPFIND(self): def do_PROPFIND(self):
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
xml_request = self.rfile.read(int(self.headers["Content-Length"])) 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_response(client.MULTI_STATUS)
self.send_header("DAV", "1, calendar-access") self.send_header("DAV", "1, calendar-access")
@ -173,10 +180,10 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
@check_rights @check_rights
def do_PUT(self): def do_PUT(self):
"""Manage PUT request.""" """Manage PUT request."""
ical_request = self.decode( ical_request = self._decode(
self.rfile.read(int(self.headers["Content-Length"]))) self.rfile.read(int(self.headers["Content-Length"])))
obj = self.headers.get("If-Match", None) 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) self.send_response(client.CREATED)
@ -184,9 +191,11 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
def do_REPORT(self): def do_REPORT(self):
"""Manage REPORT request.""" """Manage REPORT request."""
xml_request = self.rfile.read(int(self.headers["Content-Length"])) 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_response(client.MULTI_STATUS)
self.send_header("Content-Length", len(answer)) self.send_header("Content-Length", len(answer))
self.end_headers() self.end_headers()
self.wfile.write(answer) self.wfile.write(answer)
# pylint: enable-msg=C0103

View File

@ -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): FOLDER = os.path.expanduser(config.get("storage", "folder"))
"""Hash an vcalendar string."""
return str(hash(vcalendar))
class Calendar(object): # This function overrides the builtin ``open`` function for this module
"""Internal calendar class.""" # pylint: disable-msg=W0622
def __init__(self, user, cal): def open(path, mode="r"):
"""Initialize the calendar with ``cal`` and ``user`` parameters.""" """Open file at ``path`` with ``mode``, automagically managing encoding."""
# TODO: Use properties from the calendar configuration return codecs.open(path, mode, config.get("encoding", "stock"))
self.support = support.load() # pylint: enable-msg=W0622
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)
class Header(object): class Header(object):
"""Internal header class.""" """Internal header class."""
def __init__(self, vcalendar): def __init__(self, text):
"""Initialize header from ``vcalendar``.""" """Initialize header from ``text``."""
self.text = vcalendar self.text = text
class Timezone(object): class Event(object):
"""Internal timezone class.""" """Internal event class."""
def __init__(self, vcalendar): tag = "VEVENT"
"""Initialize timezone from ``vcalendar``."""
lines = vcalendar.splitlines()
for line in lines:
if line.startswith("TZID:"):
self.id = line.lstrip("TZID:")
break
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): class Todo(object):
"""Internal todo class.""" """Internal todo class."""
def __init__(self, vcalendar): # This is not a TODO!
"""Initialize todo from ``vcalendar``.""" # pylint: disable-msg=W0511
self.text = vcalendar tag = "VTODO"
# pylint: enable-msg=W0511
def __init__(self, text):
"""Initialize todo from ``text``."""
self.text = text
@property @property
def etag(self): def etag(self):
"""Etag from todo.""" """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)

View File

@ -29,10 +29,13 @@ Give a configparser-like interface to read and write configuration.
import os import os
import sys import sys
# Manage Python2/3 different modules
# pylint: disable-msg=F0401
try: try:
from configparser import RawConfigParser as ConfigParser from configparser import RawConfigParser as ConfigParser
except ImportError: except ImportError:
from ConfigParser import RawConfigParser as ConfigParser from ConfigParser import RawConfigParser as ConfigParser
# pylint: enable-msg=F0401
# Default configuration # Default configuration
@ -43,34 +46,27 @@ INITIAL_CONFIG = {
"daemon": "False", "daemon": "False",
"ssl": "False", "ssl": "False",
"certificate": "/etc/apache2/ssl/server.crt", "certificate": "/etc/apache2/ssl/server.crt",
"key": "/etc/apache2/ssl/server.key", "key": "/etc/apache2/ssl/server.key"},
},
"encoding": { "encoding": {
"request": "utf-8", "request": "utf-8",
"stock": "utf-8", "stock": "utf-8"},
},
"acl": { "acl": {
"type": "fake", "type": "fake",
"filename": "/etc/radicale/users", "filename": "/etc/radicale/users",
"encryption": "crypt", "encryption": "crypt"},
}, "storage": {
"support": { "folder": os.path.expanduser("~/.config/radicale/calendars")}}
"type": "plain",
"folder": os.path.expanduser("~/.config/radicale"),
"calendar": "radicale/cal",
},
}
# Create a ConfigParser and configure it # Create a ConfigParser and configure it
_CONFIG = ConfigParser() _CONFIG_PARSER = ConfigParser()
for section, values in INITIAL_CONFIG.items(): for section, values in INITIAL_CONFIG.items():
_CONFIG.add_section(section) _CONFIG_PARSER.add_section(section)
for key, value in values.items(): for key, value in values.items():
_CONFIG.set(section, key, value) _CONFIG_PARSER.set(section, key, value)
_CONFIG.read("/etc/radicale/config") _CONFIG_PARSER.read("/etc/radicale/config")
_CONFIG.read(os.path.expanduser("~/.config/radicale/config")) _CONFIG_PARSER.read(os.path.expanduser("~/.config/radicale/config"))
# Wrap config module into ConfigParser instance # Wrap config module into ConfigParser instance
sys.modules[__name__] = _CONFIG sys.modules[__name__] = _CONFIG_PARSER

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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"))

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@ -140,6 +140,7 @@ def propfind(xml_request, calendar, url):
def put(ical_request, calendar, url, obj): def put(ical_request, calendar, url, obj):
"""Read PUT requests.""" """Read PUT requests."""
# TODO: use url to set hreference
if obj: if obj:
# PUT is modifying obj # PUT is modifying obj
calendar.replace(obj, ical_request) calendar.replace(obj, ical_request)
@ -174,11 +175,10 @@ def report(xml_request, calendar, url):
# is that really what is needed? # is that really what is needed?
# Read rfc4791-9.[6|10] for info # Read rfc4791-9.[6|10] for info
for hreference in hreferences: for hreference in hreferences:
headers = ical.headers(calendar.vcalendar) headers = ical.headers(calendar.text)
timezones = ical.timezones(calendar.vcalendar) timezones = ical.timezones(calendar.text)
objects = \ objects = ical.events(calendar.text) + ical.todos(calendar.text)
ical.events(calendar.vcalendar) + ical.todos(calendar.vcalendar)
if not objects: if not objects:
# TODO: Read rfc4791-9.[6|10] to find a right answer # TODO: Read rfc4791-9.[6|10] to find a right answer

View File

@ -69,7 +69,7 @@ setup(
author_email="guillaume.ayoub@kozea.fr", author_email="guillaume.ayoub@kozea.fr",
url="http://www.radicale.org/", url="http://www.radicale.org/",
license="GNU GPL v3", license="GNU GPL v3",
packages=["radicale", "radicale.acl", "radicale.support"], packages=["radicale", "radicale.acl"],
scripts=["radicale.py"], scripts=["radicale.py"],
cmdclass={'clean': Clean, cmdclass={'clean': Clean,
"build_scripts": BuildScripts}) "build_scripts": BuildScripts})