Преглед изворни кода

Merge pull request #1842 from Xwite/master

书源支持输入验证码 手动验证网站防爬措施
kunfei пре 4 година
родитељ
комит
f0ea16597c

+ 7 - 0
app/src/main/AndroidManifest.xml

@@ -359,6 +359,13 @@
                 <data android:scheme="yuedu" />
             </intent-filter>
         </activity>
+        <!-- 验证码输入 -->
+        <activity
+            android:name=".ui.association.VerificationCodeActivity"
+            android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
+            android:exported="true"
+            android:theme="@style/AppTheme.Transparent">
+        </activity>
         <!-- 打开文件 -->
         <activity
             android:name=".ui.association.FileAssociationActivity"

+ 7 - 0
app/src/main/assets/help/jsHelp.md

@@ -67,12 +67,19 @@ java.webView(html: String?, url: String?, js: String?): String
 * @param url 要打开的链接
 * @param title 浏览器的标题
 java.startBrowser(url: String, title: String)
+
+* 使用内置浏览器打开链接,并等待网页结果 .body()获取网页内容
+java.startBrowserAwait(url: String, title: String): StrResponse
 ```
 * 调试
 ```
 java.log(msg)
 java.logType(var)
 ```
+* 获取用户输入的验证码
+```
+java.getVerificationCode(imageUrl)
+```
 * 弹窗提示
 ```
 java.longToast(msg: Any?)

+ 4 - 0
app/src/main/assets/updateLog.md

@@ -11,6 +11,10 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
+**2022/05/04**
+
+* js添加 getVerificationCode startBrowserAwait
+
 **2022/05/02**
 
 * 优化强调色和文字颜色一样的情况下一些文字的显示

+ 21 - 1
app/src/main/java/io/legado/app/help/CacheManager.kt

@@ -1,5 +1,6 @@
 package io.legado.app.help
 
+import androidx.collection.LruCache
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Cache
 import io.legado.app.model.analyzeRule.QueryTTF
