Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 1e6a457a authored by Sunik Kupfer's avatar Sunik Kupfer Committed by Ricki Hirner
Browse files

Fix related google calendars not being found (bitfireAT/davx5#409)



* Minor changes
- update kdoc
- rename method and variables

* Add proxy parents to related resource detection

* Rename argument, query ResourceType

* Remove unnecessary utility method

* Change parentOf to extension function; Always return URL with trailing slash

* Use calendar-proxy-read/write ResourceType from new dav4jvm

* Use max. two levels of recursion to detect shared Google calendars

* Revise test and adapt method

* Simplify HttpUrl.parent()

---------

Co-authored-by: default avatarRicki Hirner <hirner@bitfire.at>
parent 0215e983
Loading
Loading
Loading
Loading
+2 −2
Original line number Original line Diff line number Diff line
@@ -143,13 +143,13 @@ class RefreshCollectionsWorkerTest {
    }
    }


    @Test
    @Test
    fun testQueryHomesets() {
    fun testDiscoverHomesets() {
        val service = createTestService(Service.TYPE_CARDDAV)!!
        val service = createTestService(Service.TYPE_CARDDAV)!!
        val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)
        val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)


        // Query home sets
        // Query home sets
        RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
        RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
            .queryHomeSets(baseUrl)
            .discoverHomesets(baseUrl)


        // Check home sets have been saved to database
        // Check home sets have been saved to database
        assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), db.homeSetDao().getByService(service.id).first().url)
        assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), db.homeSetDao().getByService(service.id).first().url)
+59 −43
Original line number Original line Diff line number Diff line
@@ -63,6 +63,7 @@ import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import at.bitfire.davdroid.ui.account.SettingsActivity
import at.bitfire.davdroid.ui.account.SettingsActivity
import at.bitfire.davdroid.util.DavUtils.parent
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.ListenableFuture
import dagger.assisted.Assisted
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedInject
@@ -192,7 +193,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
                    // refresh home set list (from principal url)
                    // refresh home set list (from principal url)
                    service.principal?.let { principalUrl ->
                    service.principal?.let { principalUrl ->
                        Logger.log.fine("Querying principal $principalUrl for home sets")
                        Logger.log.fine("Querying principal $principalUrl for home sets")
                        refresher.queryHomeSets(principalUrl)
                        refresher.discoverHomesets(principalUrl)
                    }
                    }


                    // refresh home sets and their member collections
                    // refresh home sets and their member collections
