fix: CalDAV Autodiscovery - XML war doppelt verschachtelt
Property-Elemente wurden unter einem Container mit demselben Tag
erzeugt, z.B.:
<current-user-principal>
<current-user-principal> <!-- falsch, doppelt -->
<href>/dav/adam/</href>
</current-user-principal>
</current-user-principal>
Clients wie DAVx5 und Thunderbird erkennen dadurch den Principal
nicht und melden "Kein CalDAV-Dienst gefunden". XML-Generierung
umgebaut - Response-Helfer bekommen jetzt eine populate_prop-
Callback, die die tatsaechlichen Property-Children direkt ins
<prop>-Element setzt.
Zusaetzlich:
* /.well-known/caldav und /carddav akzeptieren jetzt auch PROPFIND,
OPTIONS, HEAD (einige Clients halten die Methode beim ersten
Aufruf bei).
* Kalender-Response enthaelt current-user-privilege-set (leer, als
Signal dass der Client nicht ACL-abhaengig pruefen muss).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e85338761d
commit
c4b381c5e9
|
|
@ -92,15 +92,14 @@ def create_app(config_class=Config):
|
|||
app.register_blueprint(dav_bp)
|
||||
|
||||
# Well-known URLs for CalDAV/CardDAV auto-discovery (iOS, DAVx5, etc.).
|
||||
# RFC 6764 says we should do a 301 to the DAV root; clients then PROPFIND
|
||||
# to walk principal/home-set/calendars. The path intentionally does NOT
|
||||
# include a username - the user authenticates via HTTP Basic and the
|
||||
# server routes them to their own principal.
|
||||
@app.route('/.well-known/caldav')
|
||||
# RFC 6764 requires 301; PROPFIND auf /.well-known/caldav gibt manche
|
||||
# Clients direkt ab (Method-Preservation) - also auch dort akzeptieren
|
||||
# und auf /dav/ weiterleiten. Alle HTTP-Methoden werden registriert.
|
||||
@app.route('/.well-known/caldav', methods=['GET', 'PROPFIND', 'OPTIONS', 'HEAD'])
|
||||
def wellknown_caldav():
|
||||
return redirect('/dav/', code=301)
|
||||
|
||||
@app.route('/.well-known/carddav')
|
||||
@app.route('/.well-known/carddav', methods=['GET', 'PROPFIND', 'OPTIONS', 'HEAD'])
|
||||
def wellknown_carddav():
|
||||
return redirect('/dav/', code=301)
|
||||
|
||||
|
|
|
|||
|
|
@ -139,79 +139,62 @@ def options(subpath=''):
|
|||
# PROPFIND
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _propstat_ok(href: str, props: dict) -> ET.Element:
|
||||
"""Build a <response> element with one 200 propstat containing the
|
||||
given (qname -> element or string or None) properties."""
|
||||
def _make_response(href: str, populate_prop) -> ET.Element:
|
||||
"""Build a <response><href/><propstat><prop>...</prop><status>200</status>
|
||||
</propstat></response> element. `populate_prop` is a callable that gets
|
||||
the <prop> element and appends the actual property sub-elements to it."""
|
||||
resp = ET.Element(_qn('d', 'response'))
|
||||
ET.SubElement(resp, _qn('d', 'href')).text = href
|
||||
propstat = ET.SubElement(resp, _qn('d', 'propstat'))
|
||||
prop = ET.SubElement(propstat, _qn('d', 'prop'))
|
||||
for qname, value in props.items():
|
||||
el = ET.SubElement(prop, qname)
|
||||
if isinstance(value, ET.Element):
|
||||
el.append(value)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
el.append(child)
|
||||
elif value is not None:
|
||||
el.text = str(value)
|
||||
populate_prop(prop)
|
||||
ET.SubElement(propstat, _qn('d', 'status')).text = 'HTTP/1.1 200 OK'
|
||||
return resp
|
||||
|
||||
|
||||
def _resource_type(*kinds: str) -> ET.Element:
|
||||
el = ET.Element(_qn('d', 'resourcetype'))
|
||||
for k in kinds:
|
||||
if k == 'collection':
|
||||
ET.SubElement(el, _qn('d', 'collection'))
|
||||
elif k == 'calendar':
|
||||
ET.SubElement(el, _qn('c', 'calendar'))
|
||||
elif k == 'principal':
|
||||
ET.SubElement(el, _qn('d', 'principal'))
|
||||
return el
|
||||
|
||||
|
||||
def _root_response(href: str, user: User) -> ET.Element:
|
||||
principal = ET.Element(_qn('d', 'current-user-principal'))
|
||||
ET.SubElement(principal, _qn('d', 'href')).text = f'/dav/{user.username}/'
|
||||
return _propstat_ok(href, {
|
||||
_qn('d', 'resourcetype'): _resource_type('collection'),
|
||||
_qn('d', 'displayname'): 'Mini-Cloud DAV',
|
||||
_qn('d', 'current-user-principal'): principal,
|
||||
})
|
||||
def populate(prop):
|
||||
rt = ET.SubElement(prop, _qn('d', 'resourcetype'))
|
||||
ET.SubElement(rt, _qn('d', 'collection'))
|
||||
ET.SubElement(prop, _qn('d', 'displayname')).text = 'Mini-Cloud DAV'
|
||||
cup = ET.SubElement(prop, _qn('d', 'current-user-principal'))
|
||||
ET.SubElement(cup, _qn('d', 'href')).text = f'/dav/{user.username}/'
|
||||
return _make_response(href, populate)
|
||||
|
||||
|
||||
def _principal_response(user: User) -> ET.Element:
|
||||
href = f'/dav/{user.username}/'
|
||||
home = ET.Element(_qn('c', 'calendar-home-set'))
|
||||
ET.SubElement(home, _qn('d', 'href')).text = href
|
||||
principal_url = ET.Element(_qn('d', 'principal-URL'))
|
||||
ET.SubElement(principal_url, _qn('d', 'href')).text = href
|
||||
return _propstat_ok(href, {
|
||||
_qn('d', 'resourcetype'): _resource_type('principal', 'collection'),
|
||||
_qn('d', 'displayname'): user.username,
|
||||
_qn('c', 'calendar-home-set'): home,
|
||||
_qn('d', 'principal-URL'): principal_url,
|
||||
})
|
||||
|
||||
def populate(prop):
|
||||
rt = ET.SubElement(prop, _qn('d', 'resourcetype'))
|
||||
ET.SubElement(rt, _qn('d', 'collection'))
|
||||
ET.SubElement(rt, _qn('d', 'principal'))
|
||||
ET.SubElement(prop, _qn('d', 'displayname')).text = user.username
|
||||
cup = ET.SubElement(prop, _qn('d', 'current-user-principal'))
|
||||
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
|
||||
return _make_response(href, populate)
|
||||
|
||||
|
||||
def _calendar_response(user: User, cal: Calendar) -> ET.Element:
|
||||
href = _href_calendar(user.username, cal.id)
|
||||
supported = ET.Element(_qn('c', 'supported-calendar-component-set'))
|
||||
comp = ET.SubElement(supported, _qn('c', 'comp'))
|
||||
comp.set('name', 'VEVENT')
|
||||
color_el = ET.Element(_qn('ic', 'calendar-color'))
|
||||
color_el.text = cal.color or '#3788d8'
|
||||
getctag = ET.Element(_qn('cs', 'getctag'))
|
||||
getctag.text = _calendar_ctag(cal)
|
||||
return _propstat_ok(href, {
|
||||
_qn('d', 'resourcetype'): _resource_type('collection', 'calendar'),
|
||||
_qn('d', 'displayname'): cal.name,
|
||||
_qn('c', 'calendar-description'): cal.description or '',
|
||||
_qn('c', 'supported-calendar-component-set'): supported,
|
||||
_qn('ic', 'calendar-color'): color_el.text,
|
||||
_qn('cs', 'getctag'): getctag.text,
|
||||
})
|
||||
|
||||
def populate(prop):
|
||||
rt = ET.SubElement(prop, _qn('d', 'resourcetype'))
|
||||
ET.SubElement(rt, _qn('d', 'collection'))
|
||||
ET.SubElement(rt, _qn('c', 'calendar'))
|
||||
ET.SubElement(prop, _qn('d', 'displayname')).text = cal.name
|
||||
ET.SubElement(prop, _qn('c', 'calendar-description')).text = cal.description or ''
|
||||
supported = ET.SubElement(prop, _qn('c', 'supported-calendar-component-set'))
|
||||
comp = ET.SubElement(supported, _qn('c', 'comp'))
|
||||
comp.set('name', 'VEVENT')
|
||||
ET.SubElement(prop, _qn('ic', 'calendar-color')).text = cal.color or '#3788d8'
|
||||
ET.SubElement(prop, _qn('cs', 'getctag')).text = _calendar_ctag(cal)
|
||||
ET.SubElement(prop, _qn('d', 'current-user-privilege-set'))
|
||||
return _make_response(href, populate)
|
||||
|
||||
|
||||
def _calendar_ctag(cal: Calendar) -> str:
|
||||
|
|
@ -223,14 +206,15 @@ def _calendar_ctag(cal: Calendar) -> str:
|
|||
|
||||
def _event_response(user: User, cal: Calendar, event: CalendarEvent, include_data: bool = False) -> ET.Element:
|
||||
href = _href_event(user.username, cal.id, event.uid)
|
||||
props = {
|
||||
_qn('d', 'getetag'): _etag_for_event(event),
|
||||
_qn('d', 'getcontenttype'): 'text/calendar; charset=utf-8; component=VEVENT',
|
||||
_qn('d', 'resourcetype'): ET.Element(_qn('d', 'resourcetype')),
|
||||
}
|
||||
if include_data:
|
||||
props[_qn('c', 'calendar-data')] = _wrap_vcalendar(cal, event)
|
||||
return _propstat_ok(href, props)
|
||||
|
||||
def populate(prop):
|
||||
ET.SubElement(prop, _qn('d', 'getetag')).text = _etag_for_event(event)
|
||||
ET.SubElement(prop, _qn('d', 'getcontenttype')).text = \
|
||||
'text/calendar; charset=utf-8; component=VEVENT'
|
||||
ET.SubElement(prop, _qn('d', 'resourcetype')) # empty -> regular resource
|
||||
if include_data:
|
||||
ET.SubElement(prop, _qn('c', 'calendar-data')).text = _wrap_vcalendar(cal, event)
|
||||
return _make_response(href, populate)
|
||||
|
||||
|
||||
def _wrap_vcalendar(cal: Calendar, event: CalendarEvent) -> str:
|
||||
|
|
|
|||
Loading…
Reference in New Issue