- 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
135 lines
5.0 KiB
Kotlin
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("<", "<")
|
|
.replace(">", ">")
|
|
.replace("\"", """)
|
|
.replace("'", "'")
|
|
}
|
|
}
|