Cross-validation: Ugly but necessary

This commit is contained in:
Philipp Heckel 2025-12-29 13:19:03 -05:00
parent f958998771
commit 92385e03cd
5 changed files with 71 additions and 33 deletions

View file

@ -14,14 +14,19 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.R
import io.heckel.ntfy.db.CustomHeader
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.AfterChangedTextWatcher
import io.heckel.ntfy.util.dangerButton
import io.heckel.ntfy.util.validUrl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CustomHeaderFragment : DialogFragment() {
private var header: CustomHeader? = null
private lateinit var baseUrlsInUse: ArrayList<String>
private lateinit var listener: CustomHeaderDialogListener
private lateinit var repository: Repository
private lateinit var baseUrlView: TextInputEditText
private lateinit var headerNameView: TextInputEditText
@ -38,6 +43,7 @@ class CustomHeaderFragment : DialogFragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
listener = activity as CustomHeaderDialogListener
repository = Repository.getInstance(context)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -50,9 +56,6 @@ class CustomHeaderFragment : DialogFragment() {
header = CustomHeader(baseUrl, headerName, headerValue)
}
// Required for validation
baseUrlsInUse = arguments?.getStringArrayList(BUNDLE_BASE_URLS_IN_USE) ?: arrayListOf()
// Build root view
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_custom_header_dialog, null)
@ -161,29 +164,44 @@ class CustomHeaderFragment : DialogFragment() {
// Clear previous errors
headerNameLayout.error = null
// Validate header name
var isValid = true
if (headerName.isNotEmpty()) {
if (!validateHeaderName(headerName)) {
headerNameLayout.error = getString(R.string.custom_headers_invalid_name)
isValid = false
} else if (isReservedHeader(headerName)) {
headerNameLayout.error = getString(R.string.custom_headers_reserved_name)
isValid = false
// Validate header name and check if a user already exists for this server
CoroutineScope(Dispatchers.Main).launch {
var isValid = true
if (headerName.isNotEmpty()) {
if (!validateHeaderName(headerName)) {
headerNameLayout.error = getString(R.string.custom_headers_invalid_name)
isValid = false
} else if (isReservedHeader(headerName)) {
headerNameLayout.error = getString(R.string.custom_headers_reserved_name)
isValid = false
} else if (headerName.equals("Authorization", ignoreCase = true)) {
// Check if a user exists for this server (async)
val targetBaseUrl = if (header != null) header!!.baseUrl else baseUrl
val userExists = if (this@CustomHeaderFragment::repository.isInitialized && validUrl(targetBaseUrl)) {
withContext(Dispatchers.IO) {
repository.getUser(targetBaseUrl) != null
}
} else {
false
}
if (userExists) {
headerNameLayout.error = getString(R.string.custom_headers_user_exists)
isValid = false
}
}
}
if (header == null) {
// New header: baseUrl, name, and value required
positiveButton.isEnabled = validUrl(baseUrl)
&& headerName.isNotEmpty()
&& headerValue.isNotEmpty()
&& isValid
} else {
// Editing header: name and value required
positiveButton.isEnabled = headerName.isNotEmpty()
&& headerValue.isNotEmpty()
&& isValid
}
}
if (header == null) {
// New header: baseUrl, name, and value required
positiveButton.isEnabled = validUrl(baseUrl)
&& headerName.isNotEmpty()
&& headerValue.isNotEmpty()
&& isValid
} else {
// Editing header: name and value required
positiveButton.isEnabled = headerName.isNotEmpty()
&& headerValue.isNotEmpty()
&& isValid
}
}
@ -215,12 +233,10 @@ class CustomHeaderFragment : DialogFragment() {
private const val BUNDLE_BASE_URL = "baseUrl"
private const val BUNDLE_HEADER_NAME = "headerName"
private const val BUNDLE_HEADER_VALUE = "headerValue"
private const val BUNDLE_BASE_URLS_IN_USE = "baseUrlsInUse"
fun newInstance(header: CustomHeader?, baseUrlsInUse: List<String>): CustomHeaderFragment {
fun newInstance(header: CustomHeader?): CustomHeaderFragment {
val fragment = CustomHeaderFragment()
val args = Bundle()
args.putStringArrayList(BUNDLE_BASE_URLS_IN_USE, ArrayList(baseUrlsInUse))
if (header != null) {
args.putString(BUNDLE_BASE_URL, header.baseUrl)
args.putString(BUNDLE_HEADER_NAME, header.name)

View file

@ -980,8 +980,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
private fun addCustomHeaderPreferences(headersByBaseUrl: List<CustomHeaderWithMetadata>) {
val baseUrlsInUse = headersByBaseUrl.map { it.baseUrl }
headersByBaseUrl.forEach { serverHeaders ->
val baseUrl = serverHeaders.baseUrl
val headers = serverHeaders.headers
@ -997,7 +995,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
preference.onPreferenceClickListener = OnPreferenceClickListener { _ ->
activity?.let {
CustomHeaderFragment
.newInstance(header, baseUrlsInUse)
.newInstance(header)
.show(it.supportFragmentManager, CustomHeaderFragment.TAG)
}
true
@ -1017,7 +1015,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
headerAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ ->
activity?.let {
CustomHeaderFragment
.newInstance(header = null, baseUrlsInUse = baseUrlsInUse)
.newInstance(header = null)
.show(it.supportFragmentManager, CustomHeaderFragment.TAG)
}
true

View file

@ -13,6 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.User
import io.heckel.ntfy.util.AfterChangedTextWatcher
import io.heckel.ntfy.util.dangerButton
@ -22,6 +23,7 @@ class UserFragment : DialogFragment() {
private var user: User? = null
private lateinit var baseUrlsInUse: ArrayList<String>
private lateinit var listener: UserDialogListener
private lateinit var repository: Repository
private lateinit var baseUrlViewLayout: TextInputLayout
private lateinit var baseUrlView: TextInputEditText
@ -38,6 +40,7 @@ class UserFragment : DialogFragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
listener = activity as UserDialogListener
repository = Repository.getInstance(context)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -155,9 +158,26 @@ class UserFragment : DialogFragment() {
val baseUrl = baseUrlView.text?.toString() ?: ""
val username = usernameView.text?.toString() ?: ""
val password = passwordView.text?.toString() ?: ""
// Clear previous errors
baseUrlViewLayout.error = null
if (user == null) {
// Check if Authorization header already exists in custom headers
val hasAuthorizationHeader = if (this::repository.isInitialized && validUrl(baseUrl)) {
repository.getCustomHeadersForServer(baseUrl)
.any { it.name.equals("Authorization", ignoreCase = true) }
} else {
false
}
if (hasAuthorizationHeader) {
baseUrlViewLayout.error = getString(R.string.user_dialog_base_url_error_authorization_header_exists)
}
positiveButton.isEnabled = validUrl(baseUrl)
&& !baseUrlsInUse.contains(baseUrl)
&& !hasAuthorizationHeader
&& username.isNotEmpty() && password.isNotEmpty()
} else {
positiveButton.isEnabled = username.isNotEmpty() // Unchanged if left blank

View file

@ -223,6 +223,7 @@
<string name="user_dialog_description_add">Du kannst hier einen Benutzer hinzufügen. Alle Themen auf dem angegebenen Server verwenden dann diesen Benutzer.</string>
<string name="user_dialog_description_edit">Du kannst Benutzernamen/Kennwort für den gewählten Benutzer ändern oder löschen.</string>
<string name="user_dialog_base_url_hint">Service-URL</string>
<string name="user_dialog_base_url_error_authorization_header_exists">Authorization-Header ist bereits in benutzerdefinierten Headern für diesen Server gesetzt</string>
<string name="user_dialog_username_hint">Benutzername</string>
<string name="user_dialog_password_hint_add">Kennwort</string>
<string name="user_dialog_password_hint_edit">Kennwort (leer lassen für keine Änderung)</string>
@ -364,6 +365,7 @@
<string name="custom_headers_error_title">Ungültiger Header</string>
<string name="custom_headers_invalid_name">Ungültige Zeichen im Header-Namen</string>
<string name="custom_headers_reserved_name">Dieser Header ist reserviert und wird von ntfy gesetzt</string>
<string name="custom_headers_user_exists">Authorization-Header kann nicht hinzugefügt werden: Ein Benutzer ist bereits für diesen Server konfiguriert</string>
<string name="custom_headers_name_hint">Name (z.B. CF-Access-Client-Id)</string>
<string name="custom_headers_value_hint">Wert</string>
<string name="custom_header_dialog_description_add">Einen benutzerdefinierten HTTP-Header hinzufügen, der mit jeder Anfrage an den angegebenen Server gesendet wird.</string>

View file

@ -458,6 +458,7 @@
<string name="user_dialog_description_add">You can add a user here. All topics for the given server will use this user.</string>
<string name="user_dialog_description_edit">You may edit username/password for the selected user, or delete it.</string>
<string name="user_dialog_base_url_hint">Service URL</string>
<string name="user_dialog_base_url_error_authorization_header_exists">Authorization header already set in custom headers for this server</string>
<string name="user_dialog_username_hint">Username</string>
<string name="user_dialog_password_hint_add">Password</string>
<string name="user_dialog_password_hint_edit">Password (unchanged if left blank)</string>
@ -479,6 +480,7 @@
<string name="custom_headers_error_title">Invalid Header</string>
<string name="custom_headers_invalid_name">Header name contains invalid characters</string>
<string name="custom_headers_reserved_name">This header is reserved and set by ntfy</string>
<string name="custom_headers_user_exists">Cannot add Authorization header: A user is already configured for this server</string>
<string name="custom_headers_name_hint">Name (e.g. CF-Access-Client-Id)</string>
<string name="custom_headers_value_hint">Value</string>
<string name="custom_header_dialog_description_add">Add a custom HTTP header that will be sent with every request to the specified server.</string>