package com.vgmlr.kerf
import android.content.Context
import android.net.Uri
import org.json.JSONArray
import org.json.JSONObject
object KerfSanitizer {
private data class Provider(
val urlPattern: Regex,
val completeProvider: Boolean,
val rules: List<Regex>,
val rawRules: List<Regex>,
val referralMarketing: List<Regex>,
val exceptions: List<Regex>,
val redirections: List<Regex>
)
@Volatile
private var providers: List<Provider>? = null
private const val MAX_REDIRECTS = 5
fun ensureLoaded(context: Context) {
if (providers != null) return
synchronized(this) {
if (providers != null) return
providers = runCatching { load(context) }.getOrDefault(emptyList())
}
}
private fun load(context: Context): List<Provider> {
val raw = context.resources.openRawResource(R.raw.clearurls_rules)
.bufferedReader().use { it.readText() }
val root = JSONObject(raw).getJSONObject("providers")
val out = mutableListOf<Provider>()
val keys = root.keys()
while (keys.hasNext()) {
val obj = root.getJSONObject(keys.next())
val pattern = obj.optString("urlPattern")
if (pattern.isBlank()) continue
val provider = runCatching {
Provider(
urlPattern = Regex(pattern, RegexOption.IGNORE_CASE),
completeProvider = obj.optBoolean("completeProvider", false),
rules = obj.optJSONArray("rules").toRegexList(),
rawRules = obj.optJSONArray("rawRules").toRegexList(),
referralMarketing = obj.optJSONArray("referralMarketing").toRegexList(),
exceptions = obj.optJSONArray("exceptions").toRegexList(),
redirections = obj.optJSONArray("redirections").toRegexList()
)
}.getOrNull()
if (provider != null) out.add(provider)
}
return out
}
private fun JSONArray?.toRegexList(): List<Regex> {
if (this == null) return emptyList()
val list = ArrayList<Regex>(length())
for (i in 0 until length()) {
runCatching { Regex(getString(i), RegexOption.IGNORE_CASE) }
.getOrNull()?.let { list.add(it) }
}
return list
}
fun cleanText(context: Context, text: String): String {
ensureLoaded(context)
if (providers.isNullOrEmpty()) return text
return KerfUtils.UrlRegex.replace(text) { match ->
val url = match.value
if (url.startsWith("http://", true) || url.startsWith("https://", true)) {
runCatching { cleanUrl(url) }.getOrDefault(url)
} else {
url
}
}
}
fun cleanUrl(url: String): String {
var current = url
var hops = 0
while (hops < MAX_REDIRECTS) {
val (result, redirected) = pass(current)
current = result
if (!redirected) break
hops++
}
return current
}
private fun pass(input: String): Pair<String, Boolean> {
val ps = providers ?: return input to false
var url = input
for (p in ps) {
if (!p.urlPattern.containsMatchIn(url)) continue
if (p.exceptions.any { it.containsMatchIn(url) }) continue
if (p.completeProvider) continue
for (r in p.redirections) {
val cap = r.find(url)?.groupValues?.getOrNull(1)
if (!cap.isNullOrEmpty()) {
val target = Uri.decode(cap)
if (target.startsWith("http", true)) return target to true
}
}
for (raw in p.rawRules) url = raw.replace(url, "")
url = stripParams(url, p.rules, p.referralMarketing)
}
return url to false
}
private fun stripParams(url: String, rules: List<Regex>, referral: List<Regex>): String {
if (rules.isEmpty() && referral.isEmpty()) return url
val qIndex = url.indexOf('?')
if (qIndex == -1) return url
val base = url.substring(0, qIndex)
var query = url.substring(qIndex + 1)
var fragment = ""
val hashIndex = query.indexOf('#')
if (hashIndex != -1) {
fragment = query.substring(hashIndex)
query = query.substring(0, hashIndex)
}
val kept = query.split('&').filter { part ->
if (part.isEmpty()) return@filter false
val name = part.substringBefore('=')
val drop = rules.any { it.matches(name) } || referral.any { it.matches(name) }
!drop
}
val newQuery = kept.joinToString("&")
return buildString {
append(base)
if (newQuery.isNotEmpty()) append('?').append(newQuery)
append(fragment)
}
}
}