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/ -
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/ -
- 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. +