mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
Convert the LoginClient to kotlin (#5479)
* Convert the result classes to kotlin * Convert response and callback to kotlin * Cleanup code-quality warnings before converting * Converted the LoginClient to kotlin * Updated the UserExtendedInfoClientTest to be kotlin, and live in the correct spot
This commit is contained in:
parent
0541aacdff
commit
02ce017c98
14 changed files with 370 additions and 450 deletions
|
|
@ -31,11 +31,10 @@ import fr.free.nrw.commons.auth.login.LoginResult;
|
||||||
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
|
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
|
||||||
import fr.free.nrw.commons.utils.ActivityUtils;
|
import fr.free.nrw.commons.utils.ActivityUtils;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
import org.wikipedia.dataclient.ServiceFactory;
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
import org.wikipedia.dataclient.WikiSite;
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||||
import fr.free.nrw.commons.auth.login.LoginClient.LoginCallback;
|
import fr.free.nrw.commons.auth.login.LoginCallback;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import org.wikipedia.dataclient.SharedPreferenceCookieManager
|
||||||
import org.wikipedia.dataclient.WikiSite
|
import org.wikipedia.dataclient.WikiSite
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||||
import fr.free.nrw.commons.auth.login.LoginClient
|
import fr.free.nrw.commons.auth.login.LoginClient
|
||||||
import fr.free.nrw.commons.auth.login.LoginClient.LoginCallback
|
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||||
import fr.free.nrw.commons.auth.login.LoginClient.LoginFailedException
|
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
@ -129,7 +129,7 @@ class CsrfTokenClient(
|
||||||
) = LoginClient()
|
) = LoginClient()
|
||||||
.request(csrfWikiSite, username, password, object : LoginCallback {
|
.request(csrfWikiSite, username, password, object : LoginCallback {
|
||||||
override fun success(loginResult: LoginResult) {
|
override fun success(loginResult: LoginResult) {
|
||||||
if (loginResult.pass()) {
|
if (loginResult.pass) {
|
||||||
sessionManager.updateAccount(loginResult)
|
sessionManager.updateAccount(loginResult)
|
||||||
retryCallback()
|
retryCallback()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
interface LoginCallback {
|
||||||
|
fun success(loginResult: LoginResult)
|
||||||
|
fun twoFactorPrompt(caught: Throwable, token: String?)
|
||||||
|
fun passwordResetPrompt(token: String?)
|
||||||
|
fun error(caught: Throwable)
|
||||||
|
}
|
||||||
|
|
@ -1,259 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth.login;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.wikipedia.dataclient.Service;
|
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.dataclient.mwapi.ListUserResponse;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwServiceError;
|
|
||||||
import org.wikipedia.util.log.L;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Callback;
|
|
||||||
import retrofit2.Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Responsible for making login related requests to the server.
|
|
||||||
*/
|
|
||||||
public class LoginClient {
|
|
||||||
@Nullable private Call<MwQueryResponse> tokenCall;
|
|
||||||
@Nullable private Call<LoginResponse> loginCall;
|
|
||||||
/**
|
|
||||||
* userLanguage
|
|
||||||
* It holds the value of the user's device language code.
|
|
||||||
* For example, if user's device language is English it will hold En
|
|
||||||
* The value will be fetched when the user clicks Login Button in the LoginActivity
|
|
||||||
*/
|
|
||||||
@NonNull private String userLanguage;
|
|
||||||
|
|
||||||
public interface LoginCallback {
|
|
||||||
void success(@NonNull LoginResult result);
|
|
||||||
void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token);
|
|
||||||
void passwordResetPrompt(@Nullable String token);
|
|
||||||
void error(@NonNull Throwable caught);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void request(@NonNull final WikiSite wiki, @NonNull final String userName,
|
|
||||||
@NonNull final String password, @NonNull final LoginCallback cb) {
|
|
||||||
cancel();
|
|
||||||
|
|
||||||
tokenCall = ServiceFactory.get(wiki, LoginInterface.class).getLoginToken();
|
|
||||||
tokenCall.enqueue(new Callback<MwQueryResponse>() {
|
|
||||||
@Override public void onResponse(@NonNull Call<MwQueryResponse> call,
|
|
||||||
@NonNull Response<MwQueryResponse> response) {
|
|
||||||
login(wiki, userName, password, null, null, response.body().query().loginToken(),
|
|
||||||
userLanguage, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable caught) {
|
|
||||||
if (call.isCanceled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cb.error(caught);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void login(@NonNull final WikiSite wiki, @NonNull final String userName, @NonNull final String password,
|
|
||||||
@Nullable final String retypedPassword, @Nullable final String twoFactorCode,
|
|
||||||
@Nullable final String loginToken, @NonNull final String userLanguage, @NonNull final LoginCallback cb) {
|
|
||||||
this.userLanguage = userLanguage;
|
|
||||||
loginCall = TextUtils.isEmpty(twoFactorCode) && TextUtils.isEmpty(retypedPassword)
|
|
||||||
? ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL)
|
|
||||||
: ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, retypedPassword, twoFactorCode, loginToken,
|
|
||||||
userLanguage, true);
|
|
||||||
loginCall.enqueue(new Callback<LoginResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<LoginResponse> call, @NonNull Response<LoginResponse> response) {
|
|
||||||
LoginResponse loginResponse = response.body();
|
|
||||||
LoginResult loginResult = loginResponse.toLoginResult(wiki, password);
|
|
||||||
if (loginResult != null) {
|
|
||||||
if (loginResult.pass() && !TextUtils.isEmpty(loginResult.getUserName())) {
|
|
||||||
// The server could do some transformations on user names, e.g. on some
|
|
||||||
// wikis is uppercases the first letter.
|
|
||||||
String actualUserName = loginResult.getUserName();
|
|
||||||
getExtendedInfo(wiki, actualUserName, loginResult, cb);
|
|
||||||
} else if ("UI".equals(loginResult.getStatus())) {
|
|
||||||
if (loginResult instanceof LoginOAuthResult) {
|
|
||||||
cb.twoFactorPrompt(new LoginFailedException(loginResult.getMessage()), loginToken);
|
|
||||||
} else if (loginResult instanceof LoginResetPasswordResult) {
|
|
||||||
cb.passwordResetPrompt(loginToken);
|
|
||||||
} else {
|
|
||||||
cb.error(new LoginFailedException(loginResult.getMessage()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cb.error(new LoginFailedException(loginResult.getMessage()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cb.error(new IOException("Login failed. Unexpected response."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<LoginResponse> call, @NonNull Throwable t) {
|
|
||||||
if (call.isCanceled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cb.error(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginBlocking(@NonNull final WikiSite wiki, @NonNull final String userName,
|
|
||||||
@NonNull final String password, @Nullable final String twoFactorCode) throws Throwable {
|
|
||||||
Response<MwQueryResponse> tokenResponse = ServiceFactory.get(wiki, LoginInterface.class).getLoginToken().execute();
|
|
||||||
if (tokenResponse.body() == null || TextUtils.isEmpty(tokenResponse.body().query().loginToken())) {
|
|
||||||
throw new IOException("Unexpected response when getting login token.");
|
|
||||||
}
|
|
||||||
String loginToken = tokenResponse.body().query().loginToken();
|
|
||||||
|
|
||||||
Call<LoginResponse> tempLoginCall = StringUtils.defaultIfEmpty(twoFactorCode, "").isEmpty()
|
|
||||||
? ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL)
|
|
||||||
: ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, null, twoFactorCode, loginToken,
|
|
||||||
userLanguage, true);
|
|
||||||
Response<LoginResponse> response = tempLoginCall.execute();
|
|
||||||
LoginResponse loginResponse = response.body();
|
|
||||||
if (loginResponse == null) {
|
|
||||||
throw new IOException("Unexpected response when logging in.");
|
|
||||||
}
|
|
||||||
LoginResult loginResult = loginResponse.toLoginResult(wiki, password);
|
|
||||||
if (loginResult == null) {
|
|
||||||
throw new IOException("Unexpected response when logging in.");
|
|
||||||
}
|
|
||||||
if ("UI".equals(loginResult.getStatus())) {
|
|
||||||
if (loginResult instanceof LoginOAuthResult) {
|
|
||||||
|
|
||||||
// TODO: Find a better way to boil up the warning about 2FA
|
|
||||||
throw new LoginFailedException(loginResult.getMessage());
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new LoginFailedException(loginResult.getMessage());
|
|
||||||
}
|
|
||||||
} else if (!loginResult.pass() || TextUtils.isEmpty(loginResult.getUserName())) {
|
|
||||||
throw new LoginFailedException(loginResult.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private void getExtendedInfo(@NonNull final WikiSite wiki, @NonNull String userName,
|
|
||||||
@NonNull final LoginResult loginResult, @NonNull final LoginCallback cb) {
|
|
||||||
ServiceFactory.get(wiki, LoginInterface.class).getUserInfo(userName)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(response -> {
|
|
||||||
ListUserResponse user = response.query().getUserResponse(userName);
|
|
||||||
int id = response.query().userInfo().id();
|
|
||||||
loginResult.setUserId(id);
|
|
||||||
loginResult.setGroups(user.getGroups());
|
|
||||||
cb.success(loginResult);
|
|
||||||
L.v("Found user ID " + id + " for " + wiki.subdomain());
|
|
||||||
}, caught -> {
|
|
||||||
L.e("Login succeeded but getting group information failed. " + caught);
|
|
||||||
cb.error(caught);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancel() {
|
|
||||||
cancelTokenRequest();
|
|
||||||
cancelLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelTokenRequest() {
|
|
||||||
if (tokenCall == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tokenCall.cancel();
|
|
||||||
tokenCall = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelLogin() {
|
|
||||||
if (loginCall == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loginCall.cancel();
|
|
||||||
loginCall = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class LoginResponse {
|
|
||||||
@SuppressWarnings("unused") @SerializedName("error") @Nullable
|
|
||||||
private MwServiceError error;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused") @SerializedName("clientlogin") @Nullable
|
|
||||||
private ClientLogin clientLogin;
|
|
||||||
|
|
||||||
@Nullable public MwServiceError getError() {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) {
|
|
||||||
return clientLogin != null ? clientLogin.toLoginResult(site, password) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ClientLogin {
|
|
||||||
@SuppressWarnings("unused,NullableProblems") @NonNull private String status;
|
|
||||||
@SuppressWarnings("unused") @Nullable private List<Request> requests;
|
|
||||||
@SuppressWarnings("unused") @Nullable private String message;
|
|
||||||
@SuppressWarnings("unused") @SerializedName("username") @Nullable private String userName;
|
|
||||||
|
|
||||||
LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) {
|
|
||||||
String userMessage = message;
|
|
||||||
if ("UI".equals(status)) {
|
|
||||||
if (requests != null) {
|
|
||||||
for (Request req : requests) {
|
|
||||||
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest".equals(req.id())) {
|
|
||||||
return new LoginOAuthResult(site, status, userName, password, message);
|
|
||||||
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest".equals(req.id())) {
|
|
||||||
return new LoginResetPasswordResult(site, status, userName, password, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!"PASS".equals(status) && !"FAIL".equals(status)) {
|
|
||||||
//TODO: String resource -- Looks like needed for others in this class too
|
|
||||||
userMessage = "An unknown error occurred.";
|
|
||||||
}
|
|
||||||
return new LoginResult(site, status, userName, password, userMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Request {
|
|
||||||
@SuppressWarnings("unused") @Nullable private String id;
|
|
||||||
//@SuppressWarnings("unused") @Nullable private JsonObject metadata;
|
|
||||||
@SuppressWarnings("unused") @Nullable private String required;
|
|
||||||
@SuppressWarnings("unused") @Nullable private String provider;
|
|
||||||
@SuppressWarnings("unused") @Nullable private String account;
|
|
||||||
@SuppressWarnings("unused") @Nullable private Map<String, RequestField> fields;
|
|
||||||
|
|
||||||
@Nullable String id() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class RequestField {
|
|
||||||
@SuppressWarnings("unused") @Nullable private String type;
|
|
||||||
@SuppressWarnings("unused") @Nullable private String label;
|
|
||||||
@SuppressWarnings("unused") @Nullable private String help;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LoginFailedException extends Throwable {
|
|
||||||
public LoginFailedException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
172
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
172
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.wikipedia.dataclient.Service
|
||||||
|
import org.wikipedia.dataclient.ServiceFactory
|
||||||
|
import org.wikipedia.dataclient.WikiSite
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for making login related requests to the server.
|
||||||
|
*/
|
||||||
|
class LoginClient {
|
||||||
|
private var tokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
private var loginCall: Call<LoginResponse?>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* userLanguage
|
||||||
|
* It holds the value of the user's device language code.
|
||||||
|
* For example, if user's device language is English it will hold En
|
||||||
|
* The value will be fetched when the user clicks Login Button in the LoginActivity
|
||||||
|
*/
|
||||||
|
private var userLanguage = ""
|
||||||
|
|
||||||
|
fun request(wiki: WikiSite, userName: String, password: String, cb: LoginCallback) {
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
tokenCall = ServiceFactory.get(wiki, LoginInterface::class.java).getLoginToken()
|
||||||
|
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||||
|
login(wiki, userName, password, null, null,
|
||||||
|
response.body()!!.query()!!.loginToken(), userLanguage, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(caught)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(
|
||||||
|
wiki: WikiSite, userName: String, password: String, retypedPassword: String?,
|
||||||
|
twoFactorCode: String?, loginToken: String?, userLanguage: String, cb: LoginCallback
|
||||||
|
) {
|
||||||
|
this.userLanguage = userLanguage
|
||||||
|
|
||||||
|
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||||
|
ServiceFactory.get(wiki, LoginInterface::class.java)
|
||||||
|
.postLogIn(userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL)
|
||||||
|
} else {
|
||||||
|
ServiceFactory.get(wiki, LoginInterface::class.java).postLogIn(
|
||||||
|
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCall!!.enqueue(object : Callback<LoginResponse?> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<LoginResponse?>,
|
||||||
|
response: Response<LoginResponse?>
|
||||||
|
) {
|
||||||
|
val loginResult = response.body()?.toLoginResult(wiki, password)
|
||||||
|
if (loginResult != null) {
|
||||||
|
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||||
|
// The server could do some transformations on user names, e.g. on some
|
||||||
|
// wikis is uppercases the first letter.
|
||||||
|
getExtendedInfo(wiki, loginResult.userName, loginResult, cb)
|
||||||
|
} else if ("UI" == loginResult.status) {
|
||||||
|
when (loginResult) {
|
||||||
|
is OAuthResult -> cb.twoFactorPrompt(
|
||||||
|
LoginFailedException(loginResult.message),
|
||||||
|
loginToken
|
||||||
|
)
|
||||||
|
|
||||||
|
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||||
|
|
||||||
|
is LoginResult.Result -> cb.error(
|
||||||
|
LoginFailedException(loginResult.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(IOException("Login failed. Unexpected response."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<LoginResponse?>, t: Throwable) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun loginBlocking(wiki: WikiSite, userName: String, password: String, twoFactorCode: String?) {
|
||||||
|
val tokenResponse = ServiceFactory.get(wiki, LoginInterface::class.java).getLoginToken().execute()
|
||||||
|
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) {
|
||||||
|
throw IOException("Unexpected response when getting login token.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||||
|
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) {
|
||||||
|
ServiceFactory.get(wiki, LoginInterface::class.java).postLogIn(
|
||||||
|
userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ServiceFactory.get(wiki, LoginInterface::class.java).postLogIn(
|
||||||
|
userName, password, null, twoFactorCode, loginToken, userLanguage, true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = tempLoginCall.execute()
|
||||||
|
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
val loginResult = loginResponse.toLoginResult(wiki, password) ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
|
||||||
|
if ("UI" == loginResult.status) {
|
||||||
|
if (loginResult is OAuthResult) {
|
||||||
|
// TODO: Find a better way to boil up the warning about 2FA
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) {
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExtendedInfo(
|
||||||
|
wiki: WikiSite, userName: String, loginResult: LoginResult, cb: LoginCallback
|
||||||
|
) = ServiceFactory.get(wiki, LoginInterface::class.java).getUserInfo(userName)
|
||||||
|
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ response: MwQueryResponse? ->
|
||||||
|
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||||
|
loginResult.groups = response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
|
||||||
|
cb.success(loginResult)
|
||||||
|
Timber.v(
|
||||||
|
"Found user ID %s for %s",
|
||||||
|
response?.query()?.userInfo()?.id(),
|
||||||
|
wiki.subdomain()
|
||||||
|
)
|
||||||
|
}, { caught: Throwable ->
|
||||||
|
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
||||||
|
cb.error(caught)
|
||||||
|
})
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
tokenCall?.let {
|
||||||
|
it.cancel()
|
||||||
|
tokenCall = null
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCall?.let {
|
||||||
|
it.cancel()
|
||||||
|
loginCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
class LoginFailedException(message: String?) : Throwable(message)
|
||||||
|
|
@ -25,7 +25,7 @@ interface LoginInterface {
|
||||||
@Field("logintoken") token: String?,
|
@Field("logintoken") token: String?,
|
||||||
@Field("uselang") userLanguage: String?,
|
@Field("uselang") userLanguage: String?,
|
||||||
@Field("loginreturnurl") url: String?
|
@Field("loginreturnurl") url: String?
|
||||||
): Call<LoginClient.LoginResponse?>
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
|
|
@ -38,7 +38,7 @@ interface LoginInterface {
|
||||||
@Field("logintoken") token: String?,
|
@Field("logintoken") token: String?,
|
||||||
@Field("uselang") userLanguage: String?,
|
@Field("uselang") userLanguage: String?,
|
||||||
@Field("logincontinue") loginContinue: Boolean
|
@Field("logincontinue") loginContinue: Boolean
|
||||||
): Call<LoginClient.LoginResponse?>
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
@GET(Service.MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
@GET(Service.MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
||||||
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?>
|
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth.login;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
|
|
||||||
public class LoginOAuthResult extends LoginResult {
|
|
||||||
|
|
||||||
public LoginOAuthResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
|
|
||||||
@Nullable String password, @Nullable String message) {
|
|
||||||
super(site, status, userName, password, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth.login;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
|
|
||||||
public class LoginResetPasswordResult extends LoginResult {
|
|
||||||
public LoginResetPasswordResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
|
|
||||||
@Nullable String password, @Nullable String message) {
|
|
||||||
super(site, status, userName, password, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.Result
|
||||||
|
import org.wikipedia.dataclient.WikiSite
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwServiceError
|
||||||
|
|
||||||
|
class LoginResponse {
|
||||||
|
@SerializedName("error")
|
||||||
|
val error: MwServiceError? = null
|
||||||
|
|
||||||
|
@SerializedName("clientlogin")
|
||||||
|
private val clientLogin: ClientLogin? = null
|
||||||
|
|
||||||
|
fun toLoginResult(site: WikiSite, password: String): LoginResult? {
|
||||||
|
return clientLogin?.toLoginResult(site, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClientLogin {
|
||||||
|
private val status: String? = null
|
||||||
|
private val requests: List<Request>? = null
|
||||||
|
private val message: String? = null
|
||||||
|
|
||||||
|
@SerializedName("username")
|
||||||
|
private val userName: String? = null
|
||||||
|
|
||||||
|
fun toLoginResult(site: WikiSite, password: String): LoginResult {
|
||||||
|
var userMessage = message
|
||||||
|
if ("UI" == status) {
|
||||||
|
if (requests != null) {
|
||||||
|
for (req in requests) {
|
||||||
|
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
|
||||||
|
return OAuthResult(site, status, userName, password, message)
|
||||||
|
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
|
||||||
|
return ResetPasswordResult(site, status, userName, password, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("PASS" != status && "FAIL" != status) {
|
||||||
|
//TODO: String resource -- Looks like needed for others in this class too
|
||||||
|
userMessage = "An unknown error occurred."
|
||||||
|
}
|
||||||
|
return Result(site, status ?: "", userName, password, userMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Request {
|
||||||
|
private val id: String? = null
|
||||||
|
private val required: String? = null
|
||||||
|
private val provider: String? = null
|
||||||
|
private val account: String? = null
|
||||||
|
private val fields: Map<String, RequestField>? = null
|
||||||
|
|
||||||
|
fun id(): String? = id
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RequestField {
|
||||||
|
private val type: String? = null
|
||||||
|
private val label: String? = null
|
||||||
|
private val help: String? = null
|
||||||
|
}
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth.login;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class LoginResult {
|
|
||||||
@NonNull private final WikiSite site;
|
|
||||||
@NonNull private final String status;
|
|
||||||
@Nullable private final String userName;
|
|
||||||
@Nullable private final String password;
|
|
||||||
@Nullable private final String message;
|
|
||||||
|
|
||||||
private int userId;
|
|
||||||
@NonNull private Set<String> groups = Collections.emptySet();
|
|
||||||
|
|
||||||
public LoginResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
|
|
||||||
@Nullable String password, @Nullable String message) {
|
|
||||||
this.site = site;
|
|
||||||
this.status = status;
|
|
||||||
this.userName = userName;
|
|
||||||
this.password = password;
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull public WikiSite getSite() {
|
|
||||||
return site;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean pass() {
|
|
||||||
return "PASS".equals(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean fail() {
|
|
||||||
return "FAIL".equals(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable public String getUserName() {
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserId(int id) {
|
|
||||||
this.userId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGroups(@NonNull Set<String> groups) {
|
|
||||||
this.groups = groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull public Set<String> getGroups() {
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import org.wikipedia.dataclient.WikiSite
|
||||||
|
|
||||||
|
sealed class LoginResult(
|
||||||
|
val site: WikiSite,
|
||||||
|
val status: String,
|
||||||
|
val userName: String?,
|
||||||
|
val password: String?,
|
||||||
|
val message: String?
|
||||||
|
) {
|
||||||
|
var userId = 0
|
||||||
|
var groups = emptySet<String>()
|
||||||
|
val pass: Boolean get() = "PASS" == status
|
||||||
|
|
||||||
|
class Result(
|
||||||
|
site: WikiSite,
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?
|
||||||
|
): LoginResult(site, status, userName, password, message)
|
||||||
|
|
||||||
|
class OAuthResult(
|
||||||
|
site: WikiSite,
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?
|
||||||
|
) : LoginResult(site, status, userName, password, message)
|
||||||
|
|
||||||
|
class ResetPasswordResult(
|
||||||
|
site: WikiSite,
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?
|
||||||
|
) : LoginResult(site, status, userName, password, message)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.stream.MalformedJsonException
|
||||||
|
import fr.free.nrw.commons.MockWebServerTest
|
||||||
|
import io.reactivex.observers.TestObserver
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.wikipedia.dataclient.WikiSite
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||||
|
import org.wikipedia.json.NamespaceTypeAdapter
|
||||||
|
import org.wikipedia.json.PostProcessingTypeAdapter
|
||||||
|
import org.wikipedia.json.UriTypeAdapter
|
||||||
|
import org.wikipedia.json.WikiSiteTypeAdapter
|
||||||
|
import org.wikipedia.page.Namespace
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
|
||||||
|
class UserExtendedInfoClientTest : MockWebServerTest() {
|
||||||
|
private var apiService: LoginInterface? = null
|
||||||
|
private val observer = TestObserver<MwQueryResponse>()
|
||||||
|
private val gson = GsonBuilder()
|
||||||
|
.registerTypeHierarchyAdapter(Uri::class.java, UriTypeAdapter().nullSafe())
|
||||||
|
.registerTypeHierarchyAdapter(Namespace::class.java, NamespaceTypeAdapter().nullSafe())
|
||||||
|
.registerTypeAdapter(WikiSite::class.java, WikiSiteTypeAdapter().nullSafe())
|
||||||
|
.registerTypeAdapterFactory(PostProcessingTypeAdapter())
|
||||||
|
.create()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
|
apiService = Retrofit.Builder()
|
||||||
|
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.baseUrl(server().url)
|
||||||
|
.build()
|
||||||
|
.create(LoginInterface::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRequestSuccess() {
|
||||||
|
enqueueFromFile("user_extended_info.json")
|
||||||
|
|
||||||
|
apiService!!.getUserInfo("USER").subscribe(observer)
|
||||||
|
|
||||||
|
observer
|
||||||
|
.assertComplete()
|
||||||
|
.assertNoErrors()
|
||||||
|
.assertValue { result: MwQueryResponse ->
|
||||||
|
result.query()!!
|
||||||
|
.userInfo()!!.id() == 24531888 && result.query()!!.getUserResponse("USER")!!
|
||||||
|
.name() == "USER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRequestResponse404() {
|
||||||
|
enqueue404()
|
||||||
|
|
||||||
|
apiService!!.getUserInfo("USER").subscribe(observer)
|
||||||
|
|
||||||
|
observer.assertError(Exception::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRequestResponseMalformed() {
|
||||||
|
enqueueMalformed()
|
||||||
|
|
||||||
|
apiService!!.getUserInfo("USER").subscribe(observer)
|
||||||
|
|
||||||
|
observer.assertError(MalformedJsonException::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
package fr.free.nrw.commons.login;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.stream.MalformedJsonException;
|
|
||||||
import fr.free.nrw.commons.MockWebServerTest;
|
|
||||||
import fr.free.nrw.commons.auth.login.LoginInterface;
|
|
||||||
import io.reactivex.observers.TestObserver;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import org.wikipedia.json.NamespaceTypeAdapter;
|
|
||||||
import org.wikipedia.json.PostProcessingTypeAdapter;
|
|
||||||
import org.wikipedia.json.UriTypeAdapter;
|
|
||||||
import org.wikipedia.json.WikiSiteTypeAdapter;
|
|
||||||
import org.wikipedia.page.Namespace;
|
|
||||||
import retrofit2.Retrofit;
|
|
||||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory;
|
|
||||||
|
|
||||||
public class UserExtendedInfoClientTest extends MockWebServerTest {
|
|
||||||
|
|
||||||
private LoginInterface apiService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp() throws Throwable {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
apiService = new Retrofit.Builder()
|
|
||||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
|
||||||
.addConverterFactory(GsonConverterFactory.create(getGson()))
|
|
||||||
.baseUrl(server().getUrl())
|
|
||||||
.build()
|
|
||||||
.create(LoginInterface.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRequestSuccess() throws Throwable {
|
|
||||||
enqueueFromFile("user_extended_info.json");
|
|
||||||
TestObserver<MwQueryResponse> observer = new TestObserver<>();
|
|
||||||
|
|
||||||
apiService.getUserInfo("USER").subscribe(observer);
|
|
||||||
|
|
||||||
observer
|
|
||||||
.assertComplete()
|
|
||||||
.assertNoErrors()
|
|
||||||
.assertValue(
|
|
||||||
result -> result.query().userInfo().id() == 24531888
|
|
||||||
&& result.query().getUserResponse("USER").name().equals("USER")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRequestResponse404() {
|
|
||||||
enqueue404();
|
|
||||||
TestObserver<MwQueryResponse> observer = new TestObserver<>();
|
|
||||||
|
|
||||||
apiService.getUserInfo("USER").subscribe(observer);
|
|
||||||
|
|
||||||
observer.assertError(Exception.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRequestResponseMalformed() {
|
|
||||||
enqueueMalformed();
|
|
||||||
TestObserver<MwQueryResponse> observer = new TestObserver<>();
|
|
||||||
|
|
||||||
apiService.getUserInfo("USER").subscribe(observer);
|
|
||||||
|
|
||||||
observer.assertError(MalformedJsonException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Gson getGson() {
|
|
||||||
return new GsonBuilder()
|
|
||||||
.registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe())
|
|
||||||
.registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe())
|
|
||||||
.registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe())
|
|
||||||
.registerTypeAdapterFactory(new PostProcessingTypeAdapter())
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue