Code Repositories xandikos / upstream/0.0.5
New upstream version 0.0.5 Jelmer Vernooij 2 years ago
41 changed file(s) with 607 addition(s) and 201 deletion(s). Raw diff Collapse all Expand all
0 Jelmer Vernooij <jelmer@jelmer.uk> Jelmer Vernooij <jelmer@jelmer.uk>
1 Jelmer Vernooij <jelmer@jelmer.uk> Jelmer Vernooij <jelmer@samba.org>
22 include COPYING
33 include Makefile
44 include TODO
5 recursive-include compat *
6 recursive-include examples *
7 recursive-include notes *
5 include compat/*.sh
6 include compat/*.rst
7 include compat/*.xml
8 include compat/*.sha256sum
9 include examples/*.ini
10 include notes/*.rst
811 include tox.ini
912 include xandikos.1
33 COVERAGE_RUN ?= $(COVERAGE) run $(COVERAGE_RUN_OPTIONS)
44 TESTSUITE = xandikos.tests.test_suite
55 LITMUS_TESTS ?= basic http
6 CALDAVTESTER_TESTS ?= CalDAV/delete.xml \
7 CalDAV/schedulenomore.xml \
8 CalDAV/options.xml \
9 CalDAV/vtodos.xml
610 XANDIKOS_COVERAGE ?= $(COVERAGE_RUN) -a --rcfile=$(shell pwd)/.coveragerc --source=xandikos -m xandikos.web
711
812 check:
913 $(PYTHON) -m unittest $(TESTSUITE)
1014
1115 style:
12 flake8 --exclude=compat/vdirsyncer/
16 flake8 --exclude=compat/vdirsyncer/,.tox
1317
1418 web:
1519 $(PYTHON) -m xandikos.web
3135 $(COVERAGE) combine -a compat/vdirsyncer/.coverage
3236
3337 check-caldavtester:
38 TESTS="$(CALDAVTESTER_TESTS)" ./compat/xandikos-caldavtester.sh
39
40 coverage-caldavtester:
41 TESTS="$(CALDAVTESTER_TESTS)" XANDIKOS="$(XANDIKOS_COVERAGE)" ./compat/xandikos-caldavtester.sh
42
43 check-caldavtester-all:
3444 ./compat/xandikos-caldavtester.sh
3545
36 coverage-caldavtester:
46 coverage-caldavtester-all:
3747 XANDIKOS="$(XANDIKOS_COVERAGE)" ./compat/xandikos-caldavtester.sh
3848
3949 check-all: check check-vdirsyncer check-litmus check-caldavtester
0 Metadata-Version: 1.1
1 Name: xandikos
2 Version: 0.0.5
3 Summary: CalDAV/CardDAV server
4 Home-page: https://www.jelmer.uk/projects/xandikos
5 Author: Jelmer Vernooij
6 Author-email: jelmer@jelmer.uk
7 License: GNU GPLv3 or later
8 Description: UNKNOWN
9 Platform: UNKNOWN
10 Classifier: Development Status :: 4 - Beta
11 Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12 Classifier: Programming Language :: Python :: 3.3
13 Classifier: Programming Language :: Python :: 3.4
14 Classifier: Programming Language :: Python :: 3.5
15 Classifier: Programming Language :: Python :: 3.6
16 Classifier: Programming Language :: Python :: Implementation :: CPython
17 Classifier: Programming Language :: Python :: Implementation :: PyPy
18 Classifier: Operating System :: POSIX
00 .. image:: https://travis-ci.org/jelmer/xandikos.png?branch=master
11 :target: https://travis-ci.org/jelmer/xandikos
22 :alt: Build Status
3
4 .. image:: https://ci.appveyor.com/api/projects/status/fjqtsk8agwmwavqk/branch/master?svg=true
5 :target: https://ci.appveyor.com/project/jelmer/xandikos/branch/master
6 :alt: Windows Build Status
7
38
49 Xandikos is a CardDAV/CalDAV server that backs onto a Git repository.
510
5358 - `akonadi <https://community.kde.org/KDE_PIM/Akonadi>`_
5459 - `CalDAV-Sync <https://dmfs.org/caldav/>`_
5560 - `CardDAV-Sync <https://dmfs.org/carddav/>`_
61 - `Calendarsync <https://play.google.com/store/apps/details?id=com.icalparse>`_
5662
5763 Dependencies
5864 ============
9096
9197 A server should now be listening on `localhost:8080 <http://localhost:8080/>`_.
9298
93 Note that Xandikos does not create any collections by default. You can either
94 create collections from your CalDAV/CardDAV client, or by creating git
95 repositories under the *contacts* or *calendars* directories it has created.
99 Note that Xandikos does not create any collections unless --defaults is
100 specified. You can also either create collections from your CalDAV/CardDAV client,
101 or by creating git repositories under the *contacts* or *calendars* directories
102 it has created.
96103
97104 Production
98105 ----------
0 environment:
1
2 matrix:
3
4 - PYTHON: "C:\\Python33"
5 PYTHON_VERSION: "3.3.x"
6 PYTHON_ARCH: "32"
7
8 - PYTHON: "C:\\Python33-x64"
9 PYTHON_VERSION: "3.3.x"
10 PYTHON_ARCH: "64"
11
12 - PYTHON: "C:\\Python34"
13 PYTHON_VERSION: "3.4.x"
14 PYTHON_ARCH: "32"
15
16 - PYTHON: "C:\\Python34-x64"
17 PYTHON_VERSION: "3.4.x"
18 PYTHON_ARCH: "64"
19
20 - PYTHON: "C:\\Python35"
21 PYTHON_VERSION: "3.5.x"
22 PYTHON_ARCH: "32"
23
24 - PYTHON: "C:\\Python35-x64"
25 PYTHON_VERSION: "3.5.x"
26 PYTHON_ARCH: "64"
27
28 - PYTHON: "C:\\Python36"
29 PYTHON_VERSION: "3.6.x"
30 PYTHON_ARCH: "32"
31
32 - PYTHON: "C:\\Python36-x64"
33 PYTHON_VERSION: "3.6.x"
34 PYTHON_ARCH: "64"
35
36 install:
37 # If there is a newer build queued for the same PR, cancel this one.
38 # The AppVeyor 'rollout builds' option is supposed to serve the same
39 # purpose but it is problematic because it tends to cancel builds pushed
40 # directly to master instead of just PR builds (or the converse).
41 # credits: JuliaLang developers.
42 - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
43 https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
44 Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
45 throw "There are newer queued builds for this pull request, failing early." }
46 - ECHO "Filesystem root:"
47 - ps: "ls \"C:/\""
48
49 - ECHO "Installed SDKs:"
50 - ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
51
52 # Install Python (from the official .msi of http://python.org) and pip when
53 # not already installed.
54 - ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
55
56 # Prepend newly installed Python to the PATH of this build (this cannot be
57 # done from inside the powershell script as it would require to restart
58 # the parent CMD process).
59 - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
60
61 # Check that we have the expected version and architecture for Python
62 - "python --version"
63 - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
64
65 # Install setuptools/wheel so that we can e.g. use bdist_wheel
66 - "pip install setuptools wheel"
67
68 - "python setup.py develop"
69
70 build_script:
71 - "python setup.py build"
72
73 test_script:
74 - "python setup.py test"
75
76 after_test:
77 - "python setup.py bdist_wheel"
78 - "python setup.py bdist_wininst"
79 - "python setup.py bdist_msi"
80 - ps: "ls dist"
81
82 artifacts:
83 - path: dist\*
2727 <waitdelay>0.25</waitdelay>
2828
2929 <features>
30 <!--
30 <feature>caldav</feature>
31 <feature>no-duplicate-uids</feature>
32 <feature>ctag</feature>
33 <!--
3134 <feature>COPY Method</feature>
3235 <feature>MOVE Method</feature>
33 <feature>ctag</feature>
3436 <feature>directory listing</feature>
35 <feature>caldav</feature>
36 <feature>no-duplicate-uids</feature>
3737 <feature>carddav</feature>
3838 <feature>default-addressbook</feature>
3939 <feature>shared-addressbooks</feature>-->
55
66 CFGDIR=$(readlink -f $(dirname $0))
77
8 run_xandikos --defaults
9
108 if which testcaldav >/dev/null; then
119 TESTCALDAV=testcaldav
1210 else
1311 TESTCALDAV="$(dirname $0)/testcaldav.sh"
1412 fi
1513
16 $TESTCALDAV -s ${CFGDIR}/serverinfo.xml ${TESTS}
14 function mkcol() {
15 p="$1"
16 t="$2"
17 git init -q "${SERVEDIR}/$p"
18 if [[ -n "$t" ]]; then
19 echo "[xandikos]" >> "${SERVEDIR}/$p/.git/config"
20 echo " type = $t" >> "${SERVEDIR}/$p/.git/config"
21 fi
22 }
23
24 function mkcalendar() {
25 p="$1"
26 mkcol "$p" "calendar"
27 }
28
29 function mkaddressbook() {
30 p="$1"
31 mkcol "$p" "addressbook"
32 }
33
34 function mkprincipal() {
35 p="$1"
36 mkcol "$p" "principal"
37 }
38
39 mkcol addressbooks
40 mkcol addressbooks/__uids__
41 for I in `seq 1 40`; do
42 mkprincipal "addressbooks/__uids__/user$(printf %02d $I)"
43 mkaddressbook addressbooks/__uids__/user$(printf %02d $I)/addressbook
44 done
45 mkcol calendars
46 mkcol calendars/__uids__
47 mkcalendar calendars/users
48 for I in `seq 1 40`; do
49 mkprincipal "calendars/__uids__/user$(printf %02d $I)"
50 mkcalendar calendars/__uids__/user$(printf %02d $I)/calendar
51 mkcalendar calendars/__uids__/user$(printf %02d $I)/tasks
52 mkcalendar calendars/__uids__/user$(printf %02d $I)/inbox
53 mkcalendar calendars/__uids__/user$(printf %02d $I)/outbox
54 done
55 mkprincipal calendars/__uids__/i18nuser
56 mkcalendar calendars/__uids__/i18nuser/calendar
57 mkcol principals
58 mkcol principals/__uids__
59 mkprincipal principals/__uids__/user01/
60 mkcol principals/users
61 mkprincipal principals/users/user01
62
63 run_xandikos --defaults
64
65 $TESTCALDAV --print-details-onfail -s ${CFGDIR}/serverinfo.xml ${TESTS}
77 plugin = python3
88 module = xandikos.wsgi:app
99 env = XANDIKOSPATH=/var/lib/xandikos/collections
10 env = CURRENT_USER_PRINCIPAL=/dav/jelmer/
10 env = CURRENT_USER_PRINCIPAL=/jelmer/
33 1. Update version in setup.py
44 2. Update version in xandikos/__init__.py
55 3. git commit -a -m "Release $VERSION"
6 4. git tag -as -m "Release $VERSION"
7 5. ./setup.py sdist --sign upload
6 4. git tag -as -m "Release $VERSION" v$VERSION
7 5. ./setup.py sdist upload --sign
00 Dulwich Store
11 =============
22
3 The main building blocks are vCard (*.vcf) and iCalendar (*.ics) files. Storage
3 The main building blocks are vCard (.vcf) and iCalendar (.ics) files. Storage
44 happens in Git repositories.
55
6 Most items are identified by a UID, which is unique for the store. Items
7 can have multiple versions, which are identified by an ETag. Each store
8 maps to a single Git repository, and can not contain directories. In the future,
9 a store could map to a subtree in a Git repository.
6 Most items are identified by a UID and a filename, both of which are unique for
7 the store. Items can have multiple versions, which are identified by an ETag.
8 Each store maps to a single Git repository, and can not contain directories. In
9 the future, a store could map to a subtree in a Git repository.
1010
1111 Stores are responsible for making sure that:
1212
0 [egg_info]
1 tag_build =
2 tag_date = 0
3
11 # encoding: utf-8
22 #
33 # Xandikos
4 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
4 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
55 #
66 # This program is free software; you can redistribute it and/or
77 # modify it under the terms of the GNU General Public License
2121
2222 from setuptools import setup
2323
24 version = "0.0.4"
24 version = "0.0.5"
2525
2626 setup(name="xandikos",
2727 description="CalDAV/CardDAV server",
2828 version=version,
29 author="Jelmer Vernooij",
29 author="Jelmer Vernooij",
3030 author_email="jelmer@jelmer.uk",
3131 license="GNU GPLv3 or later",
3232 url="https://www.jelmer.uk/projects/xandikos",
3434 packages=['xandikos', 'xandikos.tests'],
3535 package_data={'xandikos': ['templates/*.html']},
3636 scripts=['bin/xandikos'],
37 test_suite='xandikos.tests.test_suite',
3738 classifiers=[
3839 'Development Status :: 4 - Beta',
3940 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', # noqa
00 # encoding: utf-8
11 #
22 # Xandikos
3 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
3 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
44 #
55 # This program is free software; you can redistribute it and/or
66 # modify it under the terms of the GNU General Public License
2020
2121 """CalDAV/CardDAV server."""
2222
23 __version__ = (0, 0, 4)
23 __version__ = (0, 0, 5)
2424
2525 import defusedxml.ElementTree # noqa: This does some monkey-patching on-load
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
3333
3434 PRODID = '-//Jelmer Vernooij//Xandikos//EN'
3535 WELLKNOWN_CALDAV_PATH = "/.well-known/caldav"
36 EXTENDED_MKCOL_FEATURE = 'extended-mkcol'
3637
3738 # https://tools.ietf.org/html/rfc4791, section 4.2
3839 CALENDAR_RESOURCE_TYPE = '{urn:ietf:params:xml:ns:caldav}calendar'
173174 :return: A Calendar
174175 """
175176 for tag in requested:
176 if tag.name == ('{%s}comp' % NAMESPACE):
177 if tag.tag == ('{%s}comp' % NAMESPACE):
177178 for insub in incal.subcomponents:
178179 if insub.name == tag.get('name'):
179180 outsub = component_factory[insub.name]
180181 outcal.add_component(outsub)
181182 extract_from_calendar(insub, outsub, tag)
182 elif tag.name == ('{%s}prop' % NAMESPACE):
183 elif tag.tag == ('{%s}prop' % NAMESPACE):
183184 outcal[tag.get('name')] = incal[tag.get('name')]
184185 else:
185186 raise AssertionError('invalid element %r' % tag)
199200 def supported_on(self, resource):
200201 return (resource.get_content_type() == 'text/calendar')
201202
202 def get_value(self, base_href, resource, el, requested):
203 def get_value_ext(self, base_href, resource, el, requested):
203204 if len(requested) == 0:
204 el.text = b''.join(resource.get_body()).decode('utf-8')
205 serialized_cal = b''.join(resource.get_body())
205206 else:
206207 c = ICalendar()
207 extract_from_calendar(resource.calendar, c, requested)
208 el.text = c.to_ical()
208 calendar = calendar_from_resource(resource)
209 if calendar is None:
210 raise KeyError
211 extract_from_calendar(calendar, c, requested)
212 serialized_cal = c.to_ical()
209213 # TODO(jelmer): Don't hardcode encoding
214 el.text = serialized_cal.decode('utf-8')
210215
211216
212217 class CalendarMultiGetReporter(davcommon.MultiGetReporter):
515520 filter_el = None
516521 tztext = None
517522 for el in body:
518 if el.tag == '{DAV:}prop':
523 if el.tag in ('{DAV:}prop', '{DAV:}propname', '{DAV:}allprop'):
519524 requested = el
520525 elif el.tag == '{urn:ietf:params:xml:ns:caldav}filter':
521526 filter_el = el
606611 el.text = resource.get_calendar_timezone()
607612
608613 def set_value(self, href, resource, el):
609 resource.set_calendar_timezone(el.text)
614 if el is not None:
615 resource.set_calendar_timezone(el.text)
616 else:
617 resource.set_calendar_timezone(None)
610618
611619
612620 class MinDateTimeProperty(webdav.Property):
801809 raise webdav.UnsupportedMediaType(content_type)
802810 href, path, resource = app._get_resource_from_environ(environ)
803811 if resource is not None:
804 return webdav._send_method_not_allowed(
812 return webdav._send_simple_dav_error(
805813 environ, start_response,
806 app._get_allowed_methods(environ))
814 '403 Forbidden',
815 error=ET.Element('{DAV:}resource-must-be-null'),
816 description=('Something already exists at %r' % path))
807817 try:
808818 resource = app.backend.create_collection(path)
809819 except FileNotFoundError:
814824 ET.SubElement(el, '{urn:ietf:params:xml:ns:caldav}calendar')
815825 app.properties['{DAV:}resourcetype'].set_value(href, resource, el)
816826 if base_content_type in ('text/xml', 'application/xml'):
817 et = webdav._readXmlBody(environ, '{DAV:}mkcalendar')
827 et = webdav._readXmlBody(
828 environ, '{urn:ietf:params:xml:ns:caldav}mkcalendar')
818829 propstat = []
819830 for el in et:
820831 if el.tag != '{DAV:}set':
822833 'Unknown tag %s in mkcalendar' % el.tag)
823834 propstat.extend(webdav.apply_modify_prop(
824835 el, href, resource, app.properties))
825 ret = ET.Element('{DAV:}mkcalendar-response')
836 ret = ET.Element(
837 '{urn:ietf:params:xml:ns:carldav:}mkcalendar-response')
826838 for propstat_el in webdav.propstat_as_xml(propstat):
827839 ret.append(propstat_el)
828840 return webdav._send_xml_response(
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
6464 def supported_on(self, resource):
6565 return (resource.get_content_type() == 'text/vcard')
6666
67 def get_value(self, href, resource, el, requested):
67 def get_value_ext(self, href, resource, el, requested):
6868 # TODO(jelmer): Support subproperties
6969 # TODO(jelmer): Don't hardcode encoding
7070 el.text = b''.join(resource.get_body()).decode('utf-8')
8282 def get_value(self, href, resource, el):
8383 el.text = resource.get_addressbook_description()
8484
85 # TODO(jelmer): allow modification of this property
8685 def set_value(self, href, resource, el):
87 raise NotImplementedError
86 resource.set_addressbook_description(el.text)
8887
8988
9089 class AddressbookMultiGetReporter(davcommon.MultiGetReporter):
101100
102101 def get_addressbook_description(self):
103102 raise NotImplementedError(self.get_addressbook_description)
103
104 def set_addressbook_description(self, description):
105 raise NotImplementedError(self.set_addressbook_description)
104106
105107 def get_addressbook_color(self):
106108 raise NotImplementedError(self.get_addressbook_color)
288290 def apply_filter(el, resource):
289291 """Compile a filter element into a Python function.
290292 """
291 if el is None:
293 if el is None or not list(el):
292294 # Empty filter, let's not bother parsing
293295 return lambda x: True
294296 ab = addressbook_from_resource(resource)
310312 base_resource, depth):
311313 requested = None
312314 filter_el = None
315 limit = None
313316 for el in body:
314 if el.tag == '{DAV:}prop':
317 if el.tag in ('{DAV:}prop', '{DAV:}allprop', '{DAV:}propname'):
315318 requested = el
316319 elif el.tag == ('{%s}filter' % NAMESPACE):
317320 filter_el = el
321 elif el.tag == ('{%s}limit' % NAMESPACE):
322 limit = el
318323 else:
319324 raise webdav.BadRequestError(
320325 'Unknown tag %s in report %s' % (el.tag, self.name))
326 if limit is not None:
327 try:
328 [nresults_el] = list(limit)
329 except ValueError:
330 raise webdav.BadRequestError(
331 'Invalid number of subelements in limit')
332 try:
333 nresults = int(nresults_el.text)
334 except ValueError:
335 raise webdav.BadRequestError(
336 'nresults not a number')
337 else:
338 nresults = None
339
340 i = 0
321341 for (href, resource) in webdav.traverse_resource(
322342 base_resource, base_href, depth):
323343 if not apply_filter(filter_el, resource):
324344 continue
345 if nresults is not None and i >= nresults:
346 break
325347 propstat = davcommon.get_properties_with_data(
326348 self.data_property, href, resource, properties, requested)
327349 yield webdav.Status(href, '200 OK', propstat=list(propstat))
350 i += 1
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
2626 class SubbedProperty(webdav.Property):
2727 """Property with sub-components that can be queried."""
2828
29 def get_value(self, href, resource, el, requested):
29 def get_value_ext(self, href, resource, el, requested):
3030 """Get the value of a data property.
3131
3232 :param href: Resource href
3434 :param el: Element to fill in
3535 :param requested: Requested property (including subelements)
3636 """
37 raise NotImplementedError(self.get_value)
37 raise NotImplementedError(self.get_value_ext)
3838
3939
4040 def get_properties_with_data(data_property, href, resource, properties,
4141 requested):
42 for propreq in list(requested):
43 if propreq.tag == data_property.name:
44 ret = ET.Element(propreq.tag)
45 if data_property.supported_on(resource):
46 data_property.get_value(href, resource, ret, propreq)
47 statuscode = '200 OK'
48 else:
49 statuscode = '404 Not Found'
50 yield webdav.PropStatus(statuscode, None, ret)
51 else:
52 yield webdav.get_property(href, resource, properties, propreq.tag)
42 properties = dict(properties)
43 properties[data_property.name] = data_property
44 return webdav.get_properties(href, resource, properties, requested)
5345
5446
5547 class MultiGetReporter(webdav.Reporter):
6860 requested = None
6961 hrefs = []
7062 for el in body:
71 if el.tag == '{DAV:}prop':
63 if el.tag in ('{DAV:}prop', '{DAV:}allprop', '{DAV:}propname'):
7264 requested = el
7365 elif el.tag == '{DAV:}href':
7466 hrefs.append(webdav.read_href_element(el))
00 # Xandikos
1 # Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
8989
9090 DELTA_IGNORE_FIELDS = set(["LAST-MODIFIED", "SEQUENCE", "DTSTAMP", "PRODID",
9191 "CREATED", "COMPLETED", "X-MOZ-GENERATION",
92 "X-LIC-ERROR"])
92 "X-LIC-ERROR", "UID"])
9393
9494
9595 def describe_calendar_delta(old_cal, new_cal):
113113 new_component):
114114 if field.upper() in DELTA_IGNORE_FIELDS:
115115 continue
116 logging.debug("Changed %s/%s or %s/%s from %s to %s.",
117 old_component.name, field, new_component.name, field,
118 old_value, new_value)
119116 if (
120117 old_component.name.upper() == "VTODO" and
121118 field.upper() == "STATUS"
123120 yield "%s marked as %s" % (description, new_value)
124121 elif field.upper() == 'DESCRIPTION':
125122 yield "changed description of %s" % description
123 elif field.upper() == 'SUMMARY':
124 yield "changed summary of %s" % description
126125 elif field.upper() == 'LOCATION':
127126 yield "changed location of %s to %s" % (description, new_value)
128127 elif (old_component.name.upper() == "VTODO" and
135134 new_value.dt if new_value else 'none')
136135 else:
137136 yield "modified field %s in %s" % (field, description)
137 logging.debug("Changed %s/%s or %s/%s from %s to %s.",
138 old_component.name, field, new_component.name,
139 field, old_value, new_value)
138140
139141
140142 class ICalendarFile(File):
148150
149151 def validate(self):
150152 """Verify that file contents are valid."""
151 try:
152 self.calendar
153 except ValueError:
154 raise InvalidFileContents(self.content_type, self.content)
153 self.calendar
155154
156155 @property
157156 def calendar(self):
158157 if self._calendar is None:
159 self._calendar = Calendar.from_ical(b''.join(self.content))
158 try:
159 self._calendar = Calendar.from_ical(b''.join(self.content))
160 except ValueError:
161 raise InvalidFileContents(self.content_type, self.content)
160162 return self._calendar
161163
162164 def describe_delta(self, name, previous):
170172 return lines
171173
172174 def describe(self, name):
173 for component in self.calendar.subcomponents:
174 try:
175 return describe_component(component)
176 except KeyError:
177 pass
175 try:
176 subcomponents = self.calendar.subcomponents
177 except InvalidFileContents:
178 pass
179 else:
180 for component in subcomponents:
181 try:
182 return describe_component(component)
183 except KeyError:
184 pass
178185 return super(ICalendarFile, self).describe(name)
179186
180187 def get_uid(self):
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
3636
3737 STORE_TYPE_ADDRESSBOOK = 'addressbook'
3838 STORE_TYPE_CALENDAR = 'calendar'
39 STORE_TYPE_PRINCIPAL = 'principal'
3940 STORE_TYPE_OTHER = 'other'
4041 VALID_STORE_TYPES = (
4142 STORE_TYPE_ADDRESSBOOK,
4243 STORE_TYPE_CALENDAR,
44 STORE_TYPE_PRINCIPAL,
4345 STORE_TYPE_OTHER)
4446
4547 MIMETYPES = mimetypes.MimeTypes()
7981
8082 :raise NotImplementedError: If UIDs aren't supported for this format
8183 :raise KeyError: If there is no UID set on this file
84 :raise InvalidFileContents: If the file is misformatted
8285 :return: UID
8386 """
8487 raise NotImplementedError(self.get_uid)
8891
8992 :param name: File name
9093 :param previous: Previous file to compare to.
94 :raise InvalidFileContents: If the file is misformatted
9195 :return: List of strings describing change
9296 """
9397 assert name is not None
235239 def set_type(self, store_type):
236240 """Set store type.
237241
238 :param store_type: New store type (one of STORE_TYPE_ADDRESSBOOK,
239 STORE_TYPE_CALENDAR, STORE_TYPE_OTHER)
242 :param store_type: New store type (one of VALID_STORE_TYPES)
240243 """
241244 raise NotImplementedError(self.set_type)
242245
243246 def get_type(self):
244247 """Get type of this store.
245248
246 :return: one of [STORE_TYPE_ADDRESSBOOK, STORE_TYPE_CALENDAR,
247 STORE_TYPE_OTHER]
249 :return: one of VALID_STORE_TYPES
248250 """
249251 ret = STORE_TYPE_OTHER
250252 for (name, content_type, etag) in self.iter_with_etag():
377379 """
378380 fi = open_by_content_type(data, content_type, self.extra_file_handlers)
379381 if name is None:
380 name = str(uuid.uuid4()) + MIMETYPES.guess_extension(content_type)
382 name = str(uuid.uuid4())
383 extension = MIMETYPES.guess_extension(content_type)
384 if extension is not None:
385 name += extension
381386 fi.validate()
382387 try:
383388 uid = fi.get_uid()
422427 except KeyError:
423428 logger.warning('No UID found in file %s', name)
424429 uid = None
430 except InvalidFileContents:
431 logging.warning('Unable to parse file %s', name)
432 uid = None
425433 except NotImplementedError:
426434 # This file type doesn't support UIDs
427435 uid = None
570578 def set_type(self, store_type):
571579 """Set store type.
572580
573 :param store_type: New store type (one of STORE_TYPE_ADDRESSBOOK,
574 STORE_TYPE_CALENDAR, STORE_TYPE_OTHER)
581 :param store_type: New store type (one of VALID_STORE_TYPES)
575582 """
576583 config = self.repo.get_config()
577584 config.set(b'xandikos', b'type', store_type.encode(DEFAULT_ENCODING))
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
105105 else:
106106 for prop in requested:
107107 if old_resource is not None:
108 old_propstat = webdav.get_property(
109 href, old_resource, properties, prop.tag)
108 old_propstat = webdav.get_property_from_element(
109 href, old_resource, properties, prop)
110110 else:
111111 old_propstat = None
112 new_propstat = webdav.get_property(
113 href, new_resource, properties, prop.tag)
112 new_propstat = webdav.get_property_from_element(
113 href, new_resource, properties, prop)
114114 if old_propstat != new_propstat:
115115 propstat.append(new_propstat)
116116 yield webdav.Status(
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
3636 infit, scheduling, timezones)
3737 from xandikos.icalendar import ICalendarFile
3838 from xandikos.store import (
39 DuplicateUidError,
3940 TreeGitStore,
4041 GitStore,
4142 InvalidFileContents,
4344 NotStoreError,
4445 STORE_TYPE_ADDRESSBOOK,
4546 STORE_TYPE_CALENDAR,
47 STORE_TYPE_PRINCIPAL,
4648 STORE_TYPE_OTHER,
4749 )
4850 from xandikos.vcard import VCardFile
127129 raise webdav.PreconditionFailure(
128130 '{%s}valid-calendar-data' % caldav.NAMESPACE,
129131 'Not a valid calendar file.')
132 except DuplicateUidError:
133 raise webdav.PreconditionFailure(
134 '{%s}no-uid-conflict' % caldav.NAMESPACE,
135 'UID already in use.')
130136 return create_strong_etag(etag)
131137
132138 def get_content_language(self):
197203 elif resource_types == {carddav.ADDRESSBOOK_RESOURCE_TYPE,
198204 webdav.COLLECTION_RESOURCE_TYPE}:
199205 self.store.set_type(STORE_TYPE_ADDRESSBOOK)
206 elif resource_types == {webdav.PRINCIPAL_RESOURCE_TYPE}:
207 self.store.set_type(STORE_TYPE_PRINCIPAL)
200208 elif resource_types == {webdav.COLLECTION_RESOURCE_TYPE}:
201209 self.store.set_type(STORE_TYPE_OTHER)
202210 else:
262270 raise webdav.PreconditionFailure(
263271 '{%s}valid-calendar-data' % caldav.NAMESPACE,
264272 'Not a valid calendar file.')
273 except DuplicateUidError:
274 raise webdav.PreconditionFailure(
275 '{%s}no-uid-conflict' % caldav.NAMESPACE,
276 'UID already in use.')
265277 return (name, create_strong_etag(etag))
266278
267279 def iter_differences_since(self, old_token, new_token):
343355 """A generic WebDAV collection."""
344356
345357
346 class CalendarResource(StoreBasedCollection, caldav.Calendar):
358 class CalendarCollection(StoreBasedCollection, caldav.Calendar):
347359
348360 def get_calendar_description(self):
349361 return self.store.get_description()
392404 raise KeyError
393405
394406
395 class AddressbookResource(StoreBasedCollection, carddav.Addressbook):
407 class AddressbookCollection(StoreBasedCollection, carddav.Addressbook):
396408
397409 def get_addressbook_description(self):
398410 return self.store.get_description()
411
412 def set_addressbook_description(self, description):
413 self.store.set_description(description)
399414
400415 def get_supported_address_data_types(self):
401416 return [('text/vcard', '3.0')]
580595 raise KeyError
581596
582597
583 class Principal(CollectionSetResource):
584 """Principal user resource."""
585
586 resource_types = (webdav.Collection.resource_types +
587 [webdav.PRINCIPAL_RESOURCE_TYPE])
598 class Principal(webdav.Principal):
588599
589600 def get_principal_url(self):
590601 return '.'
616627 """Get group membership URLs."""
617628 return []
618629
630 def get_calendar_user_type(self):
631 # TODO(jelmer)
632 return "INDIVIDUAL"
633
634 def get_calendar_proxy_read_for(self):
635 # TODO(jelmer)
636 return []
637
638 def get_calendar_proxy_write_for(self):
639 # TODO(jelmer)
640 return []
641
642
643 class PrincipalBare(CollectionSetResource, Principal):
644 """Principal user resource."""
645
646 resource_types = [webdav.PRINCIPAL_RESOURCE_TYPE]
647
619648 @classmethod
620649 def create(cls, backend, relpath):
621 p = super(Principal, cls).create(backend, relpath)
650 p = super(PrincipalBare, cls).create(backend, relpath)
622651 to_create = set()
623652 to_create.update(p.get_addressbook_home_set())
624653 to_create.update(p.get_calendar_home_set())
629658 pass
630659 return p
631660
632 def get_calendar_user_type(self):
633 # TODO(jelmer)
634 return "INDIVIDUAL"
635
636 def get_calendar_proxy_read_for(self):
637 # TODO(jelmer)
638 return []
639
640 def get_calendar_proxy_write_for(self):
641 # TODO(jelmer)
642 return []
643
644 def get_ctag(self):
645 raise KeyError
661
662 class PrincipalCollection(Collection, Principal):
663 """Principal user resource."""
664
665 resource_types = (webdav.Collection.resource_types +
666 [webdav.PRINCIPAL_RESOURCE_TYPE])
667
668 @classmethod
669 def create(cls, backend, relpath):
670 p = super(PrincipalCollection, cls).create(backend, relpath)
671 p.store.set_type(STORE_TYPE_PRINCIPAL)
672 to_create = set()
673 to_create.update(p.get_addressbook_home_set())
674 to_create.update(p.get_calendar_home_set())
675 for n in to_create:
676 try:
677 backend.create_collection(posixpath.join(relpath, n))
678 except FileExistsError:
679 pass
680 return p
646681
647682
648683 @functools.lru_cache(maxsize=STORE_CACHE_SIZE)
670705 return Collection(self, relpath, TreeGitStore.create(p))
671706
672707 def create_principal(self, relpath, create_defaults=False):
673 principal = Principal.create(self, relpath)
708 principal = PrincipalBare.create(self, relpath)
674709 self._mark_as_principal(relpath)
675710 if create_defaults:
676711 create_principal_defaults(self, principal)
683718 if p is None:
684719 return None
685720 if os.path.isdir(p):
686 if relpath in self._user_principals:
687 return Principal(self, relpath)
688721 try:
689722 store = open_store_from_path(p)
690723 except NotStoreError:
724 if relpath in self._user_principals:
725 return PrincipalBare(self, relpath)
691726 return CollectionSetResource(self, relpath)
692727 else:
693728 return {
694 STORE_TYPE_CALENDAR: CalendarResource,
695 STORE_TYPE_ADDRESSBOOK: AddressbookResource,
729 STORE_TYPE_CALENDAR: CalendarCollection,
730 STORE_TYPE_ADDRESSBOOK: AddressbookCollection,
731 STORE_TYPE_PRINCIPAL: PrincipalCollection,
696732 STORE_TYPE_OTHER: Collection
697733 }[store.get_type()](self, relpath, store)
698734 else:
791827
792828 def __call__(self, environ, start_response):
793829 # See https://tools.ietf.org/html/rfc6764
794 if ((environ['SCRIPT_NAME'] + environ['PATH_INFO'])
795 in WELLKNOWN_DAV_PATHS):
830 path = posixpath.normpath(
831 environ['SCRIPT_NAME'] + environ['PATH_INFO'])
832 if path in WELLKNOWN_DAV_PATHS:
796833 start_response('302 Found', [
797834 ('Location', self._dav_root)])
798835 return []
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
234234 def get_single_body(self, encoding):
235235 if self.propstat and len(propstat_by_status(self.propstat)) > 1:
236236 raise NeedsMultiStatus()
237 if self.error is not None:
238 raise NeedsMultiStatus()
237239 if self.propstat:
238240 [ret] = list(propstat_as_xml(self.propstat))
239241 body = ET.tostringlist(ret, encoding)
253255 ret.append(ps)
254256 elif self.status:
255257 ET.SubElement(ret, '{DAV:}status').text = 'HTTP/1.1 ' + self.status
256 if self.error:
258 # Note the check for "is not None" here. Elements without children
259 # evaluate to False.
260 if self.error is not None:
257261 ET.SubElement(ret, '{DAV:}error').append(self.error)
258262 if self.responsedescription:
259263 ET.SubElement(ret, '{DAV:}responsedescription').text = (
524528 def get_value(self, href, resource, el):
525529 for name, reporter in self._reporters.items():
526530 if reporter.supported_on(resource):
527 ET.SubElement(el, name)
531 bel = ET.SubElement(el, '{DAV:}supported-report')
532 ET.SubElement(bel, name)
528533
529534
530535 class GetCTagProperty(Property):
848853 raise NotImplementedError(self.get_calendar_proxy_write_for)
849854
850855
851 def get_property(href, resource, properties, name):
856 def get_property_from_name(href, resource, properties, name):
852857 """Get a single property on a resource.
853858
854859 :param href: Resource href
857862 :param name: name of property to resolve
858863 :return: PropStatus items
859864 """
865 return get_property_from_element(
866 href, resource, properties, ET.Element(name))
867
868
869 def get_property_from_element(href, resource, properties, requested):
870 """Get a single property on a resource.
871
872 :param href: Resource href
873 :param resource: Resource object
874 :param properties: Dictionary of properties
875 :param requested: Requested element
876 :return: PropStatus items
877 """
860878 responsedescription = None
861 ret = ET.Element(name)
879 ret = ET.Element(requested.tag)
862880 try:
863 prop = properties[name]
881 prop = properties[requested.tag]
864882 except KeyError:
865883 statuscode = '404 Not Found'
866884 logging.warning(
867885 'Client requested unknown property %s',
868 name)
886 requested.tag)
869887 else:
870888 try:
871889 if not prop.supported_on(resource):
872890 raise KeyError
873 prop.get_value(href, resource, ret)
891 try:
892 get_value_ext = prop.get_value_ext
893 except AttributeError:
894 prop.get_value(href, resource, ret)
895 else:
896 get_value_ext(href, resource, ret, requested)
874897 except KeyError:
875898 statuscode = '404 Not Found'
876899 else:
888911 :return: Iterator over PropStatus items
889912 """
890913 for propreq in list(requested):
891 yield get_property(href, resource, properties, propreq.tag)
914 yield get_property_from_element(href, resource, properties, propreq)
915
916
917 def get_property_names(href, resource, properties, requested):
918 """Get a set of property names.
919
920 :param href: Resource Href
921 :param resource: Resource object
922 :param properties: Dictionary of properties
923 :param requested: XML {DAV:}prop element with properties to look up
924 :return: Iterator over PropStatus items
925 """
926 for name, prop in properties.items():
927 if prop.is_set(href, resource):
928 yield PropStatus('200 OK', None, ET.Element(name))
929
930
931 def get_all_properties(href, resource, properties):
932 """Get all properties.
933
934 :param href: Resource Href
935 :param resource: Resource object
936 :param properties: Dictionary of properties
937 :param requested: XML {DAV:}prop element with properties to look up
938 :return: Iterator over PropStatus items
939 """
940 for name in properties:
941 ps = get_property_from_name(href, resource, properties, name)
942 if ps.statuscode == '200 OK':
943 yield ps
892944
893945
894946 def ensure_trailing_slash(href):
927979 nextdepth = "infinity"
928980 else:
929981 raise AssertionError("invalid depth %r" % depth)
930 if COLLECTION_RESOURCE_TYPE in base_resource.resource_types:
982 if COLLECTION_RESOURCE_TYPE in resource.resource_types:
931983 for (child_name, child_resource) in resource.members():
932984 child_href = urllib.parse.urljoin(href, child_name)
933985 todo.append((child_href, child_resource, nextdepth))
10031055 for prop in prop_list:
10041056 prop_name = prop.get('name')
10051057 # FIXME: Resolve prop_name on resource
1006 propstat = get_property(href, resource, properties, prop_name)
1058 propstat = get_property_from_name(
1059 href, resource, properties, prop_name)
10071060 new_prop = ET.Element(propstat.prop.tag)
10081061 child_hrefs = [
10091062 read_href_element(prop_child)
12671320 raise NotImplementedError(self.handle)
12681321
12691322 def allow(self, environ):
1270 raise NotImplementedError(self.allow)
1323 """Is this method allowed considering the specified environ?"""
1324 return True
12711325
12721326
12731327 class DeleteMethod(Method):
13361390 start_response('412 Precondition Failed', [])
13371391 return []
13381392 if r is not None:
1393 # Item already exists; update it
13391394 try:
13401395 new_etag = r.set_body(new_contents, current_etag)
13411396 except PreconditionFailure as e:
13541409 content_type = environ.get('CONTENT_TYPE')
13551410 container_path, name = posixpath.split(path)
13561411 r = app.backend.get_resource(container_path)
1357 if r is not None:
1358 if COLLECTION_RESOURCE_TYPE not in r.resource_types:
1359 return _send_method_not_allowed(
1360 environ, start_response,
1361 app._get_allowed_methods(environ))
1362 try:
1363 (new_name, new_etag) = r.create_member(
1364 name, new_contents, content_type)
1365 except PreconditionFailure as e:
1366 return _send_simple_dav_error(
1367 environ, start_response, '412 Precondition Failed',
1368 error=ET.Element(e.precondition),
1369 description=e.description)
1370 start_response('201 Created', [
1371 ('ETag', new_etag)])
1372 return []
1373 return _send_not_found(environ, start_response)
1412 if r is None:
1413 return _send_not_found(environ, start_response)
1414 if COLLECTION_RESOURCE_TYPE not in r.resource_types:
1415 return _send_method_not_allowed(
1416 environ, start_response,
1417 app._get_allowed_methods(environ))
1418 try:
1419 (new_name, new_etag) = r.create_member(
1420 name, new_contents, content_type)
1421 except PreconditionFailure as e:
1422 return _send_simple_dav_error(
1423 environ, start_response, '412 Precondition Failed',
1424 error=ET.Element(e.precondition),
1425 description=e.description)
1426 start_response('201 Created', [
1427 ('ETag', new_etag)])
1428 return []
13741429
13751430
13761431 class ReportMethod(Method):
14181473 'CONTENT_TYPE' not in environ and
14191474 environ.get('CONTENT_LENGTH') == '0'
14201475 ):
1421 requested = ET.Element('{DAV:}allprop')
1476 requested = None
14221477 else:
14231478 et = _readXmlBody(environ, '{DAV:}propfind')
14241479 try:
14261481 except ValueError:
14271482 raise BadRequestError(
14281483 'Received more than one element in propfind.')
1429 if requested.tag == '{DAV:}prop':
1430 ret = []
1431 for href, resource in traverse_resource(
1432 base_resource, base_href, depth):
1484 ret = []
1485 for href, resource in traverse_resource(
1486 base_resource, base_href, depth):
1487 propstat = []
1488 if requested is None or requested.tag == '{DAV:}allprop':
1489 propstat = get_all_properties(
1490 href, resource, app.properties)
1491 elif requested.tag == '{DAV:}prop':
14331492 propstat = get_properties(
14341493 href, resource, app.properties, requested)
1435 ret.append(Status(href, '200 OK', propstat=list(propstat)))
1436 # By my reading of the WebDAV RFC, it should be legal to return
1437 # '200 OK' here if Depth=0, but the RFC is not super clear and
1438 # some clients don't seem to like it .
1439 return ret
1440 elif requested.tag == '{DAV:}allprop':
1441 ret = []
1442 for href, resource in traverse_resource(
1443 base_resource, base_href, depth):
1444 propstat = []
1445 for name in app.properties:
1446 ps = get_property(href, resource, app.properties, name)
1447 if ps.statuscode == '200 OK':
1448 propstat.append(ps)
1449 ret.append(Status(href, '200 OK', propstat=propstat))
1450 return ret
1451 elif requested.tag == '{DAV:}propname':
1452 ret = []
1453 for href, resource in traverse_resource(
1454 base_resource, base_href, depth):
1455 propstat = []
1456 for name, prop in app.properties.items():
1457 if prop.is_set(href, resource):
1458 propstat.append(
1459 PropStatus('200 OK', None, ET.Element(name)))
1460 ret.append(Status(href, '200 OK', propstat=propstat))
1461 return ret
1462 else:
1463 raise BadRequestError('Expected prop/allprop/propname tag, got ' +
1464 requested.tag)
1494 elif requested.tag == '{DAV:}propname':
1495 propstat = get_property_names(
1496 href, resource, app.properties, requested)
1497 else:
1498 raise BadRequestError(
1499 'Expected prop/allprop/propname tag, got ' + requested.tag)
1500 ret.append(Status(href, '200 OK', propstat=list(propstat)))
1501 # By my reading of the WebDAV RFC, it should be legal to return
1502 # '200 OK' here if Depth=0, but the RFC is not super clear and
1503 # some clients don't seem to like it .
1504 return ret
14651505
14661506
14671507 class ProppatchMethod(Method):
16511691
16521692 def _get_dav_features(self, resource):
16531693 # TODO(jelmer): Support access-control
1654 return ['1', '2', '3', 'calendar-access', 'addressbook']
1694 return ['1', '2', '3', 'calendar-access', 'addressbook',
1695 'extended-mkcol']
16551696
16561697 def _get_allowed_methods(self, environ):
16571698 """List of supported methods on this endpoint."""
1658 # TODO(jelmer): Look up resource to determine supported methods.
1659 return sorted(self.methods.keys())
1699 ret = []
1700 for name in sorted(self.methods.keys()):
1701 if self.methods[name].allow(environ):
1702 ret.append(name)
1703 return ret
16601704
16611705 def __call__(self, environ, start_response):
16621706 if environ.get('HTTP_EXPECT', '') != '':
00 # Xandikos
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>
1 # Copyright (C) 2016-2017 Jelmer Vernooij <jelmer@jelmer.uk>, et al.
22 #
33 # This program is free software; you can redistribute it and/or
44 # modify it under the terms of the GNU General Public License
1919 """WSGI wrapper for xandikos.
2020 """
2121
22 import logging
2223 import os
2324
2425 from xandikos.web import XandikosBackend, XandikosApp
2526
2627 backend = XandikosBackend(path=os.environ['XANDIKOSPATH'])
28 if not os.path.isdir(backend.path):
29 logging.warning('%r does not exist.', backend.path)
30
2731 current_user_principal = os.environ.get('CURRENT_USER_PRINCIPAL', '/user/')
32 if not backend.get_resource(current_user_principal):
33 logging.warning(
34 'default user principal \'%s\' does not exist. Create directory %s?',
35 current_user_principal, backend._map_to_file_path(
36 current_user_principal))
37
2838 backend._mark_as_principal(current_user_principal)
2939 app = XandikosApp(backend, current_user_principal)
0 Metadata-Version: 1.1
1 Name: xandikos
2 Version: 0.0.5
3 Summary: CalDAV/CardDAV server
4 Home-page: https://www.jelmer.uk/projects/xandikos
5 Author: Jelmer Vernooij
6 Author-email: jelmer@jelmer.uk
7 License: GNU GPLv3 or later
8 Description: UNKNOWN
9 Platform: UNKNOWN
10 Classifier: Development Status :: 4 - Beta
11 Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12 Classifier: Programming Language :: Python :: 3.3
13 Classifier: Programming Language :: Python :: 3.4
14 Classifier: Programming Language :: Python :: 3.5
15 Classifier: Programming Language :: Python :: 3.6
16 Classifier: Programming Language :: Python :: Implementation :: CPython
17 Classifier: Programming Language :: Python :: Implementation :: PyPy
18 Classifier: Operating System :: POSIX
0 .coveragerc
1 .gitignore
2 .mailmap
3 .testr.conf
4 .travis.yml
5 AUTHORS
6 CONTRIBUTING.rst
7 COPYING
8 MANIFEST.in
9 Makefile
10 README.rst
11 TODO
12 appveyor.yml
13 setup.py
14 tox.ini
15 xandikos.1
16 bin/xandikos
17 compat/README.rst
18 compat/common.sh
19 compat/litmus-0.13.tar.gz.sha256sum
20 compat/litmus.sh
21 compat/serverinfo.xml
22 compat/testcaldav.sh
23 compat/xandikos-caldavtester.sh
24 compat/xandikos-litmus.sh
25 compat/xandikos-vdirsyncer.sh
26 examples/uwsgi-standalone.ini
27 examples/uwsgi.ini
28 notes/auth.rst
29 notes/context.rst
30 notes/dav-compliance.rst
31 notes/file-format.rst
32 notes/goals.rst
33 notes/hacking.txt
34 notes/monitoring.rst
35 notes/principal.rst
36 notes/release-process.rst
37 notes/store.rst
38 notes/structure.rst
39 notes/webdav.rst
40 xandikos/__init__.py
41 xandikos/access.py
42 xandikos/apache.py
43 xandikos/caldav.py
44 xandikos/carddav.py
45 xandikos/davcommon.py
46 xandikos/icalendar.py
47 xandikos/infit.py
48 xandikos/quota.py
49 xandikos/scheduling.py
50 xandikos/store.py
51 xandikos/sync.py
52 xandikos/timezones.py
53 xandikos/vcard.py
54 xandikos/web.py
55 xandikos/webdav.py
56 xandikos/wsgi.py
57 xandikos.egg-info/PKG-INFO
58 xandikos.egg-info/SOURCES.txt
59 xandikos.egg-info/dependency_links.txt
60 xandikos.egg-info/requires.txt
61 xandikos.egg-info/top_level.txt
62 xandikos/templates/collection.html
63 xandikos/templates/root.html
64 xandikos/tests/__init__.py
65 xandikos/tests/test_caldav.py
66 xandikos/tests/test_icalendar.py
67 xandikos/tests/test_store.py
68 xandikos/tests/test_web.py
69 xandikos/tests/test_webdav.py
0 icalendar
1 dulwich
2 defusedxml
3 jinja2