diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt index 6ff8c4bd..438cb1a5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContext.kt @@ -35,8 +35,17 @@ public abstract class MangaLoaderContext { * @param script JavaScript source code * @return execution result as string, may be null */ + @Deprecated("Provide a base url") public abstract suspend fun evaluateJs(script: String): String? + /** + * Execute JavaScript code and return result + * @param script JavaScript source code + * @param baseUrl url of page script will be executed in context of + * @return execution result as string, may be null + */ + public abstract suspend fun evaluateJs(baseUrl: String, script: String): String? + /** * Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization) */ diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt index a9c21787..dc25d586 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParserAuthProvider.kt @@ -18,7 +18,7 @@ public interface MangaParserAuthProvider { * Quick check if user is logged in. * In most case you should check for cookies in [MangaLoaderContext.cookieJar]. */ - public val isAuthorized: Boolean + public suspend fun isAuthorized(): Boolean /** * Fetch and return current user`s name or login. diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt index 6f3f8d35..81a53164 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/BatoToParser.kt @@ -35,12 +35,11 @@ internal class BatoToParser(context: MangaLoaderContext) : LegacyPagedMangaParse override val authUrl: String get() = "https://${domain}/signin" - override val isAuthorized: Boolean - get() { - return context.cookieJar.getCookies(domain).any { - it.name.contains("skey") - } + override suspend fun isAuthorized(): Boolean { + return context.cookieJar.getCookies(domain).any { + it.name.contains("skey") } + } override suspend fun getUsername(): String { val body = webClient.httpGet("https://${domain}/account/profiles").parseHtml().body() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt index 06dc2a9b..193a83b0 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ExHentaiParser.kt @@ -37,10 +37,13 @@ internal class ExHentaiParser( override val availableSortOrders: Set = setOf(SortOrder.NEWEST) override val configKeyDomain: ConfigKey.Domain - get() = ConfigKey.Domain( - if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED, - if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED, - ) + get() { + val isAuthorized = checkAuth() + return ConfigKey.Domain( + if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED, + if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED, + ) + } override val authUrl: String get() = "https://${domain}/bounce_login.php" @@ -59,22 +62,7 @@ internal class ExHentaiParser( isAuthorSearchSupported = true, ) - override val isAuthorized: Boolean - get() { - val authorized = isAuthorized(DOMAIN_UNAUTHORIZED) - if (authorized) { - if (!isAuthorized(DOMAIN_AUTHORIZED)) { - context.cookieJar.copyCookies( - DOMAIN_UNAUTHORIZED, - DOMAIN_AUTHORIZED, - authCookies, - ) - context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder") - } - return true - } - return false - } + override suspend fun isAuthorized(): Boolean = checkAuth() init { context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2") @@ -484,4 +472,20 @@ internal class ExHentaiParser( } acc or cat } + + private fun checkAuth(): Boolean { + val authorized = isAuthorized(DOMAIN_UNAUTHORIZED) + if (authorized) { + if (!isAuthorized(DOMAIN_AUTHORIZED)) { + context.cookieJar.copyCookies( + DOMAIN_UNAUTHORIZED, + DOMAIN_AUTHORIZED, + authCookies, + ) + context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder") + } + return true + } + return false + } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt index 4fc22136..c212576a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaFireParser.kt @@ -49,12 +49,11 @@ internal abstract class MangaFireParser( override val authUrl: String get() = "https://${domain}" - override val isAuthorized: Boolean - get() { - return context.cookieJar.getCookies(domain).any { - it.value.contains("user") - } + override suspend fun isAuthorized(): Boolean { + return context.cookieJar.getCookies(domain).any { + it.value.contains("user") } + } override suspend fun getUsername(): String { val body = webClient.httpGet("https://${domain}/user/profile").parseHtml().body() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt index b8ae0fa8..59f20075 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaReaderToParser.kt @@ -36,12 +36,11 @@ internal class MangaReaderToParser(context: MangaLoaderContext) : override val authUrl: String get() = "https://${domain}/home" - override val isAuthorized: Boolean - get() { - return context.cookieJar.getCookies(domain).any { - it.name.contains("connect.sid") - } + override suspend fun isAuthorized(): Boolean { + return context.cookieJar.getCookies(domain).any { + it.name.contains("connect.sid") } + } // It will be easier to connect to a manga page, as the source redirects to a lot of advertising. override suspend fun getUsername(): String { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt index a71639fc..e609e6d4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/WeebCentral.kt @@ -30,8 +30,8 @@ internal class WeebCentral(context: MangaLoaderContext) : LegacyMangaParser(cont override val authUrl: String get() = "https://$domain" - override val isAuthorized: Boolean - get() = context.cookieJar.getCookies(domain).any { it.name == "access_token" } + override suspend fun isAuthorized(): Boolean = + context.cookieJar.getCookies(domain).any { it.name == "access_token" } override suspend fun getUsername(): String { return webClient.httpGet("https://$domain/users/me/profiles") @@ -180,8 +180,10 @@ internal class WeebCentral(context: MangaLoaderContext) : LegacyMangaParser(cont .toHttpUrl() .pathSegments[1] val author = document.select("div:contains(author) a").eachText().joinToString().nullIfEmpty() - val title = element.selectFirst("div.text-ellipsis.truncate.text-white.text-center.text-lg.z-20.w-\\[90\\%\\]")?.text() - ?: "No name" + val title = + element.selectFirst("div.text-ellipsis.truncate.text-white.text-center.text-lg.z-20.w-\\[90\\%\\]") + ?.text() + ?: "No name" Manga( id = generateUid(mangaId), url = mangaId, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt index 7121bab8..404676f4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt @@ -45,10 +45,9 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) : override val authUrl: String get() = "https://${getDomain("account")}/login?site=seiga" - override val isAuthorized: Boolean - get() = context.cookieJar.getCookies(getDomain("seiga")).any { - it.name == "user_session" - } + override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(getDomain("seiga")).any { + it.name == "user_session" + } override suspend fun getUsername(): String { val body = webClient.httpGet("https://${getDomain("app")}/my/apps").parseHtml().body() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index 27eaa500..d538aad4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -82,12 +82,11 @@ internal abstract class MadaraParser( override val authUrl: String get() = "https://${domain}" - override val isAuthorized: Boolean - get() { - return context.cookieJar.getCookies(domain).any { - it.name.contains("wordpress_logged_in") - } + override suspend fun isAuthorized(): Boolean { + return context.cookieJar.getCookies(domain).any { + it.name.contains("wordpress_logged_in") } + } override suspend fun getUsername(): String { val body = webClient.httpGet("https://${domain}/").parseHtml().body() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt index 5ff1b31f..89039f70 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/NudeMoonParser.kt @@ -25,12 +25,11 @@ internal class NudeMoonParser( override val authUrl: String get() = "https://${domain}/index.php" - override val isAuthorized: Boolean - get() { - return context.cookieJar.getCookies(domain).any { - it.name == "fusion_user" - } + override suspend fun isAuthorized(): Boolean { + return context.cookieJar.getCookies(domain).any { + it.name == "fusion_user" } + } override val availableSortOrders: Set = EnumSet.of( SortOrder.NEWEST, @@ -61,7 +60,7 @@ internal class NudeMoonParser( val url = when { !filter.query.isNullOrEmpty() -> { - if (!isAuthorized) { + if (!isAuthorized()) { throw AuthRequiredException(source) } "https://$domain/search?stext=${filter.query.urlEncoded()}&rowstart=$offset" diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt index 43c33668..5970cb68 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/RemangaParser.kt @@ -50,12 +50,11 @@ internal class RemangaParser( SortOrder.NEWEST, ) - override val isAuthorized: Boolean - get() { - return context.cookieJar.getCookies(domain).any { - it.name == "user" - } + override suspend fun isAuthorized(): Boolean { + return context.cookieJar.getCookies(domain).any { + it.name == "user" } + } private val regexLastUrlPath = Regex("/[^/]+/?$") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/AllHentaiParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/AllHentaiParser.kt index a1e7898d..adfe3b8f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/AllHentaiParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/AllHentaiParser.kt @@ -21,8 +21,8 @@ internal class AllHentaiParser( "2023.allhen.online", ) - override val isAuthorized: Boolean - get() = super.isAuthorized || context.cookieJar.getCookies(domain).any { it.name == "remember_me" } + override suspend fun isAuthorized(): Boolean = + super.isAuthorized() || context.cookieJar.getCookies(domain).any { it.name == "remember_me" } override val authUrl: String get() { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt index e29f247e..79b54d23 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/grouple/GroupleParser.kt @@ -74,8 +74,7 @@ internal abstract class GroupleParser( return "https://grouple.co/internal/auth/sso?siteId=$siteId&=targetUri=$targetUri" } - override val isAuthorized: Boolean - get() = context.cookieJar.getCookies(domain).any { it.name == "gwt" } + override suspend fun isAuthorized(): Boolean = hasAuthCookie() override val filterCapabilities: MangaListFilterCapabilities get() = MangaListFilterCapabilities( @@ -511,7 +510,7 @@ internal abstract class GroupleParser( id = generateUid(url), url = if (fullUrl.contains("one-way.work")) { // domain that does not need a token - fullUrl.substringBefore("?") + fullUrl.substringBefore("?") } else { fullUrl }, @@ -556,7 +555,7 @@ internal abstract class GroupleParser( throw AuthRequiredException(source) } if (code == HttpURLConnection.HTTP_NOT_FOUND) { - if (!isAuthorized) { + if (!hasAuthCookie()) { closeQuietly() throw AuthRequiredException(source) } else { @@ -565,4 +564,6 @@ internal abstract class GroupleParser( } return this } + + private fun hasAuthCookie() = context.cookieJar.getCookies(domain).any { it.name == "gwt" } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt index 6a83c0f8..f625a86d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/multichan/ChanParser.kt @@ -26,8 +26,7 @@ internal abstract class ChanParser( override val authUrl: String get() = "https://${domain}" - override val isAuthorized: Boolean - get() = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" } + override suspend fun isAuthorized(): Boolean = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" } override val filterCapabilities: MangaListFilterCapabilities get() = MangaListFilterCapabilities( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt index fb9a8509..2a97b64c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/rulib/LibSocialParser.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Response import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.parsers.MangaLoaderContext @@ -31,10 +33,10 @@ internal abstract class LibSocialParser( override val authUrl: String get() = "https://$domain/ru/front/auth" - override val isAuthorized: Boolean - get() = runBlocking { - runCatchingCancellable { getAuthData() }.getOrNull() != null - } + override suspend fun isAuthorized(): Boolean { + val token = getAuthData()?.optJSONObject("token")?.getStringOrNull("access_token") + return !token.isNullOrEmpty() + } override suspend fun getUsername(): String = getAuthData() ?.getJSONObject("auth") @@ -65,6 +67,18 @@ internal abstract class LibSocialParser( availableStates = EnumSet.allOf(MangaState::class.java), ) + override fun intercept(chain: Interceptor.Chain): Response { + val token = runBlocking { getAuthData() }?.optJSONObject("token")?.getStringOrNull("access_token") + return if (!token.isNullOrEmpty()) { + val request = chain.request().newBuilder() + .header("Authorization", "Bearer $token") + .build() + chain.proceed(request) + } else { + super.intercept(chain) + } + } + private val statesMap = intObjectMapOf( 1, MangaState.ONGOING, 2, MangaState.FINISHED, @@ -374,7 +388,8 @@ internal abstract class LibSocialParser( } private suspend fun getAuthData(): JSONObject? { - return JSONObject(WebViewHelper(context, domain).getLocalStorageValue("auth") ?: return null) + val raw = WebViewHelper(context).getLocalStorageValue(domain, "auth") ?: return null + return JSONObject(raw.unescapeJson().removeSurrounding('"')) } protected companion object { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt index 7af52b9c..becf9f44 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CMangaParser.kt @@ -54,8 +54,8 @@ internal class CMangaParser(context: MangaLoaderContext) : override val authUrl: String get() = domain - override val isAuthorized: Boolean - get() = context.cookieJar.getCookies(domain).any { it.name == "login_password" } + override suspend fun isAuthorized(): Boolean = + context.cookieJar.getCookies(domain).any { it.name == "login_password" } override suspend fun getUsername(): String { val userId = webClient.httpGet("https://$domain").parseRaw() diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt index 63d36494..f8108fed 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/WebViewHelper.kt @@ -4,10 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext public class WebViewHelper( private val context: MangaLoaderContext, - private val domain: String, ) { - public suspend fun getLocalStorageValue(key: String): String? { - return context.evaluateJs("window.localStorage.getItem(\"$key\")") + public suspend fun getLocalStorageValue(domain: String, key: String): String? { + return context.evaluateJs("$SCHEME_HTTPS://$domain/", "window.localStorage.getItem(\"$key\")") } } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt index 3b094bb5..04dad7f8 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaLoaderContextMock.kt @@ -40,7 +40,10 @@ internal object MangaLoaderContextMock : MangaLoaderContext() { loadTestCookies() } - override suspend fun evaluateJs(script: String): String? { + @Deprecated("Provide a base url") + override suspend fun evaluateJs(script: String): String? = evaluateJs("", script) + + override suspend fun evaluateJs(baseUrl: String, script: String): String? { return QuackContext.create().use { it.evaluate(script)?.toString() }