From f5fc51c156c9148f3f4a29bc5ca92f185f4163be Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 08:05:28 +0200 Subject: [PATCH] fix: parsing XML CalDAV namespace-aware + fallback principal Nextcloud (principals/users/) Co-Authored-By: Claude Sonnet 4.6 --- .../mobile/data/caldav/CalDavDiscovery.kt | 19 +++++++++++++------ .../mobile/data/caldav/CalDavSyncManager.kt | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt b/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt index 0d5f4bf..ad83c04 100644 --- a/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt +++ b/app/src/main/java/com/planify/mobile/data/caldav/CalDavDiscovery.kt @@ -29,14 +29,17 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { // Step 1: resolve principal URL val principalUrl = resolvePrincipalUrl(normalizedBase, credentials, username) - ?: return@withContext DiscoveryResult.Failure("Impossible de trouver le principal CalDAV") + ?: return@withContext DiscoveryResult.Failure("Étape 1 échouée : principal CalDAV introuvable pour $username sur $normalizedBase") // Step 2: find calendar home val calendarHome = resolveCalendarHome(principalUrl, credentials) - ?: return@withContext DiscoveryResult.Failure("Impossible de trouver le calendar home") + ?: return@withContext DiscoveryResult.Failure("Étape 2 échouée : calendar-home-set introuvable sur $principalUrl") // Step 3: list VTODO-capable calendars val calendars = listCalendars(calendarHome, credentials, username, baseUrl) + if (calendars.isEmpty()) { + return@withContext DiscoveryResult.Failure("Étape 3 : aucun calendrier VTODO trouvé sur $calendarHome") + } val caldavType = detectType(normalizedBase) val now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) @@ -60,7 +63,8 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { } private fun resolvePrincipalUrl(baseUrl: String, credentials: String, username: String): String? { - val wellKnown = "$baseUrl/.well-known/caldav" + val origin = baseUrl.substringBefore("://") + "://" + baseUrl.substringAfter("://").substringBefore("/") + val wellKnown = "$origin/.well-known/caldav" val body = """ @@ -75,7 +79,10 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { if (href != null) return resolveUrl(baseUrl, href) } } - // Fallback: guess principal path + // Nextcloud uses principals/users/, fallback to that first + val nextcloudFallback = "$baseUrl/principals/users/$username/" + val resp = client.propfind(nextcloudFallback, credentials, "0", body) + if (resp.isSuccess) return nextcloudFallback return "$baseUrl/principals/$username/" } @@ -115,7 +122,7 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { private fun parseCalendarList(xml: String, baseUrl: String): List { val results = mutableListOf() runCatching { - val factory = XmlPullParserFactory.newInstance() + val factory = XmlPullParserFactory.newInstance().also { it.isNamespaceAware = true } val parser = factory.newPullParser() parser.setInput(StringReader(xml)) @@ -160,7 +167,7 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) { } private fun extractHref(xml: String, parentTag: String): String? = runCatching { - val factory = XmlPullParserFactory.newInstance() + val factory = XmlPullParserFactory.newInstance().also { it.isNamespaceAware = true } val parser = factory.newPullParser() parser.setInput(StringReader(xml)) var inTarget = false diff --git a/app/src/main/java/com/planify/mobile/data/caldav/CalDavSyncManager.kt b/app/src/main/java/com/planify/mobile/data/caldav/CalDavSyncManager.kt index 123c983..6592bff 100644 --- a/app/src/main/java/com/planify/mobile/data/caldav/CalDavSyncManager.kt +++ b/app/src/main/java/com/planify/mobile/data/caldav/CalDavSyncManager.kt @@ -220,7 +220,7 @@ class CalDavSyncManager @Inject constructor( private fun parseMultiStatus(xml: String, baseUrl: String): List { val results = mutableListOf() runCatching { - val factory = XmlPullParserFactory.newInstance() + val factory = XmlPullParserFactory.newInstance().also { it.isNamespaceAware = true } val parser = factory.newPullParser() parser.setInput(StringReader(xml))