commit d17bbae470335241b63bdf1a7392ae9f9322aa23
parent e81bce5744ed9b3155e17a58a1d665015bc70b52
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Tue, 29 Jul 2025 18:56:42 +0200
Prevent leaking urls to nominatim and allow custom nominatim servers
Diffstat:
9 files changed, 150 insertions(+), 66 deletions(-)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/MainActivity.kt
@@ -58,6 +58,7 @@ import space.midnightthoughts.nordiccalendar.onboarding.OnBoardItem
import space.midnightthoughts.nordiccalendar.onboarding.onBoardingData
import space.midnightthoughts.nordiccalendar.screens.CalendarScreen
import space.midnightthoughts.nordiccalendar.screens.EventDetailsView
+import space.midnightthoughts.nordiccalendar.screens.SettingsView
import space.midnightthoughts.nordiccalendar.ui.theme.NordicCalendarTheme
import space.midnightthoughts.nordiccalendar.util.OnboardingPrefs
import space.midnightthoughts.nordiccalendar.viewmodels.CalendarViewModel
@@ -267,28 +268,6 @@ fun CalendarView(
}
}
-
-@Composable
-fun SettingsView(navController: NavHostController) {
- AppScaffold(
- title = stringResource(R.string.settings),
- selectedDestination = "settings",
- navController = navController,
-
- ) { innerPadding ->
- Column(
- modifier = innerPadding
- .fillMaxSize()
- ) {
- Text(
- stringResource(R.string.settings),
- modifier = Modifier.padding(16.dp)
- )
- // Weitere Einstellungen hier
- }
- }
-}
-
@Composable
fun AboutView(navController: NavHostController) {
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/EventDetailsView.kt
@@ -1,5 +1,6 @@
package space.midnightthoughts.nordiccalendar.screens
+import android.util.Patterns
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -100,10 +101,14 @@ fun EventDetailsView(
}.collectAsState()
val locationText = event.value?.location?.trim()
+ // Prevent leaking the url to third party services
+ val isValidUrlInLocation = remember(locationText) {
+ Patterns.WEB_URL.matcher(locationText ?: "").matches()
+ }
val locationPosition by viewModel.locationPosition.collectAsState()
val appLocaleString = appLocale.language
LaunchedEffect(locationText, appLocaleString) {
- if (!locationText.isNullOrEmpty()) {
+ if (!locationText.isNullOrEmpty() && !isValidUrlInLocation) {
viewModel.resolveLocation(locationText, appLocaleString)
}
}
@@ -236,45 +241,18 @@ fun EventDetailsView(
color = MaterialTheme.colorScheme.outlineVariant
)
Spacer(modifier = Modifier.height(16.dp))
- if (!locationText.isNullOrEmpty()) {
- if (locationPosition != null) {
- Column {
- Text(
- text = stringResource(R.string.location),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold
- )
- Spacer(modifier = Modifier.height(8.dp))
- LocationMap(
- coordinate = locationPosition!!,
- boundingBox = boundingBox.value
- )
- }
- } else {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.Top,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- text = stringResource(R.string.location),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold
- )
- Spacer(modifier = Modifier.width(24.dp))
- Text(
- text = AnnotatedString.rememberAutoLinkText(
- if (event.value?.location?.trim().isNullOrEmpty()) {
- stringResource(R.string.no_location_available)
- } else {
- event.value?.location ?: ""
- }
- ),
-
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurface,
- )
- }
+ if (locationPosition != null && !isValidUrlInLocation) {
+ Column {
+ Text(
+ text = stringResource(R.string.location),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ LocationMap(
+ coordinate = locationPosition!!,
+ boundingBox = boundingBox.value
+ )
}
} else {
Row(
@@ -289,11 +267,19 @@ fun EventDetailsView(
)
Spacer(modifier = Modifier.width(24.dp))
Text(
- text = stringResource(R.string.no_location_available),
+ text = AnnotatedString.rememberAutoLinkText(
+ if (event.value?.location?.trim().isNullOrEmpty()) {
+ stringResource(R.string.no_location_available)
+ } else {
+ event.value?.location ?: ""
+ }
+ ),
+
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
+
}
// TODO: Invitees and attendees and alerts
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/SettingsView.kt
@@ -0,0 +1,81 @@
+package space.midnightthoughts.nordiccalendar.screens
+
+import android.util.Patterns
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import space.midnightthoughts.nordiccalendar.R
+import space.midnightthoughts.nordiccalendar.components.AppScaffold
+import space.midnightthoughts.nordiccalendar.viewmodels.SettingsViewModel
+
+@Composable
+fun SettingsView(navController: NavHostController) {
+ val viewModel: SettingsViewModel = hiltViewModel()
+ val nominatimUrl = remember(viewModel) {
+ viewModel.nominatimUrl
+ }.collectAsState()
+ val textFieldValue: MutableState<String> = remember { mutableStateOf(nominatimUrl.value) }
+
+ val isValidUrl = remember(textFieldValue) {
+ Patterns.WEB_URL.matcher(textFieldValue.value).matches()
+ }
+ AppScaffold(
+ title = stringResource(R.string.settings),
+ selectedDestination = "settings",
+ navController = navController,
+
+ ) { innerPadding ->
+ Column(
+ modifier = innerPadding
+ .padding(16.dp)
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ OutlinedTextField(
+ value = textFieldValue.value,
+ onValueChange = { value: String ->
+ textFieldValue.value = value
+ },
+ label = { Text(stringResource(R.string.nominatim_url_label)) },
+ singleLine = true,
+ supportingText = { Text(stringResource(R.string.nominatim_url_hint)) },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ keyboardType = KeyboardType.Uri,
+ imeAction = ImeAction.Done
+ ),
+ isError = !isValidUrl && textFieldValue.value.isNotBlank(),
+ modifier = Modifier.fillMaxWidth()
+ )
+ Button(
+ onClick = { viewModel.setNominatimUrl(textFieldValue.value) },
+ enabled = textFieldValue.value.isNotBlank() && textFieldValue != nominatimUrl && isValidUrl,
+ modifier = Modifier.align(Alignment.End)
+ ) {
+ Text(stringResource(R.string.save))
+ }
+ }
+ }
+}
+\ No newline at end of file
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/EventDetailsViewModel.kt
@@ -97,7 +97,9 @@ class EventDetailsViewModel @Inject constructor(
fun resolveLocation(address: String, locale: String = "de") {
viewModelScope.launch {
try {
- val url = "https://nominatim.openstreetmap.org/search"
+ val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ val url =
+ prefs.getString("nominatim_url", "https://nominatim.openstreetmap.org/search")!!
val response: List<NominatimResult> = ktorClient.get(url) {
url {
parameters.append("q", address)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SettingsViewModel.kt
@@ -0,0 +1,29 @@
+package space.midnightthoughts.nordiccalendar.viewmodels
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+
+@HiltViewModel
+class SettingsViewModel @Inject constructor(
+ @param:ApplicationContext private val context: Context
+) : ViewModel() {
+ private val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ private val _nominatimUrl = MutableStateFlow(
+ prefs.getString(
+ "nominatim_url",
+ "https://nominatim.openstreetmap.org/search"
+ )!!
+ )
+ val nominatimUrl: StateFlow<String> = _nominatimUrl.asStateFlow()
+
+ fun setNominatimUrl(url: String) {
+ prefs.edit().putString("nominatim_url", url).apply()
+ _nominatimUrl.value = url
+ }
+}
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SidebarDrawerViewModel.kt
@@ -14,7 +14,7 @@ import javax.inject.Inject
@HiltViewModel
class SidebarDrawerViewModel @Inject constructor(
- @ApplicationContext private val context: Context,
+ @param:ApplicationContext private val context: Context,
private val repository: CalendarRepository
) : ViewModel() {
val calendars = repository.calendarsFlow.stateIn(
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
@@ -38,5 +38,8 @@
<string name="event_not_found">Event not found or loading…</string>
<string name="event_time_range">From %1$s to %2$s</string>
<string name="event_date_range">%1$s until %2$s</string>
+ <string name="nominatim_url_hint">Example: https://nominatim.openstreetmap.org/search</string>
+ <string name="nominatim_url_label">OpenStreetMap Nominatim URL</string>
+ <string name="save">Save</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
@@ -38,5 +38,8 @@
<string name="event_not_found">Event nicht gefunden oder wird geladen…</string>
<string name="event_time_range">Von %1$s bis %2$s</string>
<string name="event_date_range">%1$s bis %2$s</string>
+ <string name="nominatim_url_hint">Beispiel: https://nominatim.openstreetmap.org/search</string>
+ <string name="nominatim_url_label">OpenStreetMap Nominatim URL</string>
+ <string name="save">Speichern</string>
</resources>
diff --git a/gradle.properties b/gradle.properties
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g
+org.gradle.jvmargs=-Xmx6g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects