Loading app/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java +1 −1 Original line number Diff line number Diff line Loading @@ -19,7 +19,7 @@ import timber.log.Timber; public class K9StoragePersister implements StoragePersister { private static final int DB_VERSION = 21; private static final int DB_VERSION = 22; private static final String DB_NAME = "preferences_storage"; private final Context context; Loading app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo22.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line package com.fsck.k9.preferences.migrations import android.database.sqlite.SQLiteDatabase import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types /** * Fix server settings by removing line breaks from username and password. */ class StorageMigrationTo22( private val db: SQLiteDatabase, private val migrationsHelper: StorageMigrationsHelper, ) { fun fixServerSettings() { val accountUuidsListValue = migrationsHelper.readValue(db, "accountUuids") if (accountUuidsListValue.isNullOrEmpty()) { return } val accountUuids = accountUuidsListValue.split(",") for (accountUuid in accountUuids) { fixServerSettingsForAccount(accountUuid) } } private fun fixServerSettingsForAccount(accountUuid: String) { val incomingServerSettingsJson = migrationsHelper.readValue(db, "$accountUuid.incomingServerSettings") ?: return val outgoingServerSettingsJson = migrationsHelper.readValue(db, "$accountUuid.outgoingServerSettings") ?: return val adapter = createJsonAdapter() adapter.fromJson(incomingServerSettingsJson)?.let { settings -> createFixedServerSettings(settings)?.let { newSettings -> val json = adapter.toJson(newSettings) migrationsHelper.writeValue(db, "$accountUuid.incomingServerSettings", json) } } adapter.fromJson(outgoingServerSettingsJson)?.let { settings -> createFixedServerSettings(settings)?.let { newSettings -> val json = adapter.toJson(newSettings) migrationsHelper.writeValue(db, "$accountUuid.outgoingServerSettings", json) } } } private fun createFixedServerSettings(serverSettings: Map<String, Any?>): Map<String, Any?>? { val username = serverSettings["username"] as? String val password = serverSettings["password"] as? String val newUsername = username?.stripLineBreaks() val newPassword = password?.stripLineBreaks() return if (username != newUsername || password != newPassword) { serverSettings.toMutableMap().apply { this["username"] = newUsername this["password"] = newPassword // This is so we don't end up with a port value of e.g. "993.0". It would still work, but it looks odd. this["port"] = (serverSettings["port"] as? Double)?.toInt() } } else { null } } private fun createJsonAdapter(): JsonAdapter<Map<String, Any?>> { val moshi = Moshi.Builder().build() return moshi.adapter<Map<String, Any?>>( Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java), ).serializeNulls() } } private val LINE_BREAK = "[\\r\\n]".toRegex() private fun String.stripLineBreaks() = replace(LINE_BREAK, replacement = "") app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrations.kt +1 −0 Original line number Diff line number Diff line Loading @@ -28,5 +28,6 @@ internal object StorageMigrations { if (oldVersion < 19) StorageMigrationTo19(db, migrationsHelper).markGmailAccounts() if (oldVersion < 20) StorageMigrationTo20(db, migrationsHelper).fixIdentities() if (oldVersion < 21) StorageMigrationTo21(db, migrationsHelper).createPostRemoveNavigationSetting() if (oldVersion < 22) StorageMigrationTo22(db, migrationsHelper).fixServerSettings() } } app/storage/src/test/java/com/fsck/k9/preferences/PreferencesDatabaseTestHelper.kt 0 → 100644 +22 −0 Original line number Diff line number Diff line package com.fsck.k9.preferences import android.database.sqlite.SQLiteDatabase private const val TABLE_NAME = "preferences_storage" private const val PRIMARY_KEY_COLUMN = "primkey" private const val VALUE_COLUMN = "value" fun createPreferencesDatabase(): SQLiteDatabase { val database = SQLiteDatabase.create(null) database.execSQL( """ CREATE TABLE $TABLE_NAME ( $PRIMARY_KEY_COLUMN TEXT PRIMARY KEY ON CONFLICT REPLACE, $VALUE_COLUMN TEXT ) """.trimIndent(), ) return database } app/storage/src/test/java/com/fsck/k9/preferences/migrations/StorageMigrationTo22Test.kt 0 → 100644 +165 −0 Original line number Diff line number Diff line package com.fsck.k9.preferences.migrations import assertk.all import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.key import com.fsck.k9.preferences.createPreferencesDatabase import com.squareup.moshi.Moshi import com.squareup.moshi.Types import java.util.UUID import kotlin.test.Test import org.junit.After import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class StorageMigrationTo22Test { private val database = createPreferencesDatabase() private val migrationsHelper = DefaultStorageMigrationsHelper() private val migration = StorageMigrationTo22(database, migrationsHelper) @After fun tearDown() { database.close() } @Test @Suppress("LongMethod") fun `fixServerSettings() should retain values while removing line breaks from username and password`() { val accountOne = createAccount( "incomingServerSettings" to toJson( "type" to "imap", "host" to "irrelevant.invalid", "port" to 993, "connectionSecurity" to "SSL_TLS_REQUIRED", "authenticationType" to "PLAIN", "username" to "user\n", "password" to "pass\nword", "clientCertificateAlias" to null, ), "outgoingServerSettings" to toJson( "type" to "smtp", "host" to "irrelevant.invalid", "port" to 465, "connectionSecurity" to "SSL_TLS_REQUIRED", "authenticationType" to "PLAIN", "username" to "", "password" to null, "clientCertificateAlias" to null, ), ) val accountTwo = createAccount( "incomingServerSettings" to toJson( "type" to "imap", "host" to "irrelevant.test", "port" to 143, "connectionSecurity" to "NONE", "authenticationType" to "XOAUTH2", "username" to "user@domain.example\r\n", "password" to null, "clientCertificateAlias" to null, ), "outgoingServerSettings" to toJson( "type" to "smtp", "host" to "irrelevant.test", "port" to 587, "connectionSecurity" to "STARTTLS_REQUIRED", "authenticationType" to "CRAM_MD5", "username" to "username", "password" to "password", "clientCertificateAlias" to "not-null", ), ) writeAccountUuids(accountOne, accountTwo) migration.fixServerSettings() assertThat(migrationsHelper.readAllValues(database)).all { key("$accountOne.incomingServerSettings").isEqualTo( """ { "type": "imap", "host": "irrelevant.invalid", "port": 993, "connectionSecurity": "SSL_TLS_REQUIRED", "authenticationType": "PLAIN", "username": "user", "password": "password", "clientCertificateAlias": null } """.toCompactJson(), ) key("$accountOne.outgoingServerSettings").isEqualTo( """ { "type": "smtp", "host": "irrelevant.invalid", "port": 465, "connectionSecurity": "SSL_TLS_REQUIRED", "authenticationType": "PLAIN", "username": "", "password": null, "clientCertificateAlias": null } """.toCompactJson(), ) key("$accountTwo.incomingServerSettings").isEqualTo( """ { "type": "imap", "host": "irrelevant.test", "port": 143, "connectionSecurity": "NONE", "authenticationType": "XOAUTH2", "username": "user@domain.example", "password": null, "clientCertificateAlias": null } """.toCompactJson(), ) key("$accountTwo.outgoingServerSettings").isEqualTo( """ { "type": "smtp", "host": "irrelevant.test", "port": 587, "connectionSecurity": "STARTTLS_REQUIRED", "authenticationType": "CRAM_MD5", "username": "username", "password": "password", "clientCertificateAlias": "not-null" } """.toCompactJson(), ) } } private fun writeAccountUuids(vararg accounts: String) { val accountUuids = accounts.joinToString(separator = ",") migrationsHelper.insertValue(database, "accountUuids", accountUuids) } private fun createAccount(vararg pairs: Pair<String, String>): String { val accountUuid = UUID.randomUUID().toString() for ((key, value) in pairs) { migrationsHelper.insertValue(database, "$accountUuid.$key", value) } return accountUuid } private fun toJson(vararg pairs: Pair<String, Any?>): String { val moshi = Moshi.Builder().build() val adapter = moshi.adapter<Map<String, Any?>>( Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java), ).serializeNulls() return adapter.toJson(pairs.toMap()) ?: error("Failed to create JSON") } // Note: This only works for JSON strings where keys and values don't contain any spaces private fun String.toCompactJson(): String = replace(" ", "").replace("\n", "") } Loading
app/storage/src/main/java/com/fsck/k9/preferences/K9StoragePersister.java +1 −1 Original line number Diff line number Diff line Loading @@ -19,7 +19,7 @@ import timber.log.Timber; public class K9StoragePersister implements StoragePersister { private static final int DB_VERSION = 21; private static final int DB_VERSION = 22; private static final String DB_NAME = "preferences_storage"; private final Context context; Loading
app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrationTo22.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line package com.fsck.k9.preferences.migrations import android.database.sqlite.SQLiteDatabase import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types /** * Fix server settings by removing line breaks from username and password. */ class StorageMigrationTo22( private val db: SQLiteDatabase, private val migrationsHelper: StorageMigrationsHelper, ) { fun fixServerSettings() { val accountUuidsListValue = migrationsHelper.readValue(db, "accountUuids") if (accountUuidsListValue.isNullOrEmpty()) { return } val accountUuids = accountUuidsListValue.split(",") for (accountUuid in accountUuids) { fixServerSettingsForAccount(accountUuid) } } private fun fixServerSettingsForAccount(accountUuid: String) { val incomingServerSettingsJson = migrationsHelper.readValue(db, "$accountUuid.incomingServerSettings") ?: return val outgoingServerSettingsJson = migrationsHelper.readValue(db, "$accountUuid.outgoingServerSettings") ?: return val adapter = createJsonAdapter() adapter.fromJson(incomingServerSettingsJson)?.let { settings -> createFixedServerSettings(settings)?.let { newSettings -> val json = adapter.toJson(newSettings) migrationsHelper.writeValue(db, "$accountUuid.incomingServerSettings", json) } } adapter.fromJson(outgoingServerSettingsJson)?.let { settings -> createFixedServerSettings(settings)?.let { newSettings -> val json = adapter.toJson(newSettings) migrationsHelper.writeValue(db, "$accountUuid.outgoingServerSettings", json) } } } private fun createFixedServerSettings(serverSettings: Map<String, Any?>): Map<String, Any?>? { val username = serverSettings["username"] as? String val password = serverSettings["password"] as? String val newUsername = username?.stripLineBreaks() val newPassword = password?.stripLineBreaks() return if (username != newUsername || password != newPassword) { serverSettings.toMutableMap().apply { this["username"] = newUsername this["password"] = newPassword // This is so we don't end up with a port value of e.g. "993.0". It would still work, but it looks odd. this["port"] = (serverSettings["port"] as? Double)?.toInt() } } else { null } } private fun createJsonAdapter(): JsonAdapter<Map<String, Any?>> { val moshi = Moshi.Builder().build() return moshi.adapter<Map<String, Any?>>( Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java), ).serializeNulls() } } private val LINE_BREAK = "[\\r\\n]".toRegex() private fun String.stripLineBreaks() = replace(LINE_BREAK, replacement = "")
app/storage/src/main/java/com/fsck/k9/preferences/migrations/StorageMigrations.kt +1 −0 Original line number Diff line number Diff line Loading @@ -28,5 +28,6 @@ internal object StorageMigrations { if (oldVersion < 19) StorageMigrationTo19(db, migrationsHelper).markGmailAccounts() if (oldVersion < 20) StorageMigrationTo20(db, migrationsHelper).fixIdentities() if (oldVersion < 21) StorageMigrationTo21(db, migrationsHelper).createPostRemoveNavigationSetting() if (oldVersion < 22) StorageMigrationTo22(db, migrationsHelper).fixServerSettings() } }
app/storage/src/test/java/com/fsck/k9/preferences/PreferencesDatabaseTestHelper.kt 0 → 100644 +22 −0 Original line number Diff line number Diff line package com.fsck.k9.preferences import android.database.sqlite.SQLiteDatabase private const val TABLE_NAME = "preferences_storage" private const val PRIMARY_KEY_COLUMN = "primkey" private const val VALUE_COLUMN = "value" fun createPreferencesDatabase(): SQLiteDatabase { val database = SQLiteDatabase.create(null) database.execSQL( """ CREATE TABLE $TABLE_NAME ( $PRIMARY_KEY_COLUMN TEXT PRIMARY KEY ON CONFLICT REPLACE, $VALUE_COLUMN TEXT ) """.trimIndent(), ) return database }
app/storage/src/test/java/com/fsck/k9/preferences/migrations/StorageMigrationTo22Test.kt 0 → 100644 +165 −0 Original line number Diff line number Diff line package com.fsck.k9.preferences.migrations import assertk.all import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.key import com.fsck.k9.preferences.createPreferencesDatabase import com.squareup.moshi.Moshi import com.squareup.moshi.Types import java.util.UUID import kotlin.test.Test import org.junit.After import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class StorageMigrationTo22Test { private val database = createPreferencesDatabase() private val migrationsHelper = DefaultStorageMigrationsHelper() private val migration = StorageMigrationTo22(database, migrationsHelper) @After fun tearDown() { database.close() } @Test @Suppress("LongMethod") fun `fixServerSettings() should retain values while removing line breaks from username and password`() { val accountOne = createAccount( "incomingServerSettings" to toJson( "type" to "imap", "host" to "irrelevant.invalid", "port" to 993, "connectionSecurity" to "SSL_TLS_REQUIRED", "authenticationType" to "PLAIN", "username" to "user\n", "password" to "pass\nword", "clientCertificateAlias" to null, ), "outgoingServerSettings" to toJson( "type" to "smtp", "host" to "irrelevant.invalid", "port" to 465, "connectionSecurity" to "SSL_TLS_REQUIRED", "authenticationType" to "PLAIN", "username" to "", "password" to null, "clientCertificateAlias" to null, ), ) val accountTwo = createAccount( "incomingServerSettings" to toJson( "type" to "imap", "host" to "irrelevant.test", "port" to 143, "connectionSecurity" to "NONE", "authenticationType" to "XOAUTH2", "username" to "user@domain.example\r\n", "password" to null, "clientCertificateAlias" to null, ), "outgoingServerSettings" to toJson( "type" to "smtp", "host" to "irrelevant.test", "port" to 587, "connectionSecurity" to "STARTTLS_REQUIRED", "authenticationType" to "CRAM_MD5", "username" to "username", "password" to "password", "clientCertificateAlias" to "not-null", ), ) writeAccountUuids(accountOne, accountTwo) migration.fixServerSettings() assertThat(migrationsHelper.readAllValues(database)).all { key("$accountOne.incomingServerSettings").isEqualTo( """ { "type": "imap", "host": "irrelevant.invalid", "port": 993, "connectionSecurity": "SSL_TLS_REQUIRED", "authenticationType": "PLAIN", "username": "user", "password": "password", "clientCertificateAlias": null } """.toCompactJson(), ) key("$accountOne.outgoingServerSettings").isEqualTo( """ { "type": "smtp", "host": "irrelevant.invalid", "port": 465, "connectionSecurity": "SSL_TLS_REQUIRED", "authenticationType": "PLAIN", "username": "", "password": null, "clientCertificateAlias": null } """.toCompactJson(), ) key("$accountTwo.incomingServerSettings").isEqualTo( """ { "type": "imap", "host": "irrelevant.test", "port": 143, "connectionSecurity": "NONE", "authenticationType": "XOAUTH2", "username": "user@domain.example", "password": null, "clientCertificateAlias": null } """.toCompactJson(), ) key("$accountTwo.outgoingServerSettings").isEqualTo( """ { "type": "smtp", "host": "irrelevant.test", "port": 587, "connectionSecurity": "STARTTLS_REQUIRED", "authenticationType": "CRAM_MD5", "username": "username", "password": "password", "clientCertificateAlias": "not-null" } """.toCompactJson(), ) } } private fun writeAccountUuids(vararg accounts: String) { val accountUuids = accounts.joinToString(separator = ",") migrationsHelper.insertValue(database, "accountUuids", accountUuids) } private fun createAccount(vararg pairs: Pair<String, String>): String { val accountUuid = UUID.randomUUID().toString() for ((key, value) in pairs) { migrationsHelper.insertValue(database, "$accountUuid.$key", value) } return accountUuid } private fun toJson(vararg pairs: Pair<String, Any?>): String { val moshi = Moshi.Builder().build() val adapter = moshi.adapter<Map<String, Any?>>( Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java), ).serializeNulls() return adapter.toJson(pairs.toMap()) ?: error("Failed to create JSON") } // Note: This only works for JSON strings where keys and values don't contain any spaces private fun String.toCompactJson(): String = replace(" ", "").replace("\n", "") }