@@ -10,6 +11,11 @@ import splitties.init.appCtx
 object CacheManager {
 
     private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>()
+    private val memoryLruCache = object : LruCache<String, Cache>(100) {
+        override fun sizeOf(key: String, value: Cache): Int {
+            return 1
+        }
+    }
 
     /**
      * saveTime 单位为秒
@@ -23,13 +29,26 @@ object CacheManager {
             is ByteArray -> ACache.get(appCtx).put(key, value, saveTime)
             else -> {
                 val cache = Cache(key, value.toString(), deadline)
+                memoryLruCache.put(key, cache)
                 appDb.cacheDao.insert(cache)
             }
         }
     }
 
     fun get(key: String): String? {
-        return appDb.cacheDao.get(key, System.currentTimeMillis())
+        return getFromMemory(key) ?: appDb.cacheDao.get(key, System.currentTimeMillis())
+    }
+
+    //从内存中获取数据 使用lrucache 支持过期功能
+    private fun getFromMemory(key: String): String? {
+        val cache = memoryLruCache.get(key) ?: return null
+        val deadline = cache!!.deadline
+        return if (deadline == 0L || deadline > System.currentTimeMillis()) {
+            cache!!.value
+        } else {
+            memoryLruCache.remove(key)
+            null
+        }
     }
 
     fun getInt(key: String): Int? {
@@ -70,6 +89,7 @@ object CacheManager {
 
     fun delete(key: String) {
         appDb.cacheDao.delete(key)
+        memoryLruCache.remove(key)
         ACache.get(appCtx).remove(key)
     }
 }

+ 16 - 7
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -15,7 +15,6 @@ import io.legado.app.help.http.*
 import io.legado.app.model.Debug
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.QueryTTF
-import io.legado.app.ui.browser.WebViewActivity
 import io.legado.app.utils.*
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.async
@@ -133,18 +132,28 @@ interface JsExtensions {
     }
 
     /**
-     * 使用内置浏览器打开链接,可用于获取验证码 手动验证网站防爬
+     * 使用内置浏览器打开链接,手动验证网站防爬
      * @param url 要打开的链接
      * @param title 浏览器页面的标题
      */
     fun startBrowser(url: String, title: String) {
-        appCtx.startActivity<WebViewActivity> {
-            putExtra("title", title)
-            putExtra("url", url)
-            IntentData.put(url, getSource()?.getHeaderMap(true))
-        }
+        SourceVerificationHelp.startBrowser(getSource(), url, title)
+    }
+
+    /**
+     * 使用内置浏览器打开链接,并等待网页结果
+     */
+    fun startBrowserAwait(url: String, title: String): StrResponse {
+        return StrResponse(url, SourceVerificationHelp.getVerificationResult(getSource(), url, title, true))
     }
 
+    /**
+     * 打开图片验证码对话框,等待返回验证结果
+     */
+     fun getVerificationCode(imageUrl: String): String {
+         return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
+     }
+
     /**
      * 可从网络,本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本
      */

+ 69 - 0
app/src/main/java/io/legado/app/help/SourceVerificationHelp.kt

@@ -0,0 +1,69 @@
+package io.legado.app.help
+
+import io.legado.app.utils.startActivity
+import io.legado.app.constant.AppLog
+import io.legado.app.exception.NoStackTraceException
+import io.legado.app.data.entities.BaseSource
+import io.legado.app.ui.browser.WebViewActivity
+import io.legado.app.ui.association.VerificationCodeActivity
+import splitties.init.appCtx
+import kotlinx.coroutines.runBlocking
+
+
+object SourceVerificationHelp {
+
+    /** 
+     * 获取书源验证结果
+     * 图片验证码 防爬 滑动验证码 点击字符 等等
+     */
+    fun getVerificationResult(source: BaseSource?, url: String, title: String, useBrowser: Boolean): String {
+        source ?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
+        return runBlocking {
+            val key = "${source?.getKey() ?: ""}_verificationResult"
+            CacheManager.delete(key)
+
+            if (!useBrowser) {
+                appCtx.startActivity<VerificationCodeActivity> {
+                    putExtra("imageUrl", url)
+                    putExtra("sourceOrigin", source?.getKey())
+                }
+            } else {
+                startBrowser(source, url, title, true)
+            }
+
+            var waitUserInput: Boolean = false
+            while(CacheManager.get(key) == null) {
+                if (!waitUserInput) {
+                    AppLog.putDebug("等待返回验证结果...")
+                    waitUserInput = true
+                }
+            }
+            CacheManager.get(key)!!.let {
+                if (it.isBlank()) {
+                    throw NoStackTraceException("验证结果为空")
+                } else {
+                   it
+                }
+            }
+       }
+    }
+
+    /**
+     * 启动内置浏览器
+      @param saveResult 保存网页源代码到数据库
+     */
+    fun startBrowser(source: BaseSource?, url: String, title: String, saveResult: Boolean? = false) {
+        source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
+        appCtx.startActivity<WebViewActivity> {
+            putExtra("title", title)
+            putExtra("url", url)
+            putExtra("sourceOrigin", source?.getKey())
+            putExtra("sourceVerificationEnable", saveResult)
+            IntentData.put(url, source?.getHeaderMap(true))
+        }
+    }
+
+    private fun checkActivityStatus(): Boolean {
+        return true
+    }
+}

+ 26 - 0
app/src/main/java/io/legado/app/ui/association/VerificationCodeActivity.kt

@@ -0,0 +1,26 @@
+package io.legado.app.ui.association
+
+import android.os.Bundle
+import io.legado.app.base.BaseActivity
+import io.legado.app.databinding.ActivityTranslucenceBinding
+import io.legado.app.utils.showDialogFragment
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+
+/**
+ * 验证码
+ */
+class VerificationCodeActivity :
+    BaseActivity<ActivityTranslucenceBinding>() {
+
+    override val binding by viewBinding(ActivityTranslucenceBinding::inflate)
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        intent.getStringExtra("imageUrl")?.let {
+            val sourceOrigin = intent.getStringExtra("sourceOrigin")
+            showDialogFragment(
+                VerificationCodeDialog(it, sourceOrigin)
+            )
+        } ?: finish()
+    }
+
+}

+ 84 - 0
app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt

@@ -0,0 +1,84 @@
+package io.legado.app.ui.association
+
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.net.Uri
+import com.bumptech.glide.request.RequestOptions
+import io.legado.app.R
+import io.legado.app.base.BaseDialogFragment
+import io.legado.app.databinding.DialogVerificationCodeViewBinding
+import io.legado.app.help.CacheManager
+import io.legado.app.help.glide.ImageLoader
+import io.legado.app.help.glide.OkHttpModelLoader
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.widget.dialog.PhotoDialog
+import io.legado.app.utils.setLayout
+import io.legado.app.utils.showDialogFragment
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+
+/**
+ * 图片验证码对话框
+ * 结果保存在数据库中
+ * val key = "${sourceOrigin ?: ""}_verificationResult"
+ * CacheManager.get(key)
+ */
+class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view) {
+
+    constructor(imageUrl: String, sourceOrigin: String? = null) : this() {
+        arguments = Bundle().apply {
+            putString("sourceOrigin", sourceOrigin)
+            putString("imageUrl", imageUrl)
+        }
+    }
+
+    val binding by viewBinding(DialogVerificationCodeViewBinding::bind)
+
+    override fun onStart() {
+        super.onStart()
+        setLayout(1f, ViewGroup.LayoutParams.WRAP_CONTENT)
+    }
+
+    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
+        binding.run {
+            toolBar.setBackgroundColor(primaryColor)
+            val sourceOrigin = arguments?.getString("sourceOrigin")
+            val key = "${sourceOrigin ?: ""}_verificationResult"
+            arguments?.getString("imageUrl")?.let { imageUrl ->
+                ImageLoader.load(requireContext(), imageUrl).apply {
+                    sourceOrigin?.let {
+                        apply(
+                            RequestOptions().set(
+                                OkHttpModelLoader.sourceOriginOption,
+                                it
+                            )
+                        )
+                    }
+                }.error(R.drawable.image_loading_error)
+                    .into(ivImage)
+                ivImage.setOnClickListener {
+                    showDialogFragment(PhotoDialog(imageUrl, sourceOrigin))
+                }
+            }
+            tvOk.setOnClickListener {
+                val verificationCode = binding.verificationCode.text.toString()
+                verificationCode?.let {
+                    CacheManager.put(key, it)
+                    dismiss()
+                }
+            }
+            tvCancel.setOnClickListener {
+                dismiss()
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        val sourceOrigin = arguments?.getString("sourceOrigin")
+        val key = "${sourceOrigin ?: ""}_verificationResult"
+        CacheManager.get(key) ?: CacheManager.put(key, "")
+        super.onDestroy()
+        activity?.finish()
+    }
+
+}

+ 10 - 1
app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt

@@ -26,6 +26,7 @@ import io.legado.app.ui.document.HandleFileContract
 import io.legado.app.utils.*
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import java.net.URLDecoder
+import kotlinx.coroutines.runBlocking
 
 class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
 
@@ -65,6 +66,12 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
         when (item.itemId) {
             R.id.menu_open_in_browser -> openUrl(viewModel.baseUrl)
             R.id.menu_copy_url -> sendToClip(viewModel.baseUrl)
+            R.id.menu_ok -> {
+                if (viewModel.sourceVerificationEnable) {
+                    binding.titleBar.snackbar(R.string.ok)
+                    finish()
+                }
+            }
         }
         return super.onCompatOptionsItemSelected(item)
     }
@@ -135,7 +142,6 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
             viewModel.saveImage(webPic, path)
         }
     }
-
     private fun selectSaveFolder() {
         val default = arrayListOf<SelectItem<Int>>()
         val path = ACache.get(this).getAsString(imagePathKey)
@@ -176,6 +182,9 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
 
     override fun onDestroy() {
         super.onDestroy()
+        runBlocking {
+            viewModel.saveVerificationResult()
+        }
         binding.webView.destroy()
     }
 

+ 12 - 0
app/src/main/java/io/legado/app/ui/browser/WebViewModel.kt

@@ -9,6 +9,7 @@ import androidx.documentfile.provider.DocumentFile
 import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppConst
 import io.legado.app.exception.NoStackTraceException
+import io.legado.app.help.CacheManager
 import io.legado.app.help.IntentData
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
@@ -21,6 +22,8 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
     var baseUrl: String = ""
     var html: String? = null
     val headerMap: HashMap<String, String> = hashMapOf()
+    var sourceVerificationEnable: Boolean = false
+    var sourceOrigin: String = ""
 
     fun initData(
         intent: Intent,
@@ -29,6 +32,8 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
         execute {
             val url = intent.getStringExtra("url")
                 ?: throw NoStackTraceException("url不能为空")
+            sourceOrigin = intent.getStringExtra("sourceOrigin") ?: ""
+            sourceVerificationEnable = intent.getBooleanExtra("sourceVerificationEnable", false)
             val headerMapF = IntentData.get<Map<String, String>>(url)
             val analyzeUrl = AnalyzeUrl(url, headerMapF = headerMapF)
             baseUrl = analyzeUrl.url
@@ -78,5 +83,12 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
+    fun saveVerificationResult() {
+        if (sourceVerificationEnable) {
+            val key = "${sourceOrigin}_verificationResult"
+            html = AnalyzeUrl(baseUrl, headerMapF = headerMap).getStrResponse(useWebView = false).body
+            CacheManager.put(key, html ?: "")
+        }
+    }
 
 }

+ 1 - 1
app/src/main/java/io/legado/app/ui/login/WebViewLoginFragment.kt

@@ -35,7 +35,7 @@ class WebViewLoginFragment : BaseFragment(R.layout.fragment_web_view_login) {
     }
 
     override fun onCompatCreateOptionsMenu(menu: Menu) {
-        menuInflater.inflate(R.menu.source_login, menu)
+        menuInflater.inflate(R.menu.source_webview_login, menu)
     }
 
     override fun onCompatOptionsItemSelected(item: MenuItem) {

+ 70 - 0
app/src/main/res/layout/dialog_verification_code_view.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/tool_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="?attr/actionBarStyle"
+        app:title="@string/input_verification_code"
+        app:popupTheme="@style/AppTheme.PopupOverlay"
+        app:titleTextAppearance="@style/ToolbarTitle" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_image"
+        android:scaleType="fitXY"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp"
+        android:paddingTop="3dp"
+        android:background="?android:attr/selectableItemBackgroundBorderless"
+        tools:ignore="UnusedAttribute" />
+
+    <io.legado.app.ui.widget.text.TextInputLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp"
+        android:paddingTop="3dp">
+
+        <io.legado.app.lib.theme.view.ThemeEditText
+            android:id="@+id/verification_code"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/verification_code"
+            tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
+
+    </io.legado.app.ui.widget.text.TextInputLayout>
+
+            
+    <com.google.android.flexbox.FlexboxLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp"
+        app:justifyContent="flex_end">
+
+        <io.legado.app.ui.widget.text.AccentTextView
+            android:id="@+id/tv_cancel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="12dp"
+            android:text="@string/cancel"
+            tools:ignore="RtlHardcoded" />
+
+        <io.legado.app.ui.widget.text.AccentTextView
+            android:id="@+id/tv_ok"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="12dp"
+            android:text="@string/ok"
+            tools:ignore="RtlHardcoded" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</LinearLayout>

+ 13 - 0
app/src/main/res/menu/source_webview_login.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    tools:ignore="AlwaysShowAction">
+
+    <item
+        android:id="@+id/menu_ok"
+        android:icon="@drawable/ic_check"
+        android:title="@string/ok"
+        app:showAsAction="always" />
+
+</menu>

+ 6 - 0
app/src/main/res/menu/web_view.xml

@@ -2,6 +2,12 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
+    <item
+        android:id="@+id/menu_ok"
+        android:icon="@drawable/ic_check"
+        android:title="@string/ok"
+        app:showAsAction="always" />
+
     <item
         android:id="@+id/menu_open_in_browser"
         android:title="@string/open_in_browser"

+ 2 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -969,5 +969,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -972,5 +972,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -972,5 +972,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -969,5 +969,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -971,5 +971,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values-zh/strings.xml

@@ -971,5 +971,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -972,5 +972,7 @@
     <string name="change_source_batch">批量换源</string>
     <string name="book_type_different">书籍类型不一样</string>
     <string name="soure_change_source">是否确认换源</string>
+    <string name="input_verification_code">输入验证码</string>
+    <string name="verification_code">验证码</string>
     <!-- string end -->
 </resources>