Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a043619c authored by cketti's avatar cketti
Browse files

Strip line breaks from persisted server settings

parent e699090a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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;
+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 = "")
+1 −0
Original line number Diff line number Diff line
@@ -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()
    }
}
+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
}
+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", "")
}