fix: final review findings - thread safety, xss, cache
This commit is contained in:
@@ -22,24 +22,38 @@ class CardRepository(
|
||||
fun fetchCatalog(): List<Card> {
|
||||
val body = http.get(catalogUrl)
|
||||
if (body != null) {
|
||||
cache.save(KEY, body)
|
||||
Log.d(TAG, "fetchCatalog: fetched ${body.length} bytes from network")
|
||||
return parse(body)
|
||||
// 先解析、成功才缓存,避免畸形 body 被永久缓存
|
||||
// Parse first, cache only on success (never cache malformed bodies)
|
||||
val cards = parse(body)
|
||||
if (cards != null) {
|
||||
cache.save(KEY, body)
|
||||
Log.d(TAG, "fetchCatalog: fetched ${body.length} bytes from network")
|
||||
return cards
|
||||
}
|
||||
Log.w(TAG, "fetchCatalog: invalid catalog body, not cached")
|
||||
return builtin()
|
||||
}
|
||||
val cached = cache.load(KEY)
|
||||
if (cached != null) {
|
||||
Log.d(TAG, "fetchCatalog: network failed, using cache")
|
||||
return parse(cached)
|
||||
return parse(cached) ?: builtin()
|
||||
}
|
||||
Log.d(TAG, "fetchCatalog: no network and no cache, return builtin time card")
|
||||
return builtin()
|
||||
}
|
||||
|
||||
// 解析目录 JSON:{ "cards": [ {id,name,priority,entry} ] },字段缺失/非法时跳过该项
|
||||
// Parse catalog JSON; skip malformed or missing-field entries
|
||||
private fun parse(body: String): List<Card> {
|
||||
val root = gson.fromJson(body, Map::class.java)
|
||||
val list = root["cards"] as? List<*> ?: return builtin()
|
||||
// 解析目录 JSON:{ "cards": [ {id,name,priority,entry} ] },字段缺失/非法时跳过该项;
|
||||
// 畸形 JSON 或缺少 cards 字段返回 null(调用方回退内置 time 卡)
|
||||
// Parse catalog JSON; skip malformed or missing-field entries. Return null on
|
||||
// malformed JSON or a missing cards field (caller falls back to builtin time card)
|
||||
private fun parse(body: String): List<Card>? {
|
||||
val root = try {
|
||||
gson.fromJson(body, Map::class.java)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "parse: invalid catalog JSON: ${e.message}")
|
||||
return null
|
||||
}
|
||||
val list = root["cards"] as? List<*> ?: return null
|
||||
val parsed = list.mapNotNull { m ->
|
||||
val map = m as? Map<*, *> ?: return@mapNotNull null
|
||||
val id = map["id"] as? String ?: return@mapNotNull null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package top.yeij.hearth.webapp
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import top.yeij.hearth.cache.CacheManager
|
||||
|
||||
@@ -33,16 +34,30 @@ class WebAppRepository(
|
||||
fun fetchManifest(): List<WebApp> {
|
||||
val body = http.get(manifestUrl)
|
||||
if (body != null) {
|
||||
cache.save(KEY, body)
|
||||
return parse(body)
|
||||
// 先解析、成功才缓存,避免畸形 body 或空结果被永久缓存
|
||||
// Parse first, cache only on success (never cache malformed/empty bodies)
|
||||
val apps = parse(body)
|
||||
if (apps != null) {
|
||||
cache.save(KEY, body)
|
||||
return apps
|
||||
}
|
||||
Log.w(TAG, "fetchManifest: invalid body, not cached")
|
||||
return emptyList()
|
||||
}
|
||||
val cached = cache.load(KEY) ?: return emptyList()
|
||||
return parse(cached)
|
||||
return parse(cached) ?: emptyList()
|
||||
}
|
||||
|
||||
private fun parse(body: String): List<WebApp> {
|
||||
val root = gson.fromJson(body, Map::class.java)
|
||||
val apps = root["apps"] as? List<*> ?: return emptyList()
|
||||
// 解析清单 JSON;畸形 JSON 或缺少 apps 字段返回 null(调用方决定回退)
|
||||
// Parse the manifest JSON; return null on malformed JSON / missing apps field
|
||||
private fun parse(body: String): List<WebApp>? {
|
||||
val root = try {
|
||||
gson.fromJson(body, Map::class.java)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "parse: invalid manifest JSON: ${e.message}")
|
||||
return null
|
||||
}
|
||||
val apps = root["apps"] as? List<*> ?: return null
|
||||
return apps.mapNotNull { m ->
|
||||
val map = m as? Map<*, *> ?: return@mapNotNull null
|
||||
val id = map["id"] as? String ?: return@mapNotNull null
|
||||
@@ -57,6 +72,7 @@ class WebAppRepository(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HearthWebApp"
|
||||
private const val KEY = "webapp-manifest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,10 @@ class JsBridge(
|
||||
Log.d("HearthBridge", "openWebApp: unknown id=$id")
|
||||
return
|
||||
}
|
||||
container.open(id, app.url, app.name)
|
||||
// 状态变更与 View 变更统一在主线程串行,避免与 onBackPressed/listTabs 并发读写
|
||||
// Mutate tab state and view on the main thread together, serializing access
|
||||
onMain {
|
||||
container.open(id, app.url, app.name)
|
||||
host.openWebView(id, app.url)
|
||||
host.syncTabs(container.tabs())
|
||||
}
|
||||
@@ -90,14 +92,14 @@ class JsBridge(
|
||||
fun closeWebApp(id: String) {
|
||||
val container = webAppContainer ?: return
|
||||
val host = webAppHost ?: return
|
||||
container.close(id)
|
||||
val nextActiveId = container.activeTabId()
|
||||
onMain {
|
||||
container.close(id)
|
||||
val nextActiveId = container.activeTabId()
|
||||
host.closeWebView(id)
|
||||
nextActiveId?.let { host.switchWebView(it) }
|
||||
host.syncTabs(container.tabs())
|
||||
Log.d("HearthBridge", "closeWebApp: id=$id remaining=${container.tabs().size}")
|
||||
}
|
||||
Log.d("HearthBridge", "closeWebApp: id=$id remaining=${container.tabs().size}")
|
||||
}
|
||||
|
||||
// 切换激活标签:校验 id 存在后更新状态 → 主线程切换 WebView 可见性 → 同步顶栏
|
||||
@@ -107,10 +109,11 @@ class JsBridge(
|
||||
fun switchTab(id: String) {
|
||||
val container = webAppContainer ?: return
|
||||
val host = webAppHost ?: return
|
||||
if (!container.switchTo(id)) return
|
||||
onMain {
|
||||
host.switchWebView(id)
|
||||
host.syncTabs(container.tabs())
|
||||
if (container.switchTo(id)) {
|
||||
host.switchWebView(id)
|
||||
host.syncTabs(container.tabs())
|
||||
}
|
||||
}
|
||||
Log.d("HearthBridge", "switchTab: id=$id")
|
||||
}
|
||||
@@ -153,12 +156,15 @@ class JsBridge(
|
||||
Log.d("HearthBridge", "webReload called")
|
||||
}
|
||||
|
||||
// 清单内存缓存:首次拉取后缓存,后续复用(避免 openWebApp 重复 I/O)
|
||||
// In-memory manifest cache: fetch once, reuse afterwards (avoid repeated I/O)
|
||||
// 清单内存缓存:首次拉取成功后缓存,后续复用(避免 openWebApp 重复 I/O)
|
||||
// 空结果(离线失败)不缓存,下次调用重试拉取,避免空清单被永久缓存
|
||||
// In-memory manifest cache: cache only on a successful non-empty fetch (avoid
|
||||
// repeated I/O); empty results (offline failure) are never cached so the next
|
||||
// call retries instead of being permanently stuck with an empty manifest
|
||||
private fun manifest(): List<WebApp> {
|
||||
manifestCache?.let { return it }
|
||||
val apps = webAppRepository?.fetchManifest() ?: emptyList<WebApp>()
|
||||
manifestCache = apps
|
||||
if (apps.isNotEmpty()) manifestCache = apps
|
||||
return apps
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user