diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 5e45d97df..d830334e4 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -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); diff --git a/app/src/main/java/fr/free/nrw/commons/MWApi.java b/app/src/main/java/fr/free/nrw/commons/MWApi.java index aef8b5cd0..785ea7886 100644 --- a/app/src/main/java/fr/free/nrw/commons/MWApi.java +++ b/app/src/main/java/fr/free/nrw/commons/MWApi.java @@ -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; } - } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java new file mode 100644 index 000000000..29f6fcf91 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -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; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java index 6925699a1..5fddb0034 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java @@ -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 { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 6c3f6a389..224548e0d 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -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 { - - 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(""); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java new file mode 100644 index 000000000..4c8487174 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java @@ -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 { + + 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 ); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index 42448ace0..7233b6be8 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -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(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index 9be4b836c..4e4c10052 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -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) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index 27ab99e07..aae338516 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -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); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 2ead068ac..8cc9246d2 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -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 */ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index aeda37b39..b426f4553 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -62,6 +62,24 @@ + + + + + +