Horis 1 год назад
Родитель
Сommit
37cd97c451

+ 3 - 6
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -4,7 +4,8 @@ import android.webkit.WebSettings
 import androidx.annotation.Keep
 import cn.hutool.core.codec.Base64
 import cn.hutool.core.util.HexUtil
-import com.script.rhino.RhinoContext
+import com.script.rhino.rhinoContext
+import com.script.rhino.rhinoContextOrNull
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst.dateFormat
 import io.legado.app.constant.AppLog
@@ -52,7 +53,6 @@ import kotlinx.coroutines.runBlocking
 import okio.use
 import org.jsoup.Connection
 import org.jsoup.Jsoup
-import org.mozilla.javascript.Context
 import splitties.init.appCtx
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -83,9 +83,6 @@ interface JsExtensions : JsEncodeUtils {
 
     fun getSource(): BaseSource?
 
-    private val rhinoContext: RhinoContext
-        get() = Context.getCurrentContext() as RhinoContext
-
     private val context: CoroutineContext
         get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext
 
@@ -943,7 +940,7 @@ interface JsExtensions : JsEncodeUtils {
      * 输出调试日志
      */
     fun log(msg: Any?): Any? {
-        rhinoContext.ensureActive()
+        rhinoContextOrNull?.ensureActive()
         getSource()?.let {
             Debug.log(it.getKey(), msg.toString())
         } ?: Debug.log(msg.toString())

+ 12 - 0
app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt

@@ -130,6 +130,18 @@ fun Request.Builder.get(url: String, queryMap: Map<String, String>, encoded: Boo
     url(httpBuilder.build())
 }
 
+fun Request.Builder.get(url: String, encodedQuery: String?) {
+    val httpBuilder = url.toHttpUrl().newBuilder()
+    httpBuilder.encodedQuery(encodedQuery)
+    url(httpBuilder.build())
+}
+
+private val formContentType = "application/x-www-form-urlencoded".toMediaType()
+
+fun Request.Builder.postForm(encodedForm: String) {
+    post(encodedForm.toRequestBody(formContentType))
+}
+
 fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
     val formBody = FormBody.Builder()
     form.forEach {

+ 4 - 0
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt

@@ -21,6 +21,7 @@ import io.legado.app.utils.GSON
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getOrPutLimit
+import io.legado.app.utils.isDataUrl
 import io.legado.app.utils.isJson
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.splitNotBlank
@@ -100,6 +101,9 @@ class AnalyzeRule(
     }
 
     fun setRedirectUrl(url: String): URL? {
+        if (url.isDataUrl()) {
+            return redirectUrl
+        }
         try {
             redirectUrl = URL(url)
         } catch (e: Exception) {

+ 73 - 27
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
 import android.util.Base64
 import androidx.annotation.Keep
 import androidx.media3.common.MediaItem
+import cn.hutool.core.net.URLEncodeUtil
 import cn.hutool.core.util.HexUtil
 import com.bumptech.glide.load.model.GlideUrl
 import com.script.buildScriptBindings
@@ -47,7 +48,6 @@ import io.legado.app.utils.isJson
 import io.legado.app.utils.isJsonArray
 import io.legado.app.utils.isJsonObject
 import io.legado.app.utils.isXml
-import io.legado.app.utils.splitNotBlank
 import kotlinx.coroutines.runBlocking
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.OkHttpClient
@@ -56,6 +56,7 @@ import okhttp3.Response
 import java.io.ByteArrayInputStream
 import java.io.InputStream
 import java.net.URLEncoder
+import java.nio.charset.Charset
 import java.util.concurrent.TimeUnit
 import java.util.regex.Pattern
 import kotlin.coroutines.ContinuationInterceptor
@@ -101,8 +102,8 @@ class AnalyzeUrl(
         private set
     val headerMap = HashMap<String, String>()
     private var urlNoQuery: String = ""
-    private var queryStr: String? = null
-    private val fieldMap = LinkedHashMap<String, String>()
+    private var encodedForm: String? = null
+    private var encodedQuery: String? = null
     private var charset: String? = null
     private var method = RequestMethod.GET
     private var proxy: String? = null
@@ -251,7 +252,7 @@ class AnalyzeUrl(
             RequestMethod.GET -> {
                 val pos = url.indexOf('?')
                 if (pos != -1) {
-                    analyzeFields(url.substring(pos + 1))
+                    analyzeQuery(url.substring(pos + 1))
                     urlNoQuery = url.substring(0, pos)
                 }
             }
@@ -271,23 +272,68 @@ class AnalyzeUrl(
      * name=<BASE64> eg name=bmFtZQ==
      */
     private fun analyzeFields(fieldsTxt: String) {
-        queryStr = fieldsTxt
-        val queryS = fieldsTxt.splitNotBlank("&")
-        for (query in queryS) {
-            val queryPair = query.splitNotBlank("=", limit = 2)
-            val key = queryPair[0]
-            val value = queryPair.getOrNull(1) ?: ""
-            if (charset.isNullOrEmpty()) {
-                if (NetworkUtils.hasUrlEncoded(value)) {
-                    fieldMap[key] = value
-                } else {
-                    fieldMap[key] = URLEncoder.encode(value, "UTF-8")
-                }
-            } else if (charset == "escape") {
-                fieldMap[key] = EncoderUtils.escape(value)
+        encodedForm = encodeParams(fieldsTxt, charset, false)
+    }
+
+    private fun analyzeQuery(query: String) {
+        encodedQuery = encodeParams(query, charset, true)
+    }
+
+    private fun encodeParams(params: String, charset: String?, isQuery: Boolean): String {
+        val checkEncoded = charset.isNullOrEmpty()
+        val charset = when {
+            charset.isNullOrEmpty() -> Charsets.UTF_8
+            charset == "escape" -> null
+            else -> charset(charset)
+        }
+        val len = params.length
+        val sb = StringBuilder()
+        var pos = 0
+        while (pos <= len) {
+            if (sb.isNotEmpty()) {
+                sb.append("&")
+            }
+            var ampOffset = params.indexOf("&", pos)
+            if (ampOffset == -1) {
+                ampOffset = len
+            }
+            val eqOffset = params.indexOf("=", pos)
+            val key: String
+            val value: String?
+            if (eqOffset == -1 || eqOffset > ampOffset) {
+                key = params.substring(pos, ampOffset)
+                value = null
             } else {
-                fieldMap[key] = URLEncoder.encode(value, charset)
+                key = params.substring(pos, eqOffset)
+                value = params.substring(eqOffset + 1, ampOffset)
+            }
+            sb.appendEncoded(key, isQuery, checkEncoded, charset)
+            if (value != null) {
+                sb.append("=")
+                sb.appendEncoded(value, isQuery, checkEncoded, charset)
             }
+            pos = ampOffset + 1
+        }
+        return sb.toString()
+    }
+
+    private fun StringBuilder.appendEncoded(
+        value: String,
+        isQuery: Boolean,
+        checkEncoded: Boolean,
+        charset: Charset?
+    ) {
+        val needEncode = checkEncoded &&
+                ((isQuery && !NetworkUtils.encodedQuery(value)) ||
+                        (!isQuery && !NetworkUtils.encodedForm(value)))
+        if (!needEncode) {
+            append(value)
+        } else if (charset == null) {
+            append(EncoderUtils.escape(value))
+        } else if (isQuery) {
+            append(URLEncodeUtil.encodeQuery(value, charset))
+        } else {
+            append(URLEncoder.encode(value, charset))
         }
     }
 
@@ -356,8 +402,8 @@ class AnalyzeUrl(
                         val res = getClient().newCallStrResponse(retry) {
                             addHeaders(headerMap)
                             url(urlNoQuery)
-                            if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
-                                postForm(fieldMap, true)
+                            if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
+                                postForm(encodedForm ?: "")
                             } else {
                                 postJson(body)
                             }
@@ -390,8 +436,8 @@ class AnalyzeUrl(
                             url(urlNoQuery)
                             val contentType = headerMap["Content-Type"]
                             val body = body
-                            if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
-                                postForm(fieldMap, true)
+                            if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
+                                postForm(encodedForm ?: "")
                             } else if (!contentType.isNullOrBlank()) {
                                 val requestBody = body.toRequestBody(contentType.toMediaType())
                                 post(requestBody)
@@ -400,7 +446,7 @@ class AnalyzeUrl(
                             }
                         }
 
-                        else -> get(urlNoQuery, fieldMap, true)
+                        else -> get(urlNoQuery, encodedQuery)
                     }
                 }.let {
                     val isXml = it.raw.body?.contentType()?.toString()
@@ -438,8 +484,8 @@ class AnalyzeUrl(
                         url(urlNoQuery)
                         val contentType = headerMap["Content-Type"]
                         val body = body
-                        if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
-                            postForm(fieldMap, true)
+                        if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
+                            postForm(encodedForm ?: "")
                         } else if (!contentType.isNullOrBlank()) {
                             val requestBody = body.toRequestBody(contentType.toMediaType())
                             post(requestBody)
@@ -448,7 +494,7 @@ class AnalyzeUrl(
                         }
                     }
 
-                    else -> get(urlNoQuery, fieldMap, true)
+                    else -> get(urlNoQuery, encodedQuery)
                 }
             }
             return response

+ 5 - 0
app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt

@@ -26,6 +26,7 @@ import io.legado.app.utils.dpToPx
 import io.legado.app.utils.isAbsUrl
 import io.legado.app.utils.openUrl
 import io.legado.app.utils.printOnDebug
+import io.legado.app.utils.sendToClip
 import io.legado.app.utils.setLayout
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.toastOnUi
@@ -35,6 +36,7 @@ import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import splitties.init.appCtx
 import splitties.views.onClick
 
 
@@ -109,6 +111,9 @@ class SourceLoginDialog : BaseDialogFragment(R.layout.dialog_login, true) {
                     setTitle(R.string.login_header)
                     source.getLoginHeader()?.let { loginHeader ->
                         setMessage(loginHeader)
+                        positiveButton(R.string.copy_text) {
+                            appCtx.sendToClip(loginHeader)
+                        }
                     }
                 }
 

+ 47 - 4
app/src/main/java/io/legado/app/utils/NetworkUtils.kt

@@ -55,7 +55,7 @@ object NetworkUtils {
         return false
     }
 
-    private val notNeedEncoding: BitSet by lazy {
+    private val notNeedEncodingQuery: BitSet by lazy {
         val bitSet = BitSet(256)
         for (i in 'a'.code..'z'.code) {
             bitSet.set(i)
@@ -66,7 +66,24 @@ object NetworkUtils {
         for (i in '0'.code..'9'.code) {
             bitSet.set(i)
         }
-        for (char in "+-_.~$:()!*@&#,[]") {
+        for (char in "!$&()*+,-./:;=?@[\\]^_`{|}~") {
+            bitSet.set(char.code)
+        }
+        return@lazy bitSet
+    }
+
+    private val notNeedEncodingForm: BitSet by lazy {
+        val bitSet = BitSet(256)
+        for (i in 'a'.code..'z'.code) {
+            bitSet.set(i)
+        }
+        for (i in 'A'.code..'Z'.code) {
+            bitSet.set(i)
+        }
+        for (i in '0'.code..'9'.code) {
+            bitSet.set(i)
+        }
+        for (char in "*-._") {
             bitSet.set(char.code)
         }
         return@lazy bitSet
@@ -78,12 +95,38 @@ object NetworkUtils {
      * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留
      * 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F]
      */
-    fun hasUrlEncoded(str: String): Boolean {
+    fun encodedQuery(str: String): Boolean {
+        var needEncode = false
+        var i = 0
+        while (i < str.length) {
+            val c = str[i]
+            if (notNeedEncodingQuery.get(c.code)) {
+                i++
+                continue
+            }
+            if (c == '%' && i + 2 < str.length) {
+                // 判断是否符合urlEncode规范
+                val c1 = str[++i]
+                val c2 = str[++i]
+                if (isDigit16Char(c1) && isDigit16Char(c2)) {
+                    i++
+                    continue
+                }
+            }
+            // 其他字符,肯定需要urlEncode
+            needEncode = true
+            break
+        }
+
+        return !needEncode
+    }
+
+    fun encodedForm(str: String): Boolean {
         var needEncode = false
         var i = 0
         while (i < str.length) {
             val c = str[i]
-            if (notNeedEncoding.get(c.code)) {
+            if (notNeedEncodingForm.get(c.code)) {
                 i++
                 continue
             }

+ 6 - 0
modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt

@@ -11,6 +11,12 @@ import kotlin.coroutines.ContinuationInterceptor
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
 
+val rhinoContext: RhinoContext
+    get() = Context.getCurrentContext() as RhinoContext
+
+val rhinoContextOrNull: RhinoContext?
+    get() = Context.getCurrentContext() as? RhinoContext
+
 @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
 @OptIn(ExperimentalContracts::class)
 inline fun <T> suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T {