diff --git a/calendar/server.py b/calendar/server.py
index 2168c25..b876275 100644
--- a/calendar/server.py
+++ b/calendar/server.py
@@ -36,11 +36,15 @@ def _discover(user, comp):
cols = []
for base in CAL_PATHS.get(user, []):
xml = _propfind(RADICALE + base, auth)
- for m in re.finditer(r'([^<]+)', xml):
+ for m in re.finditer(r'<(?:d:)?href>([^<]+)(?:d:)?href>', xml):
href = m.group(1)
if href.rstrip("/") == base.rstrip("/"): continue
- block = xml[m.start():xml.find("", m.end())]
- if comp not in block: continue
+ block = xml[m.start():xml.find("", m.end())]
+ # Radicale returns or 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'([^<]*)', block)
cols.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
return cols
@@ -56,7 +60,7 @@ def _report_tasks(href, auth, inc=False):
def _parse(xml, comp="VEVENT"):
objs = []
- for m in re.finditer(r']*>(.*?)', 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("&","&")
try:
for c in vobject.readOne(raw).components():
diff --git a/common.py b/common.py
index 4c37d10..13369ae 100644
--- a/common.py
+++ b/common.py
@@ -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"]):
return JSONResponse({"error": "invalid_grant", "error_description": "PKCE verification failed"}, status_code=400)
- # Verify client_secret at token exchange
- user = _resolve_client(client_id, client_secret)
- if not user:
- return JSONResponse({"error": "invalid_client", "error_description": "Invalid client credentials"}, status_code=401)
+ # Verify client_secret if provided, otherwise rely on PKCE alone
+ if client_secret:
+ user = _resolve_client(client_id, client_secret)
+ if not user:
+ 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":
user = _resolve_client(client_id, client_secret)
diff --git a/contacts/server.py b/contacts/server.py
index c092725..cc66fd1 100644
--- a/contacts/server.py
+++ b/contacts/server.py
@@ -29,12 +29,12 @@ def _discover_ab(user):
for base in AB_PATHS.get(user, []):
body = ''
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'([^<]+)', r.text):
+ for m in re.finditer(r'<(?:d:)?href>([^<]+)(?:d:)?href>', r.text):
href = m.group(1)
if href.rstrip("/") == base.rstrip("/"): continue
- block = r.text[m.start():r.text.find("", m.end())]
+ block = r.text[m.start():r.text.find("", m.end())]
if "addressbook" not in block.lower(): continue
- nm = re.search(r'([^<]*)', block)
+ nm = re.search(r'<(?:d:)?displayname>([^<]*)(?:d:)?displayname>', block)
books.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
return books
@@ -42,7 +42,7 @@ def _get_contacts(href, auth):
body = ''
r = httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=60)
contacts = []
- for m in re.finditer(r']*>(.*?)', 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("&","&")
try: contacts.append(vobject.readOne(raw))
except: pass