mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Use WebView auth flow for MAL (fixes #4100)
This commit is contained in:
		| @@ -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() | ||||
|   | ||||
| @@ -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<List<TrackSearch>> { | ||||
|         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") | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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<WebviewActivityBinding>() { | ||||
| class WebViewActivity : BaseWebViewActivity() { | ||||
|  | ||||
|     private val sourceManager by injectLazy<SourceManager>() | ||||
|  | ||||
|     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<WebviewActivityBinding>() { | ||||
|             } | ||||
|             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<WebviewActivityBinding>() { | ||||
|             } | ||||
|  | ||||
|             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<WebviewActivityBinding>() { | ||||
|         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<WebviewActivityBinding>() { | ||||
|         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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user