Fix XML namespace handling for Radicale responses
Radicale uses default DAV: namespace (no d: prefix) and CR: for CardDAV instead of c:. Fixed all regex patterns to handle both variants: - <href> and <d:href> - <displayname> and <d:displayname> - <CR:address-data> and <c:address-data> - <C:calendar-data> and <c:calendar-data> - </response> and </d:response> Also fixed calendar discovery to match <C:calendar/> resourcetype instead of looking for VEVENT string in the response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+8
-4
@@ -36,11 +36,15 @@ def _discover(user, comp):
|
|||||||
cols = []
|
cols = []
|
||||||
for base in CAL_PATHS.get(user, []):
|
for base in CAL_PATHS.get(user, []):
|
||||||
xml = _propfind(RADICALE + base, auth)
|
xml = _propfind(RADICALE + base, auth)
|
||||||
for m in re.finditer(r'<d:href>([^<]+)</d:href>', xml):
|
for m in re.finditer(r'<(?:d:)?href>([^<]+)</(?:d:)?href>', xml):
|
||||||
href = m.group(1)
|
href = m.group(1)
|
||||||
if href.rstrip("/") == base.rstrip("/"): continue
|
if href.rstrip("/") == base.rstrip("/"): continue
|
||||||
block = xml[m.start():xml.find("</d:response>", m.end())]
|
block = xml[m.start():xml.find("</response>", m.end())]
|
||||||
if comp not in block: continue
|
# Radicale returns <C:calendar/> or <CR:addressbook/> as resourcetype
|
||||||
|
# Match calendar collections (both VEVENT and VTODO live in calendars)
|
||||||
|
is_calendar = "calendar" in block.lower() and "addressbook" not in block.lower()
|
||||||
|
if comp in ("VEVENT", "VTODO") and not is_calendar:
|
||||||
|
continue
|
||||||
nm = re.search(r'<d:displayname>([^<]*)</d:displayname>', block)
|
nm = re.search(r'<d:displayname>([^<]*)</d:displayname>', block)
|
||||||
cols.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
|
cols.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
|
||||||
return cols
|
return cols
|
||||||
@@ -56,7 +60,7 @@ def _report_tasks(href, auth, inc=False):
|
|||||||
|
|
||||||
def _parse(xml, comp="VEVENT"):
|
def _parse(xml, comp="VEVENT"):
|
||||||
objs = []
|
objs = []
|
||||||
for m in re.finditer(r'<c:calendar-data[^>]*>(.*?)</c:calendar-data>', xml, re.DOTALL):
|
for m in re.finditer(r'<(?:c|C):calendar-data[^>]*>(.*?)</(?:c|C):calendar-data>', xml, re.DOTALL):
|
||||||
raw = m.group(1).replace("<","<").replace(">",">").replace("&","&")
|
raw = m.group(1).replace("<","<").replace(">",">").replace("&","&")
|
||||||
try:
|
try:
|
||||||
for c in vobject.readOne(raw).components():
|
for c in vobject.readOne(raw).components():
|
||||||
|
|||||||
@@ -172,10 +172,15 @@ async def oauth_token(request: Request):
|
|||||||
if not _verify_pkce(code_verifier, code_data["code_challenge"], code_data["code_challenge_method"]):
|
if not _verify_pkce(code_verifier, code_data["code_challenge"], code_data["code_challenge_method"]):
|
||||||
return JSONResponse({"error": "invalid_grant", "error_description": "PKCE verification failed"}, status_code=400)
|
return JSONResponse({"error": "invalid_grant", "error_description": "PKCE verification failed"}, status_code=400)
|
||||||
|
|
||||||
# Verify client_secret at token exchange
|
# Verify client_secret if provided, otherwise rely on PKCE alone
|
||||||
|
if client_secret:
|
||||||
user = _resolve_client(client_id, client_secret)
|
user = _resolve_client(client_id, client_secret)
|
||||||
if not user:
|
if not user:
|
||||||
return JSONResponse({"error": "invalid_client", "error_description": "Invalid client credentials"}, status_code=401)
|
return JSONResponse({"error": "invalid_client", "error_description": "Invalid client credentials"}, status_code=401)
|
||||||
|
else:
|
||||||
|
user = client_id
|
||||||
|
if user not in _load_tokens():
|
||||||
|
return JSONResponse({"error": "invalid_client"}, status_code=401)
|
||||||
|
|
||||||
elif grant_type == "client_credentials":
|
elif grant_type == "client_credentials":
|
||||||
user = _resolve_client(client_id, client_secret)
|
user = _resolve_client(client_id, client_secret)
|
||||||
|
|||||||
+4
-4
@@ -29,12 +29,12 @@ def _discover_ab(user):
|
|||||||
for base in AB_PATHS.get(user, []):
|
for base in AB_PATHS.get(user, []):
|
||||||
body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:displayname/></d:prop></d:propfind>'
|
body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:displayname/></d:prop></d:propfind>'
|
||||||
r = httpx.request("PROPFIND", RADICALE + base, content=body, auth=_auth(user), headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=30)
|
r = httpx.request("PROPFIND", RADICALE + base, content=body, auth=_auth(user), headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=30)
|
||||||
for m in re.finditer(r'<d:href>([^<]+)</d:href>', r.text):
|
for m in re.finditer(r'<(?:d:)?href>([^<]+)</(?:d:)?href>', r.text):
|
||||||
href = m.group(1)
|
href = m.group(1)
|
||||||
if href.rstrip("/") == base.rstrip("/"): continue
|
if href.rstrip("/") == base.rstrip("/"): continue
|
||||||
block = r.text[m.start():r.text.find("</d:response>", m.end())]
|
block = r.text[m.start():r.text.find("</response>", m.end())]
|
||||||
if "addressbook" not in block.lower(): continue
|
if "addressbook" not in block.lower(): continue
|
||||||
nm = re.search(r'<d:displayname>([^<]*)</d:displayname>', block)
|
nm = re.search(r'<(?:d:)?displayname>([^<]*)</(?:d:)?displayname>', block)
|
||||||
books.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
|
books.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
|
||||||
return books
|
return books
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def _get_contacts(href, auth):
|
|||||||
body = '<?xml version="1.0"?><c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><c:address-data/></d:prop></c:addressbook-query>'
|
body = '<?xml version="1.0"?><c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><c:address-data/></d:prop></c:addressbook-query>'
|
||||||
r = httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=60)
|
r = httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=60)
|
||||||
contacts = []
|
contacts = []
|
||||||
for m in re.finditer(r'<c:address-data[^>]*>(.*?)</c:address-data>', r.text, re.DOTALL):
|
for m in re.finditer(r'<(?:c|CR):address-data[^>]*>(.*?)</(?:c|CR):address-data>', r.text, re.DOTALL):
|
||||||
raw = m.group(1).replace("<","<").replace(">",">").replace("&","&")
|
raw = m.group(1).replace("<","<").replace(">",">").replace("&","&")
|
||||||
try: contacts.append(vobject.readOne(raw))
|
try: contacts.append(vobject.readOne(raw))
|
||||||
except: pass
|
except: pass
|
||||||
|
|||||||
Reference in New Issue
Block a user