Code cleaned using Pylint, fixes various minor bugs too.

This commit is contained in:
Guillaume Ayoub 2010-02-10 18:57:21 +01:00
parent a75bb261ed
commit 21a743fcde
11 changed files with 225 additions and 175 deletions

View File

@ -19,16 +19,21 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
# This file is just a script, allow [a-z0-9]* variable names
# pylint: disable-msg=C0103
# ``import radicale`` refers to the ``radicale`` module, not ``radicale.py``
# pylint: disable-msg=W0406
"""
Radicale Server entry point.
Launch the Radicale Serve according to configuration and command-line
arguments.
"""
# TODO: Manage depth and calendars/collections (see xmlutils)
# TODO: Manage smart and configurable logs
# TODO: Manage authentication
import os
import sys
@ -62,7 +67,7 @@ parser.add_option(
"-c", "--certificate",
default=radicale.config.get("server", "certificate"),
help="certificate file ")
options, args = parser.parse_args()
options = parser.parse_args()[0]
# Update Radicale configuration according to options
for option in parser.option_list:
@ -79,5 +84,6 @@ if options.daemon:
# Launch calendar server
server_class = radicale.HTTPSServer if options.ssl else radicale.HTTPServer
server = server_class((options.host, options.port), radicale.CalendarHTTPHandler)
server = server_class(
(options.host, options.port), radicale.CalendarHTTPHandler)
server.serve_forever()

View File

@ -33,8 +33,6 @@ should have been included in this package.
"""
# TODO: Manage errors (see xmlutils)
import base64
import socket
try:
@ -43,9 +41,10 @@ except ImportError:
import httplib as client
import BaseHTTPServer as server
from radicale import acl, config, support, xmlutils
from radicale import acl, calendar, config, support, xmlutils
def check(request, function):
def _check(request, function):
"""Check if user has sufficient rights for performing ``request``."""
authorization = request.headers.get("Authorization", None)
if authorization:
@ -64,8 +63,6 @@ def check(request, function):
"Basic realm=\"Radicale Server - Password Required\"")
request.end_headers()
# Decorator checking rights before performing request
check_rights = lambda function: lambda request: check(request, function)
class HTTPServer(server.HTTPServer):
"""HTTP server."""
@ -74,6 +71,7 @@ class HTTPServer(server.HTTPServer):
server.HTTPServer.__init__(self, address, handler)
self.acl = acl.load()
class HTTPSServer(HTTPServer):
"""HTTPS server."""
def __init__(self, address, handler):
@ -91,10 +89,14 @@ class HTTPSServer(HTTPServer):
self.server_bind()
self.server_activate()
class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
"""HTTP requests handler for calendars."""
_encoding = config.get("encoding", "request")
# Decorator checking rights before performing request
check_rights = lambda function: lambda request: _check(request, function)
@property
def calendar(self):
"""The ``calendar.Calendar`` object corresponding to the given path."""
@ -109,9 +111,9 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
charsets = []
# First append content charset given in the request
contentType = self.headers["Content-Type"]
if contentType and "charset=" in contentType:
charsets.append(contentType.split("charset=")[1].strip())
content_type = self.headers["Content-Type"]
if content_type and "charset=" in content_type:
charsets.append(content_type.split("charset=")[1].strip())
# Then append default Radicale charset
charsets.append(self._encoding)
# Then append various fallbacks
@ -126,10 +128,13 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
pass
raise UnicodeDecodeError
# Naming methods ``do_*`` is OK here
# pylint: disable-msg=C0103
@check_rights
def do_GET(self):
"""Manage GET request."""
answer = self.calendar.vcalendar.encode(_encoding)
answer = self.calendar.vcalendar.encode(self._encoding)
self.send_response(client.OK)
self.send_header("Content-Length", len(answer))

View File

@ -23,11 +23,14 @@ Users and rights management.
This module loads a list of users with access rights, according to the acl
configuration.
"""
from radicale import config
def load():
"""Load list of available ACL managers."""
module = __import__("radicale.acl", globals(), locals(),
[config.get("acl", "type")])
return getattr(module, config.get("acl", "type"))

View File

