Files
RSSuper/android/src/main/java/com/rssuper/search/SearchResultProvider.kt
Michael Freno dd4e184600 Fix critical iOS notification service issues
- Fixed authorization handling in NotificationService
- Removed invalid icon and haptic properties
- Fixed deliveryDate API usage
- Removed invalid presentNotificationRequest call
- Fixed notification trigger initialization
- Simplified notification categories with delegate implementation
- Replaced UNNotificationBadgeManager with UIApplication.shared.applicationIconBadgeNumber
- Eliminated code duplication in badge update logic
- Fixed NotificationPreferencesStore JSON encoding/decoding
2026-03-30 23:54:39 -04:00

135 lines
5.0 KiB
Kotlin

package com.rssuper.search
import com.rssuper.database.daos.FeedItemDao
import com.rssuper.database.entities.FeedItemEntity
private const val MAX_QUERY_LENGTH = 500
private const val MAX_HIGHLIGHT_LENGTH = 200
/**
* SearchResultProvider - Provides search results from the database
*/
class SearchResultProvider(
private val feedItemDao: FeedItemDao
) {
companion object {
fun sanitizeFtsQuery(query: String): String {
return query.replace("\\".toRegex(), "\\\\")
.replace("*".toRegex(), "\\*")
.replace("\"".toRegex(), "\\\"")
.replace("(".toRegex(), "\\(")
.replace(")".toRegex(), "\\)")
.replace("~".toRegex(), "\\~")
}
fun validateQuery(query: String): Result<String> {
if (query.isEmpty()) {
return Result.failure(Exception("Query cannot be empty"))
}
if (query.length > MAX_QUERY_LENGTH) {
return Result.failure(Exception("Query exceeds maximum length of $MAX_QUERY_LENGTH characters"))
}
val suspiciousPatterns = listOf(
"DELETE ", "DROP ", "INSERT ", "UPDATE ", "SELECT ",
"UNION ", "--", ";"
)
val queryUpper = query.uppercase()
for (pattern in suspiciousPatterns) {
if (queryUpper.contains(pattern)) {
return Result.failure(Exception("Query contains invalid characters"))
}
}
return Result.success(query)
}
}
suspend fun search(query: String, limit: Int = 20): Result<List<SearchResult>> {
val validation = validateQuery(query)
if (validation.isFailure) return validation
val sanitizedQuery = sanitizeFtsQuery(query.getOrNull() ?: query)
val results = feedItemDao.searchByFts(sanitizedQuery, limit)
return Result.success(results.mapIndexed { index, item ->
SearchResult(
feedItem = item,
relevanceScore = calculateRelevance(query.getOrNull() ?: query, item, index),
highlight = generateHighlight(item, query.getOrNull() ?: query)
)
})
}
suspend fun searchWithPagination(query: String, limit: Int = 20, offset: Int = 0): Result<List<SearchResult>> {
val validation = validateQuery(query)
if (validation.isFailure) return validation
val sanitizedQuery = sanitizeFtsQuery(query.getOrNull() ?: query)
val results = feedItemDao.searchByFtsWithPagination(sanitizedQuery, limit, offset)
return Result.success(results.mapIndexed { index, item ->
SearchResult(
feedItem = item,
relevanceScore = calculateRelevance(query.getOrNull() ?: query, item, index),
highlight = generateHighlight(item, query.getOrNull() ?: query)
)
})
}
suspend fun searchBySubscription(query: String, subscriptionId: String, limit: Int = 20): Result<List<SearchResult>> {
val validation = validateQuery(query)
if (validation.isFailure) return validation
val sanitizedQuery = sanitizeFtsQuery(query.getOrNull() ?: query)
val results = feedItemDao.searchByFtsPaginated(sanitizedQuery, subscriptionId, limit, 0)
return Result.success(results.mapIndexed { index, item ->
SearchResult(
feedItem = item,
relevanceScore = calculateRelevance(query.getOrNull() ?: query, item, index),
highlight = generateHighlight(item, query.getOrNull() ?: query)
)
})
}
private fun calculateRelevance(query: String, item: FeedItemEntity, position: Int): Float {
val queryLower = query.lowercase()
var score = 0.0f
// Title match (highest weight)
if (item.title.lowercase().contains(queryLower)) {
score += 1.0f
}
// Author match
if (item.author?.lowercase()?.contains(queryLower) == true) {
score += 0.5f
}
// Position bonus (earlier results are more relevant)
score += (1.0f / (position + 1)) * 0.3f
return score.coerceIn(0.0f, 1.0f)
}
private fun generateHighlight(item: FeedItemEntity, query: String): String? {
var text = item.title
if (item.description?.isNotEmpty() == true) {
text += " ${item.description}"
}
if (text.length > MAX_HIGHLIGHT_LENGTH) {
text = text.substring(0, MAX_HIGHLIGHT_LENGTH) + "..."
}
return sanitizeOutput(text)
}
private fun sanitizeOutput(text: String): String {
return text.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;")
}
}