@@ -280,28 +281,25 @@ class RefreshCollectionsWorker @AssistedInject constructor(
        val httpClient: OkHttpClient
        val httpClient: OkHttpClient
    ) {
    ) {


        val alreadyQueried = mutableSetOf<HttpUrl>()

        /**
        /**
         * Checks if the given URL defines home sets and adds them to given home set list.
         * Starting at current-user-principal URL, tries to recursively find and save all user relevant home sets.
         *
         * @param principalUrl          Principal URL to query
         * @param forPersonalHomeset    Whether this is the first call of this recursive method.
         * Indicates that these found home sets are considered "personal", as they belong to the
         * current-user-principal.
         *
         *
         * Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by
         * other principals and still be considered "personal" (belonging to the current-user-principal).
         *
         *
         * *true* = found home sets belong to the current-user-principal; recurse if
         * @param principalUrl  URL of principal to query (user-provided principal or current-user-principal)
         * calendar proxies or group memberships are found
         * @param level         Current recursion level (limited to 0, 1 or 2):
         *
         *
         * *false* = found home sets don't directly belong to the current-user-principal; don't recurse
         * - 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
         *
         *
         * @throws java.io.IOException
         * @throws java.io.IOException
         * @throws HttpException
         * @throws HttpException
         * @throws at.bitfire.dav4jvm.exception.DavException
         * @throws at.bitfire.dav4jvm.exception.DavException
         */
         */
        internal fun queryHomeSets(principalUrl: HttpUrl, forPersonalHomeset: Boolean = true) {
        internal fun discoverHomesets(principalUrl: HttpUrl, level: Int = 0) {
            val related = mutableSetOf<HttpUrl>()
            Logger.log.fine("Discovering homesets of $principalUrl")
            val relatedResources = mutableSetOf<HttpUrl>()


            // Define homeset class and properties to look for
            // Define homeset class and properties to look for
            val homeSetClass: Class<out HrefListProperty>
            val homeSetClass: Class<out HrefListProperty>
@@ -309,48 +307,62 @@ class RefreshCollectionsWorker @AssistedInject constructor(
            when (service.type) {
            when (service.type) {
                Service.TYPE_CARDDAV -> {
                Service.TYPE_CARDDAV -> {
                    homeSetClass = AddressbookHomeSet::class.java
                    homeSetClass = AddressbookHomeSet::class.java
                    properties = arrayOf(DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME)
                    properties = arrayOf(DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME, ResourceType.NAME)
                }
                }
                Service.TYPE_CALDAV -> {
                Service.TYPE_CALDAV -> {
                    homeSetClass = CalendarHomeSet::class.java
                    homeSetClass = CalendarHomeSet::class.java
                    properties = arrayOf(DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME)
                    properties = arrayOf(DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME, ResourceType.NAME)
                }
                }
                else -> throw IllegalArgumentException()
                else -> throw IllegalArgumentException()
            }
            }


            val dav = DavResource(httpClient, principalUrl)
            // Query the URL
            val principal = DavResource(httpClient, principalUrl)
            val personal = level == 0
            try {
            try {
                // Query for the given service with properties
                principal.propfind(0, *properties) { davResponse, _ ->
                dav.propfind(0, *properties) { davResponse, _ ->
                    alreadyQueried += davResponse.href


                    // Check we got back the right service and save it
                    // If response holds home sets, save them
                    davResponse[homeSetClass]?.let { homeSet ->
                    davResponse[homeSetClass]?.let { homeSets ->
                        for (href in homeSet.hrefs)
                        for (homeSetHref in homeSets.hrefs)
                            dav.location.resolve(href)?.let {
                            principal.location.resolve(homeSetHref)?.let { homesetUrl ->
                                val foundUrl = UrlUtils.withTrailingSlash(it)
                                val resolvedHomeSetUrl = UrlUtils.withTrailingSlash(homesetUrl)
                                // 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 and still be considered "personal" (belonging to the current-user-principal).
                                db.homeSetDao().insertOrUpdateByUrl(
                                db.homeSetDao().insertOrUpdateByUrl(
                                    HomeSet(0, service.id, forPersonalHomeset, foundUrl)
                                    HomeSet(0, service.id, personal, resolvedHomeSetUrl)
                                )
                                )
                            }
                            }
                    }
                    }


                    // If personal (outer call of recursion), find/refresh related resources
                    // Add related principals to be queried afterwards
                    if (forPersonalHomeset) {
                    if (personal) {
                        val relatedResourcesTypes = mapOf(
                        val relatedResourcesTypes = listOf(
                            CalendarProxyReadFor::class.java to "read-only proxy for",      // calendar-proxy-read-for
                            // current resource is a read/write-proxy for other principals
                            CalendarProxyWriteFor::class.java to "read/write proxy for ",   // calendar-proxy-read/write-for
                            CalendarProxyReadFor::class.java,
                            GroupMembership::class.java to "member of group")               // direct group memberships
                            CalendarProxyWriteFor::class.java,

                            // current resource is a member of a group (principal that can also have proxies)
                        for ((type, logString) in relatedResourcesTypes) {
                            GroupMembership::class.java)
                        for (type in relatedResourcesTypes)
                            davResponse[type]?.let {
                            davResponse[type]?.let {
                                for (href in it.hrefs) {
                                for (href in it.hrefs)
                                    Logger.log.fine("Principal is a $logString for $href, checking for home sets")
                                    principal.location.resolve(href)?.let { url ->
                                    dav.location.resolve(href)?.let { url ->
                                        relatedResources += url
                                        related += 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) {
            } catch (e: HttpException) {
@@ -360,9 +372,13 @@ class RefreshCollectionsWorker @AssistedInject constructor(
                    throw e
                    throw e
            }
            }


            // query related homesets (those that do not belong to the current-user-principal)
            // query related resources
            for (resource in related)
            if (level <= 1)
                queryHomeSets(resource, false)
                for (resource in relatedResources)
                    if (alreadyQueried.contains(resource))
                        Logger.log.warning("$resource already queried, skipping")
                    else
                        discoverHomesets(resource, level + 1)
        }
        }


        /**
        /**
+25 −1
Original line number Original line Diff line number Diff line
@@ -8,8 +8,8 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.ConnectivityManager
import android.os.Build
import android.os.Build
import androidx.core.content.getSystemService
import androidx.core.content.getSystemService
import at.bitfire.davdroid.network.Android10Resolver
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.Android10Resolver
import okhttp3.HttpUrl
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -151,6 +151,30 @@ object DavUtils {


    // extension methods
    // extension methods


    /**
     * Returns parent URL (parent folder). Always with trailing slash
     */
    fun HttpUrl.parent(): HttpUrl {
        if (pathSegments.size == 1 && pathSegments[0] == "")
            // already root URL
            return this

        val builder = newBuilder()

        if (pathSegments[pathSegments.lastIndex] == "") {
            // URL ends with a slash ("/some/thing/" -> ["some","thing",""]), remove two segments ("" at lastIndex and "thing" at lastIndex - 1)
            builder.removePathSegment(pathSegments.lastIndex)
            builder.removePathSegment(pathSegments.lastIndex - 1)
        } else
            // URL doesn't end with a slash ("/some/thing" -> ["some","thing"]), remove one segment ("thing" at lastIndex)
            builder.removePathSegment(pathSegments.lastIndex)

        // append trailing slash
        builder.addPathSegment("")

        return builder.build()
    }

    /**
    /**
     * Compares MIME type and subtype of two MediaTypes. Does _not_ compare parameters
     * Compares MIME type and subtype of two MediaTypes. Does _not_ compare parameters
     * like `charset` or `version`.
     * like `charset` or `version`.
+19 −1
Original line number Original line Diff line number Diff line
@@ -5,8 +5,11 @@
package at.bitfire.davdroid
package at.bitfire.davdroid


import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.util.DavUtils.parent
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.Test
import org.xbill.DNS.DClass
import org.xbill.DNS.DClass
import org.xbill.DNS.Name
import org.xbill.DNS.Name
@@ -31,6 +34,21 @@ class DavUtilsTest {
        assertEquals("file.html", DavUtils.lastSegmentOfUrl((exampleURL + "dir/file.html").toHttpUrl()))
        assertEquals("file.html", DavUtils.lastSegmentOfUrl((exampleURL + "dir/file.html").toHttpUrl()))
    }
    }


    @Test
    fun testParent() {
        // with trailing slash
        assertEquals("http://example.com/1/2/".toHttpUrl(), "http://example.com/1/2/3/".toHttpUrl().parent())
        assertEquals("http://example.com/1/".toHttpUrl(), "http://example.com/1/2/".toHttpUrl().parent())
        assertEquals("http://example.com/".toHttpUrl(), "http://example.com/1/".toHttpUrl().parent())
        assertEquals("http://example.com/".toHttpUrl(), "http://example.com/".toHttpUrl().parent())

        // without trailing slash
        assertEquals("http://example.com/1/2/".toHttpUrl(), "http://example.com/1/2/3".toHttpUrl().parent())
        assertEquals("http://example.com/1/".toHttpUrl(), "http://example.com/1/2".toHttpUrl().parent())
        assertEquals("http://example.com/".toHttpUrl(), "http://example.com/1".toHttpUrl().parent())
        assertEquals("http://example.com/".toHttpUrl(), "http://example.com".toHttpUrl().parent())
    }

    @Test
    @Test
    fun testSelectSRVRecord() {
    fun testSelectSRVRecord() {
        assertNull(DavUtils.selectSRVRecord(emptyArray()))
        assertNull(DavUtils.selectSRVRecord(emptyArray()))
+2 −2
Original line number Original line Diff line number Diff line
@@ -10,7 +10,7 @@ buildscript {
        hilt: '2.48.1',
        hilt: '2.48.1',
        kotlin: '1.9.10',      // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion
        kotlin: '1.9.10',      // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion
                               //                   * com.google.devtools.ksp at the end of this file
                               //                   * com.google.devtools.ksp at the end of this file
        okhttp: '4.11.0',
        okhttp: '4.12.0',
        room: '2.5.2',
        room: '2.5.2',
        workManager: '2.9.0-rc01',
        workManager: '2.9.0-rc01',
        // Apache Commons versions
        // Apache Commons versions
@@ -19,7 +19,7 @@ buildscript {
        commonsText: '1.10.0',
        commonsText: '1.10.0',
        // own libraries
        // own libraries
        cert4android: '2bb3898',
        cert4android: '2bb3898',
        dav4jvm: 'da94a8b',
        dav4jvm: '1ed89c1',
        ical4android: '916f222',
        ical4android: '916f222',
        vcard4android: 'b376d2e'
        vcard4android: 'b376d2e'
    ]
    ]