Add server type column to caldav accounts

pull/1400/head
Alex Baker 3 years ago
parent 99e624e836
commit e8ad88be21

File diff suppressed because it is too large Load Diff

@ -34,6 +34,7 @@ abstract class CaldavTest : InjectingTestCase() {
preferences.setBoolean(R.string.p_debug_pro, true)
server.start()
headers.clear()
}
@After
@ -45,14 +46,16 @@ abstract class CaldavTest : InjectingTestCase() {
assertFalse(caldavDao.getAccountByUuid(account.uuid!!)!!.hasError)
}
val headers = HashMap<String, String>()
protected fun enqueue(vararg responses: String) {
responses.forEach {
server.enqueue(
MockResponse()
.setResponseCode(207)
.setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
.setBody(it)
)
.apply { this@CaldavTest.headers.forEach { (k, v) -> setHeader(k, v) } }
.setBody(it))
}
server.enqueue(MockResponse().setResponseCode(500))
}

@ -0,0 +1,99 @@
package org.tasks.caldav
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class ServerDetectionTest : CaldavTest() {
@Test
fun detectTasksServer() = runBlocking {
setup(
"DAV" to SABREDAV_COMPLIANCE,
"x-sabre-version" to "4.1.3",
accountType = TYPE_TASKS
)
sync()
assertEquals(SERVER_TASKS, loadAccount().serverType)
}
@Test
fun detectNextcloudServer() = runBlocking {
setup("DAV" to NEXTCLOUD_COMPLIANCE)
sync()
assertEquals(SERVER_OWNCLOUD, loadAccount().serverType)
}
@Test
fun detectSabredavServer() = runBlocking {
setup(
"DAV" to SABREDAV_COMPLIANCE,
"x-sabre-version" to "4.1.3"
)
sync()
assertEquals(SERVER_SABREDAV, loadAccount().serverType)
}
@Test
fun detectOpenXchangeServer() = runBlocking {
setup("server" to "Openexchange WebDAV")
sync()
assertEquals(SERVER_OPEN_XCHANGE, loadAccount().serverType)
}
@Test
fun unknownServer() = runBlocking {
setup()
sync()
assertEquals(SERVER_UNKNOWN, loadAccount().serverType)
}
private suspend fun loadAccount(): CaldavAccount =
caldavDao.getAccounts().apply { assertEquals(1, size) }.first()
private suspend fun setup(
vararg headers: Pair<String, String>,
accountType: Int = TYPE_CALDAV
) {
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = "username"
password = encryption.encrypt("password")
url = server.url("/remote.php/dav/calendars/user1/").toString()
id = caldavDao.insert(this)
this.accountType = accountType
}
this.headers.putAll(headers)
enqueue(NO_CALENDARS)
}
companion object {
private const val NO_CALENDARS = """<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"/>"""
private const val SABREDAV_COMPLIANCE = "1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendarserver-subscribed, calendar-auto-schedule, calendar-availability, resource-sharing, calendarserver-sharing"
private const val NEXTCLOUD_COMPLIANCE = "1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-auto-schedule, calendar-availability, nc-calendar-webcal-cache, calendarserver-subscribed, oc-resource-sharing, oc-calendar-publishing, calendarserver-sharing, nc-calendar-search, nc-enable-birthday-calendar"
}
}

@ -29,7 +29,7 @@ import org.tasks.notifications.NotificationDao
GoogleTaskAccount::class,
Principal::class,
],
version = 78)
version = 79)
abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao
abstract val tagDataDao: TagDataDao

