From 1ea0a1ebeb19dfbde33d68c7dd06aa4c5b69f74f Mon Sep 17 00:00:00 2001 From: maskara Date: Tue, 2 Jan 2018 23:21:08 +0530 Subject: [PATCH] Make 2FA login work with commons --- .../fr/free/nrw/commons/auth/AccountUtil.java | 2 + .../commons/auth/AuthenticatedActivity.java | 63 ++----- .../free/nrw/commons/auth/LoginActivity.java | 137 ++++++++++++-- .../free/nrw/commons/auth/SessionManager.java | 40 +++-- .../auth/WikiAccountAuthenticator.java | 170 ++++++++---------- .../auth/WikiAccountAuthenticatorService.java | 28 ++- .../ModificationsSyncAdapter.java | 18 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 20 ++- .../free/nrw/commons/mwapi/MediaWikiApi.java | 2 + 9 files changed, 271 insertions(+), 209 deletions(-) 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 index a020c1fb7..0513280b5 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -19,6 +19,8 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD public class AccountUtil { public static final String ACCOUNT_TYPE = "fr.free.nrw.commons"; + public static final String AUTH_COOKIE = "authCookie"; + public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; private final Context context; public AccountUtil(Context context) { 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 e793d5eb9..c3b570784 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 @@ -1,72 +1,35 @@ package fr.free.nrw.commons.auth; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; import android.os.Bundle; import javax.inject.Inject; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; -import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE; public abstract class AuthenticatedActivity extends NavigationBaseActivity { @Inject SessionManager sessionManager; - + @Inject + MediaWikiApi mediaWikiApi; private String authCookie; - - private void getAuthCookie(Account account, AccountManager accountManager) { - Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false)) - .subscribeOn(Schedulers.io()) - .doOnError(Timber::e) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - this:: onAuthCookieAcquired, - throwable -> onAuthFailure()); - } - - private void addAccount(AccountManager accountManager) { - Single.just(accountManager.addAccount(ACCOUNT_TYPE, null, null, - null, AuthenticatedActivity.this, null, null)) - .subscribeOn(Schedulers.io()) - .map(AccountManagerFuture::getResult) - .doOnEvent((bundle, throwable) -> { - if (!bundle.containsKey(KEY_ACCOUNT_NAME)) { - throw new RuntimeException("Bundle doesn't contain account-name key: " - + KEY_ACCOUNT_NAME); - } - }) - .map(bundle -> bundle.getString(KEY_ACCOUNT_NAME)) - .doOnError(Timber::e) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); - Account curAccount = allAccounts[0]; - getAuthCookie(curAccount, accountManager); - }, - throwable -> onAuthFailure()); - } - protected void requestAuthToken() { if (authCookie != null) { onAuthCookieAcquired(authCookie); return; } - AccountManager accountManager = AccountManager.get(this); - Account curAccount = sessionManager.getCurrentAccount(); - if (curAccount == null) { - addAccount(accountManager); - } else { - getAuthCookie(curAccount, accountManager); - } + sessionManager.getAndSetAuthCookie() + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(cookie -> { + authCookie = cookie; + onAuthCookieAcquired(authCookie); + }); } @Override @@ -74,14 +37,14 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { super.onCreate(savedInstanceState); if (savedInstanceState != null) { - authCookie = savedInstanceState.getString("authCookie"); + authCookie = savedInstanceState.getString(AUTH_COOKIE); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putString("authCookie", authCookie); + outState.putString(AUTH_COOKIE, authCookie); } protected abstract void onAuthCookieAcquired(String authCookie); 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 780f4310b..fc926649b 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,6 +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.ProgressDialog; import android.content.Intent; import android.content.SharedPreferences; @@ -8,6 +11,7 @@ import android.os.Bundle; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; import android.support.annotation.StringRes; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.NavUtils; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatDelegate; @@ -21,6 +25,8 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import java.io.IOException; + import javax.inject.Inject; import javax.inject.Named; @@ -36,10 +42,16 @@ import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; import static android.view.KeyEvent.KEYCODE_ENTER; +import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; +import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class LoginActivity extends AccountAuthenticatorActivity { @@ -58,6 +70,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { @BindView(R.id.loginTwoFactor) EditText twoFactorEdit; @BindView(R.id.error_message_container) ViewGroup errorMessageContainer; @BindView(R.id.error_message) TextView errorMessage; + @BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer; ProgressDialog progressDialog; private AppCompatDelegate delegate; private LoginTextWatcher textWatcher = new LoginTextWatcher(); @@ -122,14 +135,109 @@ public class LoginActivity extends AccountAuthenticatorActivity { super.onDestroy(); } - private LoginTask getLoginTask() { - return new LoginTask( - this, - canonicializeUsername(usernameEdit.getText().toString()), - passwordEdit.getText().toString(), - twoFactorEdit.getText().toString(), - accountUtil, mwApi, defaultPrefs - ); + private void performLogin() { + Timber.d("Login to start!"); + final String username = canonicializeUsername(usernameEdit.getText().toString()); + final String password = passwordEdit.getText().toString(); + String twoFactorCode = twoFactorEdit.getText().toString(); + + showLoggingProgressBar(); + Observable.fromCallable(() -> login(username, password, twoFactorCode)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> handleLogin(username, password, result)); + } + + private String login(String username, String password, String twoFactorCode) { + try { + if (twoFactorCode.isEmpty()) { + return mwApi.login(username, password); + } else { + return mwApi.login(username, password, twoFactorCode); + } + } catch (IOException e) { + // Do something better! + return "NetworkFailure"; + } + } + + private void handleLogin(String username, String password, String result) { + Timber.d("Login done!"); + if (result.equals("PASS")) { + handlePassResult(username, password); + } else { + handleOtherResults(result); + } + } + + private void showLoggingProgressBar() { + progressDialog = new ProgressDialog(this); + progressDialog.setIndeterminate(true); + progressDialog.setTitle(getString(R.string.logging_in_title)); + progressDialog.setMessage(getString(R.string.logging_in_message)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.show(); + } + + private void handlePassResult(String username, String password) { + showSuccessAndDismissDialog(); + requestAuthToken(); + AccountAuthenticatorResponse response = null; + + Bundle extras = 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, ACCOUNT_TYPE); + response.onResult(authResult); + } + } + + accountUtil.createAccount(response, username, password); + startMainActivity(); + } + + protected void requestAuthToken() { + AccountManager accountManager = AccountManager.get(this); + Account curAccount = sessionManager.getCurrentAccount(); + if (curAccount != null) { + accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie()); + } + } + + /** + * 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 + showMessageAndCancelDialog(R.string.login_failed_network); + } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { + // Matches nosuchuser, nosuchusershort, noname + showMessageAndCancelDialog(R.string.login_failed_username); + emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { + // Matches wrongpassword, wrongpasswordempty + showMessageAndCancelDialog(R.string.login_failed_password); + emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("throttle".toLowerCase())) { + // Matches unknown throttle error codes + showMessageAndCancelDialog(R.string.login_failed_throttled); + } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { + // Matches login-userblocked + showMessageAndCancelDialog(R.string.login_failed_blocked); + } else if (result.equals("2FA")) { + askUserForTwoFactorAuth(); + } else { + // Occurs with unhandled login failure codes + Timber.d("Login failed with reason: %s", result); + showMessageAndCancelDialog(R.string.login_failed_generic); + } } /** @@ -182,7 +290,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { public void askUserForTwoFactorAuth() { if (BuildConfig.DEBUG) { - twoFactorEdit.setVisibility(View.VISIBLE); + twoFactorContainer.setVisibility(VISIBLE); + twoFactorEdit.setVisibility(VISIBLE); showMessageAndCancelDialog(R.string.login_failed_2fa_needed); } else { showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported); @@ -209,12 +318,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { finish(); } - private void performLogin() { - Timber.d("Login to start!"); - LoginTask task = getLoginTask(); - task.execute(); - } - private void signUp() { Intent intent = new Intent(this, SignupActivity.class); startActivity(intent); @@ -238,7 +341,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { private void showMessage(@StringRes int resId, @ColorRes int colorResId) { errorMessage.setText(getString(resId)); errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - errorMessageContainer.setVisibility(View.VISIBLE); + errorMessageContainer.setVisibility(VISIBLE); } private AppCompatDelegate getDelegate() { @@ -260,7 +363,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { @Override public void afterTextChanged(Editable editable) { boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 - && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE); + && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE); loginButton.setEnabled(enabled); } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index 779b73b34..2c593a23a 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -2,17 +2,16 @@ package fr.free.nrw.commons.auth; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.content.Context; -import java.io.IOException; - import fr.free.nrw.commons.mwapi.MediaWikiApi; import io.reactivex.Completable; import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; /** * Manage the current logged in user session. @@ -51,14 +50,31 @@ public class SessionManager { } accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); - try { - String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); - mediaWikiApi.setAuthCookie(authCookie); - return true; - } catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { - e.printStackTrace(); - return false; - } + getAndSetAuthCookie().subscribeOn(Schedulers.io()) + .subscribe(authCookie -> { + mediaWikiApi.setAuthCookie(authCookie); + }); + return true; + } + + public Observable getAndSetAuthCookie() { + AccountManager accountManager = AccountManager.get(context); + Account curAccount = getCurrentAccount(); + return Observable.fromCallable(() -> { + String authCookie = accountManager.blockingGetAuthToken(curAccount, AUTH_TOKEN_TYPE, false); + if (authCookie == null) { + Timber.d("Media wiki auth cookie is %s", mediaWikiApi.getAuthCookie()); + authCookie = mediaWikiApi.getAuthCookie(); + //authCookie = currentAccount.name + "|" + currentAccount.type + "|" + mediaWikiApi.getUserAgent(); + //mediaWikiApi.setAuthCookie(authCookie); + + } + Timber.d("Auth cookie is %s", authCookie); + return authCookie; + }).onErrorReturn(throwable-> { + Timber.e(throwable, "Auth cookie is still null :("); + return null; + }); } public Completable clearAllAccounts() { 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 c08e27966..42f2c192a 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 @@ -5,53 +5,37 @@ import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; import android.accounts.NetworkErrorException; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import java.io.IOException; +import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.modifications.ModificationsContentProvider; -import fr.free.nrw.commons.mwapi.MediaWikiApi; - -import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION; -import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; -import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT; -import static android.accounts.AccountManager.KEY_ERROR_CODE; -import static android.accounts.AccountManager.KEY_ERROR_MESSAGE; -import static android.accounts.AccountManager.KEY_INTENT; import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; -import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { + private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.AUTHORITY, ModificationsContentProvider.AUTHORITY}; + @NonNull private final Context context; - private MediaWikiApi mediaWikiApi; - WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) { + public WikiAccountAuthenticator(@NonNull Context context) { super(context); this.context = context; - this.mediaWikiApi = mwApi; } - private Bundle unsupportedOperation() { + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { Bundle bundle = new Bundle(); - bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION); - - // HACK: the docs indicate that this is a required key bit it's not displayed to the user. - bundle.putString(KEY_ERROR_MESSAGE, ""); - + bundle.putString("test", "editProperties"); return bundle; } - private boolean supportedAccountType(@Nullable String type) { - return ACCOUNT_TYPE.equals(type); - } - @Override public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, @NonNull String accountType, @Nullable String authTokenType, @@ -59,86 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { throws NetworkErrorException { if (!supportedAccountType(accountType)) { - return unsupportedOperation(); + Bundle bundle = new Bundle(); + bundle.putString("test", "addAccount"); + return bundle; } return addAccount(response); } - private Bundle addAccount(AccountAuthenticatorResponse response) { - Intent Intent = new Intent(context, LoginActivity.class); - Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - - Bundle bundle = new Bundle(); - bundle.putParcelable(KEY_INTENT, Intent); - - return bundle; - } - @Override public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, @NonNull Account account, @Nullable Bundle options) throws NetworkErrorException { - return unsupportedOperation(); + Bundle bundle = new Bundle(); + bundle.putString("test", "confirmCredentials"); + return bundle; } @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - return unsupportedOperation(); - } - - private String getAuthCookie(String username, String password) throws IOException { - //TODO add 2fa support here - String result = mediaWikiApi.login(username, password); - if (result.equals("PASS")) { - return mediaWikiApi.getAuthCookie(); - } else { - return null; - } - } - - @Override - public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, - String authTokenType, Bundle options) throws NetworkErrorException { - // Extract the username and password from the Account Manager, and ask - // the server for an appropriate AuthToken. - final AccountManager am = AccountManager.get(context); - final String password = am.getPassword(account); - if (password != null) { - String authCookie; - try { - authCookie = getAuthCookie(account.name, password); - } catch (IOException e) { - // Network error! - e.printStackTrace(); - throw new NetworkErrorException(e); - } - if (authCookie != null) { - final Bundle result = new Bundle(); - result.putString(KEY_ACCOUNT_NAME, account.name); - result.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); - result.putString(KEY_AUTHTOKEN, authCookie); - return result; - } - } - - // If we get here, then we couldn't access the user's password - so we - // need to re-prompt them for their credentials. We do that by creating - // an intent to display our AuthenticatorActivity panel. - final Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(PARAM_USERNAME, account.name); - intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - final Bundle bundle = new Bundle(); - bundle.putParcelable(KEY_INTENT, intent); + public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @NonNull String authTokenType, + @Nullable Bundle options) + throws NetworkErrorException { + Bundle bundle = new Bundle(); + bundle.putString("test", "getAuthToken"); return bundle; } @Nullable @Override 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; + return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null; + } + + @Nullable + @Override + public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @Nullable String authTokenType, + @Nullable Bundle options) + throws NetworkErrorException { + Bundle bundle = new Bundle(); + bundle.putString("test", "updateCredentials"); + return bundle; } @Nullable @@ -147,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { @NonNull Account account, @NonNull String[] features) throws NetworkErrorException { Bundle bundle = new Bundle(); - bundle.putBoolean(KEY_BOOLEAN_RESULT, false); + bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); return bundle; } - @Nullable - @Override - public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable String authTokenType, - @Nullable Bundle options) throws NetworkErrorException { - return unsupportedOperation(); + private boolean supportedAccountType(@Nullable String type) { + return ACCOUNT_TYPE.equals(type); } + 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; + } + + 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; + } + + @Override + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, + Account account) throws NetworkErrorException { + Bundle result = super.getAccountRemovalAllowed(response, account); + + if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT)) { + boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); + + if (allowed) { + for (String auth : SYNC_AUTHORITIES) { + ContentResolver.cancelSync(account, auth); + } + } + } + + return result; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java index 6bc6de076..826f2ceee 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java @@ -1,30 +1,26 @@ package fr.free.nrw.commons.auth; +import android.accounts.AbstractAccountAuthenticator; import android.content.Intent; import android.os.IBinder; - -import javax.inject.Inject; +import android.support.annotation.Nullable; import fr.free.nrw.commons.di.CommonsDaggerService; -import fr.free.nrw.commons.mwapi.MediaWikiApi; - -import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT; public class WikiAccountAuthenticatorService extends CommonsDaggerService { - @Inject MediaWikiApi mwApi; - private WikiAccountAuthenticator wikiAccountAuthenticator = null; + @Nullable + private AbstractAccountAuthenticator authenticator; @Override - public IBinder onBind(Intent intent) { - if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) { - return null; - } - - if (wikiAccountAuthenticator == null) { - wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi); - } - return wikiAccountAuthenticator.getIBinder(); + public void onCreate() { + super.onCreate(); + authenticator = new WikiAccountAuthenticator(this); } + @Nullable + @Override + public IBinder onBind(Intent intent) { + return authenticator == null ? null : authenticator.getIBinder(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index fa20be595..3831ce5d2 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -1,9 +1,6 @@ package fr.free.nrw.commons.modifications; import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.Context; @@ -16,7 +13,7 @@ import java.io.IOException; import javax.inject.Inject; -import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsContentProvider; @@ -29,6 +26,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @Inject MediaWikiApi mwApi; @Inject ContributionDao contributionDao; @Inject ModifierSequenceDao modifierSequenceDao; + @Inject + SessionManager sessionManager; public ModificationsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); @@ -56,16 +55,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { return; } - String authCookie; - try { - authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false); - } catch (OperationCanceledException | AuthenticatorException e) { - throw new RuntimeException(e); - } catch (IOException e) { - Timber.d("Could not authenticate :("); - return; - } - + String authCookie = sessionManager.getAndSetAuthCookie().blockingSingle(); if (isNullOrWhiteSpace(authCookie)) { Timber.d("Could not authenticate :("); return; diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 09e89e7b4..a4b5c24fa 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -69,11 +69,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); - params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE); + params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); api = new MWApi(apiURL, httpClient); } + @Override + @NonNull + public String getUserAgent() { + return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE; + } + @VisibleForTesting public void setWikiMediaToolforgeUrl(String wikiMediaToolforgeUrl) { this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; @@ -86,11 +92,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { * @throws IOException On api request IO issue */ public String login(String username, String password) throws IOException { + String loginToken = getLoginToken(); + Timber.d("Login token is %s", loginToken); return getErrorCodeToReturn(api.action("clientlogin") .param("rememberMe", "1") .param("username", username) .param("password", password) - .param("logintoken", getLoginToken()) + .param("logintoken", loginToken) .param("loginreturnurl", "https://commons.wikimedia.org") .post()); } @@ -103,12 +111,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { * @throws IOException On api request IO issue */ public String login(String username, String password, String twoFactorCode) throws IOException { + String loginToken = getLoginToken(); + Timber.d("Login token is %s", loginToken); return getErrorCodeToReturn(api.action("clientlogin") - .param("rememberMe", "1") + .param("rememberMe", "true") .param("username", username) .param("password", password) - .param("logintoken", getLoginToken()) - .param("logincontinue", "1") + .param("logintoken", loginToken) + .param("logincontinue", "true") .param("OATHToken", twoFactorCode) .post()); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 70ab38297..da9403b62 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -12,6 +12,8 @@ import io.reactivex.Observable; import io.reactivex.Single; public interface MediaWikiApi { + String getUserAgent(); + String getAuthCookie(); void setAuthCookie(String authCookie);