/* * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. */ package at.bitfire.davdroid.servicedetection import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.Property import at.bitfire.dav4jvm.UrlUtils import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.dav4jvm.property.caldav.CalendarHomeSet import at.bitfire.dav4jvm.property.caldav.CalendarProxyReadFor import at.bitfire.dav4jvm.property.caldav.CalendarProxyWriteFor import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet import at.bitfire.dav4jvm.property.common.HrefListProperty import at.bitfire.dav4jvm.property.webdav.DisplayName import at.bitfire.dav4jvm.property.webdav.GroupMembership import at.bitfire.dav4jvm.property.webdav.ResourceType import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.repository.DavHomeSetRepository import at.bitfire.davdroid.util.DavUtils.parent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import okhttp3.HttpUrl import okhttp3.OkHttpClient import java.util.logging.Level import java.util.logging.Logger /** * ServiceRefresher is used to discover and save home sets of a given service. */ class ServiceRefresher @AssistedInject constructor( @Assisted private val service: Service, @Assisted private val httpClient: OkHttpClient, private val logger: Logger, private val homeSetRepository: DavHomeSetRepository ) { @AssistedFactory interface Factory { fun create(service: Service, httpClient: OkHttpClient): ServiceRefresher } /** * Home-set class to use depending on the given service type. */ private val homeSetClass: Class = when (service.type) { Service.TYPE_CARDDAV -> AddressbookHomeSet::class.java Service.TYPE_CALDAV -> CalendarHomeSet::class.java else -> throw IllegalArgumentException() } /** * Home-set properties to ask for in a PROPFIND request to the principal URL, * depending on the given service type. */ private val homeSetProperties: Array = arrayOf( // generic WebDAV properties DisplayName.NAME, GroupMembership.NAME, ResourceType.NAME ) + when (service.type) { // service-specific CalDAV/CardDAV properties Service.TYPE_CARDDAV -> arrayOf( AddressbookHomeSet.NAME, ) Service.TYPE_CALDAV -> arrayOf( CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME ) else -> throw IllegalArgumentException() } /** * Starting at given principal URL, tries to recursively find and save all user relevant home sets. * * @param principalUrl URL of principal to query (user-provided principal or current-user-principal) * @param level Current recursion level (limited to 0, 1 or 2): * - 0: We assume found home sets belong to the current-user-principal * - 1 or 2: We assume found home sets don't directly belong to the current-user-principal * @param alreadyQueriedPrincipals The HttpUrls of principals which have been queried already, to avoid querying principals more than once. * @param alreadySavedHomeSets The HttpUrls of home sets which have been saved to database already, to avoid saving home sets * more than once, which could overwrite the already set "personal" flag with `false`. * * @throws java.io.IOException on I/O errors * @throws HttpException on HTTP errors * @throws at.bitfire.dav4jvm.exception.DavException on application-level or logical errors */ internal fun discoverHomesets( principalUrl: HttpUrl, level: Int = 0, alreadyQueriedPrincipals: MutableSet = mutableSetOf(), alreadySavedHomeSets: MutableSet = mutableSetOf() ) { logger.fine("Discovering homesets of $principalUrl") val relatedResources = mutableSetOf() // Query the URL val principal = DavResource(httpClient, principalUrl) val personal = level == 0 try { principal.propfind(0, *homeSetProperties) { davResponse, _ -> alreadyQueriedPrincipals += davResponse.href // If response holds home sets, save them davResponse[homeSetClass]?.let { homeSets -> for (homeSetHref in homeSets.hrefs) principal.location.resolve(homeSetHref)?.let { homesetUrl -> val resolvedHomeSetUrl = UrlUtils.withTrailingSlash(homesetUrl) if (!alreadySavedHomeSets.contains(resolvedHomeSetUrl)) { homeSetRepository.insertOrUpdateByUrlBlocking( // HomeSet is considered personal if this is the outer recursion call, // This is because we assume the first call to query the current-user-principal // Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by // other principals while still being considered "personal" (belonging to the current-user-principal) // and an owned home set need not always be personal either. HomeSet(0, service.id, personal, resolvedHomeSetUrl) ) alreadySavedHomeSets += resolvedHomeSetUrl } } } // Add related principals to be queried afterwards if (personal) { val relatedResourcesTypes = listOf( // current resource is a read/write-proxy for other principals CalendarProxyReadFor::class.java, CalendarProxyWriteFor::class.java, // current resource is a member of a group (principal that can also have proxies) GroupMembership::class.java ) for (type in relatedResourcesTypes) davResponse[type]?.let { for (href in it.hrefs) principal.location.resolve(href)?.let { url -> relatedResources += url } } } // If current resource is a calendar-proxy-read/write, it's likely that its parent is a principal, too. davResponse[ResourceType::class.java]?.let { resourceType -> val proxyProperties = arrayOf( ResourceType.CALENDAR_PROXY_READ, ResourceType.CALENDAR_PROXY_WRITE, ) if (proxyProperties.any { resourceType.types.contains(it) }) relatedResources += davResponse.href.parent() } } } catch (e: HttpException) { if (e.isClientError) logger.log(Level.INFO, "Ignoring Client Error 4xx while looking for ${service.type} home sets", e) else throw e } // query related resources if (level <= 1) for (resource in relatedResources) if (alreadyQueriedPrincipals.contains(resource)) logger.warning("$resource already queried, skipping") else discoverHomesets( principalUrl = resource, level = level + 1, alreadyQueriedPrincipals = alreadyQueriedPrincipals, alreadySavedHomeSets = alreadySavedHomeSets ) } }