@ -25,6 +25,6 @@ No rights management.
"""
def has_right(user, password):
def has_right(*_):
"""Check if ``user``/``password`` couple is valid."""
return True

View File

@ -33,27 +33,35 @@ import hashlib
from radicale import config
def _plain(hash, password):
return hash == password
def _crypt(hash, password):
return crypt.crypt(password, hash) == hash
FILENAME = config.get("acl", "filename")
CHECK_PASSWORD = locals()["_%s" % config.get("acl", "encryption")]
def _sha1(hash, password):
hash = hash.replace("{SHA}", "").encode("ascii")
def _plain(hash_value, password):
"""Check if ``hash_value`` and ``password`` match using plain method."""
return hash_value == password
def _crypt(hash_value, password):
"""Check if ``hash_value`` and ``password`` match using crypt method."""
return crypt.crypt(password, hash_value) == hash_value
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.update(password)
return sha1.digest() == base64.b64decode(hash)
return sha1.digest() == base64.b64decode(hash_value)
_filename = config.get("acl", "filename")
_check_password = locals()["_%s" % config.get("acl", "encryption")]
def has_right(user, password):
"""Check if ``user``/``password`` couple is valid."""
for line in open(_filename).readlines():
for line in open(FILENAME).readlines():
if line.strip():
login, hash = line.strip().split(":")
login, hash_value = line.strip().split(":")
if login == user:
return _check_password(hash, password)
return CHECK_PASSWORD(hash_value, password)
return False

View File

@ -22,11 +22,16 @@
Radicale calendar classes.
Define the main classes of a calendar as seen from the server.
"""
from radicale import support
hash_tag = lambda vcalendar: str(hash(vcalendar))
def hash_tag(vcalendar):
"""Hash an vcalendar string."""
return str(hash(vcalendar))
class Calendar(object):
"""Internal calendar class."""
@ -67,6 +72,7 @@ class Calendar(object):
"""Etag from calendar."""
return '"%s"' % hash_tag(self.vcalendar)
class Event(object):
"""Internal event class."""
def __init__(self, vcalendar):
@ -78,12 +84,14 @@ class Event(object):
"""Etag from event."""
return '"%s"' % hash_tag(self.text)
class Header(object):
"""Internal header class."""
def __init__(self, vcalendar):
"""Initialize header from ``vcalendar``."""
self.text = vcalendar
class Timezone(object):
"""Internal timezone class."""
def __init__(self, vcalendar):
@ -91,11 +99,12 @@ class Timezone(object):
lines = vcalendar.splitlines()
for line in lines:
if line.startswith("TZID:"):
self.tzid = line.lstrip("TZID:")
self.id = line.lstrip("TZID:")
break
self.text = vcalendar
class Todo(object):
"""Internal todo class."""
def __init__(self, vcalendar):

View File

@ -22,26 +22,21 @@
Radicale configuration module.
Give a configparser-like interface to read and write configuration.
"""
# TODO: Use abstract filenames for other platforms
import os
import sys
try:
from configparser import RawConfigParser as ConfigParser
except ImportError:
from ConfigParser import RawConfigParser as ConfigParser
_config = ConfigParser()
get = _config.get
set = _config.set
getboolean = _config.getboolean
getint = _config.getint
getfloat = _config.getfloat
options = _config.options
items = _config.items
_initial = {
# Default configuration
INITIAL_CONFIG = {
"server": {
"host": "",
"port": "5232",
@ -49,17 +44,11 @@ _initial = {
"ssl": "False",
"certificate": "/etc/apache2/ssl/server.crt",
"key": "/etc/apache2/ssl/server.key",
#"log": "/var/www/radicale/server.log",
},
"encoding": {
"request": "utf-8",
"stock": "utf-8",
},
"namespace": {
"C": "urn:ietf:params:xml:ns:caldav",
"D": "DAV:",
"CS": "http://calendarserver.org/ns/",
},
"acl": {
"type": "fake",
"filename": "/etc/radicale/users",
@ -68,14 +57,20 @@ _initial = {
"support": {
"type": "plain",
"folder": os.path.expanduser("~/.config/radicale"),
"calendar": "radicale/calendar",
"calendar": "radicale/cal",
},
}
for section, values in _initial.items():
_config.add_section(section)
for key, value in values.items():
_config.set(section, key, value)
# Create a ConfigParser and configure it
_CONFIG = ConfigParser()
_config.read("/etc/radicale/config")
_config.read(os.path.expanduser("~/.config/radicale/config"))
for section, values in INITIAL_CONFIG.items():
_CONFIG.add_section(section)
for key, value in values.items():
_CONFIG.set(section, key, value)
_CONFIG.read("/etc/radicale/config")
_CONFIG.read(os.path.expanduser("~/.config/radicale/config"))
# Wrap config module into ConfigParser instance
sys.modules[__name__] = _CONFIG

View File

@ -20,18 +20,19 @@
"""
iCal parsing functions.
"""
# TODO: Manage filters (see xmlutils)
from radicale import calendar
def write_calendar(headers=[
def write_calendar(headers=(
calendar.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
calendar.Header("VERSION:2.0")],
timezones=[], todos=[], events=[]):
"""Create calendar from ``headers``, ``timezones``, ``todos``, ``events``."""
# TODO: Manage encoding and EOL
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]),
@ -41,44 +42,57 @@ def write_calendar(headers=[
"END:VCALENDAR"))
return "\n".join([line for line in cal.splitlines() if line])
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 _parse(vcalendar, tag, obj):
"""Find ``tag`` items in ``vcalendar``.
Return a list of items of type ``obj``.
"""
items = []
lines = vcalendar.splitlines()
inItem = False
itemLines = []
in_item = False
item_lines = []
for line in lines:
if line.startswith("BEGIN:%s" % tag):
inItem = True
itemLines = []
in_item = True
item_lines = []
if inItem:
# TODO: Manage encoding
itemLines.append(line)
if in_item:
item_lines.append(line)
if line.startswith("END:%s" % tag):
items.append(obj("\n".join(itemLines)))
items.append(obj("\n".join(item_lines)))
return items
events = lambda vcalendar: _parse(vcalendar, "VEVENT", calendar.Event)
todos = lambda vcalendar: _parse(vcalendar, "VTODO", calendar.Todo)
timezones = lambda vcalendar: _parse(vcalendar, "VTIMEZONE", calendar.Timezone)
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

@ -20,11 +20,13 @@
"""
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

