diff --git a/app/build.gradle b/app/build.gradle index 41d291c8829d0bb6e023b0040e0fd1ecd04d660f..e082e17c190b28733bbd2a46a5966feb4bfa7753 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ plugins { id 'dagger.hilt.android.plugin' id 'kotlin-android' id 'kotlin-kapt' // remove as soon as Hilt supports KSP [https://issuetracker.google.com/179057202] + id 'kotlin-parcelize' } // Android configuration @@ -28,7 +29,7 @@ android { minSdkVersion 24 // Android 7.0 targetSdkVersion 33 // Android 13 - buildConfigField "String", "userAgent", "\"DAVx5\"" + buildConfigField "String", "userAgent", "\"AccountManager\"" testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner" } @@ -146,6 +147,7 @@ configurations { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' implementation "com.google.dagger:hilt-android:${versions.hilt}" @@ -217,7 +219,7 @@ dependencies { implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 implementation 'com.google.code.gson:gson:2.10.1' - implementation("com.github.nextcloud:android-library:2.14.0") { + implementation("foundation.e:Nextcloud-Android-Library:1.0.5-release") { exclude group: 'com.gitlab.bitfireAT', module: 'dav4jvm' exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version exclude group: 'com.squareup.okhttp3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e519e3144d45acf1145c263a19a3545a7a0a5f89..2039cf52d24c53aa27e9e88d1ee273223b1e927c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -684,13 +684,22 @@ + + + + + + https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) { return method; @@ -380,6 +383,10 @@ public class InputStreamBinder extends IInputStreamService.Stub { } } + private static String getUserAgent() { + return "AccountManager-SSO(" + BuildConfig.VERSION_NAME + ")"; + } + private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream) throws UnsupportedOperationException, com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException, @@ -400,9 +407,10 @@ public class InputStreamBinder extends IInputStreamService.Stub { new IllegalStateException("URL need to start with a /")); } - Uri serverUri = Uri.parse(AccountUtils.getBaseUrlForAccount(context, account)); - OwnCloudClient client = OwnCloudClientFactory.createOwnCloudClient(serverUri, context, true); - client.setCredentials(OwnCloudCredentialsFactory.newBasicCredentials(account.name, getAcountPwd(account, context))); + OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); + final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context); + final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context); HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream); @@ -428,6 +436,8 @@ public class InputStreamBinder extends IInputStreamService.Stub { client.setFollowRedirects(true); int status = client.executeMethod(method); + ownCloudClientManager.saveAllClients(context, account.type); + // Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) { return new Response(method); @@ -447,11 +457,6 @@ public class InputStreamBinder extends IInputStreamService.Stub { } } - private static String getAcountPwd(Account account, Context ctx) throws AccountUtils.AccountNotFoundException { - return AccountManager.get(ctx).getPassword(account); - } - - private boolean isValid(NextcloudRequest request) { String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid()); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java index 5b0607f5310efac8dc4d852ef1ce294ac3540866..d74e50312a5149bf3c3b1b455a7eb40e604d99b9 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java @@ -35,7 +35,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -49,6 +48,7 @@ import java.util.logging.Level; import at.bitfire.davdroid.R; import at.bitfire.davdroid.log.Logger; +import at.bitfire.davdroid.util.SsoUtils; public class SsoGrantPermissionActivity extends AppCompatActivity { @@ -115,7 +115,7 @@ public class SsoGrantPermissionActivity extends AppCompatActivity { // create token String token = UUID.randomUUID().toString().replaceAll("-", ""); - String userId = sanitizeUserId(account.name); + String userId = SsoUtils.INSTANCE.sanitizeUserId(account.name); saveToken(token, account.name); setResultData(token, userId, serverUrl); @@ -136,23 +136,6 @@ public class SsoGrantPermissionActivity extends AppCompatActivity { setResult(RESULT_OK, data); } - /** - * Murena account's userId is set same as it's email address. - * For old accounts (@e.email) userId = email. - * For new accounts (@murena.io) userId is first part of email (ex: for email abc@murena.io, userId is abc). - * For api requests, we needed to pass the actual userId. This method remove the unwanted part (@murena.io) from the userId - */ - @NonNull - private static String sanitizeUserId(@NonNull String userId) { - final String murenaMailEndPart = "@murena.io"; - - if (!userId.endsWith(murenaMailEndPart)) { - return userId; - } - - return userId.split(murenaMailEndPart)[0]; - } - @Nullable private String getServerUrl() { try { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..87960a2698df4eab15bd9c9e6dedff493b6e1b6e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt @@ -0,0 +1,22 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.network + +interface CookieParser { + + fun cookiesAsString(): String +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ae2934209660e1a90778295f573734b73f363ac --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.network + +import android.accounts.Account +import android.content.Context +import at.bitfire.davdroid.R +import okhttp3.CookieJar + +fun createCookieStore(context: Context, account: Account? = null): CookieJar { + if (account == null) { + return MemoryCookieStore() + } + + val murenaAccountType = context.getString(R.string.eelo_account_type) + val murenaAddressBookAccountType = context.getString(R.string.account_type_eelo_address_book) + + if (account.type in listOf(murenaAccountType, murenaAddressBookAccountType)) { + return PersistentCookieStore(context, account) + } + + return MemoryCookieStore() +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt index 5c38b98418d17c75cef6b584ac9a333f7064d00d..ac47042b323d3fd57bec0fc6fad9f7abdda00bee 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid.network +import android.accounts.Account import android.content.Context import android.os.Build import android.security.KeyChain @@ -17,6 +18,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.syncadapter.AccountUtils import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors @@ -172,6 +174,7 @@ class HttpClient private constructor( addAuthentication( null, credential, + account = accountSettings.account, authStateCallback = { authState: AuthState -> updateCredentials(accountSettings, authState, credential.clientSecret) }) @@ -190,7 +193,7 @@ class HttpClient private constructor( ) } - fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder { + fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, account: Account? = null, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder { if (credentials.userName != null && credentials.password != null) { val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName, credentials.password, insecurePreemptive) orig.addNetworkInterceptor(authHandler) @@ -208,6 +211,10 @@ class HttpClient private constructor( orig.addNetworkInterceptor(bearerAuthInterceptor) } } + + val accountForCookie = account ?: AccountUtils.getAccount(context, credentials.userName, host) + cookieStore = createCookieStore(context, accountForCookie) + return this } @@ -340,4 +347,12 @@ class HttpClient private constructor( } + fun getCookieAsString(): String { + val cookieJar = okHttpClient.cookieJar + if (cookieJar is CookieParser) { + return cookieJar.cookiesAsString() + } + + return "" + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt index 05e7ea63f0e859024e1c8403426324b70d2748df..3eacb5ce1016c68e56bc4b449596a750f91b284e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt @@ -4,19 +4,20 @@ package at.bitfire.davdroid.network +import at.bitfire.davdroid.settings.AccountSettings import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl import org.apache.commons.collections4.keyvalue.MultiKey import org.apache.commons.collections4.map.HashedMap import org.apache.commons.collections4.map.MultiKeyMap -import java.util.* +import java.util.LinkedList /** * Primitive cookie store that stores cookies in a (volatile) hash map. * Will be sufficient for session cookies. */ -class MemoryCookieStore: CookieJar { +class MemoryCookieStore : CookieJar, CookieParser { /** * Stored cookies. The multi-key consists of three parts: name, domain, and path. @@ -56,4 +57,12 @@ class MemoryCookieStore: CookieJar { return cookies } + override fun cookiesAsString(): String { + if (storage.isEmpty) { + return "" + } + + return storage.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR) + } + } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..37eada6e859cfd0549612da9163e979e5654147b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt @@ -0,0 +1,74 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.network + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import at.bitfire.davdroid.settings.AccountSettings +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +class PersistentCookieStore(context: Context, private val account: Account): CookieJar { + + private val accountManager = AccountManager.get(context.applicationContext) + + override fun loadForRequest(url: HttpUrl): List { + return getCookieMap(url).values.filter { + it.matches(url) + } + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val cookieList = cookies.filter { + it.expiresAt > System.currentTimeMillis() + } + + if (cookieList.isEmpty()) { + return + } + + val cookieMap = getCookieMap(url) + + // replace old cookie with new one + cookieList.forEach { + cookieMap[it.name] = it + } + + val cookieString = cookieMap.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR) + accountManager.setUserData(account, AccountSettings.COOKIE_KEY, cookieString) + } + + private fun getCookieMap(url: HttpUrl): HashMap { + val result = HashMap() + val cookiesString = accountManager.getUserData(account, AccountSettings.COOKIE_KEY)?: return HashMap() + + val cookies = cookiesString.split(AccountSettings.COOKIE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + + cookies.forEach { + val cookie = Cookie.parse(url, it) ?: return@forEach + + if (cookie.expiresAt > System.currentTimeMillis()) { + result[cookie.name] = cookie + } + } + + return result + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..12be4afe5b058fa39bd19b96607ac40e240470c7 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt @@ -0,0 +1,51 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.receiver + +import android.accounts.AccountManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import at.bitfire.davdroid.syncadapter.AccountUtils +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory + +class AccountRemovedReceiver : BroadcastReceiver() { + + companion object { + private const val ACCOUNT_REMOVAL_ACTION = "android.accounts.action.ACCOUNT_REMOVED" + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (context == null || intent == null || intent.action != ACCOUNT_REMOVAL_ACTION) { + return + } + + val accountName = getAccountName(context, intent) ?: return + + val ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton() + ownCloudClientManager.removeClientForByName(accountName) + } + + private fun getAccountName(context: Context, intent: Intent): String? { + val accountType = intent.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE) + if (accountType !in AccountUtils.getMainAccountTypes(context)) { + return null + } + + return intent.extras?.getString(AccountManager.KEY_ACCOUNT_NAME) + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt similarity index 97% rename from app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt rename to app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index dbbcd89ad82f71880dfbc42ba7bc4db8f709f759..d769750815e6293becf5334a2a931129bcf60fb6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.davdroid +package at.bitfire.davdroid.receiver import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt index 72f8cba8b434b179cf640e6c2f37c87a38696a6d..befcb345883a0a0d6b4d8da76bafdc726899badf 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -111,7 +111,8 @@ class DavResourceFinder( return Configuration( cardDavConfig, calDavConfig, encountered401, - logBuffer.toString() + logBuffer.toString(), + cookies = httpClient.getCookieAsString() ) } @@ -500,7 +501,8 @@ class DavResourceFinder( val calDAV: ServiceInfo?, val encountered401: Boolean, - val logs: String + val logs: String, + val cookies: String? = null ) { data class ServiceInfo( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt index 4bd79bbd2e78b571f29dd54f9587c25256b2974a..5cda84f5d989f1381d0ae45792135a2db2f3c6ab 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -19,6 +19,7 @@ import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils +import at.bitfire.davdroid.util.SsoUtils import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod @@ -29,6 +30,8 @@ import dagger.hilt.components.SingletonComponent import net.openid.appauth.AuthState import org.apache.commons.lang3.StringUtils import java.util.logging.Level +import com.owncloud.android.lib.common.accounts.AccountUtils as NCAccountUtils + /** * Manages settings of an account. @@ -115,23 +118,29 @@ class AccountSettings( const val CONTACTS_APP_INTERACTION = "z-app-generated--contactsinteraction--recent/" + const val COOKIE_KEY = "cookie_key" + const val COOKIE_SEPARATOR = "" + /** Static property to indicate whether AccountSettings migration is currently running. * **Access must be `synchronized` with `AccountSettings::class.java`.** */ @Volatile var currentlyUpdating = false - fun initialUserData(credentials: Credentials?, baseURL: String? = null): Bundle { + fun initialUserData(credentials: Credentials?, baseURL: String? = null, cookies: String? = null): Bundle { val bundle = Bundle() bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString()) if (credentials != null) { if (credentials.userName != null) { bundle.putString(KEY_USERNAME, credentials.userName) - bundle.putString("oc_display_name", credentials.userName) + bundle.putString(NCAccountUtils.Constants.KEY_DISPLAY_NAME, credentials.userName) if (credentials.userName.contains("@")) { bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName) } + + val userId = SsoUtils.sanitizeUserId(credentials.userName) + bundle.putString(NCAccountUtils.Constants.KEY_USER_ID, userId) } if (credentials.certificateAlias != null) { @@ -148,18 +157,14 @@ class AccountSettings( } if (!baseURL.isNullOrEmpty()) { - bundle.putString("oc_base_url", getOCBaseUrl(baseURL)) + bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, AccountUtils.getOwnCloudBaseUrl(baseURL)) } - return bundle - } - - private fun getOCBaseUrl(baseURL: String): String { - if (baseURL.contains("remote.php")) { - return baseURL.split("/remote.php")[0] + if (!cookies.isNullOrEmpty()) { + bundle.putString(COOKIE_KEY, cookies) } - return baseURL + return bundle } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index 5dcaed9c6b7900f4ab1e60eed2a0f6dfa4d90cb0..d94c06b0c688ac83e32ab619a36ce1dcd79d8a73 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -10,7 +10,9 @@ import android.content.Context import android.os.Bundle import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.setAndVerifyUserData +import com.owncloud.android.lib.common.accounts.AccountUtils object AccountUtils { @@ -128,4 +130,38 @@ object AccountUtils { return accounts } + + fun getOwnCloudBaseUrl(baseURL: String): String { + if (baseURL.contains("/remote.php")) { + return baseURL.split("/remote.php")[0] + } + + return baseURL + } + + fun getAccount(context: Context, userName: String?, requestedBaseUrl: String?): Account? { + if (userName == null || requestedBaseUrl == null) { + return null + } + + val baseUrl = getOwnCloudBaseUrl(requestedBaseUrl) + + val accountManager = AccountManager.get(context.applicationContext) + + val accounts = getMainAccounts(context) + + for(account in accounts) { + val name = accountManager.getUserData(account, AccountSettings.KEY_USERNAME) + if (name != userName) { + continue + } + + val url = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL) + if (url != null && getOwnCloudBaseUrl(url) == baseUrl) { + return account + } + } + + return null + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt index 59e827633aa4dfb4a57415879f7e4f67f9c398b3..cbf740f65056e8ba1dab3914a994f5c52bbc458c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt @@ -4,12 +4,13 @@ package at.bitfire.davdroid.syncadapter -import android.accounts.Account -import android.accounts.AccountManager import android.content.Context import androidx.hilt.work.HiltWorker -import androidx.work.* -import at.bitfire.davdroid.R +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook @@ -49,15 +50,14 @@ class AccountsCleanupWorker @AssistedInject constructor( override fun doWork(): Result { lockAccountsCleanup() try { - val accountManager = AccountManager.get(applicationContext) - cleanupAccounts(applicationContext, accountManager.accounts) + cleanupAccounts(applicationContext) } finally { unlockAccountsCleanup() } return Result.success() } - private fun cleanupAccounts(context: Context, accounts: Array) { + private fun cleanupAccounts(context: Context) { Logger.log.log(Level.INFO, "Cleaning up accounts. Current accounts") val mainAccountNames = HashSet() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 29543cd067b1a5cfe29f94c78a344c8a555adc09..ced0c5c1e75fe4c20a5b53ad4f14842473a35bd4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -49,6 +49,8 @@ import at.bitfire.davdroid.syncadapter.SyncAllAccountWorker import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.vcard4android.GroupMethod import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.utils.AccountManagerUtils +import com.owncloud.android.lib.common.accounts.AccountTypeUtils import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -297,7 +299,7 @@ class AccountDetailsFragment : Fragment() { val account = Account(name, accountType) // create Android account - val userData = AccountSettings.initialUserData(credentials, baseURL) + val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies) Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) val accountManager = AccountManager.get(context) @@ -315,10 +317,22 @@ class AccountDetailsFragment : Fragment() { accountManager.setAuthToken(account, Constants.AUTH_TOKEN_TYPE, credentials?.authState?.accessToken) } + var pass: String? = null + if (!credentials?.password.isNullOrEmpty()) { + pass = credentials?.password + } + + pass?.let { + if (accountType == AccountManagerUtils.getAccountType(context)) { + accountManager.setAuthToken(account, AccountTypeUtils.getAuthTokenTypePass(account.type), it) + return@let + } + accountManager.setPassword(account, credentials?.password) } + ContentResolver.setSyncAutomatically(account, context.getString(R.string.notes_authority), true) ContentResolver.setSyncAutomatically(account, context.getString(R.string.email_authority), true) ContentResolver.setSyncAutomatically(account, context.getString(R.string.media_authority), true) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..8edee609ef8c0ee019930caeb7b1a776e119bfc2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt @@ -0,0 +1,41 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.util + +object SsoUtils { + + private const val EELO_EMAIL_DOMAIN = "@e.email" + + /** + * Murena account's userId is set same as it's email address. + * For old accounts (@e.email) userId = email. + * For new accounts (@murena.io) & other NC accounts userId is first part of email (ex: for email abc@murena.io, userId is abc). + * For api requests, we needed to pass the actual userId. This method remove the unwanted part (ex: @murena.io) from the userId + */ + fun sanitizeUserId(param: String): String { + val userId = param.trim() + if (userId.endsWith(EELO_EMAIL_DOMAIN, ignoreCase = true)) { + return userId + } + + if (userId.lastIndexOf("@") < 0) { // not email address + return userId + } + + return userId.substringBefore("@") + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt index 006b739d18fddd0785e8b474a60bb85f823914d2..79ef169cfe3bfee8707a99c8b89ad060423bedf1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt @@ -21,7 +21,7 @@ import at.bitfire.davdroid.util.DavUtils import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.RequestBody -import okhttp3.internal.headersContentLength +import okhttp3.internal.toLongOrDefault import okio.BufferedSink import org.apache.commons.io.FileUtils import java.io.IOException @@ -107,7 +107,7 @@ class StreamingFileDescriptor( dav.get(mimeType?.toString() ?: DavUtils.MIME_TYPE_ACCEPT_ALL, null) { response -> response.body?.use { body -> if (response.isSuccessful) { - val length = response.headersContentLength() + val length = response.headers["Content-Length"]?.toLongOrDefault(-1L) ?: -1L notification.setContentTitle(context.getString(R.string.webdav_notification_download)) if (length == -1L) diff --git a/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a4f70cf513b27c54fb1c84f88cafca7c4c9fdea --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.util + +import org.junit.jupiter.api.Assertions.* + +import org.junit.jupiter.api.Test + +class SsoUtilsTest { + + @Test + fun `test sanitizeUserId with empty input`() { + val userId = "" + val expected = "" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } + + @Test + fun `test sanitizeUserId with input without '@'`() { + val userId = "username" + val expected = "username" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } + + @Test + fun `test sanitizeUserId with case sensitivity`() { + val userId = "User@E.EMAIL" + val expected = "User@E.EMAIL" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } + + @Test + fun `test sanitizeUserId with leading or trailing spaces`() { + val userId = " user@domain.com " + val expected = "user" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } +} diff --git a/build.gradle b/build.gradle index de2cdb3e97d09377710b39041ba2c066b3f61803..e7341354fa58cc95cfa19c0bd857a21fdc305db3 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { hilt: '2.48.1', kotlin: '1.9.10', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion // * com.google.devtools.ksp at the end of this file - okhttp: '4.12.0', + okhttp: '5.0.0-alpha.11', room: '2.5.2', workManager: '2.9.0-rc01', // Apache Commons versions