Add principals table

pull/1386/head
Alex Baker 3 years ago
parent db4a52303a
commit 983fa6644c

File diff suppressed because it is too large Load Diff

@ -10,12 +10,16 @@ import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingOwncloudTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) {
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
@ -58,6 +62,54 @@ class SharingOwncloudTest : CaldavTest() {
assertEquals(ACCESS_READ_ONLY, caldavDao.getCalendarByUuid(calendar.uuid!!)?.access)
}
@Test
fun principalForSharee() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar().apply {
account = this@SharingOwncloudTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingOwncloudTest.account.url}test-shared/"
caldavDao.insert(this)
}
enqueue(OC_OWNER)
synchronizer.sync(account)
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("principal:principals/users/user2", principal.principal)
assertEquals("user2", principal.displayName)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(CaldavCalendar.ACCESS_READ_ONLY, principal.access)
}
@Test
fun principalForOwner() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar().apply {
account = this@SharingOwncloudTest.account.uuid
ctag = "http://sabre.io/ns/sync/2"
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/"
caldavDao.insert(this)
}
enqueue(OC_READ_ONLY)
synchronizer.sync(account)
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("principals/users/user1", principal.principal)
assertEquals(null, principal.displayName)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(CaldavCalendar.ACCESS_OWNER, principal.access)
}
companion object {
private val OC_OWNER = """
<?xml version="1.0"?>

@ -5,17 +5,23 @@ import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingSabredavTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) {
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
@ -64,6 +70,54 @@ class SharingSabredavTest : CaldavTest() {
)
}
@Test
fun principalForSharee() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}940468858232147861/"
caldavDao.insert(this)
}
enqueue(SD_OWNER)
synchronizer.sync(account)
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("mailto:user@example.com", principal.principal)
assertEquals("Example User", principal.displayName)
assertEquals(INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_READ_WRITE, principal.access)
}
@Test
fun principalForOwner() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/"
caldavDao.insert(this)
}
enqueue(SD_SHAREE)
synchronizer.sync(account)
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("/principals/user1", principal.principal)
assertEquals(null, principal.displayName)
assertEquals(INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_OWNER, principal.access)
}
companion object {
private val SD_OWNER = """
<?xml version="1.0"?>

@ -26,8 +26,10 @@ import org.tasks.notifications.NotificationDao
CaldavCalendar::class,
CaldavTask::class,
CaldavAccount::class,
GoogleTaskAccount::class],
version = 77)
GoogleTaskAccount::class,
Principal::class,
],
version = 78)
abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao
abstract val tagDataDao: TagDataDao
@ -45,6 +47,7 @@ abstract class Database : RoomDatabase() {
abstract val deletionDao: DeletionDao
abstract val contentProviderDao: ContentProviderDao
abstract val upgraderDao: UpgraderDao
abstract val principalDao: PrincipalDao
/** @return human-readable database name for debugging
*/

@ -1,11 +1,8 @@
package org.tasks.caldav
import android.content.Context
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.*
import at.bitfire.dav4jvm.DavCalendar.Companion.MIME_ICALENDAR
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.PropertyRegistry
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
@ -30,23 +27,22 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.*
import org.tasks.caldav.property.PropertyUtils.register
import org.tasks.caldav.property.ShareAccess
import org.tasks.caldav.property.ShareAccess.Companion.READ
import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE
import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER
import org.tasks.data.CaldavAccount
import org.tasks.data.*
import org.tasks.data.CaldavAccount.Companion.ERROR_UNAUTHORIZED
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.CaldavCalendar.Companion.ACCESS_UNKNOWN
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.CaldavCalendar.Companion.INVITE_DECLINED
import org.tasks.data.CaldavCalendar.Companion.INVITE_INVALID
import org.tasks.data.CaldavCalendar.Companion.INVITE_NO_RESPONSE
import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN
import timber.log.Timber
import java.io.IOException
import java.net.ConnectException
@ -67,8 +63,9 @@ class CaldavSynchronizer @Inject constructor(
private val inventory: Inventory,
private val firebase: Firebase,
private val provider: CaldavClientProvider,
private val iCal: iCalendar) {
private val iCal: iCalendar,
private val principalDao: PrincipalDao,
) {
suspend fun sync(account: CaldavAccount) {
Thread.currentThread().contextClassLoader = context.classLoader
@ -161,6 +158,13 @@ class CaldavSynchronizer @Inject constructor(
caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList()
}
resource
.principals
.onEach { it.list = calendar.id }
.let {
principalDao.deleteRemoved(calendar.id, it.mapNotNull { p -> p.principal } )
principalDao.insert(it)
}
sync(calendar, resource, caldavClient.httpClient)
}
setError(account, "")
@ -319,6 +323,8 @@ class CaldavSynchronizer @Inject constructor(
prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN")
}
private val MAILTO = "^mailto:".toRegex()
fun registerFactories() {
PropertyRegistry.register(
ShareAccess.Factory(),
@ -341,16 +347,75 @@ class CaldavSynchronizer @Inject constructor(
else -> ACCESS_UNKNOWN
}
}
this[OCOwnerPrincipal::class.java]?.owner?.let {
val current = this[CurrentUserPrincipal::class.java]?.href
if (current?.endsWith("$it/") == true) {
return ACCESS_OWNER
}
if (isOwncloudOwner) {
return ACCESS_OWNER
}
return when (this[CurrentUserPrivilegeSet::class.java]?.mayWriteContent) {
false -> ACCESS_READ_ONLY
else -> ACCESS_READ_WRITE
}
}
val Response.isOwncloudOwner: Boolean
get() = this[OCOwnerPrincipal::class.java]?.owner
?.let {
this[CurrentUserPrincipal::class.java]?.href?.endsWith("$it/") == true
}
?: false
val Response.principals: List<Principal>
get() {
val principals = ArrayList<Principal>()
this[Invite::class.java]?.sharees
?.map {
Principal().apply {
principal = it.href
it.properties.find { it is DisplayName }?.let { name ->
displayName = (name as DisplayName).displayName
?: it.href.replace(MAILTO, "")
}
inviteStatus = it.response.toStatus
access = it.access.access.toAccess
}
}
?.let { principals.addAll(it) }
this[OCInvite::class.java]?.users
?.map {
Principal().apply {
principal = it.href
displayName = it.commonName
inviteStatus = it.response.toStatus
access = it.access.access.toAccess
}
}
?.let {
if (!isOwncloudOwner) {
principals.add(Principal().apply {
principal = this@principals[OCOwnerPrincipal::class.java]?.owner
inviteStatus = INVITE_ACCEPTED
access = ACCESS_OWNER
})
}
principals.addAll(it)
}
return principals
}
val Property.Name.toAccess: Int
get() = when(this) {
SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER
READ_WRITE, OCAccess.READ_WRITE -> ACCESS_READ_WRITE
READ, OCAccess.READ -> ACCESS_READ_ONLY
else -> ACCESS_UNKNOWN
}
val Property.Name.toStatus: Int
get() = when (this) {
Sharee.INVITE_ACCEPTED, OCUser.INVITE_ACCEPTED -> INVITE_ACCEPTED
Sharee.INVITE_NORESPONSE, OCUser.INVITE_NORESPONSE -> INVITE_NO_RESPONSE
Sharee.INVITE_DECLINED, OCUser.INVITE_DECLINED -> INVITE_DECLINED
Sharee.INVITE_INVALID, OCUser.INVITE_INVALID -> INVITE_INVALID
else -> INVITE_UNKNOWN
}
}
}

@ -16,9 +16,7 @@ class OCAccess(parser: XmlPullParser) : Property {
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1) {
when (val name = parser.propertyName()) {
SHARED_OWNER, READ_WRITE, NOT_SHARED, READ -> access = name
}
access = parser.propertyName()
}
eventType = parser.next()
}

