diff --git a/app/build.gradle b/app/build.gradle index f5e3cb3dc..20ece90bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,6 @@ plugins { id 'com.github.triplet.play' version '2.2.1' apply false } - apply from: '../gitutils.gradle' apply plugin: 'com.android.application' apply plugin: 'kotlin-android' @@ -32,7 +31,7 @@ dependencies { implementation 'com.facebook.fresco:fresco:1.13.0' implementation 'com.drewnoakes:metadata-extractor:2.11.0' implementation 'org.apache.commons:commons-lang3:3.8.1' - implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.23' + implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.25' // UI implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' @@ -195,6 +194,7 @@ android { buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" + buildConfigField "String", "WIKIDATA_URL", "\"https://wikidata.org\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" @@ -226,6 +226,7 @@ android { buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" + buildConfigField "String", "WIKIDATA_URL", "\"https://wikidata.org\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java b/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java index e9f342290..24a375246 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java @@ -61,7 +61,7 @@ public class CommonsAppAdapter extends AppAdapter { @Override public void updateAccount(@NonNull LoginResult result) { - // TODO: sessionManager.updateAccount(result); + sessionManager.updateAccount(result); } @Override @@ -69,7 +69,8 @@ public class CommonsAppAdapter extends AppAdapter { if (!preferences.contains(COOKIE_STORE_NAME)) { return null; } - return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, preferences.getString(COOKIE_STORE_NAME, null)); + return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, + preferences.getString(COOKIE_STORE_NAME, null)); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java new file mode 100644 index 000000000..e2adfb5e6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java @@ -0,0 +1,58 @@ +package fr.free.nrw.commons.actions; + +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; + +import io.reactivex.Observable; +import timber.log.Timber; + +public class PageEditClient { + + private final CsrfTokenClient csrfTokenClient; + private final PageEditInterface pageEditInterface; + private final Service service; + + public PageEditClient(CsrfTokenClient csrfTokenClient, + PageEditInterface pageEditInterface, + Service service) { + this.csrfTokenClient = csrfTokenClient; + this.pageEditInterface = pageEditInterface; + this.service = service; + } + + public Observable edit(String pageTitle, String text, String summary) { + try { + return pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) + .map(editResponse -> editResponse.edit().editSucceeded()); + } catch (Throwable throwable) { + return Observable.just(false); + } + } + + public Observable appendEdit(String pageTitle, String appendText, String summary) { + try { + return pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking()) + .map(editResponse -> editResponse.edit().editSucceeded()); + } catch (Throwable throwable) { + return Observable.just(false); + } + } + + public Observable prependEdit(String pageTitle, String prependText, String summary) { + try { + return pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) + .map(editResponse -> editResponse.edit().editSucceeded()); + } catch (Throwable throwable) { + return Observable.just(false); + } + } + + public Observable addEditTag(long revisionId, String tagName, String reason) { + try { + return service.addEditTag(String.valueOf(revisionId), tagName, reason, csrfTokenClient.getTokenBlocking()) + .map(mwPostResponse -> mwPostResponse.getSuccessVal()); + } catch (Throwable throwable) { + return Observable.just(-1); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java new file mode 100644 index 000000000..537ec4d4f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.actions; + +import androidx.annotation.NonNull; + +import org.wikipedia.edit.Edit; + +import io.reactivex.Observable; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +import static org.wikipedia.dataclient.Service.MW_API_PREFIX; + +public interface PageEditInterface { + + @FormUrlEncoded + @Headers("Cache-Control: no-cache") + @POST(MW_API_PREFIX + "action=edit") + @NonNull + Observable postEdit(@NonNull @Field("title") String title, + @NonNull @Field("summary") String summary, + @NonNull @Field("text") String text, + @NonNull @Field("token") String token); + + @FormUrlEncoded + @Headers("Cache-Control: no-cache") + @POST(MW_API_PREFIX + "action=edit") + @NonNull Observable postAppendEdit(@NonNull @Field("title") String title, + @NonNull @Field("summary") String summary, + @NonNull @Field("appendtext") String text, + @NonNull @Field("token") String token); + + @FormUrlEncoded + @Headers("Cache-Control: no-cache") + @POST(MW_API_PREFIX + "action=edit") + @NonNull Observable postPrependEdit(@NonNull @Field("title") String title, + @NonNull @Field("summary") String summary, + @NonNull @Field("prependtext") String text, + @NonNull @Field("token") String token); +} diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java new file mode 100644 index 000000000..c4f96e7eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.actions; + +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import fr.free.nrw.commons.CommonsApplication; +import io.reactivex.Observable; + +@Singleton +public class ThanksClient { + + private final CsrfTokenClient csrfTokenClient; + private final Service service; + + @Inject + public ThanksClient(@Named("commons-csrf") CsrfTokenClient csrfTokenClient, + @Named("commons-service") Service service) { + this.csrfTokenClient = csrfTokenClient; + this.service = service; + } + + public Observable thank(long revisionId) { + try { + return service.thank(String.valueOf(revisionId), null, + csrfTokenClient.getTokenBlocking(), + CommonsApplication.getInstance().getUserAgent()) + .map(mwQueryResponse -> mwQueryResponse.getSuccessVal() == 1); + } catch (Throwable throwable) { + return Observable.just(false); + } + } +} 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 65e211c83..7616b4515 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 @@ -3,8 +3,8 @@ package fr.free.nrw.commons.auth; import android.accounts.Account; import android.accounts.AccountManager; import android.content.Context; -import androidx.annotation.Nullable; +import androidx.annotation.Nullable; import fr.free.nrw.commons.BuildConfig; import timber.log.Timber; @@ -12,10 +12,8 @@ public class AccountUtil { public static final String AUTH_COOKIE = "authCookie"; public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; - private final Context context; - public AccountUtil(Context context) { - this.context = context; + public AccountUtil() { } /** @@ -49,5 +47,4 @@ public class AccountUtil { private static AccountManager accountManager(Context context) { return AccountManager.get(context); } - } 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 8487e9a95..bf1a34b49 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,9 +1,6 @@ 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.Context; import android.content.Intent; @@ -20,14 +17,6 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; -import com.google.android.material.textfield.TextInputLayout; - -import java.io.IOException; -import java.util.Locale; - -import javax.inject.Inject; -import javax.inject.Named; - import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; @@ -35,6 +24,17 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.NavUtils; import androidx.core.content.ContextCompat; + +import com.google.android.material.textfield.TextInputLayout; + +import org.wikipedia.AppAdapter; +import org.wikipedia.dataclient.WikiSite; +import org.wikipedia.login.LoginClient; +import org.wikipedia.login.LoginResult; + +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @@ -52,16 +52,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; +import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Action; 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.AUTH_TOKEN_TYPE; +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE; public class LoginActivity extends AccountAuthenticatorActivity { @@ -71,10 +72,17 @@ public class LoginActivity extends AccountAuthenticatorActivity { @Inject SessionManager sessionManager; + @Inject + @Named(NAMED_COMMONS_WIKI_SITE) + WikiSite commonsWikiSite; + @Inject @Named("default_preferences") JsonKvStore applicationKvStore; + @Inject + LoginClient loginClient; + @BindView(R.id.login_button) Button loginButton; @@ -104,13 +112,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { private LoginTextWatcher textWatcher = new LoginTextWatcher(); private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private Boolean loginCurrentlyInProgress = false; - private Boolean errorMessageShown = false; - private String resultantError; - private static final String RESULTANT_ERROR = "resultantError"; - private static final String ERROR_MESSAGE_SHOWN = "errorMessageShown"; - private static final String LOGGING_IN = "loggingIn"; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -214,7 +215,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { && sessionManager.isUserLoggedIn() && sessionManager.getCachedAuthCookie() != null) { applicationKvStore.putBoolean("login_skipped", false); - sessionManager.revalidateAuthToken(); startMainActivity(); } @@ -244,7 +244,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { @OnClick(R.id.login_button) public void performLogin() { - loginCurrentlyInProgress = true; Timber.d("Login to start!"); final String username = usernameEdit.getText().toString(); final String rawUsername = usernameEdit.getText().toString().trim(); @@ -252,23 +251,37 @@ public class LoginActivity extends AccountAuthenticatorActivity { String twoFactorCode = twoFactorEdit.getText().toString(); showLoggingProgressBar(); - compositeDisposable.add(Observable.fromCallable(() -> login(username, password, twoFactorCode)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> handleLogin(username, rawUsername, password, result))); + doLogin(username, password, twoFactorCode); } - 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); + private void doLogin(String username, String password, String twoFactorCode) { + progressDialog.show(); + + Action action = () -> { + try { + loginClient.loginBlocking(commonsWikiSite, username, password, twoFactorCode); + } catch (Throwable throwable) { + throwable.printStackTrace(); } - } catch (IOException e) { - // Do something better! - return "NetworkFailure"; - } + }; + + compositeDisposable.add(Completable.fromAction(action) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> onLoginSuccess(username, password), + error -> { + if (error instanceof LoginClient.LoginFailedException) { + LoginClient.LoginFailedException exception = (LoginClient.LoginFailedException) error; + if (exception.getMessage().equals("2FA")) { + askUserForTwoFactorAuth(); + } + } + if (!progressDialog.isShowing()) { + return; + } + progressDialog.dismiss(); + showMessageAndCancelDialog(R.string.error_occurred); + })); } /** @@ -281,18 +294,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { finish(); } - private void handleLogin(String username, String rawUsername, String password, String result) { - Timber.d("Login done!"); - if (result.equals("PASS")) { - handlePassResult(username, rawUsername, password); - } else { - loginCurrentlyInProgress = false; - errorMessageShown = true; - resultantError = result; - handleOtherResults(result); - } - } - private void showLoggingProgressBar() { progressDialog = new ProgressDialog(this); progressDialog.setIndeterminate(true); @@ -302,67 +303,18 @@ public class LoginActivity extends AccountAuthenticatorActivity { progressDialog.show(); } - private void handlePassResult(String username, String rawUsername, String password) { + private void onLoginSuccess(String username, String password) { + if (!progressDialog.isShowing()) { + // no longer attached to activity! + return; + } + LoginResult loginResult = new LoginResult(commonsWikiSite, "PASS", username, password, ""); + AppAdapter.get().updateAccount(loginResult); + progressDialog.dismiss(); 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, BuildConfig.ACCOUNT_TYPE); - response.onResult(authResult); - } - } - - sessionManager.createAccount(response, username, rawUsername, 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(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { - // Matches nosuchuser, nosuchusershort, noname - showMessageAndCancelDialog(R.string.login_failed_wrong_credentials); - emptySensitiveEditFields(); - } else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) { - // Matches wrongpassword, wrongpasswordempty - showMessageAndCancelDialog(R.string.login_failed_wrong_credentials); - emptySensitiveEditFields(); - } else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) { - // Matches unknown throttle error codes - showMessageAndCancelDialog(R.string.login_failed_throttled); - } else if (result.toLowerCase(Locale.getDefault()).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); - } - } - @Override protected void onStart() { super.onStart(); @@ -402,30 +354,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { return getDelegate().getMenuInflater(); } - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(LOGGING_IN, loginCurrentlyInProgress); - outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown); - outState.putString(RESULTANT_ERROR, resultantError); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGGING_IN, false); - errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false); - if (loginCurrentlyInProgress) { - performLogin(); - } - if (errorMessageShown) { - resultantError = savedInstanceState.getString(RESULTANT_ERROR); - if (resultantError != null) { - handleOtherResults(resultantError); - } - } - } - public void askUserForTwoFactorAuth() { progressDialog.dismiss(); twoFactorContainer.setVisibility(VISIBLE); @@ -440,16 +368,18 @@ public class LoginActivity extends AccountAuthenticatorActivity { } } + public void showMessageAndCancelDialog(String error) { + showMessage(error, R.color.secondaryDarkColor); + if (progressDialog != null) { + progressDialog.cancel(); + } + } + public void showSuccessAndDismissDialog() { showMessage(R.string.login_success, R.color.primaryDarkColor); progressDialog.dismiss(); } - public void emptySensitiveEditFields() { - passwordEdit.setText(""); - twoFactorEdit.setText(""); - } - public void startMainActivity() { NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP); finish(); @@ -461,6 +391,12 @@ public class LoginActivity extends AccountAuthenticatorActivity { errorMessageContainer.setVisibility(VISIBLE); } + private void showMessage(String message, @ColorRes int colorResId) { + errorMessage.setText(message); + errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); + errorMessageContainer.setVisibility(VISIBLE); + } + private AppCompatDelegate getDelegate() { if (delegate == null) { delegate = AppCompatDelegate.create(this, null); 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 7462869b1..882704f0f 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 @@ -1,14 +1,16 @@ package fr.free.nrw.commons.auth; import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; -import android.content.ContentResolver; import android.content.Context; -import android.os.Bundle; +import android.os.Build; +import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.wikipedia.login.LoginResult; + import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; @@ -20,10 +22,6 @@ import io.reactivex.Completable; import io.reactivex.Observable; import timber.log.Timber; -import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; - /** * Manage the current logged in user session. */ @@ -34,7 +32,6 @@ public class SessionManager { private Account currentAccount; // Unlike a savings account... ;-) private JsonKvStore defaultKvStore; private static final String KEY_RAWUSERNAME = "rawusername"; - private Bundle userdata = new Bundle(); @Inject public SessionManager(Context context, @@ -46,43 +43,40 @@ public class SessionManager { this.defaultKvStore = defaultKvStore; } - /** - * Creata a new account - * - * @param response - * @param username - * @param rawusername - * @param password - */ - public void createAccount(@Nullable AccountAuthenticatorResponse response, - String username, String rawusername, String password) { - - Account account = new Account(username, BuildConfig.ACCOUNT_TYPE); - userdata.putString(KEY_RAWUSERNAME, rawusername); - boolean created = accountManager().addAccountExplicitly(account, password, userdata); - - Timber.d("account creation " + (created ? "successful" : "failure")); - - if (created) { - if (response != null) { - Bundle bundle = new Bundle(); - bundle.putString(KEY_ACCOUNT_NAME, username); - bundle.putString(KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE); - - - response.onResult(bundle); - } - - } else { - if (response != null) { - response.onError(ERROR_CODE_REMOTE_EXCEPTION, ""); - } - Timber.d("account creation failure"); + private boolean createAccount(@NonNull String userName, @NonNull String password) { + Account account = getCurrentAccount(); + if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) { + removeAccount(); + account = new Account(userName, BuildConfig.ACCOUNT_TYPE); + return accountManager().addAccountExplicitly(account, password, null); } + return true; + } - // FIXME: If the user turns it off, it shouldn't be auto turned back on - ContentResolver.setSyncAutomatically(account, BuildConfig.CONTRIBUTION_AUTHORITY, true); // Enable sync by default! - ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! + public void removeAccount() { + Account account = getCurrentAccount(); + if (account != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + accountManager().removeAccountExplicitly(account); + } else { + //noinspection deprecation + accountManager().removeAccount(account, null, null); + } + } + } + + public void updateAccount(LoginResult result) { + boolean accountCreated = createAccount(result.getUserName(), result.getPassword()); + if (accountCreated) { + setPassword(result.getPassword()); + } + } + + private void setPassword(@NonNull String password) { + Account account = getCurrentAccount(); + if (account != null) { + accountManager().setPassword(account, password); + } } /** @@ -127,25 +121,6 @@ public class SessionManager { return AccountManager.get(context); } - public Boolean revalidateAuthToken() { - AccountManager accountManager = AccountManager.get(context); - Account curAccount = getCurrentAccount(); - - if (curAccount == null) { - return false; // This should never happen - } - - accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, null); - String authCookie = getAuthCookie(); - - if (authCookie == null) { - return false; - } - - mediaWikiApi.setAuthCookie(authCookie); - return true; - } - public String getAuthCookie() { if (!isUserLoggedIn()) { Timber.e("User is not logged in"); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java deleted file mode 100644 index cd97511cd..000000000 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.category; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class CategoryImageUtils { - - /** - * The method iterates over the child nodes to return a list of Subcategory name - * sorted alphabetically - * @param childNodes - * @return - */ - public static List getSubCategoryList(NodeList childNodes) { - List subCategories = new ArrayList<>(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - subCategories.add(getFileName(node)); - } - Collections.sort(subCategories); - return subCategories; - } - - /** - * Extracts the filename of the uploaded image - * @param document - * @return - */ - private static String getFileName(Node document) { - Element element = (Element) document; - return element.getAttribute("title"); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index f37debe7b..455e365d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -1,8 +1,5 @@ package fr.free.nrw.commons.contributions; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; - import android.content.res.Configuration; import android.os.Bundle; import android.view.LayoutInflater; @@ -13,21 +10,29 @@ import android.view.animation.AnimationUtils; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.LayoutManager; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import butterknife.ButterKnife; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.kvstore.JsonKvStore; -import javax.inject.Inject; -import javax.inject.Named; +import fr.free.nrw.commons.wikidata.WikidataClient; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; /** * Created by root on 01.06.2018. @@ -53,6 +58,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { @Inject @Named("default_preferences") JsonKvStore kvStore; @Inject ContributionController controller; + @Inject + WikidataClient wikidataClient; private Animation fab_close; private Animation fab_open; @@ -163,6 +170,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { /** * Responsible to set progress bar invisible and visible + * * @param shouldShow True when contributions list should be hidden. */ public void showProgress(boolean shouldShow) { @@ -170,7 +178,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { } public void showNoContributionsUI(boolean shouldShow) { - noContributionsYet.setVisibility(shouldShow?VISIBLE:GONE); + noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); } public void onDataSetChanged() { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 56946018c..6cf85ed98 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -12,18 +12,19 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.google.android.material.tabs.TabLayout; - -import java.util.List; - -import javax.inject.Inject; - import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; + +import java.util.List; + +import javax.inject.Inject; + import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.BuildConfig; @@ -38,7 +39,6 @@ import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.upload.UploadService; -import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -305,7 +305,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag @SuppressLint("CheckResult") private void setNotificationCount() { - compositeDisposable.add(Observable.fromCallable(() -> notificationController.getNotifications(false)) + compositeDisposable.add(notificationController.getNotifications(false) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::initNotificationViews, diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index a5d7e7ef7..3cf6c6f95 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -11,19 +11,22 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Locale; +import java.util.concurrent.Callable; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.notification.NotificationHelper; import fr.free.nrw.commons.review.ReviewController; import fr.free.nrw.commons.utils.ViewUtilWrapper; +import io.reactivex.Observable; import io.reactivex.Single; +import io.reactivex.SingleSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -35,20 +38,20 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D */ @Singleton public class DeleteHelper { - private final MediaWikiApi mwApi; - private final SessionManager sessionManager; private final NotificationHelper notificationHelper; + private final PageEditClient pageEditClient; private final ViewUtilWrapper viewUtil; + private final String username; @Inject - public DeleteHelper(MediaWikiApi mwApi, - SessionManager sessionManager, - NotificationHelper notificationHelper, - ViewUtilWrapper viewUtil) { - this.mwApi = mwApi; - this.sessionManager = sessionManager; + public DeleteHelper(NotificationHelper notificationHelper, + @Named("commons-page-edit") PageEditClient pageEditClient, + ViewUtilWrapper viewUtil, + @Named("username") String username) { this.notificationHelper = notificationHelper; + this.pageEditClient = pageEditClient; this.viewUtil = viewUtil; + this.username = username; } /** @@ -59,10 +62,11 @@ public class DeleteHelper { * @return */ public Single makeDeletion(Context context, Media media, String reason) { - viewUtil.showShortToast(context, context.getString((R.string.delete_helper_make_deletion_toast), media.getDisplayTitle())); - return Single.fromCallable(() -> delete(media, reason)) - .flatMap(result -> Single.fromCallable(() -> - showDeletionNotification(context, media, result))); + viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion"); + + return delete(media, reason) + .flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result))) + .firstOrError(); } /** @@ -71,14 +75,9 @@ public class DeleteHelper { * @param reason * @return */ - private boolean delete(Media media, String reason) { - String editToken; - String authCookie; + private Observable delete(Media media, String reason) { + Timber.d("thread is delete %s", Thread.currentThread().getName()); String summary = "Nominating " + media.getFilename() + " for deletion."; - - authCookie = sessionManager.getAuthCookie(); - mwApi.setAuthCookie(authCookie); - Calendar calendar = Calendar.getInstance(); String fileDeleteString = "{{delete|reason=" + reason + "|subpage=" + media.getFilename() + @@ -99,26 +98,23 @@ public class DeleteHelper { String userPageString = "\n{{subst:idw|" + media.getFilename() + "}} ~~~~"; - try { - editToken = mwApi.getEditToken(); - - if(editToken == null) { - return false; - } - - mwApi.prependEdit(editToken, fileDeleteString + "\n", - media.getFilename(), summary); - mwApi.edit(editToken, subpageString + "\n", - "Commons:Deletion_requests/" + media.getFilename(), summary); - mwApi.appendEdit(editToken, logPageString + "\n", - "Commons:Deletion_requests/" + date, summary); - mwApi.appendEdit(editToken, userPageString + "\n", - "User_Talk:" + media.getCreator(), summary); - } catch (Exception e) { - Timber.e(e); - return false; - } - return true; + return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary) + .flatMap(result -> { + if (result) { + return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); + } + throw new RuntimeException("Failed to nominate for deletion"); + }).flatMap(result -> { + if (result) { + return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary); + } + throw new RuntimeException("Failed to nominate for deletion"); + }).flatMap(result -> { + if (result) { + return pageEditClient.appendEdit("User_Talk:" + username, userPageString + "\n", summary); + } + throw new RuntimeException("Failed to nominate for deletion"); + }); } private boolean showDeletionNotification(Context context, Media media, boolean result) { @@ -191,7 +187,12 @@ public class DeleteHelper { } } - makeDeletion(context, media, reason) + Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().getName()); + + String finalReason = reason; + + Single.defer((Callable>) () -> + makeDeletion(context, media, finalReason)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(aBoolean -> { diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 36aba1668..52dfdbd89 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -5,14 +5,11 @@ import android.content.ContentProviderClient; import android.content.Context; import android.view.inputmethod.InputMethodManager; +import androidx.collection.LruCache; + import com.google.gson.Gson; -import org.wikipedia.dataclient.WikiSite; - -import io.reactivex.Scheduler; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import org.wikipedia.dataclient.WikiSite; +import org.wikipedia.AppAdapter; import java.util.ArrayList; import java.util.HashMap; @@ -22,7 +19,6 @@ import java.util.Map; import javax.inject.Named; import javax.inject.Singleton; -import androidx.collection.LruCache; import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; @@ -37,6 +33,9 @@ import fr.free.nrw.commons.upload.UploadController; import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -85,7 +84,7 @@ public class CommonsApplicationModule { @Provides public AccountUtil providesAccountUtil(Context context) { - return new AccountUtil(context); + return new AccountUtil(); } @Provides @@ -188,7 +187,13 @@ public class CommonsApplicationModule { @Named(MAIN_THREAD) @Provides - public Scheduler providesMainThread(){ + public Scheduler providesMainThread() { return AndroidSchedulers.mainThread(); } + + @Named("username") + @Provides + public String provideLoggedInUsername() { + return AppAdapter.get().getUserName(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 0286d9640..dafcab3be 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -2,11 +2,16 @@ package fr.free.nrw.commons.di; import android.content.Context; +import androidx.annotation.NonNull; + import com.google.gson.Gson; +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.json.GsonUtil; +import org.wikipedia.login.LoginClient; import java.io.File; import java.util.concurrent.TimeUnit; @@ -14,10 +19,11 @@ import java.util.concurrent.TimeUnit; import javax.inject.Named; import javax.inject.Singleton; -import androidx.annotation.NonNull; import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.actions.PageEditClient; +import fr.free.nrw.commons.actions.PageEditInterface; import fr.free.nrw.commons.category.CategoryInterface; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.media.MediaInterface; @@ -25,6 +31,7 @@ import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.review.ReviewInterface; +import fr.free.nrw.commons.upload.UploadInterface; import okhttp3.Cache; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -41,7 +48,11 @@ public class NetworkingModule { public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; - private static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite"; + public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite"; + private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite"; + + public static final String NAMED_COMMONS_CSRF = "commons-csrf"; + public static final String NAMED_WIKI_DATA_CSRF = "wikidata-csrf"; @Provides @Singleton @@ -71,7 +82,7 @@ public class NetworkingModule { public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") JsonKvStore defaultKvStore, Gson gson) { - return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore); + return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST); } @Provides @@ -88,6 +99,27 @@ public class NetworkingModule { gson); } + @Named(NAMED_COMMONS_CSRF) + @Provides + @Singleton + public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return new CsrfTokenClient(commonsWikiSite, commonsWikiSite); + } + + @Named(NAMED_WIKI_DATA_CSRF) + @Provides + @Singleton + public CsrfTokenClient provideWikidataCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite, + @Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { + return new CsrfTokenClient(wikidataWikiSite, commonsWikiSite); + } + + @Provides + @Singleton + public LoginClient provideLoginClient() { + return new LoginClient(); + } + @Provides @Named("wikimedia_api_host") @NonNull @@ -104,6 +136,20 @@ public class NetworkingModule { return HttpUrl.parse(TOOLS_FORGE_URL); } + @Provides + @Singleton + @Named(NAMED_COMMONS_WIKI_SITE) + public WikiSite provideCommonsWikiSite() { + return new WikiSite(BuildConfig.COMMONS_URL); + } + + @Provides + @Singleton + @Named(NAMED_WIKI_DATA_WIKI_SITE) + public WikiSite provideWikidataWikiSite() { + return new WikiSite(BuildConfig.WIKIDATA_URL); + } + /** * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. * @return returns a singleton Gson instance @@ -116,9 +162,16 @@ public class NetworkingModule { @Provides @Singleton - @Named("commons-wikisite") - public WikiSite provideCommonsWikiSite() { - return new WikiSite(BuildConfig.COMMONS_URL); + @Named("commons-service") + public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite); + } + + @Provides + @Singleton + @Named("wikidata-service") + public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { + return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class); } @Provides @@ -127,6 +180,44 @@ public class NetworkingModule { return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); } + @Provides + @Singleton + public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class); + } + + @Named("commons-page-edit-service") + @Provides + @Singleton + public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class); + } + + @Named("wikidata-page-edit-service") + @Provides + @Singleton + public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) { + return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class); + } + + @Named("commons-page-edit") + @Provides + @Singleton + public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient, + @Named("commons-page-edit-service") PageEditInterface pageEditInterface, + @Named("commons-service") Service service) { + return new PageEditClient(csrfTokenClient, pageEditInterface, service); + } + + @Named("wikidata-page-edit") + @Provides + @Singleton + public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient, + @Named("wikidata-page-edit-service") PageEditInterface pageEditInterface, + @Named("wikidata-service") Service service) { + return new PageEditClient(csrfTokenClient, pageEditInterface, service); + } + @Provides @Singleton public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { 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 6043f6d03..6f8b4522c 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 @@ -12,13 +12,17 @@ import android.os.RemoteException; import java.io.IOException; import javax.inject.Inject; +import javax.inject.Named; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.actions.PageEditClient; 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.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; import timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @@ -28,6 +32,9 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @Inject ModifierSequenceDao modifierSequenceDao; @Inject SessionManager sessionManager; + @Inject + @Named("commons-page-edit") + PageEditClient commonsPageEditClient; public ModificationsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); @@ -61,15 +68,6 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { return; } - mwApi.setAuthCookie(authCookie); - String editToken; - - try { - editToken = mwApi.getEditToken(); - } catch (IOException e) { - Timber.d("Can not retreive edit token!"); - return; - } allModifications.moveToFirst(); @@ -113,22 +111,15 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { Timber.d("Page content is %s", pageContent); String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); - String editResult; - try { - editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); - } catch (IOException e) { - Timber.d("Network messed up on modifications sync!"); - continue; - } - - Timber.d("Response is %s", editResult); - - if (!"Success".equals(editResult)) { - // FIXME: Log this somewhere else - Timber.d("Non success result! %s", editResult); - } else { - modifierSequenceDao.delete(sequence); - } + Disposable disposable = commonsPageEditClient + .edit(contrib.getFilename(), processedPageContent, sequence.getEditSummary()) + .subscribe(editResult -> { + if (!editResult) { + Timber.d("Non success result!"); + } else { + modifierSequenceDao.delete(sequence); + } + }); } allModifications.moveToNext(); } 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 0ef518180..b88764e93 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 @@ -1,15 +1,10 @@ package fr.free.nrw.commons.mwapi; -import android.content.Context; -import android.net.Uri; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.gson.Gson; - -import org.apache.commons.lang3.StringUtils; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; @@ -20,28 +15,15 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreProtocolPNames; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.wikipedia.util.DateUtil; import java.io.IOException; -import java.io.InputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; -import java.util.List; -import java.util.Locale; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; -import fr.free.nrw.commons.category.QueryContinue; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.notification.Notification; -import fr.free.nrw.commons.notification.NotificationUtils; -import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Single; import timber.log.Timber; @@ -51,17 +33,8 @@ import timber.log.Timber; public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private AbstractHttpClient httpClient; private CustomMwApi api; - private CustomMwApi wikidataApi; - private Context context; - private JsonKvStore defaultKvStore; - private final String ERROR_CODE_BAD_TOKEN = "badtoken"; - - public ApacheHttpClientMediaWikiApi(Context context, - String apiURL, - String wikidatApiURL, - JsonKvStore defaultKvStore) { - this.context = context; + public ApacheHttpClientMediaWikiApi(String apiURL) { BasicHttpParams params = new BasicHttpParams(); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); @@ -74,198 +47,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor()); } api = new CustomMwApi(apiURL, httpClient); - wikidataApi = new CustomMwApi(wikidatApiURL, httpClient); - this.defaultKvStore = defaultKvStore; - } - - /** - * @param username String - * @param password String - * @return String as returned by this.getErrorCodeToReturn() - * @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", loginToken) - .param("loginreturnurl", "https://commons.wikimedia.org") - .post()); - } - - /** - * @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 loginToken = getLoginToken(); - Timber.d("Login token is %s", loginToken); - return getErrorCodeToReturn(api.action("clientlogin") - .param("rememberMe", "true") - .param("username", username) - .param("password", password) - .param("logintoken", loginToken) - .param("logincontinue", "true") - .param("OATHToken", twoFactorCode) - .post()); - } - - private String getLoginToken() throws IOException { - return api.action("query") - .param("action", "query") - .param("meta", "tokens") - .param("type", "login") - .post() - .getString("/api/query/tokens/@logintoken"); - } - - /** - * @param loginCustomApiResult CustomApiResult 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(CustomApiResult loginCustomApiResult) { - String status = loginCustomApiResult.getString("/api/clientlogin/@status"); - if (status.equals("PASS")) { - api.isLoggedIn = true; - setAuthCookieOnLogin(true); - return status; - } else if (status.equals("FAIL")) { - setAuthCookieOnLogin(false); - return loginCustomApiResult.getString("/api/clientlogin/@messagecode"); - } else if ( - status.equals("UI") - && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") - && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") - ) { - setAuthCookieOnLogin(false); - return "2FA"; - } - - // UI, REDIRECT, RESTART - return "genericerror-" + status; - } - - private void setAuthCookieOnLogin(boolean isLoggedIn) { - if (isLoggedIn) { - defaultKvStore.putBoolean("isUserLoggedIn", true); - defaultKvStore.putString("getAuthCookie", api.getAuthCookie()); - } else { - defaultKvStore.putBoolean("isUserLoggedIn", false); - defaultKvStore.remove("getAuthCookie"); - } - } - - @Override - public String getAuthCookie() { - return api.getAuthCookie(); - } - - @Override - public void setAuthCookie(String authCookie) { - api.setAuthCookie(authCookie); - } - - @Override - public boolean validateLogin() throws IOException { - boolean validateLoginResp = api.validateLogin(); - Timber.d("Validate login response is %s", validateLoginResp); - return validateLoginResp; - } - - @Override - public String getEditToken() throws IOException { - String editToken = api.action("query") - .param("meta", "tokens") - .post() - .getString("/api/query/tokens/@csrftoken"); - Timber.d("MediaWiki edit token is %s", editToken); - return editToken; - } - - @Override - public String getCentralAuthToken() throws IOException { - CustomApiResult result = api.action("centralauthtoken").get(); - String centralAuthToken = result.getString("/api/centralauthtoken/@centralauthtoken"); - - Timber.d("MediaWiki Central auth token is %s", centralAuthToken); - - if ((centralAuthToken == null || centralAuthToken.isEmpty()) - && "notLoggedIn".equals(result.getString("api/error/@code"))) { - Timber.d("Central auth token isn't valid. Trying to fetch a fresh token"); - api.removeAllCookies(); - String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context)); - if (loginResultCode.equals("PASS")) { - return getCentralAuthToken(); - } else if (loginResultCode.equals("2FA")) { - Timber.e("Cannot refresh session for 2FA enabled user. Login required"); - } else { - Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode); - } - } else { - Timber.e("Error occurred while fetching auth token. Error code is %s and message is %s", - result.getString("api/error/@code"), - result.getString("api/error/@info")); - } - return centralAuthToken; - } - - @Override - public boolean thank(String editToken, long revision) throws IOException { - CustomApiResult res = api.action("thank") - .param("rev", revision) - .param("token", editToken) - .param("source", CommonsApplication.getInstance().getUserAgent()) - .post(); - String r = res.getString("/api/result/@success"); - // Does this correctly check the success/failure? - // The docs https://www.mediawiki.org/wiki/Extension:Thanks seems unclear about that. - return r.equals("success"); - } - - @Override - @Nullable - public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException { - return api.action("edit") - .param("title", filename) - .param("token", getEditToken()) - .param("text", processedPageContent) - .param("summary", summary) - .post() - .getString("/api/edit/@result"); - } - - - @Override - @Nullable - public String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException { - return api.action("edit") - .param("title", filename) - .param("token", getEditToken()) - .param("appendtext", processedPageContent) - .param("summary", summary) - .post() - .getString("/api/edit/@result"); - } - - @Override - @Nullable - public String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException { - return api.action("edit") - .param("title", filename) - .param("token", getEditToken()) - .param("prependtext", processedPageContent) - .param("summary", summary) - .post() - .getString("/api/edit/@result"); } @Override @@ -297,89 +78,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { }); } - @Override - public String getWikidataCsrfToken() throws IOException { - String wikidataCsrfToken = wikidataApi.action("query") - .param("action", "query") - .param("centralauthtoken", getCentralAuthToken()) - .param("meta", "tokens") - .post() - .getString("/api/query/tokens/@csrftoken"); - Timber.d("Wikidata csrf token is %s", wikidataCsrfToken); - return wikidataCsrfToken; - } - - /** - * Creates a new claim using the wikidata API - * https://www.mediawiki.org/wiki/Wikibase/API - * - * @param entityId the wikidata entity to be edited - * @param property the property to be edited, for eg P18 for images - * @param snaktype the type of value stored for that property - * @param value the actual value to be stored for the property, for eg filename in case of P18 - * @return returns revisionId if the claim is successfully created else returns null - * @throws IOException - */ - @Nullable - @Override - public String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { - Timber.d("Filename is %s", value); - CustomApiResult result = wikidataApi.action("wbcreateclaim") - .param("entity", entityId) - .param("centralauthtoken", getCentralAuthToken()) - .param("token", getWikidataCsrfToken()) - .param("snaktype", snaktype) - .param("property", property) - .param("value", value) - .post(); - - if (result == null || result.getNode("api") == null) { - return null; - } - - Node node = result.getNode("api").getDocument(); - Element element = (Element) node; - - if (element != null && element.getAttribute("success").equals("1")) { - return result.getString("api/pageinfo/@lastrevid"); - } else { - Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); - } - return null; - } - - /** - * Adds the wikimedia-commons-app tag to the edits made on wikidata - * - * @param revisionId - * @return - * @throws IOException - */ - @Nullable - @Override - public boolean addWikidataEditTag(String revisionId) throws IOException { - CustomApiResult result = wikidataApi.action("tag") - .param("revid", revisionId) - .param("centralauthtoken", getCentralAuthToken()) - .param("token", getWikidataCsrfToken()) - .param("add", "wikimedia-commons-app") - .param("reason", "Add tag for edits made using Android Commons app") - .post(); - - if (result == null || result.getNode("api") == null) { - return false; - } - - if ("success".equals(result.getString("api/tag/result/@status"))) { - return true; - } else { - Timber.e("Error occurred in creating claim. Error code is: %s and message is %s", - result.getString("api/error/@code"), - result.getString("api/error/@info")); - } - return false; - } - @Override @NonNull public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { @@ -428,128 +126,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .getString("/api/query/pages/page/revisions/rev"); } - @Override - @NonNull - public List getNotifications(boolean archived) { - CustomApiResult notificationNode = null; - String notfilter; - try { - if (archived) { - notfilter = "read"; - } else { - notfilter = "!read"; - } - String language = Locale.getDefault().getLanguage(); - if (StringUtils.isBlank(language)) { - //if no language is set we use the default user language defined on wikipedia - language = "user"; - } - notificationNode = api.action("query") - .param("notprop", "list") - .param("format", "xml") - .param("meta", "notifications") - .param("notformat", "model") - .param("notwikis", "wikidatawiki|commonswiki|enwiki") - .param("notfilter", notfilter) - .param("uselang", language) - .get() - .getNode("/api/query/notifications/list"); - } catch (IOException e) { - Timber.e(e, "Failed to obtain searchCategories"); - } - - if (notificationNode == null - || notificationNode.getDocument() == null - || notificationNode.getDocument().getChildNodes() == null - || notificationNode.getDocument().getChildNodes().getLength() == 0) { - return new ArrayList<>(); - } - NodeList childNodes = notificationNode.getDocument().getChildNodes(); - return NotificationUtils.getNotificationsFromList(context, childNodes); - } - - @Override - public boolean markNotificationAsRead(Notification notification) throws IOException { - Timber.d("Trying to mark notification as read: %s", notification.toString()); - String result = api.action("echomarkread") - .param("token", getEditToken()) - .param("list", notification.notificationId) - .post() - .getString("/api/query/echomarkread/@result"); - - if (StringUtils.isBlank(result)) { - return false; - } - - return result.equals("success"); - } - - - @Override - @NonNull - public Single uploadFile( - String filename, - @NonNull InputStream file, - long dataLength, - Uri fileUri, - Uri contentProviderUri, - ProgressListener progressListener) { - return Single.fromCallable(() -> { - CustomApiResult result = api.uploadToStash(filename, file, dataLength, getEditToken(), progressListener::onProgress); - - Timber.wtf("Result: " + result.toString()); - - String resultStatus = result.getString("/api/upload/@result"); - if (!resultStatus.equals("Success")) { - String errorCode = result.getString("/api/error/@code"); - Timber.e(errorCode); - - if (errorCode.equals(ERROR_CODE_BAD_TOKEN)) { - ViewUtil.showLongToast(context, R.string.bad_token_error_proposed_solution); - } - return new UploadStash(errorCode, resultStatus, filename, ""); - } else { - String filekey = result.getString("/api/upload/@filekey"); - return new UploadStash("", resultStatus, filename, filekey); - } - }); - } - - - @Override - @NonNull - public Single uploadFileFinalize( - String filename, - String filekey, - String pageContents, - String editSummary) throws IOException { - return Single.fromCallable(() -> { - CustomApiResult result = api.uploadFromStash( - filename, filekey, pageContents, editSummary, - getEditToken()); - - Timber.d("Result: %s", result.toString()); - - String resultStatus = result.getString("/api/upload/@result"); - if (!resultStatus.equals("Success")) { - String errorCode = result.getString("/api/error/@code"); - Timber.e(errorCode); - - if (errorCode.equals(ERROR_CODE_BAD_TOKEN)) { - ViewUtil.showLongToast(context, R.string.bad_token_error_proposed_solution); - } - return new UploadResult(resultStatus, errorCode); - } else { - Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); - String canonicalFilename = "File:" + result.getString("/api/upload/@filename") - .replace("_", " ") - .trim(); // Title vs Filename - String imageUrl = result.getString("/api/upload/imageinfo/@url"); - return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl); - } - }); - } - /** * Checks to see if a user is currently blocked from Commons * 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 d3072f46e..a4330d102 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 @@ -1,69 +1,19 @@ package fr.free.nrw.commons.mwapi; -import android.net.Uri; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import fr.free.nrw.commons.notification.Notification; -import io.reactivex.Observable; + +import java.io.IOException; + import io.reactivex.Single; public interface MediaWikiApi { - String getAuthCookie(); - - void setAuthCookie(String authCookie); - - String login(String username, String password) throws IOException; - - String login(String username, String password, String twoFactorCode) throws IOException; - - boolean validateLogin() throws IOException; - - String getEditToken() throws IOException; - - String getWikidataCsrfToken() throws IOException; - - String getCentralAuthToken() throws IOException; - - @NonNull - Single uploadFile(String filename, InputStream file, - long dataLength, Uri fileUri, Uri contentProviderUri, - final ProgressListener progressListener); - - @NonNull - Single uploadFileFinalize(String filename, String filekey, - String pageContents, String editSummary) throws IOException; - @Nullable - String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; - - @Nullable - String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; - - @Nullable - String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; - - @Nullable - String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException; - - @Nullable - boolean addWikidataEditTag(String revisionId) throws IOException; - Single parseWikicode(String source); @NonNull Single fetchMediaByFilename(String filename); - @NonNull - List getNotifications(boolean archived) throws IOException; - - @NonNull - boolean markNotificationAsRead(Notification notification) throws IOException; - @Nullable String revisionsByFilename(String filename) throws IOException; @@ -76,8 +26,6 @@ public interface MediaWikiApi { // Single getCampaigns(); - boolean thank(String editToken, long revision) throws IOException; - interface ProgressListener { void onProgress(long transferred, long total); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java deleted file mode 100644 index 98a476ffa..000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import org.jetbrains.annotations.NotNull; - -import java.util.Date; - -public class UploadResult { - private String errorCode; - private String resultStatus; - private Date dateUploaded; - private String imageUrl; - private String canonicalFilename; - - /** - * Minimal constructor - * - * @param resultStatus Upload result status - * @param errorCode Upload error code - */ - UploadResult(String resultStatus, String errorCode) { - this.resultStatus = resultStatus; - this.errorCode = errorCode; - } - - /** - * Full-fledged constructor - * @param resultStatus Upload result status - * @param dateUploaded Uploaded date - * @param canonicalFilename Uploaded file name - * @param imageUrl Uploaded image file name - */ - UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) { - this.resultStatus = resultStatus; - this.dateUploaded = dateUploaded; - this.canonicalFilename = canonicalFilename; - this.imageUrl = imageUrl; - } - - @NotNull - @Override - public String toString() { - return "UploadResult{" + - "errorCode='" + errorCode + '\'' + - ", resultStatus='" + resultStatus + '\'' + - ", dateUploaded='" + (dateUploaded == null ? "" : dateUploaded.toString()) + '\'' + - ", imageUrl='" + imageUrl + '\'' + - ", canonicalFilename='" + canonicalFilename + '\'' + - '}'; - } - - /** - * Gets uploaded date - * @return Upload date - */ - public Date getDateUploaded() { - return dateUploaded; - } - - /** - * Gets image url - * @return Uploaded image url - */ - public String getImageUrl() { - return imageUrl; - } - - /** - * Gets canonical file name - * @return Uploaded file name - */ - public String getCanonicalFilename() { - return canonicalFilename; - } - - /** - * Gets upload error code - * @return Error code - */ - public String getErrorCode() { - return errorCode; - } - - /** - * Gets upload result status - * @return Upload result status - */ - public String getResultStatus() { - return resultStatus; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java deleted file mode 100644 index f74b67298..000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadStash.java +++ /dev/null @@ -1,70 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class UploadStash { - @NonNull - private String errorCode; - @NonNull - private String resultStatus; - @NonNull - private String filename; - @NonNull - private String filekey; - - @NonNull - public final String getErrorCode() { - return this.errorCode; - } - - @NonNull - public final String getResultStatus() { - return this.resultStatus; - } - - @NonNull - public final String getFilename() { - return this.filename; - } - - @NonNull - public final String getFilekey() { - return this.filekey; - } - - public UploadStash(@NonNull String errorCode, @NonNull String resultStatus, @NonNull String filename, @NonNull String filekey) { - this.errorCode = errorCode; - this.resultStatus = resultStatus; - this.filename = filename; - this.filekey = filekey; - } - - public String toString() { - return "UploadStash(errorCode=" + this.errorCode + ", resultStatus=" + this.resultStatus + ", filename=" + this.filename + ", filekey=" + this.filekey + ")"; - } - - public int hashCode() { - return ((this.errorCode.hashCode() * 31 + this.resultStatus.hashCode() - ) * 31 + this.filename.hashCode() - ) * 31 + this.filekey.hashCode(); - } - - public boolean equals(@Nullable Object obj) { - if (this != obj) { - if (obj instanceof UploadStash) { - UploadStash that = (UploadStash)obj; - if (this.errorCode.equals(that.errorCode) - && this.resultStatus.equals(that.resultStatus) - && this.filename.equals(that.filename) - && this.filekey.equals(that.filekey)) { - return true; - } - } - - return false; - } else { - return true; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java index fec22f34d..8052cfaa7 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -1,5 +1,9 @@ package fr.free.nrw.commons.notification; +import org.wikipedia.util.DateUtil; + +import fr.free.nrw.commons.utils.CommonsDateUtil; + /** * Created by root on 18.12.2017. */ @@ -8,33 +12,44 @@ public class Notification { public NotificationType notificationType; public String notificationText; public String date; - public String description; public String link; public String iconUrl; - public String dateWithYear; public String notificationId; - public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear, String notificationId) { + public Notification(NotificationType notificationType, + String notificationText, + String date, + String link, + String iconUrl, + String notificationId) { this.notificationType = notificationType; this.notificationText = notificationText; this.date = date; - this.description = description; this.link = link; this.iconUrl = iconUrl; - this.dateWithYear = dateWithYear; this.notificationId=notificationId; } + public static Notification from(org.wikipedia.notifications.Notification wikiNotification) { + org.wikipedia.notifications.Notification.Contents contents = wikiNotification.getContents(); + String notificationLink = contents == null || contents.getLinks() == null + || contents.getLinks().getPrimary() == null ? "" : contents.getLinks().getPrimary().getUrl(); + return new Notification(NotificationType.UNKNOWN, + contents == null ? "" : contents.getCompactHeader(), + DateUtil.getMonthOnlyDateString(wikiNotification.getTimestamp()), + notificationLink, + "", + String.valueOf(wikiNotification.id())); + } + @Override public String toString() { return "Notification" + "notificationType='" + notificationType + '\'' + ", notificationText='" + notificationText + '\'' + ", date='" + date + '\'' + - ", description='" + description + '\'' + ", link='" + link + '\'' + ", iconUrl='" + iconUrl + '\'' + - ", dateWithYear=" + dateWithYear + ", notificationId='" + notificationId + '\'' + '}'; } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index b1f775a5b..bb2078d24 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -5,11 +5,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import androidx.constraintlayout.widget.ConstraintLayout; -import com.google.android.material.snackbar.Snackbar; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -19,10 +14,17 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.snackbar.Snackbar; import com.pedrogomez.renderers.RVRendererAdapter; import java.util.Collections; import java.util.List; +import java.util.concurrent.Callable; import javax.inject.Inject; @@ -34,7 +36,9 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; +import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -78,25 +82,25 @@ public class NotificationActivity extends NavigationBaseActivity { @SuppressLint("CheckResult") public void removeNotification(Notification notification) { - compositeDisposable.add(Observable.fromCallable(() -> controller.markAsRead(notification)) + Disposable disposable = Observable.defer((Callable>) + () -> controller.markAsRead(notification)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { - if (result){ + if (result) { notificationList.remove(notification); setAdapter(notificationList); adapter.notifyDataSetChanged(); Snackbar snackbar = Snackbar - .make(relativeLayout,"Notification marked as read", Snackbar.LENGTH_LONG); + .make(relativeLayout, "Notification marked as read", Snackbar.LENGTH_LONG); snackbar.show(); - if (notificationList.size()==0){ + if (notificationList.size() == 0) { setEmptyView(); relativeLayout.setVisibility(View.GONE); no_notification.setVisibility(View.VISIBLE); } - } - else { + } else { adapter.notifyDataSetChanged(); setAdapter(notificationList); Toast.makeText(NotificationActivity.this, "There was some error!", Toast.LENGTH_SHORT).show(); @@ -107,7 +111,8 @@ public class NotificationActivity extends NavigationBaseActivity { throwable.printStackTrace(); ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications); progressBar.setVisibility(View.GONE); - })); + }); + compositeDisposable.add(disposable); } @@ -140,11 +145,8 @@ public class NotificationActivity extends NavigationBaseActivity { private void addNotifications(boolean archived) { Timber.d("Add notifications"); if (mNotificationWorkerFragment == null) { - compositeDisposable.add(Observable.fromCallable(() -> { - progressBar.setVisibility(View.VISIBLE); - return controller.getNotifications(archived); - - }) + progressBar.setVisibility(View.VISIBLE); + compositeDisposable.add(controller.getNotifications(archived) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(notificationList -> { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java new file mode 100644 index 000000000..8643ceaf4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.java @@ -0,0 +1,45 @@ +package fr.free.nrw.commons.notification; + +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import io.reactivex.Observable; +import io.reactivex.Single; + +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF; + +@Singleton +public class NotificationClient { + + private final Service service; + private final CsrfTokenClient csrfTokenClient; + + @Inject + public NotificationClient(@Named("commons-service") Service service, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { + this.service = service; + this.csrfTokenClient = csrfTokenClient; + } + + public Single> getNotifications(boolean archived) { + return service.getAllNotifications("wikidatawiki|commonswiki|enwiki", archived ? "read" : "!read", null) + .map(mwQueryResponse -> mwQueryResponse.query().notifications().list()) + .flatMap(Observable::fromIterable) + .map(notification -> Notification.from(notification)) + .toList(); + } + + public Observable markNotificationAsRead(String notificationId) { + try { + return service.markRead(csrfTokenClient.getTokenBlocking(), notificationId, "") + .map(mwQueryResponse -> mwQueryResponse.success()); + } catch (Throwable throwable) { + return Observable.just(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java index 112735127..5d0ac010b 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java @@ -1,14 +1,12 @@ package fr.free.nrw.commons.notification; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +import io.reactivex.Observable; +import io.reactivex.Single; /** * Created by root on 19.12.2017. @@ -16,27 +14,19 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; @Singleton public class NotificationController { - private MediaWikiApi mediaWikiApi; - private SessionManager sessionManager; + private NotificationClient notificationClient; + @Inject - public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) { - this.mediaWikiApi = mediaWikiApi; - this.sessionManager = sessionManager; + public NotificationController(NotificationClient notificationClient) { + this.notificationClient = notificationClient; } - public List getNotifications(boolean archived) throws IOException { - if (mediaWikiApi.validateLogin()) { - return mediaWikiApi.getNotifications(archived); - } else { - Boolean authTokenValidated = sessionManager.revalidateAuthToken(); - if (authTokenValidated != null && authTokenValidated) { - return mediaWikiApi.getNotifications(archived); - } - } - return new ArrayList<>(); + public Single> getNotifications(boolean archived) { + return notificationClient.getNotifications(archived); } - public boolean markAsRead(Notification notification) throws IOException{ - return mediaWikiApi.markNotificationAsRead(notification); + + Observable markAsRead(Notification notification) { + return notificationClient.markNotificationAsRead(notification.notificationId); } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java deleted file mode 100644 index 1c92103cd..000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ /dev/null @@ -1,293 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.annotation.SuppressLint; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.List; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; - -import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; - -public class NotificationUtils { - - private static final String COMMONS_WIKI = "commonswiki"; - private static final String WIKIDATA_WIKI = "wikidatawiki"; - private static final String WIKIPEDIA_WIKI = "enwiki"; - - /** - * Returns true if the wiki attribute corresponds to commonswiki - * @param document - * @return boolean representing whether the wiki attribute corresponds to commonswiki - */ - public static boolean isCommonsNotification(Node document) { - if (document == null || !document.hasAttributes()) { - return false; - } - Element element = (Element) document; - return COMMONS_WIKI.equals(element.getAttribute("wiki")); - } - - /** - * Returns true if the wiki attribute corresponds to wikidatawiki - * @param document - * @return boolean representing whether the wiki attribute corresponds to wikidatawiki - */ - public static boolean isWikidataNotification(Node document) { - if (document == null || !document.hasAttributes()) { - return false; - } - Element element = (Element) document; - return WIKIDATA_WIKI.equals(element.getAttribute("wiki")); - } - - /** - * Returns true if the wiki attribute corresponds to enwiki - * @param document - * @return - */ - public static boolean isWikipediaNotification(Node document) { - if (document == null || !document.hasAttributes()) { - return false; - } - Element element = (Element) document; - return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki")); - } - - /** - * Returns document notification type - * @param document - * @return the document's NotificationType - */ - public static NotificationType getNotificationType(Node document) { - Element element = (Element) document; - String type = element.getAttribute("type"); - return NotificationType.handledValueOf(type); - } - - public static String getNotificationId(Node document) { - Element element = (Element) document; - return element.getAttribute("id"); - } - - public static List getNotificationsFromBundle(Context context, Node document) { - Element bundledNotifications = getBundledNotifications(document); - NodeList childNodes = bundledNotifications.getChildNodes(); - - List notifications = new ArrayList<>(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - if (isUsefulNotification(node)) { - notifications.add(getNotificationFromApiResult(context, node)); - } - } - return notifications; - } - - @NonNull - public static List getNotificationsFromList(Context context, NodeList childNodes) { - List notifications = new ArrayList<>(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node node = childNodes.item(i); - if (isUsefulNotification(node)) { - if (isBundledNotification(node)) { - notifications.addAll(getNotificationsFromBundle(context, node)); - } else { - notifications.add(getNotificationFromApiResult(context, node)); - } - } - } - - return notifications; - } - - /** - * Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia - * This function returns true only if the notification belongs to any of the above wikis and is of a known notification type - * @param node - * @return whether a notification is from one of Commons, Wikidata or Wikipedia - */ - private static boolean isUsefulNotification(Node node) { - return (isCommonsNotification(node) - || isWikidataNotification(node) - || isWikipediaNotification(node)) - && !getNotificationType(node).equals(UNKNOWN); - } - - public static boolean isBundledNotification(Node document) { - Element bundleElement = getBundledNotifications(document); - if (bundleElement == null) { - return false; - } - - return bundleElement.getChildNodes().getLength() > 0; - } - - private static Element getBundledNotifications(Node document) { - return (Element) getNode(document, "bundledNotifications"); - } - - public static Notification getNotificationFromApiResult(Context context, Node document) { - NotificationType type = getNotificationType(document); - - String notificationText = ""; - String link = getPrimaryLink(document); - String description = getNotificationDescription(document); - String iconUrl = getNotificationIconUrl(document); - - switch (type) { - case THANK_YOU_EDIT: - notificationText = getThankYouEditDescription(document); - break; - case EDIT_USER_TALK: - notificationText = getNotificationText(document); - break; - case MENTION: - notificationText = getMentionMessage(context, document); - description = getMentionDescription(document); - break; - case WELCOME: - notificationText = getWelcomeMessage(context, document); - break; - } - return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl, getTimestampWithYear(document), - getNotificationId(document)); - } - - private static String getNotificationText(Node document) { - String notificationBody = getNotificationBody(document); - if (notificationBody == null || notificationBody.trim().equals("")) { - return getNotificationHeader(document); - } - return notificationBody; - } - - private static String getNotificationHeader(Node document) { - Node body = getNode(getModel(document), "header"); - if (body != null) { - String textContent = body.getTextContent(); - return textContent.replace("", "").replace("", ""); - } else { - return ""; - } - } - - private static String getNotificationBody(Node document) { - Node body = getNode(getModel(document), "body"); - if (body != null) { - String textContent = body.getTextContent(); - return textContent.replace("", "").replace("", ""); - } else { - return ""; - } - } - - private static String getMentionDescription(Node document) { - Node body = getNode(getModel(document), "body"); - return body != null ? body.getTextContent() : ""; - } - - /** - * Gets the header node returned in the XML document to form the description for thank you edits - * @param document - * @return - */ - private static String getThankYouEditDescription(Node document) { - Node body = getNode(getModel(document), "header"); - return body != null ? body.getTextContent() : ""; - } - - private static String getNotificationIconUrl(Node document) { - String format = "%s%s"; - Node iconUrl = getNode(getModel(document), "iconUrl"); - if (iconUrl == null) { - return null; - } else { - String url = iconUrl.getTextContent(); - return String.format(format, BuildConfig.COMMONS_URL, url); - } - } - - public static String getMentionMessage(Context context, Node document) { - String format = context.getString(R.string.notifications_mention); - return String.format(format, getAgent(document), getNotificationDescription(document)); - } - - @SuppressLint("StringFormatMatches") - public static String getUserTalkMessage(Context context, Node document) { - String format = context.getString(R.string.notifications_talk_page_message); - return String.format(format, getAgent(document)); - } - - @SuppressLint("StringFormatInvalid") - public static String getWelcomeMessage(Context context, Node document) { - String welcomeMessageFormat = context.getString(R.string.notifications_welcome); - return String.format(welcomeMessageFormat, getAgent(document)); - } - - private static String getPrimaryLink(Node document) { - Node links = getNode(getModel(document), "links"); - Element primaryLink = (Element) getNode(links, "primary"); - if (primaryLink != null) { - return primaryLink.getAttribute("url"); - } - return ""; - } - - private static Node getModel(Node document) { - return getNode(document, "_.2A."); - } - - private static String getAgent(Node document) { - Element agentElement = (Element) getNode(document, "agent"); - if (agentElement != null) { - return agentElement.getAttribute("name"); - } - return ""; - } - - private static String getTimestamp(Node document) { - Element timestampElement = (Element) getNode(document, "timestamp"); - if (timestampElement != null) { - return timestampElement.getAttribute("date"); - } - return ""; - } - - private static String getTimestampWithYear(Node document) { - Element timestampElement = (Element) getNode(document, "timestamp"); - if (timestampElement != null) { - return timestampElement.getAttribute("utcunix"); - } - return ""; - } - - private static String getNotificationDescription(Node document) { - Element titleElement = (Element) getNode(document, "title"); - if (titleElement != null) { - return titleElement.getAttribute("text"); - } - return ""; - } - - @Nullable - public static Node getNode(Node node, String nodeName) { - NodeList childNodes = node.getChildNodes(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node nodeItem = childNodes.item(i); - Element item = (Element) nodeItem; - if (item.getTagName().equals(nodeName)) { - return nodeItem; - } - } - return null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java index c98c7ed3e..1aaeb6a81 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java @@ -4,8 +4,6 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.NotificationManager; import android.content.Context; -import android.view.Gravity; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,17 +11,23 @@ import androidx.core.app.NotificationCompat; import org.wikipedia.dataclient.mwapi.MwQueryPage; +import java.util.ArrayList; +import java.util.concurrent.Callable; + import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.actions.PageEditClient; +import fr.free.nrw.commons.actions.ThanksClient; import fr.free.nrw.commons.delete.DeleteHelper; import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; +import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -32,13 +36,15 @@ import timber.log.Timber; public class ReviewController { private static final int NOTIFICATION_SEND_THANK = 0x102; private static final int NOTIFICATION_CHECK_CATEGORY = 0x101; + protected static ArrayList categories; + @Inject + ThanksClient thanksClient; private final DeleteHelper deleteHelper; @Nullable MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName @Inject - MediaWikiApi mwApi; - @Inject - SessionManager sessionManager; + @Named("commons-page-edit") + PageEditClient pageEditClient; private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; private Media media; @@ -89,40 +95,16 @@ public class ReviewController { .getCommonsApplicationComponent() .inject(this); - Toast toast = new Toast(context); - toast.setGravity(Gravity.CENTER, 0, 0); - toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); - toast.show(); + ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle())); - Observable.fromCallable(() -> { - publishProgress(context, 0); - - String editToken; - String authCookie; - String summary = context.getString(R.string.check_category_edit_summary); - - authCookie = sessionManager.getAuthCookie(); - mwApi.setAuthCookie(authCookie); - - try { - editToken = mwApi.getEditToken(); - - if (editToken == null) { - return false; - } - publishProgress(context, 1); - - mwApi.appendEdit(editToken, "\n{{subst:chc}}\n", media.getFilename(), summary); - publishProgress(context, 2); - } catch (Exception e) { - Timber.d(e); - return false; - } - return true; - }) + publishProgress(context, 0); + String summary = context.getString(R.string.check_category_edit_summary); + Observable.defer((Callable>) () -> + pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((result) -> { + publishProgress(context, 2); String message; String title; @@ -136,15 +118,7 @@ public class ReviewController { reviewCallback.onFailure(); } - notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_HIGH); - notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); + showNotification(title, message); }, Timber::e); } @@ -172,39 +146,20 @@ public class ReviewController { .getInstance(context) .getCommonsApplicationComponent() .inject(this); - Toast toast = new Toast(context); - toast.setGravity(Gravity.CENTER, 0, 0); - toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT); - toast.show(); + ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle())); - Observable.fromCallable(() -> { - publishProgress(context, 0); + publishProgress(context, 0); + if (firstRevision == null) { + return; + } - String editToken; - String authCookie; - authCookie = sessionManager.getAuthCookie(); - mwApi.setAuthCookie(authCookie); - - try { - editToken = mwApi.getEditToken(); - if (editToken == null) { - return false; - } - publishProgress(context, 1); - assert firstRevision != null; - mwApi.thank(editToken, firstRevision.getRevisionId()); - publishProgress(context, 2); - } catch (Exception e) { - Timber.d(e); - return false; - } - return true; - }) + Observable.defer((Callable>) () -> thanksClient.thank(firstRevision.getRevisionId())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((result) -> { - String message = ""; - String title = ""; + publishProgress(context, 2); + String message; + String title; if (result) { title = context.getString(R.string.send_thank_success_title); message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); @@ -213,19 +168,23 @@ public class ReviewController { message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle()); } - notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_HIGH); - notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + showNotification(title, message); }, Timber::e); } + private void showNotification(String title, String message) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(title) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH); + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); + } + public interface ReviewCallback { void onSuccess(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 9e4c9d334..1770bb3e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -140,7 +140,7 @@ public class FileUtils { return mimeType; } - public static String getFileExt(String fileName) { + static String getFileExt(String fileName) { //Default filePath extension String extension = ".jpg"; @@ -151,7 +151,7 @@ public class FileUtils { return extension; } - public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { + static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { return new FileInputStream(filePath); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java new file mode 100644 index 000000000..87f1b69ed --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java @@ -0,0 +1,67 @@ +package fr.free.nrw.commons.upload; + +import android.content.Context; +import android.net.Uri; + +import org.wikipedia.csrf.CsrfTokenClient; + +import java.io.File; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import fr.free.nrw.commons.contributions.Contribution; +import io.reactivex.Observable; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF; + +@Singleton +public class UploadClient { + + private final UploadInterface uploadInterface; + private final CsrfTokenClient csrfTokenClient; + + @Inject + public UploadClient(UploadInterface uploadInterface, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { + this.uploadInterface = uploadInterface; + this.csrfTokenClient = csrfTokenClient; + } + + Observable uploadFileToStash(Context context, String filename, File file) { + RequestBody requestFile = RequestBody + .create(MediaType.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))), file); + + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", filename, requestFile); + RequestBody fileNameRequestBody = RequestBody.create(okhttp3.MultipartBody.FORM, filename); + RequestBody tokenRequestBody; + try { + tokenRequestBody = RequestBody.create(MultipartBody.FORM, csrfTokenClient.getTokenBlocking()); + return uploadInterface.uploadFileToStash(fileNameRequestBody, tokenRequestBody, filePart) + .map(stashUploadResponse -> stashUploadResponse.getUpload()); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return Observable.error(throwable); + } + } + + Observable uploadFileFromStash(Context context, + Contribution contribution, + String uniqueFileName, + String fileKey) { + try { + return uploadInterface + .uploadFileFromStash(csrfTokenClient.getTokenBlocking(), + contribution.getPageContents(context), + contribution.getEditSummary(), + uniqueFileName, + fileKey).map(uploadResponse -> uploadResponse.getUpload()); + } catch (Throwable throwable) { + throwable.printStackTrace(); + return Observable.error(throwable); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java new file mode 100644 index 000000000..af3ffd77d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadInterface.java @@ -0,0 +1,34 @@ +package fr.free.nrw.commons.upload; + +import androidx.annotation.NonNull; + +import io.reactivex.Observable; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.Headers; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; + +import static org.wikipedia.dataclient.Service.MW_API_PREFIX; + +public interface UploadInterface { + + @Multipart + @POST(MW_API_PREFIX + "action=upload&stash=1&ignorewarnings=1") + Observable uploadFileToStash(@Part("filename") RequestBody filename, + @Part("token") RequestBody token, + @Part MultipartBody.Part filePart); + + @Headers("Cache-Control: no-cache") + @POST(MW_API_PREFIX + "action=upload&ignorewarnings=1") + @FormUrlEncoded + @NonNull + Observable uploadFileFromStash(@NonNull @Field("token") String token, + @NonNull @Field("text") String text, + @NonNull @Field("comment") String comment, + @NonNull @Field("filename") String filename, + @NonNull @Field("filekey") String filekey); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java new file mode 100644 index 000000000..80d612c69 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadResponse.java @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.upload; + +public class UploadResponse { + private final UploadResult upload; + + public UploadResponse(UploadResult upload) { + this.upload = upload; + } + + public UploadResult getUpload() { + return upload; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java new file mode 100644 index 000000000..1dea597d4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadResult.java @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.upload; + +import org.wikipedia.gallery.ImageInfo; + +public class UploadResult { + private final String result; + private final String filekey; + private final String filename; + private final String sessionkey; + private final ImageInfo imageinfo; + + public UploadResult(String result, String filekey, String filename, String sessionkey, ImageInfo imageinfo) { + this.result = result; + this.filekey = filekey; + this.filename = filename; + this.sessionkey = sessionkey; + this.imageinfo = imageinfo; + } + + public String getResult() { + return result; + } + + public String getFilekey() { + return filekey; + } + + public String getSessionkey() { + return sessionkey; + } + + public ImageInfo getImageinfo() { + return imageinfo; + } + + public String getFilename() { + return filename; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 7e10846a9..c9e5a2ee8 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -36,8 +36,9 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.wikidata.WikidataEditService; -import io.reactivex.Single; +import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -56,8 +57,8 @@ public class UploadService extends HandlerService { @Inject WikidataEditService wikidataEditService; @Inject SessionManager sessionManager; @Inject ContributionDao contributionDao; - @Inject - MediaClient mediaClient; + @Inject UploadClient uploadClient; + @Inject MediaClient mediaClient; private NotificationManagerCompat notificationManager; private NotificationCompat.Builder curNotification; @@ -208,9 +209,9 @@ public class UploadService extends HandlerService { return; } String notificationTag = localUri.toString(); - + File file1; try { - File file1 = new File(localUri.getPath()); + file1 = new File(localUri.getPath()); fileInputStream = new FileInputStream(file1); } catch (FileNotFoundException e) { Timber.d("File not found"); @@ -233,22 +234,8 @@ public class UploadService extends HandlerService { contribution ); - Single.fromCallable(() -> { - if (!mwApi.validateLogin()) { - // Need to revalidate! - if (sessionManager.revalidateAuthToken()) { - Timber.d("Successfully revalidated token!"); - } else { - Timber.d("Unable to revalidate :("); - stopForeground(true); - sessionManager.forceLogin(UploadService.this); - throw new RuntimeException(getString(R.string.authentication_failed)); - } - } - return "Temp_" + contribution.hashCode() + filename; - }).flatMap(stashFilename -> mwApi.uploadFile( - stashFilename, fileInputStream, contribution.getDataLength(), - localUri, contribution.getContentProviderUri(), notificationUpdater)) + Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename) + .flatMap(stashFilename -> uploadClient.uploadFileToStash(getApplicationContext(), stashFilename, file1)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .doFinally(() -> { @@ -267,22 +254,20 @@ public class UploadService extends HandlerService { Timber.d("Stash upload response 1 is %s", uploadStash.toString()); - String resultStatus = uploadStash.getResultStatus(); + String resultStatus = uploadStash.getResult(); if (!resultStatus.equals("Success")) { Timber.d("Contribution upload failed. Wikidata entity won't be edited"); showFailedNotification(contribution); - return Single.never(); + return Observable.never(); } else { - synchronized (unfinishedUploads) { - Timber.d("making sure of uniqueness of name: %s", filename); - String uniqueFilename = findUniqueFilename(filename); - unfinishedUploads.add(uniqueFilename); - return mwApi.uploadFileFinalize( - uniqueFilename, - uploadStash.getFilekey(), - contribution.getPageContents(getApplicationContext()), - contribution.getEditSummary()); - } + Timber.d("making sure of uniqueness of name: %s", filename); + String uniqueFilename = findUniqueFilename(filename); + unfinishedUploads.add(uniqueFilename); + return uploadClient.uploadFileFromStash( + getApplicationContext(), + contribution, + uniqueFilename, + uploadStash.getFilekey()); } }) .subscribe(uploadResult -> { @@ -290,19 +275,20 @@ public class UploadService extends HandlerService { notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); - String resultStatus = uploadResult.getResultStatus(); + String resultStatus = uploadResult.getResult(); if (!resultStatus.equals("Success")) { Timber.d("Contribution upload failed. Wikidata entity won't be edited"); showFailedNotification(contribution); } else { - String canonicalFilename = uploadResult.getCanonicalFilename(); + String canonicalFilename = "File:" + uploadResult.getFilename(); Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s", contribution.getWikiDataEntityId()); wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename); contribution.setFilename(canonicalFilename); - contribution.setImageUrl(uploadResult.getImageUrl()); + contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl()); contribution.setState(Contribution.STATE_COMPLETED); - contribution.setDateUploaded(uploadResult.getDateUploaded()); + contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort() + .parse(uploadResult.getImageinfo().getTimestamp())); contributionDao.save(contribution); } }, throwable -> { diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java new file mode 100644 index 000000000..74c05564c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataClient.java @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.wikidata; + +import org.wikipedia.csrf.CsrfTokenClient; +import org.wikipedia.dataclient.Service; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import io.reactivex.Observable; + +import static fr.free.nrw.commons.di.NetworkingModule.NAMED_WIKI_DATA_CSRF; + +@Singleton +public class WikidataClient { + + private final Service service; + private final CsrfTokenClient csrfTokenClient; + + @Inject + public WikidataClient(@Named("wikidata-service") Service service, + @Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient) { + this.service = service; + this.csrfTokenClient = csrfTokenClient; + } + + public Observable createClaim(String entityId, String property, String snaktype, String value) { + try { + return service.postCreateClaim(entityId, snaktype, property, value, "en", csrfTokenClient.getTokenBlocking()) + .map(mwPostResponse -> { + if (mwPostResponse.getSuccessVal() == 1) { + return 1L; + } + return -1L; + }); + } catch (Throwable throwable) { + return Observable.just(-1L); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index eac1f7cde..8bb753cf9 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -10,10 +10,10 @@ import javax.inject.Named; import javax.inject.Singleton; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; @@ -26,20 +26,26 @@ import timber.log.Timber; @Singleton public class WikidataEditService { + private final static String COMMONS_APP_TAG = "wikimedia-commons-app"; + private final static String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app"; + private final Context context; - private final MediaWikiApi mediaWikiApi; private final WikidataEditListener wikidataEditListener; private final JsonKvStore directKvStore; + private final WikidataClient wikidataClient; + private final PageEditClient wikiDataPageEditClient; @Inject public WikidataEditService(Context context, - MediaWikiApi mediaWikiApi, WikidataEditListener wikidataEditListener, - @Named("default_preferences") JsonKvStore directKvStore) { + @Named("default_preferences") JsonKvStore directKvStore, + WikidataClient wikidataClient, + @Named("wikidata-page-edit") PageEditClient wikiDataPageEditClient) { this.context = context; - this.mediaWikiApi = mediaWikiApi; this.wikidataEditListener = wikidataEditListener; this.directKvStore = directKvStore; + this.wikidataClient = wikidataClient; + this.wikiDataPageEditClient = wikiDataPageEditClient; } /** @@ -77,13 +83,20 @@ public class WikidataEditService { private void editWikidataProperty(String wikidataEntityId, String fileName) { Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId); Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId); - Observable.fromCallable(() -> { - String propertyValue = getFileName(fileName); - return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue); - }) + + String propertyValue = getFileName(fileName); + + Timber.d(propertyValue); + wikidataClient.createClaim(wikidataEntityId, "P18", "value", propertyValue) + .flatMap(revisionId -> { + if (revisionId != -1) { + return wikiDataPageEditClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON); + } + throw new RuntimeException("Unable to edit wikidata item"); + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> { + .subscribe(revisionId -> handleClaimResult(wikidataEntityId, String.valueOf(revisionId)), throwable -> { Timber.e(throwable, "Error occurred while making claim"); ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); }); @@ -95,31 +108,12 @@ public class WikidataEditService { wikidataEditListener.onSuccessfulWikidataEdit(); } showSuccessToast(); - logEdit(revisionId); } else { Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId); ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); } } - /** - * Log the Wikidata edit by adding Wikimedia Commons App tag to the edit - * @param revisionId - */ - @SuppressLint("CheckResult") - private void logEdit(String revisionId) { - Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - Timber.d("Wikidata edit was tagged successfully"); - } else { - Timber.d("Wikidata edit couldn't be tagged"); - } - }, throwable -> Timber.e(throwable, "Error occurred while adding tag to the edit")); - } - /** * Show a success toast when the edit is made successfully */ diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index 46d7dfbfc..8df1971a1 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -1,19 +1,22 @@ package fr.free.nrw.commons.delete -import android.accounts.Account import android.content.Context import fr.free.nrw.commons.Media -import fr.free.nrw.commons.auth.SessionManager -import fr.free.nrw.commons.mwapi.MediaWikiApi +import fr.free.nrw.commons.actions.PageEditClient import fr.free.nrw.commons.notification.NotificationHelper import fr.free.nrw.commons.utils.ViewUtilWrapper +import io.reactivex.Observable import org.junit.Assert.* import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatchers import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.wikipedia.AppAdapter +import javax.inject.Inject +import javax.inject.Named /** * Tests for delete helper @@ -21,17 +24,15 @@ import org.mockito.MockitoAnnotations class DeleteHelperTest { @Mock - internal var mwApi: MediaWikiApi? = null - - @Mock - internal var sessionManager: SessionManager? = null - - @Mock - internal var notificationHelper: NotificationHelper? = null + @field:[Inject Named("commons-page-edit")] + internal var pageEditClient: PageEditClient? = null @Mock internal var context: Context? = null + @Mock + internal var notificationHelper: NotificationHelper? = null + @Mock internal var viewUtil: ViewUtilWrapper? = null @@ -54,9 +55,13 @@ class DeleteHelperTest { */ @Test fun makeDeletion() { - `when`(mwApi?.editToken).thenReturn("token") - `when`(sessionManager?.authCookie).thenReturn("Mock cookie") - `when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test")) + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(media?.displayTitle).thenReturn("Test file") `when`(media?.filename).thenReturn("Test file.jpg") @@ -68,16 +73,45 @@ class DeleteHelperTest { /** * Test a failed deletion */ - @Test - fun makeDeletionForNullToken() { - `when`(mwApi?.editToken).thenReturn(null) - `when`(sessionManager?.authCookie).thenReturn("Mock cookie") - `when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test")) + @Test(expected = RuntimeException::class) + fun makeDeletionForPrependEditFailure() { + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(false)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) `when`(media?.displayTitle).thenReturn("Test file") `when`(media?.filename).thenReturn("Test file.jpg") - val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() - assertNotNull(makeDeletion) - assertFalse(makeDeletion!!) + deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() + } + + @Test(expected = RuntimeException::class) + fun makeDeletionForEditFailure() { + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(false)) + `when`(media?.displayTitle).thenReturn("Test file") + `when`(media?.filename).thenReturn("Test file.jpg") + + deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() + } + + @Test(expected = RuntimeException::class) + fun makeDeletionForAppendEditFailure() { + `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(false)) + `when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) + .thenReturn(Observable.just(true)) + `when`(media?.displayTitle).thenReturn("Test file") + `when`(media?.filename).thenReturn("Test file.jpg") + + deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() } } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt index 2aa5e47f5..42519646f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -2,7 +2,6 @@ package fr.free.nrw.commons.mwapi import android.os.Build import androidx.test.core.app.ApplicationProvider -import com.google.gson.Gson import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.utils.ConfigUtils @@ -38,7 +37,7 @@ class ApacheHttpClientMediaWikiApiTest { wikidataServer = MockWebServer() okHttpClient = OkHttpClient() sharedPreferences = mock(JsonKvStore::class.java) - testObject = ApacheHttpClientMediaWikiApi(ApplicationProvider.getApplicationContext(), "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences) + testObject = ApacheHttpClientMediaWikiApi("http://" + server.hostName + ":" + server.port + "/") } @After @@ -46,181 +45,6 @@ class ApacheHttpClientMediaWikiApiTest { server.shutdown() } - @Test - fun authCookiesAreHandled() { - assertEquals("", testObject.authCookie) - - testObject.authCookie = "cookie=chocolate-chip" - - assertEquals("cookie=chocolate-chip", testObject.authCookie) - } - - @Test - fun simpleLoginWithWrongPassword() { - server.enqueue(MockResponse().setBody("")) - server.enqueue(MockResponse().setBody("")) - - val result = testObject.login("foo", "bar") - - assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> - parseBody(loginTokenRequest.body.readUtf8()).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("login", body["type"]) - assertEquals("tokens", body["meta"]) - } - } - - assertBasicRequestParameters(server, "POST").let { loginRequest -> - parseBody(loginRequest.body.readUtf8()).let { body -> - assertEquals("1", body["rememberMe"]) - assertEquals("foo", body["username"]) - assertEquals("bar", body["password"]) - assertEquals("baz", body["logintoken"]) - assertEquals("https://commons.wikimedia.org", body["loginreturnurl"]) - assertEquals("xml", body["format"]) - } - } - - assertEquals("wrongpassword", result) - } - - @Test - fun simpleLogin() { - server.enqueue(MockResponse().setBody("")) - server.enqueue(MockResponse().setBody("")) - - val result = testObject.login("foo", "bar") - - assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> - parseBody(loginTokenRequest.body.readUtf8()).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("login", body["type"]) - assertEquals("tokens", body["meta"]) - } - } - - assertBasicRequestParameters(server, "POST").let { loginRequest -> - parseBody(loginRequest.body.readUtf8()).let { body -> - assertEquals("1", body["rememberMe"]) - assertEquals("foo", body["username"]) - assertEquals("bar", body["password"]) - assertEquals("baz", body["logintoken"]) - assertEquals("https://commons.wikimedia.org", body["loginreturnurl"]) - assertEquals("xml", body["format"]) - } - } - - assertEquals("PASS", result) - } - - @Test - fun twoFactorLogin() { - server.enqueue(MockResponse().setBody("")) - server.enqueue(MockResponse().setBody("")) - - val result = testObject.login("foo", "bar", "2fa") - - assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> - parseBody(loginTokenRequest.body.readUtf8()).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("login", body["type"]) - assertEquals("tokens", body["meta"]) - } - } - - assertBasicRequestParameters(server, "POST").let { loginRequest -> - parseBody(loginRequest.body.readUtf8()).let { body -> - assertEquals("true", body["rememberMe"]) - assertEquals("foo", body["username"]) - assertEquals("bar", body["password"]) - assertEquals("baz", body["logintoken"]) - assertEquals("true", body["logincontinue"]) - assertEquals("2fa", body["OATHToken"]) - assertEquals("xml", body["format"]) - } - } - - assertEquals("PASS", result) - } - - @Test - fun validateLoginForLoggedInUser() { - server.enqueue(MockResponse().setBody("")) - - val result = testObject.validateLogin() - - assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> - parseQueryParams(loginTokenRequest).let { body -> - assertEquals("xml", body["format"]) - assertEquals("query", body["action"]) - assertEquals("userinfo", body["meta"]) - } - } - - assertTrue(result) - } - - @Test - fun validateLoginForLoggedOutUser() { - server.enqueue(MockResponse().setBody("")) - - val result = testObject.validateLogin() - - assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> - parseQueryParams(loginTokenRequest).let { params -> - assertEquals("xml", params["format"]) - assertEquals("query", params["action"]) - assertEquals("userinfo", params["meta"]) - } - } - - assertFalse(result) - } - - @Test - fun editToken() { - server.enqueue(MockResponse().setBody("")) - - val result = testObject.editToken - - assertBasicRequestParameters(server, "POST").let { editTokenRequest -> - parseBody(editTokenRequest.body.readUtf8()).let { body -> - assertEquals("query", body["action"]) - assertEquals("tokens", body["meta"]) - } - } - - assertEquals("baz", result) - } - - @Test - fun getWikidataEditToken() { - server.enqueue(MockResponse().setBody("")) - wikidataServer.enqueue(MockResponse().setBody("")) - - val result = testObject.wikidataCsrfToken - - assertBasicRequestParameters(server, "GET").let { centralAuthTokenRequest -> - parseQueryParams(centralAuthTokenRequest).let { params -> - assertEquals("xml", params["format"]) - assertEquals("centralauthtoken", params["action"]) - } - } - - assertBasicRequestParameters(wikidataServer, "POST").let { editTokenRequest -> - parseBody(editTokenRequest.body.readUtf8()).let { body -> - assertEquals("query", body["action"]) - assertEquals("abc", body["centralauthtoken"]) - assertEquals("tokens", body["meta"]) - } - } - - assertEquals("baz", result) - } - @Test fun isUserBlockedFromCommonsForInfinitelyBlockedUser() { server.enqueue(MockResponse().setBody(""))