diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 601ffb5..aac77da 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -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) diff --git a/backend/app/dav/caldav.py b/backend/app/dav/caldav.py index d9158cd..f4f291e 100644 --- a/backend/app/dav/caldav.py +++ b/backend/app/dav/caldav.py @@ -139,79 +139,62 @@ def options(subpath=''): # PROPFIND # --------------------------------------------------------------------------- -def _propstat_ok(href: str, props: dict) -> ET.Element: - """Build a 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 ...200 + element. `populate_prop` is a callable that gets + the 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: