fix: CalDAV fuer DAVx5 - Well-Known intern dispatchen, mehr Properties
Aenderungen fuer besseren DAVx5-Support: * /.well-known/caldav reagiert jetzt direkt auf PROPFIND/OPTIONS ohne Redirect-Zickerei. GET/HEAD redirecten weiterhin auf /dav/ als visuelle Fallback. * strict_slashes app-weit aus: /dav und /dav/ sind gleichwertig, ebenso die Unterpfade. DAVx5 nutzt beides gemischt. * Jede DAV-Response traegt jetzt den DAV-Header (1, 2, 3, calendar-access), nicht nur OPTIONS. * Kalender-Response enthaelt jetzt supported-report-set mit calendar-query + calendar-multiget (DAVx5 prueft das). * current-user-privilege-set wird mit konkreten Privilegien gefuellt (read, write, write-properties, write-content, bind, unbind) statt leer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c4b381c5e9
commit
3f0d823dbf
|
|
@ -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/<token>')
|
||||
|
|
|
|||
|
|
@ -51,7 +51,11 @@ def _qn(prefix: str, local: str) -> str:
|
|||
|
||||
def _xml_response(root: ET.Element, status: int = 207) -> Response:
|
||||
body = b'<?xml version="1.0" encoding="utf-8"?>\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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue