订阅源网页重定向功能 #171

This commit is contained in:
HapeLee
2025-10-28 21:05:39 +08:00
parent 698f79d583
commit a9023465d4
14 changed files with 2066 additions and 7 deletions
File diff suppressed because it is too large Load Diff
@@ -65,7 +65,7 @@ val appDb by lazy {
}
@Database(
version = 77,
version = 78,
exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -107,7 +107,8 @@ val appDb by lazy {
AutoMigration(from = 73, to = 74),
AutoMigration(from = 74, to = 75),
AutoMigration(from = 75, to = 76),
AutoMigration(from = 76, to = 77)
AutoMigration(from = 76, to = 77),
AutoMigration(from = 77, to = 78)
]
)
abstract class AppDatabase : RoomDatabase() {
@@ -128,6 +128,9 @@ interface RssSourceDao {
@Query("update rssSources set enabled = :enable where sourceUrl = :sourceUrl")
fun enable(sourceUrl: String, enable: Boolean)
@Query("UPDATE rssSources SET redirectPolicy = :redirectPolicy WHERE sourceUrl = :sourceUrl")
suspend fun updateRedirectPolicy(sourceUrl: String, redirectPolicy: String)
private fun dealGroups(list: List<String>): List<String> {
val groups = linkedSetOf<String>()
list.forEach {
@@ -91,7 +91,10 @@ data class RssSource(
@ColumnInfo(defaultValue = "0")
var lastUpdateTime: Long = 0,
@ColumnInfo(defaultValue = "0")
var customOrder: Int = 0
var customOrder: Int = 0,
//用户控制的重定向
@ColumnInfo(defaultValue = "ASK_CROSS_ORIGIN")
var redirectPolicy: String = "ASK_CROSS_ORIGIN"
) : Parcelable, BaseSource {
override fun getTag(): String {
@@ -5,6 +5,7 @@ package io.legado.app.ui.rss.article
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
@@ -19,6 +20,7 @@ import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityRssArtivlesBinding
import io.legado.app.help.source.sortUrls
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.rss.read.ReadRssActivity.RedirectPolicy
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.dialog.VariableDialog
import io.legado.app.utils.*
@@ -45,16 +47,19 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
}
}
var redirectPolicy: RedirectPolicy = RedirectPolicy.ALLOW_ALL
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(binding.topBar)
viewModel.titleLiveData.observe(this) {
binding.topBar.title = it
binding.ctlBar.title = it
}
viewModel.initData(intent) {
upFragments()
redirectPolicy = RedirectPolicy.fromString(viewModel.rssSource?.redirectPolicy)
}
binding.titleBar.addOnOffsetChangedListener({ appBarLayout, verticalOffset ->
@@ -98,7 +103,8 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
putExtra("sourceUrl", it)
}
}
R.id.menu_redirect_policy ->
showRedirectPolicySubMenu()
R.id.menu_clear -> {
viewModel.url?.let {
viewModel.clearArticles()
@@ -117,6 +123,49 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
return super.onCompatOptionsItemSelected(item)
}
private fun showRedirectPolicySubMenu() {
val popup = PopupMenu(this, binding.titleBar, Gravity.END)
popup.menuInflater.inflate(R.menu.menu_redirect_policy_submenu, popup.menu)
setCurrentRedirectPolicyChecked(popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
handleRedirectPolicySelection(menuItem)
true
}
popup.show()
}
private fun setCurrentRedirectPolicyChecked(menu: Menu) {
val currentPolicy = redirectPolicy
val menuItemId = when (currentPolicy) {
RedirectPolicy.ALLOW_ALL -> R.id.menu_redirect_allow_all
RedirectPolicy.ASK_ALWAYS -> R.id.menu_redirect_ask_always
RedirectPolicy.ASK_CROSS_ORIGIN -> R.id.menu_redirect_ask_cross_origin
RedirectPolicy.BLOCK_CROSS_ORIGIN -> R.id.menu_redirect_block_cross_origin
}
menu.findItem(menuItemId)?.isChecked = true
}
private fun handleRedirectPolicySelection(menuItem: MenuItem) {
val selectedPolicy = when (menuItem.itemId) {
R.id.menu_redirect_allow_all -> RedirectPolicy.ALLOW_ALL
R.id.menu_redirect_ask_always -> RedirectPolicy.ASK_ALWAYS
R.id.menu_redirect_ask_cross_origin -> RedirectPolicy.ASK_CROSS_ORIGIN
R.id.menu_redirect_block_cross_origin -> RedirectPolicy.BLOCK_CROSS_ORIGIN
else -> RedirectPolicy.ALLOW_ALL
}
updateRedirectPolicy(selectedPolicy)
toastOnUi("重定向策略已更新")
}
fun updateRedirectPolicy(policy: RedirectPolicy) {
viewModel.rssSource?.let { source ->
source.redirectPolicy = policy.name
redirectPolicy = policy
viewModel.updateRssSourceRedirectPolicy(source.sourceUrl)
}
}
private fun upFragments() {
lifecycleScope.launch {
val sorts = viewModel.rssSource?.sortUrls().orEmpty()
@@ -9,6 +9,8 @@ import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssReadRecord
import io.legado.app.data.entities.RssSource
import io.legado.app.help.source.removeSortCache
import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
class RssSortViewModel(application: Application) : BaseViewModel(application) {
@@ -92,4 +94,14 @@ class RssSortViewModel(application: Application) : BaseViewModel(application) {
}
}
fun updateRssSourceRedirectPolicy(redirectPolicy: String) {
execute {
val source = rssSource ?: return@execute
source.redirectPolicy = redirectPolicy
appDb.rssSourceDao.updateRedirectPolicy(source.sourceUrl, redirectPolicy)
}.onError {
appCtx.toastOnUi("保存失败: ${it.localizedMessage}")
}
}
}
@@ -7,6 +7,7 @@ import android.net.Uri
import android.net.http.SslError
import android.os.Bundle
import android.os.SystemClock
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -22,6 +23,8 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toUri
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.size
@@ -73,11 +76,28 @@ import java.util.regex.PatternSyntaxException
class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>(),
RssFavoritesDialog.Callback {
enum class RedirectPolicy {
ALLOW_ALL,
ASK_ALWAYS,
ASK_CROSS_ORIGIN,
BLOCK_CROSS_ORIGIN;
companion object {
fun fromString(value: String?): RedirectPolicy {
return entries.find { it.name.equals(value, ignoreCase = true) } ?: ALLOW_ALL
}
}
}
var redirectPolicy: RedirectPolicy = RedirectPolicy.ALLOW_ALL
override val binding by viewBinding(ActivityRssReadBinding::inflate)
override val viewModel by viewModels<ReadRssViewModel>()
private var starMenuItem: MenuItem? = null
private var ttsMenuItem: MenuItem? = null
private var redirectPolicyMenu: MenuItem? = null
private var isFullScreen = false
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private val rssJsExtensions by lazy { RssJsExtensions(this) }
@@ -138,6 +158,11 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
starMenuItem = menu.findItem(R.id.menu_rss_star)
ttsMenuItem = menu.findItem(R.id.menu_aloud)
redirectPolicyMenu = menu.findItem(R.id.menu_redirect_policy)
redirectPolicyMenu?.setOnMenuItemClickListener {
showRedirectPolicySubMenu()
true
}
upStarMenu()
return super.onPrepareOptionsMenu(menu)
}
@@ -160,7 +185,6 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
showDialogFragment(RssFavoritesDialog(it))
}
}
R.id.menu_share_it -> {
binding.webView.url?.let {
share(it)
@@ -203,6 +227,41 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
return AppConfig.isNightTheme
}
private fun showRedirectPolicySubMenu() {
val popup = PopupMenu(this, binding.titleBar, Gravity.END)
popup.menuInflater.inflate(R.menu.menu_redirect_policy_submenu, popup.menu)
setCurrentRedirectPolicyChecked(popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
handleRedirectPolicySelection(menuItem)
true
}
popup.show()
}
private fun setCurrentRedirectPolicyChecked(menu: Menu) {
val currentPolicy = redirectPolicy
val menuItemId = when (currentPolicy) {
RedirectPolicy.ALLOW_ALL -> R.id.menu_redirect_allow_all
RedirectPolicy.ASK_ALWAYS -> R.id.menu_redirect_ask_always
RedirectPolicy.ASK_CROSS_ORIGIN -> R.id.menu_redirect_ask_cross_origin
RedirectPolicy.BLOCK_CROSS_ORIGIN -> R.id.menu_redirect_block_cross_origin
}
menu.findItem(menuItemId)?.isChecked = true
}
private fun handleRedirectPolicySelection(menuItem: MenuItem) {
val selectedPolicy = when (menuItem.itemId) {
R.id.menu_redirect_allow_all -> RedirectPolicy.ALLOW_ALL
R.id.menu_redirect_ask_always -> RedirectPolicy.ASK_ALWAYS
R.id.menu_redirect_ask_cross_origin -> RedirectPolicy.ASK_CROSS_ORIGIN
R.id.menu_redirect_block_cross_origin -> RedirectPolicy.BLOCK_CROSS_ORIGIN
else -> RedirectPolicy.ALLOW_ALL
}
updateRedirectPolicy(selectedPolicy)
toastOnUi("重定向策略已更新")
}
private fun toggleFullScreen() {
isFullScreen = !isFullScreen
@@ -277,6 +336,9 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
private fun initLiveData() {
viewModel.contentLiveData.observe(this) { content ->
viewModel.rssArticle?.let {
if (viewModel.rssSource != null) {
redirectPolicy = RedirectPolicy.fromString(viewModel.rssSource?.redirectPolicy)
}
upJavaScriptEnable()
val url = NetworkUtils.getAbsoluteURL(it.origin, it.link)
val html = viewModel.clHtml(content)
@@ -306,6 +368,14 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
}
}
fun updateRedirectPolicy(policy: RedirectPolicy) {
viewModel.rssSource?.let { source ->
source.redirectPolicy = policy.name
redirectPolicy = policy
viewModel.updateRssSourceRedirectPolicy(source.sourceUrl)
}
}
private fun upStarMenu() {
starMenuItem?.isVisible = viewModel.rssArticle != null
if (viewModel.rssStar != null) {
@@ -382,19 +452,74 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
}
inner class CustomWebViewClient : WebViewClient() {
private var lastUrl: String? = null
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val currentUrl = lastUrl ?: view.url
val targetUrl = request.url.toString()
lastUrl = targetUrl
if (!request.isForMainFrame) return false
if (handleRedirect(view, currentUrl, targetUrl)) {
return true
}
return shouldOverrideUrlLoading(request.url)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "KotlinRedundantDiagnosticSuppress")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
val currentUrl = lastUrl ?: view.url
val targetUrl = url
lastUrl = targetUrl
if (handleRedirect(view, currentUrl, targetUrl)) {
return true
}
return shouldOverrideUrlLoading(url.toUri())
}
private fun handleRedirect(view: WebView, fromUrl: String?, toUrl: String): Boolean {
val fromHost = fromUrl?.toUri()?.host
val toHost = toUrl.toUri().host
val crossOrigin = fromHost != null && toHost != null && fromHost != toHost
return when (redirectPolicy) {
RedirectPolicy.ALLOW_ALL -> false
RedirectPolicy.ASK_ALWAYS -> {
askUser(fromUrl, toUrl) { if (it) view.loadUrl(toUrl) }
true
}
RedirectPolicy.ASK_CROSS_ORIGIN -> {
if (crossOrigin) {
askUser(fromUrl, toUrl) { if (it) view.loadUrl(toUrl) }
true
} else {
false
}
}
RedirectPolicy.BLOCK_CROSS_ORIGIN -> {
if (crossOrigin) {
toastOnUi("已阻止跨域重定向")
true
} else {
false
}
}
}
}
private fun askUser(fromUrl: String?, toUrl: String, onResult: (Boolean) -> Unit) {
AlertDialog.Builder(this@ReadRssActivity)
.setTitle("重定向请求")
.setMessage("是否允许页面跳转?\n\n来源:${fromUrl ?: "未知"}\n目标:$toUrl")
.setPositiveButton("允许") { _, _ -> onResult(true) }
.setNegativeButton("拒绝") { _, _ -> onResult(false) }
.setCancelable(true)
.show()
}
/**
* 如果有黑名单,黑名单匹配返回空白,
* 没有黑名单再判断白名单,在白名单中的才通过,
@@ -8,6 +8,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.script.rhino.runScriptWithContext
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.data.appDb
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
@@ -242,6 +243,16 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
tts?.speak(text)
}
fun updateRssSourceRedirectPolicy(redirectPolicy: String) {
execute {
val source = rssSource ?: return@execute
source.redirectPolicy = redirectPolicy
appDb.rssSourceDao.updateRedirectPolicy(source.sourceUrl, redirectPolicy)
}.onError {
appCtx.toastOnUi("保存失败: ${it.localizedMessage}")
}
}
override fun onCleared() {
super.onCleared()
tts?.clearTts()
@@ -16,6 +16,7 @@
android:stateListAnimator="@null">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/ctl_bar"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
style="?attr/collapsingToolbarLayoutMediumStyle"
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/menu_redirect_allow_all"
android:title="@string/redirect_allow_all" />
<item
android:id="@+id/menu_redirect_ask_always"
android:title="@string/redirect_ask_always" />
<item
android:id="@+id/menu_redirect_ask_cross_origin"
android:title="@string/redirect_ask_cross_origin" />
<item
android:id="@+id/menu_redirect_block_cross_origin"
android:title="@string/redirect_block_cross_origin" />
</group>
</menu>
+5
View File
@@ -37,4 +37,9 @@
android:title="@string/clear"
app:showAsAction="never" />
<item
android:id="@+id/menu_redirect_policy"
android:title="@string/redirect_policy"
app:showAsAction="never" />
</menu>
+5
View File
@@ -38,6 +38,11 @@
android:title="@string/login"
app:showAsAction="never" />
<item
android:id="@+id/menu_redirect_policy"
android:title="@string/redirect_policy"
app:showAsAction="never" />
<item
android:id="@+id/menu_browser_open"
android:title="@string/open_in_browser"
+5
View File
@@ -1313,4 +1313,9 @@
<string name="screen_settings">屏幕显示与亮度</string>
<string name="reading_behavior">阅读行为</string>
<string name="page_control">翻页控制</string>
<string name="redirect_policy">网页重定向策略</string>
<string name="redirect_allow_all">允许所有重定向</string>
<string name="redirect_ask_always">总是询问</string>
<string name="redirect_ask_cross_origin">跨域时询问</string>
<string name="redirect_block_cross_origin">阻止跨域重定向</string>
</resources>
+5
View File
@@ -1314,4 +1314,9 @@
<string name="screen_settings">屏幕显示与亮度</string>
<string name="reading_behavior">阅读行为</string>
<string name="page_control">翻页控制</string>
<string name="redirect_policy">网页重定向策略</string>
<string name="redirect_allow_all">允许所有重定向</string>
<string name="redirect_ask_always">总是询问</string>
<string name="redirect_ask_cross_origin">跨域时询问</string>
<string name="redirect_block_cross_origin">阻止跨域重定向</string>
</resources>