fix: parsing XML CalDAV namespace-aware + fallback principal Nextcloud (principals/users/)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 08:05:28 +02:00
parent e2085a8dc2
commit f5fc51c156
2 changed files with 14 additions and 7 deletions
@@ -29,14 +29,17 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) {
// Step 1: resolve principal URL // Step 1: resolve principal URL
val principalUrl = resolvePrincipalUrl(normalizedBase, credentials, username) 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 // Step 2: find calendar home
val calendarHome = resolveCalendarHome(principalUrl, credentials) 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 // Step 3: list VTODO-capable calendars
val calendars = listCalendars(calendarHome, credentials, username, baseUrl) 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 caldavType = detectType(normalizedBase)
val now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) 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? { 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 = """ val body = """
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:"> <propfind xmlns="DAV:">
@@ -75,7 +79,10 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) {
if (href != null) return resolveUrl(baseUrl, href) if (href != null) return resolveUrl(baseUrl, href)
} }
} }
// Fallback: guess principal path // Nextcloud uses principals/users/<username>, 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/" return "$baseUrl/principals/$username/"
} }
@@ -115,7 +122,7 @@ class CalDavDiscovery @Inject constructor(private val client: CalDavClient) {
private fun parseCalendarList(xml: String, baseUrl: String): List<CalendarInfo> { private fun parseCalendarList(xml: String, baseUrl: String): List<CalendarInfo> {
val results = mutableListOf<CalendarInfo>() val results = mutableListOf<CalendarInfo>()
runCatching { runCatching {
val factory = XmlPullParserFactory.newInstance() val factory = XmlPullParserFactory.newInstance().also { it.isNamespaceAware = true }
val parser = factory.newPullParser() val parser = factory.newPullParser()
parser.setInput(StringReader(xml)) 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 { 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() val parser = factory.newPullParser()
parser.setInput(StringReader(xml)) parser.setInput(StringReader(xml))
var inTarget = false var inTarget = false
@@ -220,7 +220,7 @@ class CalDavSyncManager @Inject constructor(
private fun parseMultiStatus(xml: String, baseUrl: String): List<MultiStatusItem> { private fun parseMultiStatus(xml: String, baseUrl: String): List<MultiStatusItem> {
val results = mutableListOf<MultiStatusItem>() val results = mutableListOf<MultiStatusItem>()
runCatching { runCatching {
val factory = XmlPullParserFactory.newInstance() val factory = XmlPullParserFactory.newInstance().also { it.isNamespaceAware = true }
val parser = factory.newPullParser() val parser = factory.newPullParser()
parser.setInput(StringReader(xml)) parser.setInput(StringReader(xml))