diff --git a/backend/app/__init__.py b/backend/app/__init__.py index aac77da..b85719d 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -69,6 +69,9 @@ def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) + # DAV-Clients setzen Trailing-Slashes uneinheitlich - daher deaktivieren + # wir die strikte Pruefung app-weit. Betrifft alle Blueprints. + app.url_map.strict_slashes = False # Ensure data directories exist Path(app.config['UPLOAD_PATH']).mkdir(parents=True, exist_ok=True) @@ -92,16 +95,24 @@ def create_app(config_class=Config): app.register_blueprint(dav_bp) # Well-known URLs for CalDAV/CardDAV auto-discovery (iOS, DAVx5, etc.). - # 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(): + # 301-Redirect bei PROPFIND ist bei einigen Clients zickig, deshalb + # delegieren wir intern direkt an die DAV-Handler, statt zu redirecten. + from app.dav.caldav import propfind as dav_propfind, options as dav_options + + @app.route('/.well-known/caldav', methods=['GET', 'HEAD']) + @app.route('/.well-known/carddav', methods=['GET', 'HEAD']) + def wellknown_redirect(): return redirect('/dav/', code=301) - @app.route('/.well-known/carddav', methods=['GET', 'PROPFIND', 'OPTIONS', 'HEAD']) - def wellknown_carddav(): - return redirect('/dav/', code=301) + @app.route('/.well-known/caldav', methods=['PROPFIND']) + @app.route('/.well-known/carddav', methods=['PROPFIND']) + def wellknown_propfind(): + return dav_propfind(subpath='') + + @app.route('/.well-known/caldav', methods=['OPTIONS']) + @app.route('/.well-known/carddav', methods=['OPTIONS']) + def wellknown_options(): + return dav_options() # iCal export (public, no auth) @app.route('/ical/') diff --git a/backend/app/dav/caldav.py b/backend/app/dav/caldav.py index f4f291e..489d679 100644 --- a/backend/app/dav/caldav.py +++ b/backend/app/dav/caldav.py @@ -51,7 +51,11 @@ def _qn(prefix: str, local: str) -> str: def _xml_response(root: ET.Element, status: int = 207) -> Response: body = b'\n' + ET.tostring(root, encoding='utf-8') - return Response(body, status=status, mimetype='application/xml; charset=utf-8') + headers = { + 'DAV': '1, 2, 3, calendar-access', + 'Content-Type': 'application/xml; charset=utf-8', + } + return Response(body, status=status, headers=headers) # --------------------------------------------------------------------------- @@ -191,9 +195,20 @@ def _calendar_response(user: User, cal: Calendar) -> ET.Element: supported = ET.SubElement(prop, _qn('c', 'supported-calendar-component-set')) comp = ET.SubElement(supported, _qn('c', 'comp')) comp.set('name', 'VEVENT') + # supported-report-set: advertise which REPORTs this collection handles + srs = ET.SubElement(prop, _qn('d', 'supported-report-set')) + for report_name in ('calendar-query', 'calendar-multiget'): + sup = ET.SubElement(srs, _qn('d', 'supported-report')) + rep = ET.SubElement(sup, _qn('d', 'report')) + ET.SubElement(rep, _qn('c', report_name)) 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')) + # current-user-privilege-set: advertise what the authenticated user is + # allowed to do. DAVx5 checks this to decide read-only vs read-write. + cups = ET.SubElement(prop, _qn('d', 'current-user-privilege-set')) + for priv_name in ('read', 'write', 'write-properties', 'write-content', 'bind', 'unbind'): + p = ET.SubElement(cups, _qn('d', 'privilege')) + ET.SubElement(p, _qn('d', priv_name)) return _make_response(href, populate)