diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index 60e41e8b3d5..c0f0e4a037c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -6,13 +6,12 @@ import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.splitUrlParameters import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING -import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames -import java.net.URI import java.security.SecureRandom data class AuthLoginPage( @@ -172,10 +171,8 @@ abstract class AuthAPI { get() = unixTimeMS fun splitRedirectUrl(redirectUrl: String): Map { - return splitQuery( - URI( - redirectUrl.replace(APP_STRING, "https").replace("/#", "?") - ).toURL() + return splitUrlParameters( + redirectUrl.replace(APP_STRING, "https").replace("/#", "?") ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 7278fcdd74f..1f2050bb9c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -90,12 +90,9 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.Cache import java.io.File -import java.net.URL -import java.net.URLDecoder import java.util.concurrent.Executor import java.util.concurrent.Executors - object AppContextUtils { fun RecyclerView.isRecyclerScrollable(): Boolean { val layoutManager = @@ -630,16 +627,17 @@ object AppContextUtils { } } - fun splitQuery(url: URL): Map { - val queryPairs: MutableMap = LinkedHashMap() - val query: String = url.query - val pairs = query.split("&").toTypedArray() - for (pair in pairs) { - val idx = pair.indexOf("=") - queryPairs[URLDecoder.decode(pair.substring(0, idx), "UTF-8")] = - URLDecoder.decode(pair.substring(idx + 1), "UTF-8") - } - return queryPairs + // Deprecate after next stable + /* @Deprecated( + message = "Use splitUrlParameters instead.", + replaceWith = ReplaceWith( + expression = "splitUrlParameters(url.toString())", + imports = ["com.lagradost.cloudstream3.splitUrlParameters"], + ), + level = DeprecationLevel.WARNING, + ) */ + fun splitQuery(url: java.net.URL): Map { + return com.lagradost.cloudstream3.splitUrlParameters(url.toString()) } /**| S1:E2 Hello World @@ -896,4 +894,4 @@ object AppContextUtils { } else null return currentAudioFocusRequest } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 30167332049..d6d9a06cdbf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -758,18 +758,64 @@ fun MainAPI.fixUrl(url: String): String { } } -/** Sort the urls based on quality +/** + * Sort the urls based on quality + * * @param urls Set of [ExtractorLink] - * */ + */ fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } -/** Capitalize the first letter of string. +/** + * Splits the parameters of a [Url] into a map of key-value pairs. + * + * Unlike a manual `split("&")` / `split("=")` implementation, this relies on Ktor's + * built-in parameters parser ([Url.parameters]), which already handles URL-decoding, + * malformed pairs, and parameters without a value. + * + * Note: if a parameter key appears multiple times (e.g. `?a=1&a=2`), only the **first** + * value is kept, since the return type is `Map`. Use [Url.parameters] + * directly if you need all values for repeated keys. + * + * @param url the [Url] whose parameters should be extracted. + * @return a map of decoded parameter names to their first decoded value. + * + * @sample + * splitUrlParameters(Url("https://example.com/path?foo=bar&baz=qux")) + * // returns {"foo": "bar", "baz": "qux"} + */ +@Prerelease +fun splitUrlParameters(url: Url): Map { + return url.parameters.entries().associate { (key, values) -> key to values.firstOrNull().orEmpty() } +} + +/** + * Splits the parameters of a raw URL [String] into a map of key-value pairs. + * + * Convenience overload for callers that have a URL as plain text rather than a parsed + * [Url] instance. Internally parses [url] with Ktor's [Url] constructor and delegates + * to [splitUrlParameters]. + * + * @param url the URL string whose parameters should be extracted. + * @return a map of decoded parameter names to their first decoded value. + * + * @sample + * splitUrlParameters("https://example.com/path?foo=bar&baz=qux") + * // returns {"foo": "bar", "baz": "qux"} + */ +@Prerelease +fun splitUrlParameters(url: String): Map { + return splitUrlParameters(Url(url)) +} + +/** + * Capitalize the first letter of string. + * * @param str String to be capitalized * @return non-nullable String * @see capitalizeStringNullable - * */ + */ fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitUrlParametersTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitUrlParametersTest.kt new file mode 100644 index 00000000000..7e58c70be84 --- /dev/null +++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitUrlParametersTest.kt @@ -0,0 +1,76 @@ +package com.lagradost.cloudstream3 + +import io.ktor.http.Url +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SplitUrlParametersTest { + + @Test + fun splitsBasicParameters() { + val url = Url("https://example.com/path?foo=bar&baz=qux") + val result = splitUrlParameters(url) + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), result) + } + + @Test + fun decodesUrlEncodedKeysAndValues() { + val url = Url("https://example.com/path?na%20me=hello%20world&sp%26ec=a%2Bb") + val result = splitUrlParameters(url) + assertEquals(mapOf("na me" to "hello world", "sp&ec" to "a+b"), result) + } + + @Test + fun returnsEmptyMapWhenThereAreNoParameters() { + val url = Url("https://example.com/path") + val result = splitUrlParameters(url) + assertTrue(result.isEmpty()) + } + + @Test + fun keepsOnlyFirstValueForRepeatedKeys() { + val url = Url("https://example.com/path?a=1&a=2&a=3") + val result = splitUrlParameters(url) + assertEquals(mapOf("a" to "1"), result) + } + + @Test + fun handlesParameterWithNoValue() { + val url = Url("https://example.com/path?flag&foo=bar") + val result = splitUrlParameters(url) + assertEquals("bar", result["foo"]) + assertEquals("", result["flag"]) + } + + @Test + fun stringOverloadSplitsBasicParameters() { + val result = splitUrlParameters("https://example.com/path?foo=bar&baz=qux") + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), result) + } + + @Test + fun stringOverloadDecodesUrlEncodedKeysAndValues() { + val result = splitUrlParameters("https://example.com/path?na%20me=hello%20world&sp%26ec=a%2Bb") + assertEquals(mapOf("na me" to "hello world", "sp&ec" to "a+b"), result) + } + + @Test + fun stringOverloadReturnsEmptyMapWhenThereAreNoParameters() { + val result = splitUrlParameters("https://example.com/path") + assertTrue(result.isEmpty()) + } + + @Test + fun stringOverloadKeepsOnlyFirstValueForRepeatedKeys() { + val result = splitUrlParameters("https://example.com/path?a=1&a=2&a=3") + assertEquals(mapOf("a" to "1"), result) + } + + @Test + fun stringOverloadHandlesParameterWithNoValue() { + val result = splitUrlParameters("https://example.com/path?flag&foo=bar") + assertEquals("bar", result["foo"]) + assertEquals("", result["flag"]) + } +}