@ -134,6 +134,12 @@ class CaldavCalendar : Parcelable {
const val ACCESS_READ_WRITE = 1
const val ACCESS_READ_ONLY = 2
const val INVITE_UNKNOWN = -1
const val INVITE_ACCEPTED = 0
const val INVITE_NO_RESPONSE = 1
const val INVITE_DECLINED = 2
const val INVITE_INVALID = 3
@JvmField val TABLE = Table("caldav_lists")
val ACCOUNT = TABLE.column("cdl_account")
@JvmField val UUID = TABLE.column("cdl_uuid")

@ -0,0 +1,65 @@
package org.tasks.data
import androidx.room.*
@Entity(
tableName = "principals",
foreignKeys = [ForeignKey(
entity = CaldavCalendar::class,
parentColumns = arrayOf("cdl_id"),
childColumns = arrayOf("principal_list"),
onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["principal_list", "principal"], unique = true)]
)
class Principal {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "principal_id")
@Transient
var id: Long = 0
@ColumnInfo(name = "principal_list")
var list: Long = 0
@ColumnInfo(name = "principal")
var principal: String? = null
@ColumnInfo(name = "display_name")
var displayName: String? = null
@ColumnInfo(name = "invite")
var inviteStatus: Int = CaldavCalendar.INVITE_UNKNOWN
@ColumnInfo(name = "access")
var access: Int = CaldavCalendar.ACCESS_UNKNOWN
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Principal
if (id != other.id) return false
if (list != other.list) return false
if (principal != other.principal) return false
if (displayName != other.displayName) return false
if (inviteStatus != other.inviteStatus) return false
if (access != other.access) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + list.hashCode()
result = 31 * result + (principal?.hashCode() ?: 0)
result = 31 * result + (displayName?.hashCode() ?: 0)
result = 31 * result + inviteStatus
result = 31 * result + access
return result
}
override fun toString(): String {
return "Principal(id=$id, list=$list, principal=$principal, displayName=$displayName, inviteStatus=$inviteStatus, access=$access)"
}
}

