diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java deleted file mode 100644 index 1d5f7a05b..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java +++ /dev/null @@ -1,504 +0,0 @@ -/* - * - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.nextcloud.talk.controllers; - -import android.annotation.SuppressLint; -import android.content.pm.ActivityInfo; -import android.graphics.Bitmap; -import android.net.http.SslCertificate; -import android.net.http.SslError; -import android.os.Build; -import android.os.Bundle; -import android.security.KeyChain; -import android.security.KeyChainException; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.ClientCertRequest; -import android.webkit.CookieSyncManager; -import android.webkit.SslErrorHandler; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.ProgressBar; - -import com.bluelinelabs.conductor.RouterTransaction; -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; -import com.nextcloud.talk.R; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.base.BaseController; -import com.nextcloud.talk.events.CertificateEvent; -import com.nextcloud.talk.jobs.PushRegistrationWorker; -import com.nextcloud.talk.models.LoginData; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.utils.DisplayUtils; -import com.nextcloud.talk.utils.bundle.BundleKeys; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder; -import com.nextcloud.talk.utils.ssl.MagicTrustManager; - -import org.greenrobot.eventbus.EventBus; - -import java.lang.reflect.Field; -import java.net.CookieManager; -import java.net.URLDecoder; -import java.security.PrivateKey; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.res.ResourcesCompat; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import autodagger.AutoInjector; -import butterknife.BindView; -import de.cotech.hw.fido.WebViewFidoBridge; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import io.requery.Persistable; -import io.requery.reactivex.ReactiveEntityStore; - -@AutoInjector(NextcloudTalkApplication.class) -public class WebViewLoginController extends BaseController { - - public static final String TAG = "WebViewLoginController"; - - private final String PROTOCOL_SUFFIX = "://"; - private final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; - - @Inject - UserUtils userUtils; - @Inject - AppPreferences appPreferences; - @Inject - ReactiveEntityStore dataStore; - @Inject - MagicTrustManager magicTrustManager; - @Inject - EventBus eventBus; - @Inject - CookieManager cookieManager; - - - @BindView(R.id.webview) - WebView webView; - - @BindView(R.id.progress_bar) - ProgressBar progressBar; - - private String assembledPrefix; - - private Disposable userQueryDisposable; - - private String baseUrl; - private boolean isPasswordUpdate; - - private String username; - private String password; - private int loginStep = 0; - - private boolean automatedLoginAttempted = false; - - private WebViewFidoBridge webViewFidoBridge; - - public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) { - this.baseUrl = baseUrl; - this.isPasswordUpdate = isPasswordUpdate; - } - - public WebViewLoginController(String baseUrl, boolean isPasswordUpdate, String username, String password) { - this.baseUrl = baseUrl; - this.isPasswordUpdate = isPasswordUpdate; - this.username = username; - this.password = password; - } - - public WebViewLoginController(Bundle args) { - super(args); - } - - private String getWebLoginUserAgent() { - return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + - Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " (" - + getResources().getString(R.string.nc_app_product_name) + ")"; - } - - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_web_view_login, container, false); - } - - @SuppressLint("SetJavaScriptEnabled") - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - - if (getActivity() != null) { - getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - - if (getActionBar() != null) { - getActionBar().hide(); - } - - assembledPrefix = getResources().getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"; - - webView.getSettings().setAllowFileAccess(false); - webView.getSettings().setAllowFileAccessFromFileURLs(false); - webView.getSettings().setJavaScriptEnabled(true); - webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false); - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setUserAgentString(getWebLoginUserAgent()); - webView.getSettings().setSaveFormData(false); - webView.getSettings().setSavePassword(false); - webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH); - webView.clearCache(true); - webView.clearFormData(); - webView.clearHistory(); - WebView.clearClientCertPreferences(null); - - webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView((AppCompatActivity) getActivity(), webView); - - CookieSyncManager.createInstance(getActivity()); - android.webkit.CookieManager.getInstance().removeAllCookies(null); - - Map headers = new HashMap<>(); - headers.put("OCS-APIRequest", "true"); - - webView.setWebViewClient(new WebViewClient() { - private boolean basePageLoaded; - - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - webViewFidoBridge.delegateShouldInterceptRequest(view, request); - return super.shouldInterceptRequest(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - super.onPageStarted(view, url, favicon); - webViewFidoBridge.delegateOnPageStarted(view, url, favicon); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith(assembledPrefix)) { - parseAndLoginFromWebView(url); - return true; - } - return false; - } - - @Override - public void onPageFinished(WebView view, String url) { - loginStep++; - - if (!basePageLoaded) { - if (progressBar != null) { - progressBar.setVisibility(View.GONE); - } - - if (webView != null) { - webView.setVisibility(View.VISIBLE); - } - basePageLoaded = true; - } - - if (!TextUtils.isEmpty(username) && webView != null) { - if (loginStep == 1) { - webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };"); - } else if (!automatedLoginAttempted) { - automatedLoginAttempted = true; - if (TextUtils.isEmpty(password)) { - webView.loadUrl("javascript:var justStore = document.getElementById('user').value = '" + username + "';"); - } else { - webView.loadUrl("javascript: {" + - "document.getElementById('user').value = '" + username + "';" + - "document.getElementById('password').value = '" + password + "';" + - "document.getElementById('submit').click(); };"); - } - } - } - - super.onPageFinished(view, url); - } - - @Override - public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { - UserEntity userEntity = userUtils.getCurrentUser(); - - String alias = null; - if (!isPasswordUpdate) { - alias = appPreferences.getTemporaryClientCertAlias(); - } - - if (TextUtils.isEmpty(alias) && (userEntity != null)) { - alias = userEntity.getClientCertificate(); - } - - if (!TextUtils.isEmpty(alias)) { - String finalAlias = alias; - new Thread(() -> { - try { - PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias); - X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), finalAlias); - if (privateKey != null && certificates != null) { - request.proceed(privateKey, certificates); - } else { - request.cancel(); - } - } catch (KeyChainException | InterruptedException e) { - request.cancel(); - } - }).start(); - } else { - KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> { - if (chosenAlias != null) { - appPreferences.setTemporaryClientCertAlias(chosenAlias); - new Thread(() -> { - PrivateKey privateKey = null; - try { - privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias); - X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), chosenAlias); - if (privateKey != null && certificates != null) { - request.proceed(privateKey, certificates); - } else { - request.cancel(); - } - } catch (KeyChainException | InterruptedException e) { - request.cancel(); - } - }).start(); - } else { - request.cancel(); - } - }, new String[]{"RSA", "EC"}, null, request.getHost(), request.getPort(), null); - } - } - - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - try { - SslCertificate sslCertificate = error.getCertificate(); - Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate"); - f.setAccessible(true); - X509Certificate cert = (X509Certificate) f.get(sslCertificate); - - if (cert == null) { - handler.cancel(); - } else { - try { - magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic"); - handler.proceed(); - } catch (CertificateException exception) { - eventBus.post(new CertificateEvent(cert, magicTrustManager, handler)); - } - } - } catch (Exception exception) { - handler.cancel(); - } - } - - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - super.onReceivedError(view, errorCode, description, failingUrl); - } - }); - - webView.loadUrl(baseUrl + "/index.php/login/flow", headers); - } - - private void dispose() { - if (userQueryDisposable != null && !userQueryDisposable.isDisposed()) { - userQueryDisposable.dispose(); - } - - userQueryDisposable = null; - } - - private void parseAndLoginFromWebView(String dataString) { - LoginData loginData = parseLoginData(assembledPrefix, dataString); - - if (loginData != null) { - dispose(); - - UserEntity currentUser = userUtils.getCurrentUser(); - - ApplicationWideMessageHolder.MessageType messageType = null; - - if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(), baseUrl)) { - messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED; - } - - if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) { - ApplicationWideMessageHolder.getInstance().setMessageType( - ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION); - - if (!isPasswordUpdate) { - getRouter().popToRoot(); - } else { - getRouter().popCurrentController(); - } - } - - ApplicationWideMessageHolder.MessageType finalMessageType = messageType; - cookieManager.getCookieStore().removeAll(); - - if (!isPasswordUpdate && finalMessageType == null) { - Bundle bundle = new Bundle(); - bundle.putString(BundleKeys.INSTANCE.getKEY_USERNAME(), loginData.getUsername()); - bundle.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), loginData.getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_BASE_URL(), loginData.getServerUrl()); - String protocol = ""; - - if (baseUrl.startsWith("http://")) { - protocol = "http://"; - } else if (baseUrl.startsWith("https://")) { - protocol = "https://"; - } - - if (!TextUtils.isEmpty(protocol)) { - bundle.putString(BundleKeys.INSTANCE.getKEY_ORIGINAL_PROTOCOL(), protocol); - } - - getRouter().pushController(RouterTransaction.with(new AccountVerificationController - (bundle)).pushChangeHandler(new HorizontalChangeHandler()) - .popChangeHandler(new HorizontalChangeHandler())); - } else { - if (isPasswordUpdate) { - if (currentUser != null) { - userQueryDisposable = userUtils.createOrUpdateUser(null, loginData.getToken(), - null, null, "", Boolean.TRUE, - null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(userEntity -> { - if (finalMessageType != null) { - ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType); - } - - Data data = - new Data.Builder().putString(PushRegistrationWorker.ORIGIN, - "WebViewLoginController#parseAndLoginFromWebView").build(); - OneTimeWorkRequest pushRegistrationWork = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class) - .setInputData(data) - .build(); - WorkManager.getInstance().enqueue(pushRegistrationWork); - - getRouter().popCurrentController(); - }, throwable -> dispose(), - this::dispose); - } - } else { - if (finalMessageType != null) { - // FIXME when the user registers a new account that was setup before (aka - // ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED) - // The token is not updated in the database and therefor the account not visible/usable - ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType); - } - getRouter().popToRoot(); - - } - } - } - } - - private LoginData parseLoginData(String prefix, String dataString) { - if (dataString.length() < prefix.length()) { - return null; - } - - LoginData loginData = new LoginData(); - - // format is xxx://login/server:xxx&user:xxx&password:xxx - String data = dataString.substring(prefix.length()); - - String[] values = data.split("&"); - - if (values.length != 3) { - return null; - } - - for (String value : values) { - if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginData.setUsername(URLDecoder.decode( - value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); - } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginData.setToken(URLDecoder.decode( - value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); - } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginData.setServerUrl(URLDecoder.decode( - value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); - } else { - return null; - } - } - - if (!TextUtils.isEmpty(loginData.getServerUrl()) && !TextUtils.isEmpty(loginData.getUsername()) && - !TextUtils.isEmpty(loginData.getToken())) { - return loginData; - } else { - return null; - } - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - - if (getActivity() != null && getResources() != null) { - DisplayUtils.applyColorToStatusBar(getActivity(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null)); - DisplayUtils.applyColorToNavigationBar(getActivity().getWindow(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null)); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - dispose(); - } - - @Override - protected void onDestroyView(@NonNull View view) { - super.onDestroyView(view); - if (getActivity() != null) { - getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); - } - } - - @Override - public AppBarLayoutType getAppBarLayoutType() { - return AppBarLayoutType.EMPTY; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt new file mode 100644 index 000000000..a3724915c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt @@ -0,0 +1,473 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.controllers + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.os.Bundle +import android.security.KeyChain +import android.security.KeyChainException +import android.text.TextUtils +import android.view.View +import android.webkit.ClientCertRequest +import android.webkit.CookieSyncManager +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.databinding.ControllerWebViewLoginBinding +import com.nextcloud.talk.events.CertificateEvent +import com.nextcloud.talk.jobs.PushRegistrationWorker +import com.nextcloud.talk.models.LoginData +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder +import com.nextcloud.talk.utils.ssl.MagicTrustManager +import de.cotech.hw.fido.WebViewFidoBridge +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.requery.Persistable +import io.requery.reactivex.ReactiveEntityStore +import org.greenrobot.eventbus.EventBus +import java.lang.reflect.Field +import java.net.CookieManager +import java.net.URLDecoder +import java.security.PrivateKey +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.HashMap +import java.util.Locale +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class WebViewLoginController(args: Bundle? = null) : NewBaseController( + R.layout.controller_web_view_login, + args +) { + private val binding: ControllerWebViewLoginBinding by viewBinding(ControllerWebViewLoginBinding::bind) + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var dataStore: ReactiveEntityStore + + @Inject + lateinit var magicTrustManager: MagicTrustManager + + @Inject + lateinit var eventBus: EventBus + + @Inject + lateinit var cookieManager: CookieManager + + private var assembledPrefix: String? = null + private var userQueryDisposable: Disposable? = null + private var baseUrl: String? = null + private var isPasswordUpdate = false + private var username: String? = null + private var password: String? = null + private var loginStep = 0 + private var automatedLoginAttempted = false + private var webViewFidoBridge: WebViewFidoBridge? = null + + constructor(baseUrl: String?, isPasswordUpdate: Boolean) : this() { + this.baseUrl = baseUrl + this.isPasswordUpdate = isPasswordUpdate + } + + constructor(baseUrl: String?, isPasswordUpdate: Boolean, username: String?, password: String?) : this() { + this.baseUrl = baseUrl + this.isPasswordUpdate = isPasswordUpdate + this.username = username + this.password = password + } + + private val webLoginUserAgent: String + get() = ( + Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + + Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + + " " + + Build.MODEL + + " (" + + resources!!.getString(R.string.nc_app_product_name) + ")" + ) + + @SuppressLint("SetJavaScriptEnabled") + override fun onViewBound(view: View) { + super.onViewBound(view) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + + actionBar?.hide() + + assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/" + binding.webview.settings.allowFileAccess = false + binding.webview.settings.allowFileAccessFromFileURLs = false + binding.webview.settings.javaScriptEnabled = true + binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false + binding.webview.settings.domStorageEnabled = true + binding.webview.settings.setUserAgentString(webLoginUserAgent) + binding.webview.settings.saveFormData = false + binding.webview.settings.savePassword = false + binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH) + binding.webview.clearCache(true) + binding.webview.clearFormData() + binding.webview.clearHistory() + WebView.clearClientCertPreferences(null) + webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, binding.webview) + CookieSyncManager.createInstance(activity) + android.webkit.CookieManager.getInstance().removeAllCookies(null) + val headers: MutableMap = HashMap() + headers.put("OCS-APIRequest", "true") + binding.webview.webViewClient = object : WebViewClient() { + private var basePageLoaded = false + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { + webViewFidoBridge?.delegateShouldInterceptRequest(view, request) + return super.shouldInterceptRequest(view, request) + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + webViewFidoBridge?.delegateOnPageStarted(view, url, favicon) + } + + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + if (url.startsWith(assembledPrefix!!)) { + parseAndLoginFromWebView(url) + return true + } + return false + } + + override fun onPageFinished(view: WebView, url: String) { + loginStep++ + if (!basePageLoaded) { + binding.progressBar.visibility = View.GONE + binding.webview.visibility = View.VISIBLE + + basePageLoaded = true + } + if (!TextUtils.isEmpty(username)) { + if (loginStep == 1) { + binding.webview.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };") + } else if (!automatedLoginAttempted) { + automatedLoginAttempted = true + if (TextUtils.isEmpty(password)) { + binding.webview.loadUrl( + "javascript:var justStore = document.getElementById('user').value = '$username';" + ) + } else { + binding.webview.loadUrl( + "javascript: {" + + "document.getElementById('user').value = '" + username + "';" + + "document.getElementById('password').value = '" + password + "';" + + "document.getElementById('submit').click(); };" + ) + } + } + } + super.onPageFinished(view, url) + } + + override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) { + val userEntity = userUtils.currentUser + var alias: String? = null + if (!isPasswordUpdate) { + alias = appPreferences!!.temporaryClientCertAlias + } + if (TextUtils.isEmpty(alias) && userEntity != null) { + alias = userEntity.clientCertificate + } + if (!TextUtils.isEmpty(alias)) { + val finalAlias = alias + Thread { + try { + val privateKey = KeyChain.getPrivateKey(activity!!, finalAlias!!) + val certificates = KeyChain.getCertificateChain( + activity!!, finalAlias + ) + if (privateKey != null && certificates != null) { + request.proceed(privateKey, certificates) + } else { + request.cancel() + } + } catch (e: KeyChainException) { + request.cancel() + } catch (e: InterruptedException) { + request.cancel() + } + }.start() + } else { + KeyChain.choosePrivateKeyAlias(activity!!, { chosenAlias: String? -> + if (chosenAlias != null) { + appPreferences!!.temporaryClientCertAlias = chosenAlias + Thread { + var privateKey: PrivateKey? = null + try { + privateKey = KeyChain.getPrivateKey(activity!!, chosenAlias) + val certificates = KeyChain.getCertificateChain( + activity!!, chosenAlias + ) + if (privateKey != null && certificates != null) { + request.proceed(privateKey, certificates) + } else { + request.cancel() + } + } catch (e: KeyChainException) { + request.cancel() + } catch (e: InterruptedException) { + request.cancel() + } + }.start() + } else { + request.cancel() + } + }, arrayOf("RSA", "EC"), null, request.host, request.port, null) + } + } + + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + try { + val sslCertificate = error.certificate + val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate") + f.isAccessible = true + val cert = f[sslCertificate] as X509Certificate + if (cert == null) { + handler.cancel() + } else { + try { + magicTrustManager.checkServerTrusted(arrayOf(cert), "generic") + handler.proceed() + } catch (exception: CertificateException) { + eventBus.post(CertificateEvent(cert, magicTrustManager, handler)) + } + } + } catch (exception: Exception) { + handler.cancel() + } + } + + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + super.onReceivedError(view, errorCode, description, failingUrl) + } + } + binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers) + } + + private fun dispose() { + if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) { + userQueryDisposable!!.dispose() + } + userQueryDisposable = null + } + + private fun parseAndLoginFromWebView(dataString: String) { + val loginData = parseLoginData(assembledPrefix, dataString) + if (loginData != null) { + dispose() + val currentUser = userUtils.currentUser + var messageType: ApplicationWideMessageHolder.MessageType? = null + if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.username, baseUrl)) { + messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED + } + if (userUtils.checkIfUserIsScheduledForDeletion(loginData.username, baseUrl)) { + ApplicationWideMessageHolder.getInstance().setMessageType( + ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION + ) + if (!isPasswordUpdate) { + router.popToRoot() + } else { + router.popCurrentController() + } + } + val finalMessageType = messageType + cookieManager.cookieStore.removeAll() + if (!isPasswordUpdate && finalMessageType == null) { + val bundle = Bundle() + bundle.putString(KEY_USERNAME, loginData.username) + bundle.putString(KEY_TOKEN, loginData.token) + bundle.putString(KEY_BASE_URL, loginData.serverUrl) + var protocol = "" + if (baseUrl!!.startsWith("http://")) { + protocol = "http://" + } else if (baseUrl!!.startsWith("https://")) { + protocol = "https://" + } + if (!TextUtils.isEmpty(protocol)) { + bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) + } + router.pushController( + RouterTransaction.with(AccountVerificationController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()) + ) + } else { + if (isPasswordUpdate) { + if (currentUser != null) { + userQueryDisposable = userUtils.createOrUpdateUser( + null, + loginData.token, + null, + null, + "", + java.lang.Boolean.TRUE, + null, + currentUser.id, + null, + appPreferences!!.temporaryClientCertAlias, + null + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (finalMessageType != null) { + ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType) + } + val data = Data.Builder().putString( + PushRegistrationWorker.ORIGIN, + "WebViewLoginController#parseAndLoginFromWebView" + ).build() + val pushRegistrationWork = OneTimeWorkRequest.Builder( + PushRegistrationWorker::class.java + ) + .setInputData(data) + .build() + WorkManager.getInstance().enqueue(pushRegistrationWork) + router.popCurrentController() + }, + { dispose() } + ) { dispose() } + } + } else { + if (finalMessageType != null) { + // FIXME when the user registers a new account that was setup before (aka + // ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED) + // The token is not updated in the database and therefor the account not visible/usable + ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType) + } + router.popToRoot() + } + } + } + } + + private fun parseLoginData(prefix: String?, dataString: String): LoginData? { + if (dataString.length < prefix!!.length) { + return null + } + val loginData = LoginData() + + // format is xxx://login/server:xxx&user:xxx&password:xxx + val data: String = dataString.substring(prefix.length) + val values: Array = data.split("&").toTypedArray() + if (values.size != 3) { + return null + } + for (value in values) { + if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { + loginData.username = URLDecoder.decode( + value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length) + ) + } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { + loginData.token = URLDecoder.decode( + value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length) + ) + } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { + loginData.serverUrl = URLDecoder.decode( + value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length) + ) + } else { + return null + } + } + return if (!TextUtils.isEmpty(loginData.serverUrl) && !TextUtils.isEmpty(loginData.username) && + !TextUtils.isEmpty(loginData.token) + ) { + loginData + } else { + null + } + } + + override fun onAttach(view: View) { + super.onAttach(view) + if (activity != null && resources != null) { + DisplayUtils.applyColorToStatusBar( + activity, + ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) + ) + DisplayUtils.applyColorToNavigationBar( + activity!!.window, + ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) + ) + } + } + + public override fun onDestroy() { + super.onDestroy() + dispose() + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR + } + + init { + sharedApplication!!.componentApplication.inject(this) + } + + override val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.EMPTY + + companion object { + const val TAG = "WebViewLoginController" + private const val PROTOCOL_SUFFIX = "://" + private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":" + } +}