Add MangaDex login
This commit is contained in:
parent
5c2fbec80a
commit
ea7ff432b2
@ -219,7 +219,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.captcha.SolveCaptchaActivity"
|
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:theme="@style/Theme.EHActivity" />
|
android:theme="@style/Theme.EHActivity" />
|
||||||
<activity android:name="exh.ui.webview.WebViewActivity" />
|
<activity android:name="exh.ui.webview.WebViewActivity" />
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.network
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import exh.log.maybeInjectEHLogger
|
import exh.log.maybeInjectEHLogger
|
||||||
|
import exh.patch.attachMangaDexLogin
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -14,23 +15,24 @@ import java.security.KeyStore
|
|||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import javax.net.ssl.*
|
import javax.net.ssl.*
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
open class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
val cookieManager = AndroidCookieJar(context)
|
open val cookieManager = AndroidCookieJar(context)
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
open val client = OkHttpClient.Builder()
|
||||||
.cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
.enableTLS12()
|
.enableTLS12()
|
||||||
.maybeInjectEHLogger()
|
.maybeInjectEHLogger()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cloudflareClient = client.newBuilder()
|
open val cloudflareClient = client.newBuilder()
|
||||||
.addInterceptor(CloudflareInterceptor(context))
|
.addInterceptor(CloudflareInterceptor(context))
|
||||||
|
.attachMangaDexLogin()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
|
private fun OkHttpClient.Builder.enableTLS12(): OkHttpClient.Builder {
|
||||||
|
@ -24,7 +24,6 @@ import exh.metadata.metadata.PervEdenLang
|
|||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.elvishew.xlog.XLog
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.*
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.ui.captcha.SolveCaptchaActivity
|
import exh.ui.captcha.BrowserActionActivity
|
||||||
|
import exh.util.interceptAsHtml
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -28,7 +27,19 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
*/
|
*/
|
||||||
protected val network: NetworkHelper by injectLazy()
|
protected val network: NetworkHelper by lazy {
|
||||||
|
val original = Injekt.get<NetworkHelper>()
|
||||||
|
object : NetworkHelper(Injekt.get<Application>()) {
|
||||||
|
override val client: OkHttpClient?
|
||||||
|
get() = delegate?.networkHttpClient ?: original.client
|
||||||
|
|
||||||
|
override val cloudflareClient: OkHttpClient?
|
||||||
|
get() = delegate?.networkCloudflareClient ?: original.cloudflareClient
|
||||||
|
|
||||||
|
override val cookieManager: AndroidCookieJar
|
||||||
|
get() = original.cookieManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * Preferences that a source may need.
|
// * Preferences that a source may need.
|
||||||
@ -68,36 +79,21 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
open val client: OkHttpClient
|
open val client: OkHttpClient
|
||||||
get() = network.client.newBuilder().addInterceptor { chain ->
|
get() = delegate?.baseHttpClient ?: network.client.newBuilder().addInterceptor { chain ->
|
||||||
|
// Automatic captcha detection
|
||||||
val response = chain.proceed(chain.request())
|
val response = chain.proceed(chain.request())
|
||||||
val body = response.body()
|
if(!response.isSuccessful) {
|
||||||
if(!response.isSuccessful && body != null) {
|
response.interceptAsHtml { doc ->
|
||||||
if(body.contentType()?.type() == "text"
|
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
||||||
&& body.contentType()?.subtype() == "html") {
|
// Found it, allow the user to solve this thing
|
||||||
val bodyString = body.string()
|
BrowserActionActivity.launchUniversal(
|
||||||
val rebuiltResponse = response.newBuilder()
|
Injekt.get<Application>(),
|
||||||
.body(ResponseBody.create(body.contentType(), bodyString))
|
this,
|
||||||
.build()
|
chain.request().url().toString()
|
||||||
try {
|
)
|
||||||
// Search for captcha
|
|
||||||
val parsed = response.asJsoup(html = bodyString)
|
|
||||||
if(parsed.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
|
||||||
// Found it, allow the user to solve this thing
|
|
||||||
SolveCaptchaActivity.launchUniversal(
|
|
||||||
Injekt.get<Application>(),
|
|
||||||
this,
|
|
||||||
chain.request().url().toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch(t: Throwable) {
|
|
||||||
// Ignore all errors
|
|
||||||
XLog.w("Captcha detection error!", t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return@addInterceptor rebuiltResponse
|
|
||||||
}
|
}
|
||||||
}
|
} else response
|
||||||
response
|
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -397,4 +393,14 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Returns the list of filters for the source.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
override fun getFilterList() = FilterList()
|
override fun getFilterList() = FilterList()
|
||||||
|
|
||||||
|
// EXH -->
|
||||||
|
private var delegate: DelegatedHttpSource? = null
|
||||||
|
get() = if(Injekt.get<PreferencesHelper>().eh_delegateSources().getOrDefault())
|
||||||
|
field
|
||||||
|
else null
|
||||||
|
fun bindDelegate(delegate: DelegatedHttpSource) {
|
||||||
|
this.delegate = delegate
|
||||||
|
}
|
||||||
|
// EXH <--
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import exh.TSUMINO_SOURCE_ID
|
import exh.TSUMINO_SOURCE_ID
|
||||||
import exh.ui.captcha.CaptchaCompletionVerifier
|
import exh.ui.captcha.ActionCompletionVerifier
|
||||||
import exh.ui.captcha.SolveCaptchaActivity
|
import exh.ui.captcha.BrowserActionActivity
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
|
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
@ -33,7 +33,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<TsuminoSearchMetadata, Document>, CaptchaCompletionVerifier {
|
class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<TsuminoSearchMetadata, Document>, ActionCompletionVerifier {
|
||||||
override val metaClass = TsuminoSearchMetadata::class
|
override val metaClass = TsuminoSearchMetadata::class
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
@ -343,7 +343,7 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
|||||||
else
|
else
|
||||||
emptyMap()
|
emptyMap()
|
||||||
|
|
||||||
SolveCaptchaActivity.launch(context,
|
BrowserActionActivity.launchCaptcha(context,
|
||||||
this,
|
this,
|
||||||
cookiesMap,
|
cookiesMap,
|
||||||
CAPTCHA_SCRIPT,
|
CAPTCHA_SCRIPT,
|
||||||
@ -356,7 +356,7 @@ class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource<Tsum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verifyNoCaptcha(url: String): Boolean {
|
override fun verifyComplete(url: String): Boolean {
|
||||||
return Uri.parse(url).pathSegments.getOrNull(1) == "View"
|
return Uri.parse(url).pathSegments.getOrNull(1) == "View"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
title = "Enable delegated sources"
|
title = "Enable delegated sources"
|
||||||
key = PreferenceKeys.eh_delegateSources
|
key = PreferenceKeys.eh_delegateSources
|
||||||
defaultValue = true
|
defaultValue = true
|
||||||
summary = "Apply ${context.getString(R.string.app_name)} enhancements to the following sources if they are installed: ${DELEGATED_SOURCES.values.joinToString { it.sourceName }}"
|
summary = "Apply ${context.getString(R.string.app_name)} enhancements to the following sources if they are installed: ${DELEGATED_SOURCES.values.map { it.sourceName }.distinct().joinToString()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
intListPreference {
|
intListPreference {
|
||||||
|
46
app/src/main/java/exh/patch/MangaDexLogin.kt
Normal file
46
app/src/main/java/exh/patch/MangaDexLogin.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package exh.patch
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import exh.ui.captcha.BrowserActionActivity
|
||||||
|
import exh.util.interceptAsHtml
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
private val HIDE_SCRIPT = """
|
||||||
|
document.querySelector("#forgot_button").style.visibility = "hidden";
|
||||||
|
document.querySelector("#signup_button").style.visibility = "hidden";
|
||||||
|
document.querySelector("#announcement").style.visibility = "hidden";
|
||||||
|
document.querySelector("nav").style.visibility = "hidden";
|
||||||
|
document.querySelector("footer").style.visibility = "hidden";
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private fun verifyComplete(url: String): Boolean {
|
||||||
|
return HttpUrl.parse(url)?.let { parsed ->
|
||||||
|
parsed.host() == "mangadex.org" && parsed.pathSegments().none { it.isNotBlank() }
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.attachMangaDexLogin() =
|
||||||
|
addInterceptor { chain ->
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
|
if(response.request().url().host() == "mangadex.org") {
|
||||||
|
response.interceptAsHtml { doc ->
|
||||||
|
if (doc.title().trim().equals("Login - MangaDex", true)) {
|
||||||
|
BrowserActionActivity.launchAction(
|
||||||
|
Injekt.get<Application>(),
|
||||||
|
::verifyComplete,
|
||||||
|
HIDE_SCRIPT,
|
||||||
|
"https://mangadex.org/login",
|
||||||
|
"Login",
|
||||||
|
(Injekt.get<SourceManager>().get(2499283573021220255) as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||||
|
it.value.joinToString(",")
|
||||||
|
} ?: emptyMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else response
|
||||||
|
}
|
@ -2,6 +2,7 @@ package exh.source
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -93,16 +94,16 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
|||||||
/**
|
/**
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
*/
|
*/
|
||||||
override val baseUrl = delegate.baseUrl
|
override val baseUrl get() = delegate.baseUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the source has support for latest updates.
|
* Whether the source has support for latest updates.
|
||||||
*/
|
*/
|
||||||
override val supportsLatest = delegate.supportsLatest
|
override val supportsLatest get() = delegate.supportsLatest
|
||||||
/**
|
/**
|
||||||
* Name of the source.
|
* Name of the source.
|
||||||
*/
|
*/
|
||||||
final override val name = delegate.name
|
final override val name get() = delegate.name
|
||||||
|
|
||||||
// ===> OPTIONAL FIELDS
|
// ===> OPTIONAL FIELDS
|
||||||
|
|
||||||
@ -111,11 +112,18 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
|||||||
* of the MD5 of the string: sourcename/language/versionId
|
* of the MD5 of the string: sourcename/language/versionId
|
||||||
* Note the generated id sets the sign bit to 0.
|
* Note the generated id sets the sign bit to 0.
|
||||||
*/
|
*/
|
||||||
override val id = delegate.id
|
override val id get() = delegate.id
|
||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
override val client = delegate.client
|
final override val client get() = delegate.client
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You must NEVER call super.client if you override this!
|
||||||
|
*/
|
||||||
|
open val baseHttpClient: OkHttpClient? = null
|
||||||
|
open val networkHttpClient: OkHttpClient get() = network.client
|
||||||
|
open val networkCloudflareClient: OkHttpClient get() = network.cloudflareClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
@ -235,4 +243,8 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class IncompatibleDelegateException(message: String) : RuntimeException(message)
|
class IncompatibleDelegateException(message: String) : RuntimeException(message)
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegate.bindDelegate(this)
|
||||||
|
}
|
||||||
}
|
}
|
@ -96,21 +96,21 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
/**
|
/**
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
*/
|
*/
|
||||||
override val baseUrl = source().baseUrl
|
override val baseUrl get() = source().baseUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the source has support for latest updates.
|
* Whether the source has support for latest updates.
|
||||||
*/
|
*/
|
||||||
override val supportsLatest = source().supportsLatest
|
override val supportsLatest get() = source().supportsLatest
|
||||||
/**
|
/**
|
||||||
* Name of the source.
|
* Name of the source.
|
||||||
*/
|
*/
|
||||||
override val name = source().name
|
override val name get() = source().name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||||
*/
|
*/
|
||||||
override val lang = source().lang
|
override val lang get() = source().lang
|
||||||
|
|
||||||
// ===> OPTIONAL FIELDS
|
// ===> OPTIONAL FIELDS
|
||||||
|
|
||||||
@ -119,11 +119,11 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
* of the MD5 of the string: sourcename/language/versionId
|
* of the MD5 of the string: sourcename/language/versionId
|
||||||
* Note the generated id sets the sign bit to 0.
|
* Note the generated id sets the sign bit to 0.
|
||||||
*/
|
*/
|
||||||
override val id = source().id
|
override val id get() = source().id
|
||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
override val client = source().client
|
override val client get() = source().client
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
|
@ -6,17 +6,17 @@ import android.webkit.WebResourceRequest
|
|||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import exh.ui.captcha.SolveCaptchaActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
|
import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
|
||||||
import org.jsoup.nodes.DataNode
|
import org.jsoup.nodes.DataNode
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
class AutoSolvingWebViewClient(activity: SolveCaptchaActivity,
|
class AutoSolvingWebViewClient(activity: BrowserActionActivity,
|
||||||
source: CaptchaCompletionVerifier,
|
verifyComplete: (String) -> Boolean,
|
||||||
injectScript: String?,
|
injectScript: String?,
|
||||||
headers: Map<String, String>)
|
headers: Map<String, String>)
|
||||||
: HeadersInjectingWebViewClient(activity, source, injectScript, headers) {
|
: HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||||
// Inject our custom script into the recaptcha iframes
|
// Inject our custom script into the recaptcha iframes
|
||||||
|
@ -3,13 +3,13 @@ package exh.ui.captcha
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
|
||||||
open class BasicWebViewClient(protected val activity: SolveCaptchaActivity,
|
open class BasicWebViewClient(protected val activity: BrowserActionActivity,
|
||||||
protected val source: CaptchaCompletionVerifier,
|
protected val verifyComplete: (String) -> Boolean,
|
||||||
private val injectScript: String?) : WebViewClient() {
|
private val injectScript: String?) : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
|
|
||||||
if(source.verifyNoCaptcha(url)) {
|
if(verifyComplete(url)) {
|
||||||
activity.finish()
|
activity.finish()
|
||||||
} else {
|
} else {
|
||||||
if(injectScript != null) view.loadUrl("javascript:(function() {$injectScript})();")
|
if(injectScript != null) view.loadUrl("javascript:(function() {$injectScript})();")
|
||||||
|
@ -31,8 +31,10 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.util.melt
|
import exh.util.melt
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import java.io.Serializable
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
class SolveCaptchaActivity : AppCompatActivity() {
|
class BrowserActionActivity : AppCompatActivity() {
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val preferencesHelper: PreferencesHelper by injectLazy()
|
private val preferencesHelper: PreferencesHelper by injectLazy()
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
@ -54,31 +56,38 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
||||||
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
|
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
|
||||||
val source = if(originalSource != null) {
|
val source = if(originalSource != null) {
|
||||||
originalSource as? CaptchaCompletionVerifier
|
originalSource as? ActionCompletionVerifier
|
||||||
?: run {
|
?: run {
|
||||||
(originalSource as? HttpSource)?.let {
|
(originalSource as? HttpSource)?.let {
|
||||||
NoopCaptchaCompletionVerifier(it)
|
NoopActionCompletionVerifier(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
val headers = (source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
val headers = ((source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
||||||
it.value.joinToString(",")
|
it.value.joinToString(",")
|
||||||
} ?: emptyMap()
|
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||||
|
|
||||||
val cookies: HashMap<String, String>?
|
val cookies: HashMap<String, String>?
|
||||||
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||||
|
|
||||||
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
||||||
|
|
||||||
val url: String? = intent.getStringExtra(URL_EXTRA)
|
val url: String? = intent.getStringExtra(URL_EXTRA)
|
||||||
|
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
||||||
|
|
||||||
if(source == null || url == null) {
|
val verifyComplete = if(source != null) {
|
||||||
|
source::verifyComplete!!
|
||||||
|
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
|
||||||
|
|
||||||
|
if(verifyComplete == null || url == null) {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbar.title = source.name + ": Solve captcha"
|
val actionStr = actionName ?: "Solve captcha"
|
||||||
|
|
||||||
|
toolbar.title = if(source != null) {
|
||||||
|
"${source.name}: $actionStr"
|
||||||
|
} else actionStr
|
||||||
|
|
||||||
val parsedUrl = URL(url)
|
val parsedUrl = URL(url)
|
||||||
|
|
||||||
@ -94,6 +103,9 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
webview.settings.javaScriptEnabled = true
|
webview.settings.javaScriptEnabled = true
|
||||||
webview.settings.domStorageEnabled = true
|
webview.settings.domStorageEnabled = true
|
||||||
|
headers.entries.find { it.key.equals("user-agent", true) }?.let {
|
||||||
|
webview.settings.userAgentString = it.value
|
||||||
|
}
|
||||||
|
|
||||||
var loadedInners = 0
|
var loadedInners = 0
|
||||||
|
|
||||||
@ -125,7 +137,7 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
webview.webViewClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
webview.webViewClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
|
if(actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
|
||||||
// Fetch auto-solve credentials early for speed
|
// Fetch auto-solve credentials early for speed
|
||||||
credentialsObservable = httpClient.newCall(Request.Builder()
|
credentialsObservable = httpClient.newCall(Request.Builder()
|
||||||
// Rob demo credentials
|
// Rob demo credentials
|
||||||
@ -139,13 +151,13 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
json["token"].string
|
json["token"].string
|
||||||
}.melt()
|
}.melt()
|
||||||
|
|
||||||
webview.addJavascriptInterface(this@SolveCaptchaActivity, "exh")
|
webview.addJavascriptInterface(this@BrowserActionActivity, "exh")
|
||||||
AutoSolvingWebViewClient(this, source, script, headers)
|
AutoSolvingWebViewClient(this, verifyComplete, script, headers)
|
||||||
} else {
|
} else {
|
||||||
HeadersInjectingWebViewClient(this, source, script, headers)
|
HeadersInjectingWebViewClient(this, verifyComplete, script, headers)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BasicWebViewClient(this, source, script)
|
BasicWebViewClient(this, verifyComplete, script)
|
||||||
}
|
}
|
||||||
|
|
||||||
webview.loadUrl(url, headers)
|
webview.loadUrl(url, headers)
|
||||||
@ -490,11 +502,14 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val VERIFY_LAMBDA_EXTRA = "verify_lambda_extra"
|
||||||
const val SOURCE_ID_EXTRA = "source_id_extra"
|
const val SOURCE_ID_EXTRA = "source_id_extra"
|
||||||
const val COOKIES_EXTRA = "cookies_extra"
|
const val COOKIES_EXTRA = "cookies_extra"
|
||||||
const val SCRIPT_EXTRA = "script_extra"
|
const val SCRIPT_EXTRA = "script_extra"
|
||||||
const val URL_EXTRA = "url_extra"
|
const val URL_EXTRA = "url_extra"
|
||||||
const val ASBTN_EXTRA = "asbtn_extra"
|
const val ASBTN_EXTRA = "asbtn_extra"
|
||||||
|
const val ACTION_NAME_EXTRA = "action_name_extra"
|
||||||
|
const val HEADERS_EXTRA = "headers_extra"
|
||||||
|
|
||||||
const val STAGE_CHECKBOX = 0
|
const val STAGE_CHECKBOX = 0
|
||||||
const val STAGE_GET_AUDIO_BTN_LOCATION = 1
|
const val STAGE_GET_AUDIO_BTN_LOCATION = 1
|
||||||
@ -600,13 +615,13 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]")
|
val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]")
|
||||||
val SPACE_DEDUPE_REGEX = Regex(" +")
|
val SPACE_DEDUPE_REGEX = Regex(" +")
|
||||||
|
|
||||||
fun launch(context: Context,
|
fun launchCaptcha(context: Context,
|
||||||
source: CaptchaCompletionVerifier,
|
source: ActionCompletionVerifier,
|
||||||
cookies: Map<String, String>,
|
cookies: Map<String, String>,
|
||||||
script: String,
|
script: String?,
|
||||||
url: String,
|
url: String,
|
||||||
autoSolveSubmitBtnSelector: String? = null) {
|
autoSolveSubmitBtnSelector: String? = null) {
|
||||||
val intent = Intent(context, SolveCaptchaActivity::class.java).apply {
|
val intent = Intent(context, BrowserActionActivity::class.java).apply {
|
||||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
putExtra(SOURCE_ID_EXTRA, source.id)
|
||||||
putExtra(COOKIES_EXTRA, HashMap(cookies))
|
putExtra(COOKIES_EXTRA, HashMap(cookies))
|
||||||
putExtra(SCRIPT_EXTRA, script)
|
putExtra(SCRIPT_EXTRA, script)
|
||||||
@ -620,25 +635,57 @@ class SolveCaptchaActivity : AppCompatActivity() {
|
|||||||
fun launchUniversal(context: Context,
|
fun launchUniversal(context: Context,
|
||||||
source: HttpSource,
|
source: HttpSource,
|
||||||
url: String) {
|
url: String) {
|
||||||
val intent = Intent(context, SolveCaptchaActivity::class.java).apply {
|
val intent = Intent(context, BrowserActionActivity::class.java).apply {
|
||||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
putExtra(SOURCE_ID_EXTRA, source.id)
|
||||||
putExtra(URL_EXTRA, url)
|
putExtra(URL_EXTRA, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun launchAction(context: Context,
|
||||||
|
completionVerifier: ActionCompletionVerifier,
|
||||||
|
script: String?,
|
||||||
|
url: String,
|
||||||
|
actionName: String) {
|
||||||
|
val intent = Intent(context, BrowserActionActivity::class.java).apply {
|
||||||
|
putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
|
||||||
|
putExtra(SCRIPT_EXTRA, script)
|
||||||
|
putExtra(URL_EXTRA, url)
|
||||||
|
putExtra(ACTION_NAME_EXTRA, actionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchAction(context: Context,
|
||||||
|
completionVerifier: (String) -> Boolean,
|
||||||
|
script: String?,
|
||||||
|
url: String,
|
||||||
|
actionName: String,
|
||||||
|
headers: Map<String, String>? = emptyMap()) {
|
||||||
|
val intent = Intent(context, BrowserActionActivity::class.java).apply {
|
||||||
|
putExtra(HEADERS_EXTRA, HashMap(headers))
|
||||||
|
putExtra(VERIFY_LAMBDA_EXTRA, completionVerifier as Serializable)
|
||||||
|
putExtra(SCRIPT_EXTRA, script)
|
||||||
|
putExtra(URL_EXTRA, url)
|
||||||
|
putExtra(ACTION_NAME_EXTRA, actionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoopCaptchaCompletionVerifier(private val source: HttpSource): DelegatedHttpSource(source),
|
class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHttpSource(source),
|
||||||
CaptchaCompletionVerifier {
|
ActionCompletionVerifier {
|
||||||
override val versionId get() = source.versionId
|
override val versionId get() = source.versionId
|
||||||
override val lang: String get() = source.lang
|
override val lang: String get() = source.lang
|
||||||
|
|
||||||
override fun verifyNoCaptcha(url: String) = false
|
override fun verifyComplete(url: String) = false
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CaptchaCompletionVerifier : Source {
|
interface ActionCompletionVerifier : Source {
|
||||||
fun verifyNoCaptcha(url: String): Boolean
|
fun verifyComplete(url: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -7,11 +7,11 @@ import android.webkit.WebResourceResponse
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
open class HeadersInjectingWebViewClient(activity: SolveCaptchaActivity,
|
open class HeadersInjectingWebViewClient(activity: BrowserActionActivity,
|
||||||
source: CaptchaCompletionVerifier,
|
verifyComplete: (String) -> Boolean,
|
||||||
injectScript: String?,
|
injectScript: String?,
|
||||||
private val headers: Map<String, String>)
|
private val headers: Map<String, String>)
|
||||||
: BasicWebViewClient(activity, source, injectScript) {
|
: BasicWebViewClient(activity, verifyComplete, injectScript) {
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||||
// Temp disabled as it's unreliable
|
// Temp disabled as it's unreliable
|
||||||
|
31
app/src/main/java/exh/util/OkHttpUtil.kt
Normal file
31
app/src/main/java/exh/util/OkHttpUtil.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package exh.util
|
||||||
|
|
||||||
|
import com.elvishew.xlog.XLog
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
|
fun Response.interceptAsHtml(block: (Document) -> Unit): Response {
|
||||||
|
val body = body()
|
||||||
|
if(body != null) {
|
||||||
|
if (body.contentType()?.type() == "text"
|
||||||
|
&& body.contentType()?.subtype() == "html") {
|
||||||
|
val bodyString = body.string()
|
||||||
|
val rebuiltResponse = newBuilder()
|
||||||
|
.body(ResponseBody.create(body.contentType(), bodyString))
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
// Search for captcha
|
||||||
|
val parsed = asJsoup(html = bodyString)
|
||||||
|
block(parsed)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Ignore all errors
|
||||||
|
XLog.w("Interception error!", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rebuiltResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user