Use WebView auth flow for MAL (fixes #4100)

This commit is contained in:
arkon 2020-12-08 22:19:59 -05:00
parent c2b8fea291
commit 2bb7a33bc3
8 changed files with 183 additions and 151 deletions

View file

@ -92,6 +92,9 @@
android:scheme="tachiyomi" /> android:scheme="tachiyomi" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:configChanges="uiMode|orientation|screenSize" />
<activity <activity
android:name=".ui.setting.track.ShikimoriLoginActivity" android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori"> android:label="Shikimori">

View file

@ -100,37 +100,18 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
} }
} }
override fun login(username: String, password: String): Completable { fun login(csrfToken: String): Completable = login("myanimelist", csrfToken)
logout()
return Observable.fromCallable { api.login(username, password) } override fun login(username: String, password: String): Completable {
.doOnNext { csrf -> saveCSRF(csrf) } return Observable.fromCallable { saveCSRF(password) }
.doOnNext { saveCredentials(username, password) } .doOnNext { saveCredentials(username, password) }
.doOnError { logout() } .doOnError { logout() }
.toCompletable() .toCompletable()
} }
fun refreshLogin() {
val username = getUsername()
val password = getPassword()
logout()
try {
val csrf = api.login(username, password)
saveCSRF(csrf)
saveCredentials(username, password)
} catch (e: Exception) {
logout()
throw e
}
}
// Attempt to login again if cookies have been cleared but credentials are still filled
fun ensureLoggedIn() { fun ensureLoggedIn() {
if (isAuthorized) return if (isAuthorized) return
if (!isLogged) throw Exception("MAL Login Credentials not found") if (!isLogged) throw Exception("MAL login credentials not found")
refreshLogin()
} }
override fun logout() { override fun logout() {
@ -139,7 +120,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
} }
val isAuthorized: Boolean private val isAuthorized: Boolean
get() = super.isLogged && get() = super.isLogged &&
getCSRF().isNotEmpty() && getCSRF().isNotEmpty() &&
checkCookies() checkCookies()

View file

@ -133,30 +133,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.map { it ?: throw Exception("Could not find manga") } .map { it ?: throw Exception("Could not find manga") }
} }
fun login(username: String, password: String): String {
val csrf = getSessionInfo()
login(username, password, csrf)
return csrf
}
private fun getSessionInfo(): String {
val response = client.newCall(GET(loginUrl())).execute()
return Jsoup.parse(response.consumeBody())
.select("meta[name=csrf_token]")
.attr("content")
}
private fun login(username: String, password: String, csrf: String) {
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
response.use {
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
}
}
private fun getList(): Observable<List<TrackSearch>> { private fun getList(): Observable<List<TrackSearch>> {
return getListUrl() return getListUrl()
.flatMap { url -> .flatMap { url ->
@ -258,12 +234,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private const val PREFIX_MY = "my:" private const val PREFIX_MY = "my:"
private const val TD = "td" private const val TD = "td"
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId fun loginUrl() = baseUrl.toUri().buildUpon()
private fun loginUrl() = baseUrl.toUri().buildUpon()
.appendPath("login.php") .appendPath("login.php")
.toString() .toString()
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun searchUrl(query: String): String { private fun searchUrl(query: String): String {
val col = "c[]" val col = "c[]"
return baseUrl.toUri().buildUpon() return baseUrl.toUri().buildUpon()
@ -292,17 +268,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath("add.json") .appendPath("add.json")
.toString() .toString()
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
return FormBody.Builder()
.add("user_name", username)
.add("password", password)
.add("cookie", "1")
.add("sublogin", "Login")
.add("submit", "1")
.add(CSRF, csrf)
.build()
}
private fun exportPostBody(): RequestBody { private fun exportPostBody(): RequestBody {
return FormBody.Builder() return FormBody.Builder()
.add("type", "2") .add("type", "2")

View file

@ -14,15 +14,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
myanimelist.ensureLoggedIn() myanimelist.ensureLoggedIn()
val request = chain.request() val request = chain.request()
var response = chain.proceed(updateRequest(request)) return chain.proceed(updateRequest(request))
if (response.code == 400) {
myanimelist.refreshLogin()
response.close()
response = chain.proceed(updateRequest(request))
}
return response
} }
private fun updateRequest(request: Request): Request { private fun updateRequest(request: Request): Request {

View file

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
import eu.kanade.tachiyomi.ui.setting.track.MyAnimeListLoginActivity
import eu.kanade.tachiyomi.ui.setting.track.TrackLoginDialog import eu.kanade.tachiyomi.ui.setting.track.TrackLoginDialog
import eu.kanade.tachiyomi.ui.setting.track.TrackLogoutDialog import eu.kanade.tachiyomi.ui.setting.track.TrackLogoutDialog
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
@ -43,9 +44,7 @@ class SettingsTrackingController :
titleRes = R.string.services titleRes = R.string.services
trackPreference(trackManager.myAnimeList) { trackPreference(trackManager.myAnimeList) {
val dialog = TrackLoginDialog(trackManager.myAnimeList) startActivity(MyAnimeListLoginActivity.newIntent(activity!!))
dialog.targetController = this@SettingsTrackingController
dialog.showDialog(router)
} }
trackPreference(trackManager.aniList) { trackPreference(trackManager.aniList) {
val tabsIntent = CustomTabsIntent.Builder() val tabsIntent = CustomTabsIntent.Builder()
@ -106,6 +105,7 @@ class SettingsTrackingController :
super.onActivityResumed(activity) super.onActivityResumed(activity)
// Manually refresh OAuth trackers' holders // Manually refresh OAuth trackers' holders
updatePreference(trackManager.myAnimeList.id)
updatePreference(trackManager.aniList.id) updatePreference(trackManager.aniList.id)
updatePreference(trackManager.shikimori.id) updatePreference(trackManager.shikimori.id)
updatePreference(trackManager.bangumi.id) updatePreference(trackManager.bangumi.id)

View file

@ -0,0 +1,76 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.webkit.WebView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.webview.BaseWebViewActivity
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class MyAnimeListLoginActivity : BaseWebViewActivity() {
private val trackManager: TrackManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.login)
if (bundle == null) {
binding.webview.webViewClient = object : WebViewClientCompat() {
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
view.loadUrl(url)
return true
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// Get CSRF token from HTML after post-login redirect
if (url == "https://myanimelist.net/") {
view?.evaluateJavascript(
"(function(){return document.querySelector('meta[name=csrf_token]').getAttribute('content')})();"
) {
trackManager.myAnimeList.login(it.replace("\"", ""))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
returnToSettings()
},
{
returnToSettings()
}
)
}
}
}
}
binding.webview.loadUrl(MyAnimeListApi.loginUrl())
}
}
private fun returnToSettings() {
finish()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
companion object {
fun newIntent(context: Context): Intent {
val intent = Intent(context, MyAnimeListLoginActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
return intent
}
}
}

View file

@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.ui.webview
import android.content.pm.ApplicationInfo
import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.widget.Toast
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.navigationClicks
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
internal var bundle: Bundle? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!WebViewUtil.supportsWebView(this)) {
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish()
}
try {
binding = WebviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
} catch (e: Exception) {
// Potentially throws errors like "Error inflating class android.webkit.WebView"
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish()
}
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.navigationClicks()
.onEach { super.onBackPressed() }
.launchIn(scope)
binding.swipeRefresh.isEnabled = false
binding.swipeRefresh.refreshes()
.onEach { refreshPage() }
.launchIn(scope)
if (bundle == null) {
binding.webview.setDefaultSettings()
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG && 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) {
WebView.setWebContentsDebuggingEnabled(true)
}
binding.webview.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
binding.progressBar.isVisible = true
binding.progressBar.progress = newProgress
if (newProgress == 100) {
binding.progressBar.isInvisible = true
}
super.onProgressChanged(view, newProgress)
}
}
} else {
binding.webview.restoreState(bundle)
}
}
override fun onDestroy() {
binding.webview?.destroy()
super.onDestroy()
}
override fun onBackPressed() {
if (binding.webview.canGoBack()) binding.webview.goBack()
else super.onBackPressed()
}
fun refreshPage() {
binding.swipeRefresh.isRefreshing = true
binding.webview.reload()
}
}

View file

@ -1,72 +1,31 @@
package eu.kanade.tachiyomi.ui.webview package eu.kanade.tachiyomi.ui.webview
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.navigationClicks
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class WebViewActivity : BaseActivity<WebviewActivityBinding>() { class WebViewActivity : BaseWebViewActivity() {
private val sourceManager by injectLazy<SourceManager>() private val sourceManager by injectLazy<SourceManager>()
private var bundle: Bundle? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!WebViewUtil.supportsWebView(this)) {
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish()
}
try {
binding = WebviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
} catch (e: Exception) {
// Potentially throws errors like "Error inflating class android.webkit.WebView"
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
finish()
}
title = intent.extras?.getString(TITLE_KEY) title = intent.extras?.getString(TITLE_KEY)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.navigationClicks()
.onEach { super.onBackPressed() }
.launchIn(scope)
binding.swipeRefresh.isEnabled = false
binding.swipeRefresh.refreshes()
.onEach { refreshPage() }
.launchIn(scope)
if (bundle == null) { if (bundle == null) {
val url = intent.extras!!.getString(URL_KEY) ?: return val url = intent.extras!!.getString(URL_KEY) ?: return
@ -79,26 +38,8 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
} }
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
binding.webview.setDefaultSettings()
supportActionBar?.subtitle = url supportActionBar?.subtitle = url
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG && 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) {
WebView.setWebContentsDebuggingEnabled(true)
}
binding.webview.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
binding.progressBar.isVisible = true
binding.progressBar.progress = newProgress
if (newProgress == 100) {
binding.progressBar.isInvisible = true
}
super.onProgressChanged(view, newProgress)
}
}
binding.webview.webViewClient = object : WebViewClientCompat() { binding.webview.webViewClient = object : WebViewClientCompat() {
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
view.loadUrl(url, headers) view.loadUrl(url, headers)
@ -124,16 +65,9 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
} }
binding.webview.loadUrl(url, headers) binding.webview.loadUrl(url, headers)
} else {
binding.webview.restoreState(bundle)
} }
} }
override fun onDestroy() {
binding.webview?.destroy()
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.webview, menu) menuInflater.inflate(R.menu.webview, menu)
return true return true
@ -153,11 +87,6 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
override fun onBackPressed() {
if (binding.webview.canGoBack()) binding.webview.goBack()
else super.onBackPressed()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_web_back -> binding.webview.goBack() R.id.action_web_back -> binding.webview.goBack()
@ -169,11 +98,6 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun refreshPage() {
binding.swipeRefresh.isRefreshing = true
binding.webview.reload()
}
private fun shareWebpage() { private fun shareWebpage() {
try { try {
val intent = Intent(Intent.ACTION_SEND).apply { val intent = Intent(Intent.ACTION_SEND).apply {