Skip to content
653 changes: 653 additions & 0 deletions app/schemas/at.bitfire.davdroid.db.AppDatabase/19.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class CollectionTest {
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
info = Collection.fromDavResponse(response, null) ?: throw IllegalArgumentException()
}
assertEquals(Collection.TYPE_ADDRESSBOOK, info.type)
assertTrue(info.privWriteContent)
Expand Down Expand Up @@ -127,7 +127,7 @@ class CollectionTest {
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response)!!
info = Collection.fromDavResponse(response, null)!!
}
assertEquals(Collection.TYPE_CALENDAR, info.type)
assertFalse(info.privWriteContent)
Expand Down Expand Up @@ -163,7 +163,7 @@ class CollectionTest {
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response)!!
info = Collection.fromDavResponse(response, null)!!
}
assertEquals(Collection.TYPE_CALENDAR, info.type)
assertFalse(info.privWriteContent)
Expand Down Expand Up @@ -197,7 +197,7 @@ class CollectionTest {
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
info = Collection.fromDavResponse(response, null) ?: throw IllegalArgumentException()
}
assertEquals(Collection.TYPE_WEBCAL, info.type)
assertEquals("Sample Subscription", info.displayName)
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/kotlin/at/bitfire/davdroid/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import javax.inject.Singleton
SyncStats::class,
WebDavDocument::class,
WebDavMount::class
], exportSchema = true, version = 18, autoMigrations = [
], exportSchema = true, version = 19, autoMigrations = [
AutoMigration(from = 18, to = 19), // collection: add personal flag
AutoMigration(from = 17, to = 18, spec = AutoMigration18::class),
AutoMigration(from = 16, to = 17), // collection: add VAPID key
AutoMigration(from = 15, to = 16, spec = AutoMigration16::class),
Expand Down
12 changes: 10 additions & 2 deletions app/src/main/kotlin/at/bitfire/davdroid/db/Collection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import at.bitfire.dav4jvm.property.push.WebPush
import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.webdav.DisplayName
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.davdroid.servicedetection.ServiceDetectionUtils
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.davdroid.util.trimToNull
import at.bitfire.ical4android.util.DateUtils
Expand Down Expand Up @@ -100,6 +101,12 @@ data class Collection(
*/
val forceReadOnly: Boolean = false,

/**
* Whether this collection's `DAV:owner` property matches `current-user-principal`.
* `null` indicates an unknown principal or that `DAV:owner` was not set.
*/
val personal: Boolean? = null,

/**
* Human-readable name of the collection
*/
Expand Down Expand Up @@ -162,7 +169,7 @@ data class Collection(
* @param dav WebDAV response
* @return null if the response doesn't represent a collection
*/
fun fromDavResponse(dav: Response): Collection? {
fun fromDavResponse(dav: Response, principalUrl: HttpUrl?): Collection? {
val url = UrlUtils.withTrailingSlash(dav.href)
val type: String = dav[ResourceType::class.java]?.let { resourceType ->
when {
Expand Down Expand Up @@ -252,7 +259,8 @@ data class Collection(
source = source,
supportsWebPush = supportsWebPush,
pushVapidKey = vapidPublicKey,
pushTopic = pushTopic
pushTopic = pushTopic,
personal = ServiceDetectionUtils.isPersonal(principalUrl, dav)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class CollectionsWithoutHomeSetRefresher @AssistedInject constructor(
}

// Save or update the collection, if usable, otherwise delete it
Collection.fromDavResponse(response)?.let { collection ->
Collection.fromDavResponse(response, service.principal)?.let { collection ->
if (!ServiceDetectionUtils.isUsableCollection(service, collection))
return@let
collectionRepository.insertOrUpdateByUrlRememberSync(collection.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ class DavResourceFinder @AssistedInject constructor(
davResponse[ResourceType::class.java]?.let {
// Is it a calendar or an address book, ...
if (it.types.contains(resourceType))
Collection.fromDavResponse(davResponse)?.let { info ->
Collection.fromDavResponse(davResponse, principal)?.let { info ->
log.info("Found resource of type $resourceType at ${info.url}")
config.collections[info.url] = info
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ class HomeSetRefresher @AssistedInject constructor(
homeSetRepository.insertOrUpdateByUrlBlocking(
localHomeset.copy(
displayName = response[DisplayName::class.java]?.displayName,
privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind != false
privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind != false,
personal = ServiceDetectionUtils.isPersonal(service.principal, response) == true
)
)

// in any case, check whether the response is about a usable collection
var collection = Collection.fromDavResponse(response) ?: return@propfind
var collection = Collection.fromDavResponse(response, service.principal) ?: return@propfind
collection = collection.copy(
serviceId = service.id,
homeSetId = localHomeset.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package at.bitfire.davdroid.servicedetection

import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.equalsForWebDAV
import at.bitfire.dav4jvm.property.caldav.CalendarColor
import at.bitfire.dav4jvm.property.caldav.CalendarDescription
import at.bitfire.dav4jvm.property.caldav.CalendarTimezone
Expand All @@ -21,6 +23,7 @@ import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.db.ServiceType
import okhttp3.HttpUrl

object ServiceDetectionUtils {

Expand Down Expand Up @@ -63,4 +66,29 @@ object ServiceDetectionUtils {
(service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
(collection.type == Collection.TYPE_WEBCAL && collection.source != null)

/**
* Evaluates whether a response is personal or not.
* It takes the [Owner] property from the response, and compares its value against [principal].
*
* If either one of those is not set (`null`), this function returns `null`.
* @param principal The current principal url to compare the owner against.
* @param davResponse The response to process.
* @return
* - `null` if either [principal] or [davResponse]'s [Owner] are null, or if the owner url cannot be resolved on [principal].
* - `true` if the owner matches a principal.
* - `false` if the owner doesn't match a principal or the owner and/or principal is not set / unknown.
*/
fun isPersonal(principal: HttpUrl?, davResponse: Response): Boolean? {
// Owner must be set in order to check if the home set is personal
val ownerHref = davResponse[Owner::class.java]?.href ?: return null
principal ?: return null

// Try to resolve the owner href
val ownerResolvedHref = principal.resolve(ownerHref)
if (ownerResolvedHref == null) return null

// If both fields are set, compare them
return ownerResolvedHref.equalsForWebDAV(principal)
}

}