mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
commit
364199dcb2
12 changed files with 431 additions and 222 deletions
|
|
@ -19,6 +19,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
|
|||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import fr.free.nrw.commons.auth.AccountUtil;
|
||||
import org.acra.ACRA;
|
||||
import org.acra.ReportingInteractionMode;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
|
|
@ -35,7 +36,6 @@ import org.apache.http.params.CoreProtocolPNames;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import fr.free.nrw.commons.auth.WikiAccountAuthenticator;
|
||||
import fr.free.nrw.commons.caching.CacheController;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -73,6 +73,8 @@ public class CommonsApplication extends Application {
|
|||
|
||||
public CacheController cacheData;
|
||||
|
||||
public static CommonsApplication app;
|
||||
|
||||
public static AbstractHttpClient createHttpClient() {
|
||||
BasicHttpParams params = new BasicHttpParams();
|
||||
SchemeRegistry schemeRegistry = new SchemeRegistry();
|
||||
|
|
@ -91,6 +93,7 @@ public class CommonsApplication extends Application {
|
|||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
app = this;
|
||||
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
|
||||
|
|
@ -167,7 +170,7 @@ public class CommonsApplication extends Application {
|
|||
public Account getCurrentAccount() {
|
||||
if(currentAccount == null) {
|
||||
AccountManager accountManager = AccountManager.get(this);
|
||||
Account[] allAccounts = accountManager.getAccountsByType(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
|
||||
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
|
||||
if(allAccounts.length != 0) {
|
||||
currentAccount = allAccounts[0];
|
||||
}
|
||||
|
|
@ -183,7 +186,7 @@ public class CommonsApplication extends Application {
|
|||
return false; // This should never happen
|
||||
}
|
||||
|
||||
accountManager.invalidateAuthToken(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE, api.getAuthCookie());
|
||||
accountManager.invalidateAuthToken(AccountUtil.accountType(), api.getAuthCookie());
|
||||
try {
|
||||
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
|
||||
api.setAuthCookie(authCookie);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import org.mediawiki.api.ApiResult;
|
|||
*/
|
||||
public class MWApi extends org.mediawiki.api.MWApi {
|
||||
|
||||
/** We don't actually use this but need to pass it in requests */
|
||||
private static String LOGIN_RETURN_TO_URL = "https://commons.wikimedia.org";
|
||||
|
||||
public MWApi(String apiURL, AbstractHttpClient client) {
|
||||
super(apiURL, client);
|
||||
}
|
||||
|
|
@ -17,41 +20,69 @@ public class MWApi extends org.mediawiki.api.MWApi {
|
|||
/**
|
||||
* @param username String
|
||||
* @param password String
|
||||
* @return String On success: "PASS"
|
||||
* continue: "2FA" (More information required for 2FA)
|
||||
* failure: A failure message code (defined by mediawiki)
|
||||
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
|
||||
* @return String as returned by this.getErrorCodeToReturn()
|
||||
* @throws IOException On api request IO issue
|
||||
*/
|
||||
public String login(String username, String password) throws IOException {
|
||||
|
||||
/** Request a login token to be used later to log in. */
|
||||
ApiResult tokenData = this.action("query")
|
||||
.param("action", "query")
|
||||
.param("meta", "tokens")
|
||||
.param("type", "login")
|
||||
.post();
|
||||
String token = tokenData.getString("/api/query/tokens/@logintoken");
|
||||
|
||||
/** Actually log in. */
|
||||
ApiResult loginData = this.action("clientlogin")
|
||||
String token = this.getLoginToken();
|
||||
ApiResult loginApiResult = this.action("clientlogin")
|
||||
.param("rememberMe", "1")
|
||||
.param("username", username)
|
||||
.param("password", password)
|
||||
.param("logintoken", token)
|
||||
.param("loginreturnurl", "http://example.com/")//TODO return to url?
|
||||
.param("loginreturnurl", LOGIN_RETURN_TO_URL)
|
||||
.post();
|
||||
String status = loginData.getString("/api/clientlogin/@status");
|
||||
return this.getErrorCodeToReturn( loginApiResult );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param username String
|
||||
* @param password String
|
||||
* @param twoFactorCode String
|
||||
* @return String as returned by this.getErrorCodeToReturn()
|
||||
* @throws IOException On api request IO issue
|
||||
*/
|
||||
public String login(String username, String password, String twoFactorCode) throws IOException {
|
||||
String token = this.getLoginToken();//TODO cache this instead of calling again when 2FAing
|
||||
ApiResult loginApiResult = this.action("clientlogin")
|
||||
.param("rememberMe", "1")
|
||||
.param("username", username)
|
||||
.param("password", password)
|
||||
.param("logintoken", token)
|
||||
.param("logincontinue", "1")
|
||||
.param("OATHToken", twoFactorCode)
|
||||
.post();
|
||||
|
||||
return this.getErrorCodeToReturn( loginApiResult );
|
||||
}
|
||||
|
||||
private String getLoginToken() throws IOException {
|
||||
ApiResult tokenResult = this.action("query")
|
||||
.param("action", "query")
|
||||
.param("meta", "tokens")
|
||||
.param("type", "login")
|
||||
.post();
|
||||
return tokenResult.getString("/api/query/tokens/@logintoken");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loginApiResult ApiResult Any clientlogin api result
|
||||
* @return String On success: "PASS"
|
||||
* continue: "2FA" (More information required for 2FA)
|
||||
* failure: A failure message code (defined by mediawiki)
|
||||
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
|
||||
*/
|
||||
private String getErrorCodeToReturn( ApiResult loginApiResult ) {
|
||||
String status = loginApiResult.getString("/api/clientlogin/@status");
|
||||
if (status.equals("PASS")) {
|
||||
this.isLoggedIn = true;
|
||||
return status;
|
||||
} else if (status.equals("FAIL")) {
|
||||
return loginData.getString("/api/clientlogin/@messagecode");
|
||||
return loginApiResult.getString("/api/clientlogin/@messagecode");
|
||||
} else if (
|
||||
status.equals("UI")
|
||||
&& loginData.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
||||
&& loginData.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
|
||||
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
|
||||
) {
|
||||
return "2FA";
|
||||
}
|
||||
|
|
@ -60,5 +91,4 @@ public class MWApi extends org.mediawiki.api.MWApi {
|
|||
return "genericerror-" + status;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
61
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
Normal file
61
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class AccountUtil {
|
||||
|
||||
public static void createAccount(@Nullable AccountAuthenticatorResponse response,
|
||||
String username, String password) {
|
||||
|
||||
Account account = new Account(username, accountType());
|
||||
boolean created = accountManager().addAccountExplicitly(account, password, null);
|
||||
|
||||
Timber.d("account creation " + (created ? "successful" : "failure"));
|
||||
|
||||
if (created) {
|
||||
if (response != null) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
||||
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType());
|
||||
|
||||
|
||||
response.onResult(bundle);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (response != null) {
|
||||
response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "");
|
||||
}
|
||||
Timber.d("account creation failure");
|
||||
}
|
||||
|
||||
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
||||
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String accountType() {
|
||||
return "fr.free.nrw.commons";
|
||||
}
|
||||
|
||||
private static AccountManager accountManager() {
|
||||
return AccountManager.get(app());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static CommonsApplication app() {
|
||||
return CommonsApplication.app;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,8 +20,8 @@ public abstract class AuthenticatedActivity extends BaseActivity {
|
|||
|
||||
private String authCookie;
|
||||
|
||||
public AuthenticatedActivity(String accountType) {
|
||||
this.accountType = accountType;
|
||||
public AuthenticatedActivity() {
|
||||
this.accountType = AccountUtil.accountType();
|
||||
}
|
||||
|
||||
private class GetAuthCookieTask extends AsyncTask<Void, String, String> {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorActivity;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.text.Editable;
|
||||
|
|
@ -21,18 +15,12 @@ import android.view.inputmethod.EditorInfo;
|
|||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.EventLog;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
|
|
@ -40,139 +28,49 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
|
||||
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
|
||||
|
||||
private CommonsApplication app;
|
||||
|
||||
private SharedPreferences prefs = null;
|
||||
|
||||
Button loginButton;
|
||||
Button signupButton;
|
||||
EditText usernameEdit;
|
||||
private Button loginButton;
|
||||
private EditText usernameEdit;
|
||||
EditText passwordEdit;
|
||||
ProgressDialog dialog;
|
||||
|
||||
private class LoginTask extends AsyncTask<String, String, String> {
|
||||
|
||||
Activity context;
|
||||
String username;
|
||||
String password;
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
super.onPostExecute(result);
|
||||
Timber.d("Login done!");
|
||||
|
||||
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
|
||||
.param("username", username)
|
||||
.param("result", result)
|
||||
.log();
|
||||
|
||||
if (result.equals("PASS")) {
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
Toast successToast = Toast.makeText(context, R.string.login_success, Toast.LENGTH_SHORT);
|
||||
successToast.show();
|
||||
Account account = new Account(username, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
|
||||
boolean accountCreated = AccountManager.get(context).addAccountExplicitly(account, password, null);
|
||||
|
||||
Bundle extras = context.getIntent().getExtras();
|
||||
|
||||
if (extras != null) {
|
||||
Timber.d("Bundle of extras: %s", extras);
|
||||
if (accountCreated) { // Pass the new account back to the account manager
|
||||
AccountAuthenticatorResponse response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
||||
Bundle authResult = new Bundle();
|
||||
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
||||
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
|
||||
|
||||
if (response != null) {
|
||||
response.onResult(authResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: If the user turns it off, it shouldn't be auto turned back on
|
||||
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
|
||||
|
||||
Intent intent = new Intent(context, ContributionsActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
|
||||
} else {
|
||||
int response;
|
||||
// Match known failure message codes and provide messages
|
||||
if(result.equals("NetworkFailure")) {
|
||||
// Matches NetworkFailure which is created by the doInBackground method
|
||||
response = R.string.login_failed_network;
|
||||
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
||||
// Matches nosuchuser, nosuchusershort, noname
|
||||
response = R.string.login_failed_username;
|
||||
passwordEdit.setText("");
|
||||
|
||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
||||
// Matches wrongpassword, wrongpasswordempty
|
||||
response = R.string.login_failed_password;
|
||||
passwordEdit.setText("");
|
||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
||||
// Matches unknown throttle error codes
|
||||
response = R.string.login_failed_throttled;
|
||||
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
||||
// Matches login-userblocked
|
||||
response = R.string.login_failed_blocked;
|
||||
} else if (result.equals("2FA")){
|
||||
response = R.string.login_failed_2fa_not_supported;
|
||||
} else {
|
||||
// Occurs with unhandled login failure codes
|
||||
Timber.d("Login failed with reason: %s", result);
|
||||
response = R.string.login_failed_generic;
|
||||
}
|
||||
Toast.makeText(getApplicationContext(), response, Toast.LENGTH_LONG).show();
|
||||
dialog.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
dialog = new ProgressDialog(context);
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setTitle(getString(R.string.logging_in_title));
|
||||
dialog.setMessage(getString(R.string.logging_in_message));
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
LoginTask(Activity context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
username = params[0];
|
||||
password = params[1];
|
||||
try {
|
||||
return app.getApi().login(username, password);
|
||||
} catch (IOException e) {
|
||||
// Do something better!
|
||||
return "NetworkFailure";
|
||||
}
|
||||
}
|
||||
}
|
||||
EditText twoFactorEdit;
|
||||
ProgressDialog progressDialog;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
app = (CommonsApplication) this.getApplicationContext();
|
||||
|
||||
setContentView(R.layout.activity_login);
|
||||
final LoginActivity that = this;
|
||||
|
||||
loginButton = (Button) findViewById(R.id.loginButton);
|
||||
signupButton = (Button) findViewById(R.id.signupButton);
|
||||
Button signupButton = (Button) findViewById(R.id.signupButton);
|
||||
usernameEdit = (EditText) findViewById(R.id.loginUsername);
|
||||
passwordEdit = (EditText) findViewById(R.id.loginPassword);
|
||||
final LoginActivity that = this;
|
||||
twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor);
|
||||
|
||||
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
|
||||
|
||||
TextWatcher loginEnabler = new TextWatcher() {
|
||||
TextWatcher loginEnabler = newLoginTextWatcher();
|
||||
usernameEdit.addTextChangedListener(loginEnabler);
|
||||
passwordEdit.addTextChangedListener(loginEnabler);
|
||||
twoFactorEdit.addTextChangedListener(loginEnabler);
|
||||
passwordEdit.setOnEditorActionListener( newLoginInputActionListener() );
|
||||
|
||||
loginButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
that.performLogin();
|
||||
}
|
||||
});
|
||||
signupButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) { that.signUp(v); }
|
||||
});
|
||||
}
|
||||
|
||||
private TextWatcher newLoginTextWatcher() {
|
||||
return new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { }
|
||||
|
||||
|
|
@ -181,17 +79,21 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
if(usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0) {
|
||||
if(
|
||||
usernameEdit.getText().length() != 0 &&
|
||||
passwordEdit.getText().length() != 0 &&
|
||||
( BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE )
|
||||
) {
|
||||
loginButton.setEnabled(true);
|
||||
} else {
|
||||
loginButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
usernameEdit.addTextChangedListener(loginEnabler);
|
||||
passwordEdit.addTextChangedListener(loginEnabler);
|
||||
passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
private TextView.OnEditorActionListener newLoginInputActionListener() {
|
||||
return new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||
if (loginButton.isEnabled()) {
|
||||
|
|
@ -205,36 +107,28 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
loginButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
that.performLogin();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (prefs.getBoolean("firstrun", true)) {
|
||||
// Do first run stuff here then set 'firstrun' as false
|
||||
Intent welcomeIntent = new Intent(this, WelcomeActivity.class);
|
||||
startActivity(welcomeIntent);
|
||||
this.startWelcomeIntent();
|
||||
prefs.edit().putBoolean("firstrun", false).apply();
|
||||
}
|
||||
}
|
||||
|
||||
private void startWelcomeIntent() {
|
||||
Intent welcomeIntent = new Intent(this, WelcomeActivity.class);
|
||||
startActivity(welcomeIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
try {
|
||||
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
if (progressDialog != null && progressDialog.isShowing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
|
@ -243,15 +137,27 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
}
|
||||
|
||||
private void performLogin() {
|
||||
String username = usernameEdit.getText().toString();
|
||||
// Because Mediawiki is upercase-first-char-then-case-sensitive :)
|
||||
String canonicalUsername = Utils.capitalize(username.substring(0,1)) + username.substring(1);
|
||||
|
||||
String password = passwordEdit.getText().toString();
|
||||
|
||||
Timber.d("Login to start!");
|
||||
LoginTask task = new LoginTask(this);
|
||||
task.execute(canonicalUsername, password);
|
||||
LoginTask task = getLoginTask();
|
||||
task.execute();
|
||||
}
|
||||
|
||||
private LoginTask getLoginTask() {
|
||||
return new LoginTask(
|
||||
this,
|
||||
canonicializeUsername( usernameEdit.getText().toString() ),
|
||||
passwordEdit.getText().toString(),
|
||||
twoFactorEdit.getText().toString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
|
||||
* @param username String
|
||||
* @return String canonicial username
|
||||
*/
|
||||
private String canonicializeUsername( String username ) {
|
||||
return Utils.capitalize(username.substring(0,1)) + username.substring(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -264,9 +170,42 @@ public class LoginActivity extends AccountAuthenticatorActivity {
|
|||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
//Called when Sign Up button is clicked
|
||||
/**
|
||||
* Called when Sign Up button is clicked.
|
||||
* @param view View
|
||||
*/
|
||||
public void signUp(View view) {
|
||||
Intent intent = new Intent(this, SignupActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void askUserForTwoFactorAuth() {
|
||||
if(BuildConfig.DEBUG) {
|
||||
twoFactorEdit.setVisibility(View.VISIBLE);
|
||||
showUserToastAndCancelDialog( R.string.login_failed_2fa_needed );
|
||||
}else{
|
||||
showUserToastAndCancelDialog( R.string.login_failed_2fa_not_supported );
|
||||
}
|
||||
}
|
||||
|
||||
public void showUserToastAndCancelDialog( int resId ) {
|
||||
showUserToast( resId );
|
||||
progressDialog.cancel();
|
||||
}
|
||||
|
||||
private void showUserToast( int resId ) {
|
||||
Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public void showSuccessToastAndDismissDialog() {
|
||||
Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT);
|
||||
successToast.show();
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
|
||||
public void emptySensitiveEditFields() {
|
||||
passwordEdit.setText("");
|
||||
twoFactorEdit.setText("");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
129
app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java
Normal file
129
app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package fr.free.nrw.commons.auth;
|
||||
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.EventLog;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import timber.log.Timber;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class LoginTask extends AsyncTask<String, String, String> {
|
||||
|
||||
private LoginActivity loginActivity;
|
||||
private String username;
|
||||
private String password;
|
||||
private String twoFactorCode = "";
|
||||
private CommonsApplication app;
|
||||
|
||||
public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) {
|
||||
this.loginActivity = loginActivity;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.twoFactorCode = twoFactorCode;
|
||||
app = (CommonsApplication) loginActivity.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
loginActivity.progressDialog = new ProgressDialog(loginActivity);
|
||||
loginActivity.progressDialog.setIndeterminate(true);
|
||||
loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title));
|
||||
loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message));
|
||||
loginActivity.progressDialog.setCanceledOnTouchOutside(false);
|
||||
loginActivity.progressDialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
try {
|
||||
if (twoFactorCode.isEmpty()) {
|
||||
return app.getApi().login(username, password);
|
||||
} else {
|
||||
return app.getApi().login(username, password, twoFactorCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Do something better!
|
||||
return "NetworkFailure";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
super.onPostExecute(result);
|
||||
Timber.d("Login done!");
|
||||
|
||||
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
|
||||
.param("username", username)
|
||||
.param("result", result)
|
||||
.log();
|
||||
|
||||
if (result.equals("PASS")) {
|
||||
handlePassResult();
|
||||
} else {
|
||||
handleOtherResults( result );
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePassResult() {
|
||||
loginActivity.showSuccessToastAndDismissDialog();
|
||||
|
||||
AccountAuthenticatorResponse response = null;
|
||||
|
||||
Bundle extras = loginActivity.getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
Timber.d("Bundle of extras: %s", extras);
|
||||
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
|
||||
if (response != null) {
|
||||
Bundle authResult = new Bundle();
|
||||
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
|
||||
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType());
|
||||
response.onResult(authResult);
|
||||
}
|
||||
}
|
||||
|
||||
AccountUtil.createAccount( response, username, password );
|
||||
|
||||
Intent intent = new Intent(loginActivity, ContributionsActivity.class);
|
||||
loginActivity.startActivity(intent);
|
||||
loginActivity.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Match known failure message codes and provide messages
|
||||
* @param result String
|
||||
*/
|
||||
private void handleOtherResults( String result ) {
|
||||
if (result.equals("NetworkFailure")) {
|
||||
// Matches NetworkFailure which is created by the doInBackground method
|
||||
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_network );
|
||||
} else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
|
||||
// Matches nosuchuser, nosuchusershort, noname
|
||||
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_username );
|
||||
loginActivity.emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
|
||||
// Matches wrongpassword, wrongpasswordempty
|
||||
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_password );
|
||||
loginActivity.emptySensitiveEditFields();
|
||||
} else if (result.toLowerCase().contains("throttle".toLowerCase())) {
|
||||
// Matches unknown throttle error codes
|
||||
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_throttled );
|
||||
} else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
|
||||
// Matches login-userblocked
|
||||
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_blocked );
|
||||
} else if (result.equals("2FA")) {
|
||||
loginActivity.askUserForTwoFactorAuth();
|
||||
} else {
|
||||
// Occurs with unhandled login failure codes
|
||||
Timber.d("Login failed with reason: %s", result);
|
||||
loginActivity.showUserToastAndCancelDialog( R.string.login_failed_generic );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,39 +11,72 @@ import android.os.Bundle;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.MWApi;
|
||||
|
||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||
|
||||
public static final String COMMONS_ACCOUNT_TYPE = "fr.free.nrw.commons";
|
||||
private Context context;
|
||||
|
||||
public WikiAccountAuthenticator(Context context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private Bundle unsupportedOperation() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
|
||||
|
||||
// HACK: the docs indicate that this is a required key bit it's not displayed to the user.
|
||||
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "");
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private boolean supportedAccountType(@Nullable String type) {
|
||||
return AccountUtil.accountType().equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
|
||||
final Intent intent = new Intent(context, LoginActivity.class);
|
||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull String accountType, @Nullable String authTokenType,
|
||||
@Nullable String[] requiredFeatures, @Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
|
||||
if (!supportedAccountType(accountType)) {
|
||||
return unsupportedOperation();
|
||||
}
|
||||
|
||||
return addAccount(response);
|
||||
}
|
||||
|
||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
||||
Intent Intent = new Intent(context, LoginActivity.class);
|
||||
Intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(AccountManager.KEY_INTENT, Intent);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
return unsupportedOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
return null;
|
||||
return unsupportedOperation();
|
||||
}
|
||||
|
||||
private String getAuthCookie(String username, String password) throws IOException {
|
||||
MWApi api = CommonsApplication.createMWApi();
|
||||
//TODO add 2fa support here
|
||||
String result = api.login(username, password);
|
||||
if(result.equals("PASS")) {
|
||||
return api.getAuthCookie();
|
||||
|
|
@ -69,7 +102,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|||
if (authCookie != null) {
|
||||
final Bundle result = new Bundle();
|
||||
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
|
||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, COMMONS_ACCOUNT_TYPE);
|
||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType());
|
||||
result.putString(AccountManager.KEY_AUTHTOKEN, authCookie);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -86,21 +119,31 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getAuthTokenLabel(String authTokenType) {
|
||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
||||
//Note: the wikipedia app actually returns a string here....
|
||||
//return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
|
||||
final Bundle result = new Bundle();
|
||||
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||
return result;
|
||||
public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @NonNull String[] features)
|
||||
throws NetworkErrorException {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
||||
@NonNull Account account, @Nullable String authTokenType,
|
||||
@Nullable Bundle options)
|
||||
throws NetworkErrorException {
|
||||
return unsupportedOperation();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import fr.free.nrw.commons.HandlerService;
|
|||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||
import fr.free.nrw.commons.auth.WikiAccountAuthenticator;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.UploadService;
|
||||
|
|
@ -62,10 +61,6 @@ public class ContributionsActivity
|
|||
*/
|
||||
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, " + Contribution.Table.COLUMN_UPLOADED + " DESC , (" + Contribution.Table.COLUMN_TIMESTAMP + " * " + Contribution.Table.COLUMN_STATE + ")";
|
||||
|
||||
public ContributionsActivity() {
|
||||
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
|
||||
}
|
||||
|
||||
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder binder) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import fr.free.nrw.commons.EventLog;
|
|||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||
import fr.free.nrw.commons.auth.WikiAccountAuthenticator;
|
||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
|
|
@ -53,10 +52,6 @@ public class MultipleShareActivity
|
|||
|
||||
private UploadController uploadController;
|
||||
|
||||
public MultipleShareActivity() {
|
||||
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Media getMediaAtPosition(int i) {
|
||||
return photosList.get(i);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import fr.free.nrw.commons.EventLog;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||
import fr.free.nrw.commons.auth.WikiAccountAuthenticator;
|
||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
||||
|
|
@ -77,10 +76,6 @@ public class ShareActivity
|
|||
private String description;
|
||||
private Snackbar snackbar;
|
||||
|
||||
public ShareActivity() {
|
||||
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user taps the submit button
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -62,6 +62,24 @@
|
|||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:passwordToggleEnabled="false"
|
||||
>
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/loginTwoFactor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/_2fa_code"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/loginButton"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -75,7 +93,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:onClick="signUp"
|
||||
android:text="@string/signup"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
<string name="login_failed_password">Unable to login - please check your password</string>
|
||||
<string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string>
|
||||
<string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string>
|
||||
<string name="login_failed_2fa_not_supported">The app doesn\'t currently support 2 Factor Authentication.</string>
|
||||
<string name="login_failed_2fa_needed">You must provide your two factor authentication code.</string>
|
||||
<string name="login_failed_generic">Login failed</string>
|
||||
<string name="share_upload_button">Upload</string>
|
||||
<string name="multiple_share_base_title">Name this set</string>
|
||||
|
|
@ -177,9 +177,11 @@ Tap this message (or hit back) to skip this step.</string>
|
|||
<string name="map_theme_light">mapbox://styles/mapbox/traffic-day-v2</string>
|
||||
<string name="map_theme_dark">mapbox://styles/mapbox/traffic-night-v2</string>
|
||||
<string name="mapbox_commons_app_token">pk.eyJ1IjoibWFza2FyYXZpdmVrIiwiYSI6ImNqMmxvdzFjMTAwMHYzM283ZWM3eW5tcDAifQ.ib5SZ9EVjwJe6GSKve0bcg</string>
|
||||
<string name="_2fa_code">2FA Code</string>
|
||||
<string name="number_of_uploads">My Recent Upload Limit</string>
|
||||
<string name="maximum_limit">Maximum Limit</string>
|
||||
<string name="maximum_limit_alert">Unable to display more than 500</string>
|
||||
<string name="set_limit">Set Recent Upload Limit</string>
|
||||
<string name="login_failed_2fa_not_supported">Two factor authentication is currently not supported.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue