From 2bb7a33bc317e0fb657364bd81394c26024dbe7a Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 8 Dec 2020 22:19:59 -0500 Subject: [PATCH] Use WebView auth flow for MAL (fixes #4100) --- app/src/main/AndroidManifest.xml | 3 + .../data/track/myanimelist/MyAnimeList.kt | 29 +----- .../data/track/myanimelist/MyAnimeListApi.kt | 41 +-------- .../myanimelist/MyAnimeListInterceptor.kt | 10 +- .../ui/setting/SettingsTrackingController.kt | 6 +- .../setting/track/MyAnimeListLoginActivity.kt | 76 ++++++++++++++++ .../ui/webview/BaseWebViewActivity.kt | 91 +++++++++++++++++++ .../tachiyomi/ui/webview/WebViewActivity.kt | 78 +--------------- 8 files changed, 183 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index daea68caa0..2c31ccb459 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,6 +92,9 @@ android:scheme="tachiyomi" /> + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 5aaf8c065b..a5f4f2f61a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -100,37 +100,18 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { } } - override fun login(username: String, password: String): Completable { - logout() + fun login(csrfToken: String): Completable = login("myanimelist", csrfToken) - return Observable.fromCallable { api.login(username, password) } - .doOnNext { csrf -> saveCSRF(csrf) } + override fun login(username: String, password: String): Completable { + return Observable.fromCallable { saveCSRF(password) } .doOnNext { saveCredentials(username, password) } .doOnError { logout() } .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() { if (isAuthorized) return - if (!isLogged) throw Exception("MAL Login Credentials not found") - - refreshLogin() + if (!isLogged) throw Exception("MAL login credentials not found") } override fun logout() { @@ -139,7 +120,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!) } - val isAuthorized: Boolean + private val isAuthorized: Boolean get() = super.isLogged && getCSRF().isNotEmpty() && checkCookies() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 39ae211258..ba3ce71813 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -133,30 +133,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .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> { return getListUrl() .flatMap { url -> @@ -258,12 +234,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI private const val PREFIX_MY = "my:" private const val TD = "td" - private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId - - private fun loginUrl() = baseUrl.toUri().buildUpon() + fun loginUrl() = baseUrl.toUri().buildUpon() .appendPath("login.php") .toString() + private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId + private fun searchUrl(query: String): String { val col = "c[]" return baseUrl.toUri().buildUpon() @@ -292,17 +268,6 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendPath("add.json") .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 { return FormBody.Builder() .add("type", "2") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index a39f8f6edb..e8d61814bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -14,15 +14,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor myanimelist.ensureLoggedIn() val request = chain.request() - var response = chain.proceed(updateRequest(request)) - - if (response.code == 400) { - myanimelist.refreshLogin() - response.close() - response = chain.proceed(updateRequest(request)) - } - - return response + return chain.proceed(updateRequest(request)) } private fun updateRequest(request: Request): Request { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt index ffb2daee8d..315e117e41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -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.bangumi.BangumiApi 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.TrackLogoutDialog import eu.kanade.tachiyomi.util.preference.defaultValue @@ -43,9 +44,7 @@ class SettingsTrackingController : titleRes = R.string.services trackPreference(trackManager.myAnimeList) { - val dialog = TrackLoginDialog(trackManager.myAnimeList) - dialog.targetController = this@SettingsTrackingController - dialog.showDialog(router) + startActivity(MyAnimeListLoginActivity.newIntent(activity!!)) } trackPreference(trackManager.aniList) { val tabsIntent = CustomTabsIntent.Builder() @@ -106,6 +105,7 @@ class SettingsTrackingController : super.onActivityResumed(activity) // Manually refresh OAuth trackers' holders + updatePreference(trackManager.myAnimeList.id) updatePreference(trackManager.aniList.id) updatePreference(trackManager.shikimori.id) updatePreference(trackManager.bangumi.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt new file mode 100644 index 0000000000..3f17502f4c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/MyAnimeListLoginActivity.kt @@ -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 + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt new file mode 100644 index 0000000000..b0f80e2c33 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/BaseWebViewActivity.kt @@ -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() { + + 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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 89b7933f9b..ce2becb968 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -1,72 +1,31 @@ package eu.kanade.tachiyomi.ui.webview -import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.pm.ApplicationInfo import android.graphics.Bitmap import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.webkit.WebChromeClient import android.webkit.WebView -import android.widget.Toast 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.databinding.WebviewActivityBinding import eu.kanade.tachiyomi.source.SourceManager 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.WebViewUtil import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser -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 import uy.kohesive.injekt.injectLazy -class WebViewActivity : BaseActivity() { +class WebViewActivity : BaseWebViewActivity() { private val sourceManager by injectLazy() - private var bundle: Bundle? = null - - @SuppressLint("SetJavaScriptEnabled") 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() - } - 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) { val url = intent.extras!!.getString(URL_KEY) ?: return @@ -79,26 +38,8 @@ class WebViewActivity : BaseActivity() { } headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH - binding.webview.setDefaultSettings() - 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() { override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { view.loadUrl(url, headers) @@ -124,16 +65,9 @@ class WebViewActivity : BaseActivity() { } binding.webview.loadUrl(url, headers) - } else { - binding.webview.restoreState(bundle) } } - override fun onDestroy() { - binding.webview?.destroy() - super.onDestroy() - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.webview, menu) return true @@ -153,11 +87,6 @@ class WebViewActivity : BaseActivity() { return super.onPrepareOptionsMenu(menu) } - override fun onBackPressed() { - if (binding.webview.canGoBack()) binding.webview.goBack() - else super.onBackPressed() - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_web_back -> binding.webview.goBack() @@ -169,11 +98,6 @@ class WebViewActivity : BaseActivity() { return super.onOptionsItemSelected(item) } - private fun refreshPage() { - binding.swipeRefresh.isRefreshing = true - binding.webview.reload() - } - private fun shareWebpage() { try { val intent = Intent(Intent.ACTION_SEND).apply {