@ -20,6 +20,7 @@
"""
Plain text storage.
"""
import os
@ -28,39 +29,47 @@ import codecs
from radicale import config, ical
_folder = os.path.expanduser(config.get("support", "folder"))
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."""
calendars = []
available_calendars = []
for folder in os.listdir(_folder):
for cal in os.listdir(os.path.join(_folder, folder)):
calendars.append(posixpath.join(folder, cal))
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
return 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))
fd = _open(os.path.join(_folder, user, cal), "w")
fd.write(ical.write_calendar())
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))
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_tzs = [tz.tzid for tz in ical.timezones(old_calendar)]
path = os.path.join(_folder, cal.replace(posixpath.sep, os.path.sep))
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)])
@ -70,37 +79,36 @@ def append(cal, vcalendar):
objects.extend(ical.events(vcalendar))
objects.extend(ical.todos(vcalendar))
for tz in ical.timezones(vcalendar):
if tz.tzid not in old_tzs:
# TODO: Manage position and EOL
fd = _open(path)
lines = [line for line in fd.readlines() if line]
fd.close()
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(tz.text.splitlines()):
for i, line in enumerate(timezone.text.splitlines()):
lines.insert(2 + i, line + "\n")
fd = _open(path, "w")
fd.writelines(lines)
fd.close()
descriptor = _open(path, "w")
descriptor.writelines(lines)
descriptor.close()
for obj in objects:
if obj.etag not in old_objects:
# TODO: Manage position and EOL
fd = _open(path)
lines = [line for line in fd.readlines() if line]
fd.close()
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")
fd = _open(path, "w")
fd.writelines(lines)
fd.close()
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))
path = os.path.join(FOLDER, cal.replace(posixpath.sep, os.path.sep))
cal = read(cal)
@ -109,11 +117,12 @@ def remove(cal, etag):
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.write_calendar(headers, timezones, todos, events))
fd.close()
descriptor = _open(path, "w")
descriptor.write(ical.write_calendar(headers, timezones, todos, events))
descriptor.close()
if config.get("support", "calendar"):
user, cal = config.get("support", "calendar").split(posixpath.sep)
if not os.path.exists(os.path.join(_folder, user, cal)):
mkcalendar(config.get("support", "calendar"))
# Create default calendar if not present
if DEFAULT_CALENDAR:
if DEFAULT_CALENDAR not in calendars():
mkcalendar(DEFAULT_CALENDAR)

View File

@ -24,33 +24,42 @@ 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)
# TODO: Manage depth and calendars/collections
import xml.etree.ElementTree as ET
from radicale import client, config, ical
# TODO: This is a well-known and accepted hack for ET to avoid ET from renaming
# namespaces, which is accepted in XML norm but often not in XML
# readers. Is there another clean solution to force namespaces?
for key, value in config.items("namespace"):
PROTECTED_NAMESPACES = {
"C": "urn:ietf:params:xml:ns:caldav",
"D": "DAV:",
"CS": "http://calendarserver.org/ns/"}
for key, value in PROTECTED_NAMESPACES.items():
ET._namespace_map[value] = key
def _tag(short_name, local):
"""Get XML Clark notation {uri(``short_name``)}``local``."""
return "{%s}%s" % (config.get("namespace", short_name), local)
return "{%s}%s" % (PROTECTED_NAMESPACES[short_name], local)
def _response(code):
"""Return full W3C names from HTTP status codes."""
return "HTTP/1.1 %i %s" % (code, client.responses[code])
def delete(obj, calendar, url):
"""Read and answer DELETE requests.
Read rfc4918-9.6 for info.
"""
# Reading request
calendar.remove(obj)
@ -74,13 +83,14 @@ def propfind(xml_request, calendar, url):
"""Read and answer PROPFIND requests.
Read rfc4918-9.1 for info.
"""
# Reading request
root = ET.fromstring(xml_request)
propElement = root.find(_tag("D", "prop"))
propList = propElement.getchildren()
properties = [property.tag for property in propList]
prop_element = root.find(_tag("D", "prop"))
prop_list = prop_element.getchildren()
props = [prop.tag for prop in prop_list]
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
@ -97,30 +107,30 @@ def propfind(xml_request, calendar, url):
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("C", "calendar")))
prop.append(resourcetype)
if _tag("D", "resourcetype") in props:
element = ET.Element(_tag("D", "resourcetype"))
element.append(ET.Element(_tag("C", "calendar")))
prop.append(element)
if _tag("D", "owner") in properties:
owner = ET.Element(_tag("D", "owner"))
owner.text = calendar.owner
prop.append(owner)
if _tag("D", "owner") in props:
element = ET.Element(_tag("D", "owner"))
element.text = calendar.owner
prop.append(element)
if _tag("D", "getcontenttype") in properties:
getcontenttype = ET.Element(_tag("D", "getcontenttype"))
getcontenttype.text = "text/calendar"
prop.append(getcontenttype)
if _tag("D", "getcontenttype") in props:
element = ET.Element(_tag("D", "getcontenttype"))
element.text = "text/calendar"
prop.append(element)
if _tag("D", "getetag") in properties:
getetag = ET.Element(_tag("D", "getetag"))
getetag.text = calendar.etag
prop.append(getetag)
if _tag("D", "getetag") in props:
element = ET.Element(_tag("D", "getetag"))
element.text = calendar.etag
prop.append(element)
if _tag("CS", "getctag") in properties:
getctag = ET.Element(_tag("CS", "getctag"))
getctag.text = calendar.ctag
prop.append(getctag)
if _tag("CS", "getctag") in props:
element = ET.Element(_tag("CS", "getctag"))
element.text = calendar.ctag
prop.append(element)
status = ET.Element(_tag("D", "status"))
status.text = _response(200)
@ -128,40 +138,32 @@ def propfind(xml_request, calendar, url):
return ET.tostring(multistatus, config.get("encoding", "request"))
def put(icalRequest, calendar, url, obj):
def put(ical_request, calendar, url, obj):
"""Read PUT requests."""
if obj:
# PUT is modifying obj
calendar.replace(obj, icalRequest)
calendar.replace(obj, ical_request)
else:
# PUT is adding a new object
calendar.append(icalRequest)
calendar.append(ical_request)
def report(xml_request, calendar, url):
"""Read and answer REPORT requests.
Read rfc3253-3.6 for info.
"""
# Reading request
root = ET.fromstring(xml_request)
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")] = {}
prop_element = root.find(_tag("D", "prop"))
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([hrefElement.text for hrefElement in root.findall(_tag("D", "href"))])
hreferences = set([href_element.text for href_element
in root.findall(_tag("D", "href"))])
else:
hreferences = [url]
@ -173,12 +175,10 @@ def report(xml_request, calendar, url):
# 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))
objects = \
ical.events(calendar.vcalendar) + ical.todos(calendar.vcalendar)
if not objects:
# TODO: Read rfc4791-9.[6|10] to find a right answer
@ -209,17 +209,16 @@ def report(xml_request, calendar, url):
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("D", "getetag") in props:
element = ET.Element(_tag("D", "getetag"))
element.text = obj.etag
prop.append(element)
if _tag("C", "calendar-data") in properties:
cdata = ET.Element(_tag("C", "calendar-data"))
if _tag("C", "calendar-data") in props:
element = ET.Element(_tag("C", "calendar-data"))
# TODO: Maybe assume that events and todos are not the same
cdata.text = ical.write_calendar(headers, timezones, [obj])
prop.append(cdata)
element.text = ical.write_calendar(headers, timezones, [obj])
prop.append(element)
status = ET.Element(_tag("D", "status"))
status.text = _response(200)