Files added

git-svn-id: http://svn.32rwr.info/radicale/trunk@2 74e4794c-479d-4a33-9dda-c6c359d70f12
This commit is contained in:
(no author) 2008-12-30 16:25:42 +00:00
parent 1308ca0505
commit b1591aea6f
10 changed files with 872 additions and 0 deletions

52
main.py Executable file
View File

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

146
radicale/__init__.py Normal file
View File

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

23
radicale/acl/__init__.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
from .. import config
_acl = __import__(config.get("acl", "type"), locals(), globals())
users = _acl.users

25
radicale/acl/htpasswd.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
from .. import config
def users():
"""
Get the List of all Users
"""
return [line.split(":")[0] for line in open(config.get("acl", "filename")).readlines()]

103
radicale/calendar.py Normal file
View File

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

68
radicale/config.py Normal file
View File

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

119
radicale/ical.py Normal file
View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
from .. import config
_support = __import__(config.get("support", "type"), locals(), globals())
append = _support.append
calendars =_support.calendars
read = _support.read
remove = _support.remove

104
radicale/support/plain.py Normal file
View File

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

206
radicale/xmlutils.py Normal file
View File

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