fix: Separate CalDAV/CardDAV Home-Sets + UI-URLs ohne /dav/

Kalender und Adressbuecher teilten sich den gleichen Home-Set
(/dav/<user>/). DAVx5 hat bei Depth-1-PROPFIND beide Collection-
Typen angezeigt und mangels bekanntem Resourcetype als
"DEFAULT_TASK_CALENDAR_NAME"-Kacheln gelistet.

Loesung:
* calendar-home-set zeigt auf /dav/<user>/calendars/
* addressbook-home-set zeigt auf /dav/<user>/addressbooks/
* Beide Pfade sind eigene Container-Collections - PROPFIND Depth 1
  liefert nur den jeweils passenden Typ
* /dav/<user>/ selbst gibt bei Depth 1 keine Kinder mehr zurueck,
  Clients folgen den Home-Sets
* Die konkreten URLs cal-<id> / ab-<id> liegen weiterhin unter
  /dav/<user>/ (keine Breaking Change fuer existierende Clients;
  nur die Discovery-URL aendert sich)

Frontend:
CalendarView + ContactsView zeigen als Auto-Discovery-URL nur
noch den Hostname - PROPFIND auf / funktioniert ja jetzt. Die
Direkt-URL bleibt vollstaendig mit /dav/<user>/cal-<id> bzw.
ab-<id> fuer Clients die das brauchen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 15:22:29 +02:00
parent 9c102823e4
commit 24a6015841
3 changed files with 53 additions and 18 deletions
+41 -9
View File
@@ -178,12 +178,12 @@ def _principal_response(user: User) -> ET.Element:
ET.SubElement(cup, _qn('d', 'href')).text = href
pu = ET.SubElement(prop, _qn('d', 'principal-URL'))
ET.SubElement(pu, _qn('d', 'href')).text = href
home = ET.SubElement(prop, _qn('c', 'calendar-home-set'))
ET.SubElement(home, _qn('d', 'href')).text = href
# CardDAV address-book home set - same principal URL, addressbook
# collections live next to calendars under /dav/<username>/
# Separate home-sets so clients (DAVx5!) don't mix calendars and
# addressbooks in the same listing.
cal_home = ET.SubElement(prop, _qn('c', 'calendar-home-set'))
ET.SubElement(cal_home, _qn('d', 'href')).text = f'/dav/{user.username}/calendars/'
ab_home = ET.SubElement(prop, '{urn:ietf:params:xml:ns:carddav}addressbook-home-set')
ET.SubElement(ab_home, _qn('d', 'href')).text = href
ET.SubElement(ab_home, _qn('d', 'href')).text = f'/dav/{user.username}/addressbooks/'
return _make_response(href, populate)
@@ -269,17 +269,49 @@ def propfind(subpath=''):
multistatus.append(_principal_response(user))
return _xml_response(multistatus)
# /dav/<username>/ : principal + list calendars AND addressbooks
# /dav/<username>/ : principal only (no child collections in this listing
# so clients don't mix calendars and addressbooks). Clients follow
# calendar-home-set / addressbook-home-set for the actual lists.
if len(parts) == 1:
if parts[0] != user.username:
return Response('', 403)
multistatus.append(_principal_response(user))
return _xml_response(multistatus)
# /dav/<username>/calendars/ : only calendar collections
if len(parts) == 2 and parts[1] == 'calendars':
if parts[0] != user.username:
return Response('', 403)
# A plain collection container
container = ET.Element(_qn('d', 'response'))
ET.SubElement(container, _qn('d', 'href')).text = f'/dav/{user.username}/calendars/'
propstat = ET.SubElement(container, _qn('d', 'propstat'))
prop = ET.SubElement(propstat, _qn('d', 'prop'))
rt = ET.SubElement(prop, _qn('d', 'resourcetype'))
ET.SubElement(rt, _qn('d', 'collection'))
ET.SubElement(prop, _qn('d', 'displayname')).text = 'Kalender'
ET.SubElement(propstat, _qn('d', 'status')).text = 'HTTP/1.1 200 OK'
multistatus.append(container)
if depth != '0':
for cal in _user_calendars(user):
multistatus.append(_calendar_response(user, cal))
# Addressbooks live next to calendars. Import here to avoid a
# circular import at module load time.
from .carddav import _addressbook_response, _user_addressbooks
return _xml_response(multistatus)
# /dav/<username>/addressbooks/ : only addressbook collections
if len(parts) == 2 and parts[1] == 'addressbooks':
if parts[0] != user.username:
return Response('', 403)
from .carddav import _addressbook_response, _user_addressbooks
container = ET.Element(_qn('d', 'response'))
ET.SubElement(container, _qn('d', 'href')).text = f'/dav/{user.username}/addressbooks/'
propstat = ET.SubElement(container, _qn('d', 'propstat'))
prop = ET.SubElement(propstat, _qn('d', 'prop'))
rt = ET.SubElement(prop, _qn('d', 'resourcetype'))
ET.SubElement(rt, _qn('d', 'collection'))
ET.SubElement(prop, _qn('d', 'displayname')).text = 'Adressbücher'
ET.SubElement(propstat, _qn('d', 'status')).text = 'HTTP/1.1 200 OK'
multistatus.append(container)
if depth != '0':
for ab in _user_addressbooks(user):
multistatus.append(_addressbook_response(user, ab))
return _xml_response(multistatus)