@ -16,6 +16,7 @@ import com.todoroo.astrid.helper.UUIDHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
@ -92,15 +93,18 @@ open class CaldavClient(
.findHomeset()
}
suspend fun calendars(): List<Response> =
DavResource(httpClient, httpUrl!!)
.propfind(1, *calendarProperties)
.filter { (response, relation) ->
relation == HrefRelation.MEMBER &&
response[ResourceType::class.java]?.types?.contains(CALENDAR) == true &&
response[SupportedCalendarComponentSet::class.java]?.supportsTasks == true
}
.map { (response, _) -> response }
suspend fun calendars(interceptor: Interceptor): List<Response> =
DavResource(
httpClient.newBuilder().addNetworkInterceptor(interceptor).build(),
httpUrl!!
)
.propfind(1, *calendarProperties)
.filter { (response, relation) ->
relation == HrefRelation.MEMBER &&
response[ResourceType::class.java]?.types?.contains(CALENDAR) == true &&
response[SupportedCalendarComponentSet::class.java]?.supportsTasks == true
}
.map { (response, _) -> response }
@Throws(IOException::class, HttpException::class)
suspend fun deleteCollection() = withContext(Dispatchers.IO) {

@ -1,8 +1,12 @@
package org.tasks.caldav
import android.content.Context
import at.bitfire.dav4jvm.*
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.DavCalendar.Companion.MIME_ICALENDAR
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Property
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
@ -17,7 +21,9 @@ import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext
import net.fortuna.ical4j.model.property.ProdId
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import org.tasks.BuildConfig
@ -34,6 +40,11 @@ import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE
import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER
import org.tasks.data.*
import org.tasks.data.CaldavAccount.Companion.ERROR_UNAUTHORIZED
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
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
@ -116,10 +127,20 @@ class CaldavSynchronizer @Inject constructor(
}
}
@Throws(IOException::class, DavException::class, KeyManagementException::class, NoSuchAlgorithmException::class)
private suspend fun synchronize(account: CaldavAccount) {
val caldavClient = provider.forAccount(account)
val resources = caldavClient.calendars()
var serverType = SERVER_UNKNOWN
val resources = caldavClient.calendars(object : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val response = chain.proceed(chain.request())
serverType = getServerType(account, response.headers)
return response
}
})
if (serverType != account.serverType) {
account.serverType = serverType
caldavDao.update(account)
}
val urls = resources.map { it.href.toString() }.toHashSet()
Timber.d("Found calendars: %s", urls)
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, ArrayList(urls))) {
@ -170,6 +191,14 @@ class CaldavSynchronizer @Inject constructor(
setError(account, "")
}
private fun getServerType(account: CaldavAccount, headers: Headers) = when {
account.isTasksOrg -> SERVER_TASKS
headers["DAV"]?.contains("oc-resource-sharing") == true -> SERVER_OWNCLOUD
headers["x-sabre-version"]?.isNotBlank() == true -> SERVER_SABREDAV
headers["server"] == "Openexchange WebDAV" -> SERVER_OPEN_XCHANGE
else -> SERVER_UNKNOWN
}
private suspend fun setError(account: CaldavAccount, message: String?) {
account.error = message
caldavDao.update(account)
@ -179,7 +208,6 @@ class CaldavSynchronizer @Inject constructor(
}
}
@Throws(DavException::class)
private suspend fun sync(
caldavCalendar: CaldavCalendar,
resource: Response,
@ -287,7 +315,6 @@ class CaldavSynchronizer @Inject constructor(
return true
}
@Throws(IOException::class)
private suspend fun pushTask(task: Task, httpClient: OkHttpClient, httpUrl: HttpUrl) {
Timber.d("pushing %s", task)
val caldavTask = caldavDao.getTask(task.id) ?: return
@ -354,10 +381,10 @@ class CaldavSynchronizer @Inject constructor(
}
}
val Response.isOwncloudOwner: Boolean
private val Response.isOwncloudOwner: Boolean
get() = this[OCOwnerPrincipal::class.java]?.owner?.let { isCurrentUser(it) } ?: false
fun Response.isCurrentUser(href: String) =
private fun Response.isCurrentUser(href: String) =
this[CurrentUserPrincipal::class.java]?.href?.endsWith("$href/") == true
val Response.principals: List<Principal>
@ -399,15 +426,15 @@ class CaldavSynchronizer @Inject constructor(
return principals
}
val Property.Name.toAccess: Int
get() = when(this) {
private 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
private 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

@ -64,11 +64,14 @@ class CaldavAccount : Parcelable {
var encryptionKey: String? = null
@ColumnInfo(name = "cda_account_type")
var accountType = 0
var accountType = TYPE_CALDAV
@ColumnInfo(name = "cda_collapsed")
var isCollapsed = false
@ColumnInfo(name = "cda_server_type")
var serverType = SERVER_UNKNOWN
constructor()
@Ignore
@ -84,6 +87,7 @@ class CaldavAccount : Parcelable {
accountType = source.readInt()
encryptionKey = source.readString()
isCollapsed = ParcelCompat.readBoolean(source)
serverType = source.readInt()
}
fun getPassword(encryption: KeyStoreEncryption): String {
@ -143,12 +147,15 @@ class CaldavAccount : Parcelable {
writeInt(accountType)
writeString(encryptionKey)
ParcelCompat.writeBoolean(this, isCollapsed)
writeInt(serverType)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CaldavAccount) return false
if (javaClass != other?.javaClass) return false
other as CaldavAccount
if (id != other.id) return false
if (uuid != other.uuid) return false
@ -161,6 +168,7 @@ class CaldavAccount : Parcelable {
if (encryptionKey != other.encryptionKey) return false
if (accountType != other.accountType) return false
if (isCollapsed != other.isCollapsed) return false
if (serverType != other.serverType) return false
return true
}
@ -177,11 +185,12 @@ class CaldavAccount : Parcelable {
result = 31 * result + (encryptionKey?.hashCode() ?: 0)
result = 31 * result + accountType
result = 31 * result + isCollapsed.hashCode()
result = 31 * result + serverType
return result
}
override fun toString(): String {
return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, password=$password, error=$error, isSuppressRepeatingTasks=$isSuppressRepeatingTasks, encryptionKey=$encryptionKey, accountType=$accountType, isCollapsed=$isCollapsed)"
return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, password=$password, error=$error, isSuppressRepeatingTasks=$isSuppressRepeatingTasks, encryptionKey=$encryptionKey, accountType=$accountType, isCollapsed=$isCollapsed, serverType=$serverType)"
}
fun isTasksSubscription(context: Context): Boolean {
@ -230,6 +239,12 @@ class CaldavAccount : Parcelable {
const val TYPE_TASKS = 4
const val TYPE_ETEBASE = 5
const val SERVER_UNKNOWN = -1
const val SERVER_TASKS = 0
const val SERVER_OWNCLOUD = 1
const val SERVER_SABREDAV = 2
const val SERVER_OPEN_XCHANGE = 3
const val ERROR_UNAUTHORIZED = "HTTP ${HttpURLConnection.HTTP_UNAUTHORIZED}"
const val ERROR_PAYMENT_REQUIRED = "HTTP ${HttpURLConnection.HTTP_PAYMENT_REQUIRED}"

@ -4,6 +4,7 @@ import android.database.sqlite.SQLiteException
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.todoroo.astrid.api.FilterListItem.NO_ORDER
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import timber.log.Timber
object Migrations {
@ -374,6 +375,14 @@ object Migrations {
}
}
private val MIGRATION_78_79: Migration = object : Migration(78, 79) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE `caldav_accounts` ADD COLUMN `cda_server_type` INTEGER NOT NULL DEFAULT $SERVER_UNKNOWN"
)
}
}
val MIGRATIONS = arrayOf(
MIGRATION_35_36,
MIGRATION_36_37,
@ -409,6 +418,7 @@ object Migrations {
MIGRATION_75_76,
MIGRATION_76_77,
MIGRATION_77_78,
MIGRATION_78_79,
)
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

Loading…
Cancel
Save