@ -0,0 +1,22 @@
package org.tasks.data
import androidx.room.*
@Dao
interface PrincipalDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(principal: List<Principal>)
@Query("""
DELETE
FROM principals
WHERE principal_list = :list
AND principal NOT IN (:principals)""")
fun deleteRemoved(list: Long, principals: List<String>)
@Delete
fun delete(principals: List<Principal>)
@Query("SELECT * FROM principals")
fun getAll(): List<Principal>
}

@ -364,6 +364,16 @@ object Migrations {
}
}
private val MIGRATION_77_78: Migration = object : Migration(77, 78) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `principals` (`principal_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `principal_list` INTEGER NOT NULL, `principal` TEXT, `display_name` TEXT, `invite` INTEGER NOT NULL, `access` INTEGER NOT NULL, FOREIGN KEY(`principal_list`) REFERENCES `caldav_lists`(`cdl_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
database.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_principal_list_principal` ON `principals` (`principal_list`, `principal`)"
)
}
}
val MIGRATIONS = arrayOf(
MIGRATION_35_36,
MIGRATION_36_37,
@ -398,6 +408,7 @@ object Migrations {
MIGRATION_74_75,
MIGRATION_75_76,
MIGRATION_76_77,
MIGRATION_77_78,
)
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

@ -91,6 +91,10 @@ class ApplicationModule {
@Singleton
fun getUpgraderDao(db: Database) = db.upgraderDao
@Provides
@Singleton
fun getPrincipalDao(db: Database) = db.principalDao
@Provides
fun getBillingClient(@ApplicationContext context: Context, inventory: Inventory, firebase: Firebase): BillingClient
= BillingClientImpl(context, inventory, firebase)

Loading…
Cancel
Save