From 24a60158412c9b56a9a6b5274bf1c56c3c7772a0 Mon Sep 17 00:00:00 2001
From: Stefan Hacker
Date: Sun, 12 Apr 2026 15:22:29 +0200
Subject: [PATCH] fix: Separate CalDAV/CardDAV Home-Sets + UI-URLs ohne /dav/
Kalender und Adressbuecher teilten sich den gleichen Home-Set
(/dav//). 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//calendars/
* addressbook-home-set zeigt auf /dav//addressbooks/
* Beide Pfade sind eigene Container-Collections - PROPFIND Depth 1
liefert nur den jeweils passenden Typ
* /dav// selbst gibt bei Depth 1 keine Kinder mehr zurueck,
Clients folgen den Home-Sets
* Die konkreten URLs cal- / ab- liegen weiterhin unter
/dav// (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//cal- bzw.
ab- fuer Clients die das brauchen.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/dav/caldav.py | 50 +++++++++++++++++++++++------
frontend/src/views/CalendarView.vue | 10 +++---
frontend/src/views/ContactsView.vue | 11 ++++---
3 files changed, 53 insertions(+), 18 deletions(-)
diff --git a/backend/app/dav/caldav.py b/backend/app/dav/caldav.py
index 431809d..c6dcc78 100644
--- a/backend/app/dav/caldav.py
+++ b/backend/app/dav/caldav.py
@@ -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//
+ # 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// : principal + list calendars AND addressbooks
+ # /dav// : 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//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//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)
diff --git a/frontend/src/views/CalendarView.vue b/frontend/src/views/CalendarView.vue
index 8613fb0..c01cab6 100644
--- a/frontend/src/views/CalendarView.vue
+++ b/frontend/src/views/CalendarView.vue
@@ -223,9 +223,9 @@
und Passwort sind deine normalen Mini-Cloud-Zugangsdaten.
- Auto-Discovery
- {{ origin }}/dav/
-
+ Server (Auto-Discovery)
+ {{ origin }}
+
Dieser Kalender (direkt)
@@ -234,8 +234,8 @@
@click="copyText(`${origin}/dav/${username}/cal-${selectedCal.id}/`)" />
-
Thunderbird / DAVx5 / Apple: Auto-Discovery-URL benutzen
-
Outlook (CalDAV-Synchronizer): Direkt-URL
+
Thunderbird / DAVx5 / Apple: Server-URL reicht
+
Outlook (CalDAV-Synchronizer): Direkt-URL des Kalenders
diff --git a/frontend/src/views/ContactsView.vue b/frontend/src/views/ContactsView.vue
index a6c3aba..0d26754 100644
--- a/frontend/src/views/ContactsView.vue
+++ b/frontend/src/views/ContactsView.vue
@@ -129,16 +129,19 @@
- Auto-Discovery
- {{ origin }}/dav/
-
+ Server (Auto-Discovery)
+ {{ origin }}
+
- Dieses Adressbuch
+ Dieses Adressbuch (direkt)
{{ origin }}/dav/{{ username }}/ab-{{ menuBook.id }}/
+
+ Bei DAVx5/Apple/Thunderbird reicht die Auto-Discovery-URL. Benutzername + Passwort wie im Web.
+