Consume login client from data client library (#2894)

Fix actions for review client

Use data client library for notifications

With delete helper migrated to data client

With wikidata edits

With notifications and modifications migrated to data client

With upload migrated to retrofit

Delete unused code

Reuse thank interface from the library
This commit is contained in:
Vivek Maskara 2019-07-07 14:27:45 +05:30
parent 623c2f5467
commit d427a77c2a
35 changed files with 883 additions and 1668 deletions

View file

@ -1,7 +1,6 @@
plugins { plugins {
id 'com.github.triplet.play' version '2.2.1' apply false id 'com.github.triplet.play' version '2.2.1' apply false
} }
apply from: '../gitutils.gradle' apply from: '../gitutils.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
@ -32,7 +31,7 @@ dependencies {
implementation 'com.facebook.fresco:fresco:1.13.0' implementation 'com.facebook.fresco:fresco:1.13.0'
implementation 'com.drewnoakes:metadata-extractor:2.11.0' implementation 'com.drewnoakes:metadata-extractor:2.11.0'
implementation 'org.apache.commons:commons-lang3:3.8.1' 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 // UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' 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", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" 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", "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_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\"" 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", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" 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", "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_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\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""

View file

@ -61,7 +61,7 @@ public class CommonsAppAdapter extends AppAdapter {
@Override @Override
public void updateAccount(@NonNull LoginResult result) { public void updateAccount(@NonNull LoginResult result) {
// TODO: sessionManager.updateAccount(result); sessionManager.updateAccount(result);
} }
@Override @Override
@ -69,7 +69,8 @@ public class CommonsAppAdapter extends AppAdapter {
if (!preferences.contains(COOKIE_STORE_NAME)) { if (!preferences.contains(COOKIE_STORE_NAME)) {
return null; 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 @Override

View file

@ -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<Boolean> 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<Boolean> 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<Boolean> 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<Integer> 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);
}
}
}

View file

@ -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<Edit> 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<Edit> 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<Edit> postPrependEdit(@NonNull @Field("title") String title,
@NonNull @Field("summary") String summary,
@NonNull @Field("prependtext") String text,
@NonNull @Field("token") String token);
}

View file

@ -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<Boolean> 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);
}
}
}

View file

@ -3,8 +3,8 @@ package fr.free.nrw.commons.auth;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.content.Context; import android.content.Context;
import androidx.annotation.Nullable;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import timber.log.Timber; import timber.log.Timber;
@ -12,10 +12,8 @@ public class AccountUtil {
public static final String AUTH_COOKIE = "authCookie"; public static final String AUTH_COOKIE = "authCookie";
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
private final Context context;
public AccountUtil(Context context) { public AccountUtil() {
this.context = context;
} }
/** /**
@ -49,5 +47,4 @@ public class AccountUtil {
private static AccountManager accountManager(Context context) { private static AccountManager accountManager(Context context) {
return AccountManager.get(context); return AccountManager.get(context);
} }
} }

View file

@ -1,9 +1,6 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity; import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -20,14 +17,6 @@ import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; 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.ColorRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
@ -35,6 +24,17 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NavUtils; import androidx.core.app.NavUtils;
import androidx.core.content.ContextCompat; 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.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; 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.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable; import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Action;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; 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 { public class LoginActivity extends AccountAuthenticatorActivity {
@ -71,10 +72,17 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Inject @Inject
SessionManager sessionManager; SessionManager sessionManager;
@Inject
@Named(NAMED_COMMONS_WIKI_SITE)
WikiSite commonsWikiSite;
@Inject @Inject
@Named("default_preferences") @Named("default_preferences")
JsonKvStore applicationKvStore; JsonKvStore applicationKvStore;
@Inject
LoginClient loginClient;
@BindView(R.id.login_button) @BindView(R.id.login_button)
Button loginButton; Button loginButton;
@ -104,13 +112,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private LoginTextWatcher textWatcher = new LoginTextWatcher(); private LoginTextWatcher textWatcher = new LoginTextWatcher();
private CompositeDisposable compositeDisposable = new CompositeDisposable(); 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 @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -214,7 +215,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
&& sessionManager.isUserLoggedIn() && sessionManager.isUserLoggedIn()
&& sessionManager.getCachedAuthCookie() != null) { && sessionManager.getCachedAuthCookie() != null) {
applicationKvStore.putBoolean("login_skipped", false); applicationKvStore.putBoolean("login_skipped", false);
sessionManager.revalidateAuthToken();
startMainActivity(); startMainActivity();
} }
@ -244,7 +244,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@OnClick(R.id.login_button) @OnClick(R.id.login_button)
public void performLogin() { public void performLogin() {
loginCurrentlyInProgress = true;
Timber.d("Login to start!"); Timber.d("Login to start!");
final String username = usernameEdit.getText().toString(); final String username = usernameEdit.getText().toString();
final String rawUsername = usernameEdit.getText().toString().trim(); final String rawUsername = usernameEdit.getText().toString().trim();
@ -252,23 +251,37 @@ public class LoginActivity extends AccountAuthenticatorActivity {
String twoFactorCode = twoFactorEdit.getText().toString(); String twoFactorCode = twoFactorEdit.getText().toString();
showLoggingProgressBar(); showLoggingProgressBar();
compositeDisposable.add(Observable.fromCallable(() -> login(username, password, twoFactorCode)) doLogin(username, password, twoFactorCode);
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> handleLogin(username, rawUsername, password, result)));
} }
private String login(String username, String password, String twoFactorCode) { private void doLogin(String username, String password, String twoFactorCode) {
try { progressDialog.show();
if (twoFactorCode.isEmpty()) {
return mwApi.login(username, password); Action action = () -> {
} else { try {
return mwApi.login(username, password, twoFactorCode); 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(); 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() { private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this); progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true); progressDialog.setIndeterminate(true);
@ -302,67 +303,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
progressDialog.show(); 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(); 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(); 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 @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
@ -402,30 +354,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
return getDelegate().getMenuInflater(); 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() { public void askUserForTwoFactorAuth() {
progressDialog.dismiss(); progressDialog.dismiss();
twoFactorContainer.setVisibility(VISIBLE); 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() { public void showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor); showMessage(R.string.login_success, R.color.primaryDarkColor);
progressDialog.dismiss(); progressDialog.dismiss();
} }
public void emptySensitiveEditFields() {
passwordEdit.setText("");
twoFactorEdit.setText("");
}
public void startMainActivity() { public void startMainActivity() {
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP); NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish(); finish();
@ -461,6 +391,12 @@ public class LoginActivity extends AccountAuthenticatorActivity {
errorMessageContainer.setVisibility(VISIBLE); 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() { private AppCompatDelegate getDelegate() {
if (delegate == null) { if (delegate == null) {
delegate = AppCompatDelegate.create(this, null); delegate = AppCompatDelegate.create(this, null);

View file

@ -1,14 +1,16 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context; 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 androidx.annotation.Nullable;
import org.wikipedia.login.LoginResult;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -20,10 +22,6 @@ import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import timber.log.Timber; 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. * Manage the current logged in user session.
*/ */
@ -34,7 +32,6 @@ public class SessionManager {
private Account currentAccount; // Unlike a savings account... ;-) private Account currentAccount; // Unlike a savings account... ;-)
private JsonKvStore defaultKvStore; private JsonKvStore defaultKvStore;
private static final String KEY_RAWUSERNAME = "rawusername"; private static final String KEY_RAWUSERNAME = "rawusername";
private Bundle userdata = new Bundle();
@Inject @Inject
public SessionManager(Context context, public SessionManager(Context context,
@ -46,43 +43,40 @@ public class SessionManager {
this.defaultKvStore = defaultKvStore; this.defaultKvStore = defaultKvStore;
} }
/** private boolean createAccount(@NonNull String userName, @NonNull String password) {
* Creata a new account Account account = getCurrentAccount();
* if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
* @param response removeAccount();
* @param username account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
* @param rawusername return accountManager().addAccountExplicitly(account, password, null);
* @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");
} }
return true;
}
// FIXME: If the user turns it off, it shouldn't be auto turned back on public void removeAccount() {
ContentResolver.setSyncAutomatically(account, BuildConfig.CONTRIBUTION_AUTHORITY, true); // Enable sync by default! Account account = getCurrentAccount();
ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! 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); 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() { public String getAuthCookie() {
if (!isUserLoggedIn()) { if (!isUserLoggedIn()) {
Timber.e("User is not logged in"); Timber.e("User is not logged in");

View file

@ -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<String> getSubCategoryList(NodeList childNodes) {
List<String> 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");
}
}

View file

@ -1,8 +1,5 @@
package fr.free.nrw.commons.contributions; 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.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -13,21 +10,29 @@ import android.view.animation.AnimationUtils;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager; 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.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import javax.inject.Inject; import fr.free.nrw.commons.wikidata.WikidataClient;
import javax.inject.Named;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/** /**
* Created by root on 01.06.2018. * Created by root on 01.06.2018.
@ -53,6 +58,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@Inject @Named("default_preferences") JsonKvStore kvStore; @Inject @Named("default_preferences") JsonKvStore kvStore;
@Inject ContributionController controller; @Inject ContributionController controller;
@Inject
WikidataClient wikidataClient;
private Animation fab_close; private Animation fab_close;
private Animation fab_open; private Animation fab_open;
@ -163,6 +170,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
/** /**
* Responsible to set progress bar invisible and visible * Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden. * @param shouldShow True when contributions list should be hidden.
*/ */
public void showProgress(boolean shouldShow) { public void showProgress(boolean shouldShow) {
@ -170,7 +178,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
} }
public void showNoContributionsUI(boolean shouldShow) { public void showNoContributionsUI(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow?VISIBLE:GONE); noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
} }
public void onDataSetChanged() { public void onDataSetChanged() {

View file

@ -12,18 +12,19 @@ import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; 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.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager; 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.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig; 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.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.upload.UploadService;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -305,7 +305,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void setNotificationCount() { private void setNotificationCount() {
compositeDisposable.add(Observable.fromCallable(() -> notificationController.getNotifications(false)) compositeDisposable.add(notificationController.getNotifications(false)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::initNotificationViews, .subscribe(this::initNotificationViews,

View file

@ -11,19 +11,22 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.Callable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; 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.mwapi.MediaWikiApi;
import fr.free.nrw.commons.notification.NotificationHelper; import fr.free.nrw.commons.notification.NotificationHelper;
import fr.free.nrw.commons.review.ReviewController; import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.utils.ViewUtilWrapper; import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -35,20 +38,20 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D
*/ */
@Singleton @Singleton
public class DeleteHelper { public class DeleteHelper {
private final MediaWikiApi mwApi;
private final SessionManager sessionManager;
private final NotificationHelper notificationHelper; private final NotificationHelper notificationHelper;
private final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil; private final ViewUtilWrapper viewUtil;
private final String username;
@Inject @Inject
public DeleteHelper(MediaWikiApi mwApi, public DeleteHelper(NotificationHelper notificationHelper,
SessionManager sessionManager, @Named("commons-page-edit") PageEditClient pageEditClient,
NotificationHelper notificationHelper, ViewUtilWrapper viewUtil,
ViewUtilWrapper viewUtil) { @Named("username") String username) {
this.mwApi = mwApi;
this.sessionManager = sessionManager;
this.notificationHelper = notificationHelper; this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
this.viewUtil = viewUtil; this.viewUtil = viewUtil;
this.username = username;
} }
/** /**
@ -59,10 +62,11 @@ public class DeleteHelper {
* @return * @return
*/ */
public Single<Boolean> makeDeletion(Context context, Media media, String reason) { public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
viewUtil.showShortToast(context, context.getString((R.string.delete_helper_make_deletion_toast), media.getDisplayTitle())); viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
return Single.fromCallable(() -> delete(media, reason))
.flatMap(result -> Single.fromCallable(() -> return delete(media, reason)
showDeletionNotification(context, media, result))); .flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
.firstOrError();
} }
/** /**
@ -71,14 +75,9 @@ public class DeleteHelper {
* @param reason * @param reason
* @return * @return
*/ */
private boolean delete(Media media, String reason) { private Observable<Boolean> delete(Media media, String reason) {
String editToken; Timber.d("thread is delete %s", Thread.currentThread().getName());
String authCookie;
String summary = "Nominating " + media.getFilename() + " for deletion."; String summary = "Nominating " + media.getFilename() + " for deletion.";
authCookie = sessionManager.getAuthCookie();
mwApi.setAuthCookie(authCookie);
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
String fileDeleteString = "{{delete|reason=" + reason + String fileDeleteString = "{{delete|reason=" + reason +
"|subpage=" + media.getFilename() + "|subpage=" + media.getFilename() +
@ -99,26 +98,23 @@ public class DeleteHelper {
String userPageString = "\n{{subst:idw|" + media.getFilename() + String userPageString = "\n{{subst:idw|" + media.getFilename() +
"}} ~~~~"; "}} ~~~~";
try { return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
editToken = mwApi.getEditToken(); .flatMap(result -> {
if (result) {
if(editToken == null) { return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
return false; }
} throw new RuntimeException("Failed to nominate for deletion");
}).flatMap(result -> {
mwApi.prependEdit(editToken, fileDeleteString + "\n", if (result) {
media.getFilename(), summary); return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
mwApi.edit(editToken, subpageString + "\n", }
"Commons:Deletion_requests/" + media.getFilename(), summary); throw new RuntimeException("Failed to nominate for deletion");
mwApi.appendEdit(editToken, logPageString + "\n", }).flatMap(result -> {
"Commons:Deletion_requests/" + date, summary); if (result) {
mwApi.appendEdit(editToken, userPageString + "\n", return pageEditClient.appendEdit("User_Talk:" + username, userPageString + "\n", summary);
"User_Talk:" + media.getCreator(), summary); }
} catch (Exception e) { throw new RuntimeException("Failed to nominate for deletion");
Timber.e(e); });
return false;
}
return true;
} }
private boolean showDeletionNotification(Context context, Media media, boolean result) { 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<SingleSource<Boolean>>) () ->
makeDeletion(context, media, finalReason))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(aBoolean -> { .subscribe(aBoolean -> {

View file

@ -5,14 +5,11 @@ import android.content.ContentProviderClient;
import android.content.Context; import android.content.Context;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import androidx.collection.LruCache;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.AppAdapter;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import org.wikipedia.dataclient.WikiSite;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -22,7 +19,6 @@ import java.util.Map;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import androidx.collection.LruCache;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import fr.free.nrw.commons.BuildConfig; 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.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@Module @Module
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
@ -85,7 +84,7 @@ public class CommonsApplicationModule {
@Provides @Provides
public AccountUtil providesAccountUtil(Context context) { public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil(context); return new AccountUtil();
} }
@Provides @Provides
@ -188,7 +187,13 @@ public class CommonsApplicationModule {
@Named(MAIN_THREAD) @Named(MAIN_THREAD)
@Provides @Provides
public Scheduler providesMainThread(){ public Scheduler providesMainThread() {
return AndroidSchedulers.mainThread(); return AndroidSchedulers.mainThread();
} }
@Named("username")
@Provides
public String provideLoggedInUsername() {
return AppAdapter.get().getUserName();
}
} }

View file

@ -2,11 +2,16 @@ package fr.free.nrw.commons.di;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonUtil; import org.wikipedia.json.GsonUtil;
import org.wikipedia.login.LoginClient;
import java.io.File; import java.io.File;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -14,10 +19,11 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import androidx.annotation.NonNull;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import fr.free.nrw.commons.BuildConfig; 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.category.CategoryInterface;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaInterface; 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.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.review.ReviewInterface; import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface;
import okhttp3.Cache; import okhttp3.Cache;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -41,7 +48,11 @@ public class NetworkingModule {
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; 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 @Provides
@Singleton @Singleton
@ -71,7 +82,7 @@ public class NetworkingModule {
public MediaWikiApi provideMediaWikiApi(Context context, public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") JsonKvStore defaultKvStore, @Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) { Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore); return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
} }
@Provides @Provides
@ -88,6 +99,27 @@ public class NetworkingModule {
gson); 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 @Provides
@Named("wikimedia_api_host") @Named("wikimedia_api_host")
@NonNull @NonNull
@ -104,6 +136,20 @@ public class NetworkingModule {
return HttpUrl.parse(TOOLS_FORGE_URL); 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. * 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 * @return returns a singleton Gson instance
@ -116,9 +162,16 @@ public class NetworkingModule {
@Provides @Provides
@Singleton @Singleton
@Named("commons-wikisite") @Named("commons-service")
public WikiSite provideCommonsWikiSite() { public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return new WikiSite(BuildConfig.COMMONS_URL); 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 @Provides
@ -127,6 +180,44 @@ public class NetworkingModule {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); 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 @Provides
@Singleton @Singleton
public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {

View file

@ -12,13 +12,17 @@ import android.os.RemoteException;
import java.io.IOException; import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig; 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.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import timber.log.Timber; import timber.log.Timber;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@ -28,6 +32,9 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Inject ModifierSequenceDao modifierSequenceDao; @Inject ModifierSequenceDao modifierSequenceDao;
@Inject @Inject
SessionManager sessionManager; SessionManager sessionManager;
@Inject
@Named("commons-page-edit")
PageEditClient commonsPageEditClient;
public ModificationsSyncAdapter(Context context, boolean autoInitialize) { public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize); super(context, autoInitialize);
@ -61,15 +68,6 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return; return;
} }
mwApi.setAuthCookie(authCookie);
String editToken;
try {
editToken = mwApi.getEditToken();
} catch (IOException e) {
Timber.d("Can not retreive edit token!");
return;
}
allModifications.moveToFirst(); allModifications.moveToFirst();
@ -113,22 +111,15 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
Timber.d("Page content is %s", pageContent); Timber.d("Page content is %s", pageContent);
String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
String editResult; Disposable disposable = commonsPageEditClient
try { .edit(contrib.getFilename(), processedPageContent, sequence.getEditSummary())
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); .subscribe(editResult -> {
} catch (IOException e) { if (!editResult) {
Timber.d("Network messed up on modifications sync!"); Timber.d("Non success result!");
continue; } else {
} modifierSequenceDao.delete(sequence);
}
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);
}
} }
allModifications.moveToNext(); allModifications.moveToNext();
} }

View file

@ -1,15 +1,10 @@
package fr.free.nrw.commons.mwapi; package fr.free.nrw.commons.mwapi;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme; 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.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams; import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames; 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 org.wikipedia.util.DateUtil;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; 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 io.reactivex.Single;
import timber.log.Timber; import timber.log.Timber;
@ -51,17 +33,8 @@ import timber.log.Timber;
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private AbstractHttpClient httpClient; private AbstractHttpClient httpClient;
private CustomMwApi api; private CustomMwApi api;
private CustomMwApi wikidataApi;
private Context context;
private JsonKvStore defaultKvStore;
private final String ERROR_CODE_BAD_TOKEN = "badtoken"; public ApacheHttpClientMediaWikiApi(String apiURL) {
public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
String wikidatApiURL,
JsonKvStore defaultKvStore) {
this.context = context;
BasicHttpParams params = new BasicHttpParams(); BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry(); SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
@ -74,198 +47,6 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor()); httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor());
} }
api = new CustomMwApi(apiURL, httpClient); 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 @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 @Override
@NonNull @NonNull
public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { 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"); .getString("/api/query/pages/page/revisions/rev");
} }
@Override
@NonNull
public List<Notification> 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<UploadStash> 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<UploadResult> 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 * Checks to see if a user is currently blocked from Commons
* *

View file

@ -1,69 +1,19 @@
package fr.free.nrw.commons.mwapi; 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.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable; import java.io.IOException;
import io.reactivex.Single; import io.reactivex.Single;
public interface MediaWikiApi { 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<UploadStash> uploadFile(String filename, InputStream file,
long dataLength, Uri fileUri, Uri contentProviderUri,
final ProgressListener progressListener);
@NonNull
Single<UploadResult> 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<String> parseWikicode(String source); Single<String> parseWikicode(String source);
@NonNull @NonNull
Single<MediaResult> fetchMediaByFilename(String filename); Single<MediaResult> fetchMediaByFilename(String filename);
@NonNull
List<Notification> getNotifications(boolean archived) throws IOException;
@NonNull
boolean markNotificationAsRead(Notification notification) throws IOException;
@Nullable @Nullable
String revisionsByFilename(String filename) throws IOException; String revisionsByFilename(String filename) throws IOException;
@ -76,8 +26,6 @@ public interface MediaWikiApi {
// Single<CampaignResponseDTO> getCampaigns(); // Single<CampaignResponseDTO> getCampaigns();
boolean thank(String editToken, long revision) throws IOException;
interface ProgressListener { interface ProgressListener {
void onProgress(long transferred, long total); void onProgress(long transferred, long total);
} }

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.notification; 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. * Created by root on 18.12.2017.
*/ */
@ -8,33 +12,44 @@ public class Notification {
public NotificationType notificationType; public NotificationType notificationType;
public String notificationText; public String notificationText;
public String date; public String date;
public String description;
public String link; public String link;
public String iconUrl; public String iconUrl;
public String dateWithYear;
public String notificationId; 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.notificationType = notificationType;
this.notificationText = notificationText; this.notificationText = notificationText;
this.date = date; this.date = date;
this.description = description;
this.link = link; this.link = link;
this.iconUrl = iconUrl; this.iconUrl = iconUrl;
this.dateWithYear = dateWithYear;
this.notificationId=notificationId; 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 @Override
public String toString() { public String toString() {
return "Notification" + return "Notification" +
"notificationType='" + notificationType + '\'' + "notificationType='" + notificationType + '\'' +
", notificationText='" + notificationText + '\'' + ", notificationText='" + notificationText + '\'' +
", date='" + date + '\'' + ", date='" + date + '\'' +
", description='" + description + '\'' +
", link='" + link + '\'' + ", link='" + link + '\'' +
", iconUrl='" + iconUrl + '\'' + ", iconUrl='" + iconUrl + '\'' +
", dateWithYear=" + dateWithYear +
", notificationId='" + notificationId + '\'' + ", notificationId='" + notificationId + '\'' +
'}'; '}';
} }

View file

@ -5,11 +5,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; 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.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@ -19,10 +14,17 @@ import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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 com.pedrogomez.renderers.RVRendererAdapter;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable;
import javax.inject.Inject; 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.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -78,25 +82,25 @@ public class NotificationActivity extends NavigationBaseActivity {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
public void removeNotification(Notification notification) { public void removeNotification(Notification notification) {
compositeDisposable.add(Observable.fromCallable(() -> controller.markAsRead(notification)) Disposable disposable = Observable.defer((Callable<ObservableSource<Boolean>>)
() -> controller.markAsRead(notification))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(result -> {
if (result){ if (result) {
notificationList.remove(notification); notificationList.remove(notification);
setAdapter(notificationList); setAdapter(notificationList);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
Snackbar snackbar = Snackbar Snackbar snackbar = Snackbar
.make(relativeLayout,"Notification marked as read", Snackbar.LENGTH_LONG); .make(relativeLayout, "Notification marked as read", Snackbar.LENGTH_LONG);
snackbar.show(); snackbar.show();
if (notificationList.size()==0){ if (notificationList.size() == 0) {
setEmptyView(); setEmptyView();
relativeLayout.setVisibility(View.GONE); relativeLayout.setVisibility(View.GONE);
no_notification.setVisibility(View.VISIBLE); no_notification.setVisibility(View.VISIBLE);
} }
} } else {
else {
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
setAdapter(notificationList); setAdapter(notificationList);
Toast.makeText(NotificationActivity.this, "There was some error!", Toast.LENGTH_SHORT).show(); Toast.makeText(NotificationActivity.this, "There was some error!", Toast.LENGTH_SHORT).show();
@ -107,7 +111,8 @@ public class NotificationActivity extends NavigationBaseActivity {
throwable.printStackTrace(); throwable.printStackTrace();
ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications); ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications);
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
})); });
compositeDisposable.add(disposable);
} }
@ -140,11 +145,8 @@ public class NotificationActivity extends NavigationBaseActivity {
private void addNotifications(boolean archived) { private void addNotifications(boolean archived) {
Timber.d("Add notifications"); Timber.d("Add notifications");
if (mNotificationWorkerFragment == null) { if (mNotificationWorkerFragment == null) {
compositeDisposable.add(Observable.fromCallable(() -> { progressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE); compositeDisposable.add(controller.getNotifications(archived)
return controller.getNotifications(archived);
})
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationList -> { .subscribe(notificationList -> {

View file

@ -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<List<Notification>> 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<Boolean> markNotificationAsRead(String notificationId) {
try {
return service.markRead(csrfTokenClient.getTokenBlocking(), notificationId, "")
.map(mwQueryResponse -> mwQueryResponse.success());
} catch (Throwable throwable) {
return Observable.just(false);
}
}
}

View file

@ -1,14 +1,12 @@
package fr.free.nrw.commons.notification; package fr.free.nrw.commons.notification;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import fr.free.nrw.commons.auth.SessionManager; import io.reactivex.Observable;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import io.reactivex.Single;
/** /**
* Created by root on 19.12.2017. * Created by root on 19.12.2017.
@ -16,27 +14,19 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
@Singleton @Singleton
public class NotificationController { public class NotificationController {
private MediaWikiApi mediaWikiApi; private NotificationClient notificationClient;
private SessionManager sessionManager;
@Inject @Inject
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) { public NotificationController(NotificationClient notificationClient) {
this.mediaWikiApi = mediaWikiApi; this.notificationClient = notificationClient;
this.sessionManager = sessionManager;
} }
public List<Notification> getNotifications(boolean archived) throws IOException { public Single<List<Notification>> getNotifications(boolean archived) {
if (mediaWikiApi.validateLogin()) { return notificationClient.getNotifications(archived);
return mediaWikiApi.getNotifications(archived);
} else {
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
if (authTokenValidated != null && authTokenValidated) {
return mediaWikiApi.getNotifications(archived);
}
}
return new ArrayList<>();
} }
public boolean markAsRead(Notification notification) throws IOException{
return mediaWikiApi.markNotificationAsRead(notification); Observable<Boolean> markAsRead(Notification notification) {
return notificationClient.markNotificationAsRead(notification.notificationId);
} }
} }

View file

@ -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<Notification> getNotificationsFromBundle(Context context, Node document) {
Element bundledNotifications = getBundledNotifications(document);
NodeList childNodes = bundledNotifications.getChildNodes();
List<Notification> 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<Notification> getNotificationsFromList(Context context, NodeList childNodes) {
List<Notification> 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("<strong>", "").replace("</strong>", "");
} else {
return "";
}
}
private static String getNotificationBody(Node document) {
Node body = getNode(getModel(document), "body");
if (body != null) {
String textContent = body.getTextContent();
return textContent.replace("<strong>", "").replace("</strong>", "");
} 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;
}
}

View file

@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.view.Gravity;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -13,17 +11,23 @@ import androidx.core.app.NotificationCompat;
import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.dataclient.mwapi.MwQueryPage;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; 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.delete.DeleteHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection; 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.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -32,13 +36,15 @@ import timber.log.Timber;
public class ReviewController { public class ReviewController {
private static final int NOTIFICATION_SEND_THANK = 0x102; private static final int NOTIFICATION_SEND_THANK = 0x102;
private static final int NOTIFICATION_CHECK_CATEGORY = 0x101; private static final int NOTIFICATION_CHECK_CATEGORY = 0x101;
protected static ArrayList<String> categories;
@Inject
ThanksClient thanksClient;
private final DeleteHelper deleteHelper; private final DeleteHelper deleteHelper;
@Nullable @Nullable
MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName
@Inject @Inject
MediaWikiApi mwApi; @Named("commons-page-edit")
@Inject PageEditClient pageEditClient;
SessionManager sessionManager;
private NotificationManager notificationManager; private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder; private NotificationCompat.Builder notificationBuilder;
private Media media; private Media media;
@ -89,40 +95,16 @@ public class ReviewController {
.getCommonsApplicationComponent() .getCommonsApplicationComponent()
.inject(this); .inject(this);
Toast toast = new Toast(context); ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()));
toast.setGravity(Gravity.CENTER, 0, 0);
toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT);
toast.show();
Observable.fromCallable(() -> { publishProgress(context, 0);
publishProgress(context, 0); String summary = context.getString(R.string.check_category_edit_summary);
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
String editToken; pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary))
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;
})
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> { .subscribe((result) -> {
publishProgress(context, 2);
String message; String message;
String title; String title;
@ -136,15 +118,7 @@ public class ReviewController {
reviewCallback.onFailure(); reviewCallback.onFailure();
} }
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) showNotification(title, message);
.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());
}, Timber::e); }, Timber::e);
} }
@ -172,39 +146,20 @@ public class ReviewController {
.getInstance(context) .getInstance(context)
.getCommonsApplicationComponent() .getCommonsApplicationComponent()
.inject(this); .inject(this);
Toast toast = new Toast(context); ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()));
toast.setGravity(Gravity.CENTER, 0, 0);
toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT);
toast.show();
Observable.fromCallable(() -> { publishProgress(context, 0);
publishProgress(context, 0); if (firstRevision == null) {
return;
}
String editToken; Observable.defer((Callable<ObservableSource<Boolean>>) () -> thanksClient.thank(firstRevision.getRevisionId()))
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;
})
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> { .subscribe((result) -> {
String message = ""; publishProgress(context, 2);
String title = ""; String message;
String title;
if (result) { if (result) {
title = context.getString(R.string.send_thank_success_title); title = context.getString(R.string.send_thank_success_title);
message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); 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()); message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle());
} }
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) showNotification(title, message);
.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());
}, Timber::e); }, 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 { public interface ReviewCallback {
void onSuccess(); void onSuccess();

View file

@ -140,7 +140,7 @@ public class FileUtils {
return mimeType; return mimeType;
} }
public static String getFileExt(String fileName) { static String getFileExt(String fileName) {
//Default filePath extension //Default filePath extension
String extension = ".jpg"; String extension = ".jpg";
@ -151,7 +151,7 @@ public class FileUtils {
return extension; return extension;
} }
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(filePath); return new FileInputStream(filePath);
} }

View file

@ -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<UploadResult> 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<UploadResult> 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);
}
}
}

View file

@ -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<UploadResponse> 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<UploadResponse> 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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -36,8 +36,9 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService; import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Single; import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -56,8 +57,8 @@ public class UploadService extends HandlerService<Contribution> {
@Inject WikidataEditService wikidataEditService; @Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager; @Inject SessionManager sessionManager;
@Inject ContributionDao contributionDao; @Inject ContributionDao contributionDao;
@Inject @Inject UploadClient uploadClient;
MediaClient mediaClient; @Inject MediaClient mediaClient;
private NotificationManagerCompat notificationManager; private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder curNotification; private NotificationCompat.Builder curNotification;
@ -208,9 +209,9 @@ public class UploadService extends HandlerService<Contribution> {
return; return;
} }
String notificationTag = localUri.toString(); String notificationTag = localUri.toString();
File file1;
try { try {
File file1 = new File(localUri.getPath()); file1 = new File(localUri.getPath());
fileInputStream = new FileInputStream(file1); fileInputStream = new FileInputStream(file1);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Timber.d("File not found"); Timber.d("File not found");
@ -233,22 +234,8 @@ public class UploadService extends HandlerService<Contribution> {
contribution contribution
); );
Single.fromCallable(() -> { Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
if (!mwApi.validateLogin()) { .flatMap(stashFilename -> uploadClient.uploadFileToStash(getApplicationContext(), stashFilename, file1))
// 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))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.doFinally(() -> { .doFinally(() -> {
@ -267,22 +254,20 @@ public class UploadService extends HandlerService<Contribution> {
Timber.d("Stash upload response 1 is %s", uploadStash.toString()); Timber.d("Stash upload response 1 is %s", uploadStash.toString());
String resultStatus = uploadStash.getResultStatus(); String resultStatus = uploadStash.getResult();
if (!resultStatus.equals("Success")) { if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited"); Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution); showFailedNotification(contribution);
return Single.never(); return Observable.never();
} else { } else {
synchronized (unfinishedUploads) { Timber.d("making sure of uniqueness of name: %s", filename);
Timber.d("making sure of uniqueness of name: %s", filename); String uniqueFilename = findUniqueFilename(filename);
String uniqueFilename = findUniqueFilename(filename); unfinishedUploads.add(uniqueFilename);
unfinishedUploads.add(uniqueFilename); return uploadClient.uploadFileFromStash(
return mwApi.uploadFileFinalize( getApplicationContext(),
uniqueFilename, contribution,
uploadStash.getFilekey(), uniqueFilename,
contribution.getPageContents(getApplicationContext()), uploadStash.getFilekey());
contribution.getEditSummary());
}
} }
}) })
.subscribe(uploadResult -> { .subscribe(uploadResult -> {
@ -290,19 +275,20 @@ public class UploadService extends HandlerService<Contribution> {
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
String resultStatus = uploadResult.getResultStatus(); String resultStatus = uploadResult.getResult();
if (!resultStatus.equals("Success")) { if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited"); Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution); showFailedNotification(contribution);
} else { } else {
String canonicalFilename = uploadResult.getCanonicalFilename(); String canonicalFilename = "File:" + uploadResult.getFilename();
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s", Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
contribution.getWikiDataEntityId()); contribution.getWikiDataEntityId());
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename); wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
contribution.setFilename(canonicalFilename); contribution.setFilename(canonicalFilename);
contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED); contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(uploadResult.getDateUploaded()); contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
.parse(uploadResult.getImageinfo().getTimestamp()));
contributionDao.save(contribution); contributionDao.save(contribution);
} }
}, throwable -> { }, throwable -> {

View file

@ -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<Long> 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);
}
}
}

View file

@ -10,10 +10,10 @@ import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import fr.free.nrw.commons.R; 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.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -26,20 +26,26 @@ import timber.log.Timber;
@Singleton @Singleton
public class WikidataEditService { 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 Context context;
private final MediaWikiApi mediaWikiApi;
private final WikidataEditListener wikidataEditListener; private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore; private final JsonKvStore directKvStore;
private final WikidataClient wikidataClient;
private final PageEditClient wikiDataPageEditClient;
@Inject @Inject
public WikidataEditService(Context context, public WikidataEditService(Context context,
MediaWikiApi mediaWikiApi,
WikidataEditListener wikidataEditListener, WikidataEditListener wikidataEditListener,
@Named("default_preferences") JsonKvStore directKvStore) { @Named("default_preferences") JsonKvStore directKvStore,
WikidataClient wikidataClient,
@Named("wikidata-page-edit") PageEditClient wikiDataPageEditClient) {
this.context = context; this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.wikidataEditListener = wikidataEditListener; this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore; this.directKvStore = directKvStore;
this.wikidataClient = wikidataClient;
this.wikiDataPageEditClient = wikiDataPageEditClient;
} }
/** /**
@ -77,13 +83,20 @@ public class WikidataEditService {
private void editWikidataProperty(String wikidataEntityId, String fileName) { private void editWikidataProperty(String wikidataEntityId, String fileName) {
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId); Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId); Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
Observable.fromCallable(() -> {
String propertyValue = getFileName(fileName); String propertyValue = getFileName(fileName);
return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
}) 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()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .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"); Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}); });
@ -95,31 +108,12 @@ public class WikidataEditService {
wikidataEditListener.onSuccessfulWikidataEdit(); wikidataEditListener.onSuccessfulWikidataEdit();
} }
showSuccessToast(); showSuccessToast();
logEdit(revisionId);
} else { } else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId); Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); 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 * Show a success toast when the edit is made successfully
*/ */

View file

@ -1,19 +1,22 @@
package fr.free.nrw.commons.delete package fr.free.nrw.commons.delete
import android.accounts.Account
import android.content.Context import android.content.Context
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.mwapi.MediaWikiApi
import fr.free.nrw.commons.notification.NotificationHelper import fr.free.nrw.commons.notification.NotificationHelper
import fr.free.nrw.commons.utils.ViewUtilWrapper import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.wikipedia.AppAdapter
import javax.inject.Inject
import javax.inject.Named
/** /**
* Tests for delete helper * Tests for delete helper
@ -21,17 +24,15 @@ import org.mockito.MockitoAnnotations
class DeleteHelperTest { class DeleteHelperTest {
@Mock @Mock
internal var mwApi: MediaWikiApi? = null @field:[Inject Named("commons-page-edit")]
internal var pageEditClient: PageEditClient? = null
@Mock
internal var sessionManager: SessionManager? = null
@Mock
internal var notificationHelper: NotificationHelper? = null
@Mock @Mock
internal var context: Context? = null internal var context: Context? = null
@Mock
internal var notificationHelper: NotificationHelper? = null
@Mock @Mock
internal var viewUtil: ViewUtilWrapper? = null internal var viewUtil: ViewUtilWrapper? = null
@ -54,9 +55,13 @@ class DeleteHelperTest {
*/ */
@Test @Test
fun makeDeletion() { fun makeDeletion() {
`when`(mwApi?.editToken).thenReturn("token") `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
`when`(sessionManager?.authCookie).thenReturn("Mock cookie") .thenReturn(Observable.just(true))
`when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test")) `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?.displayTitle).thenReturn("Test file")
`when`(media?.filename).thenReturn("Test file.jpg") `when`(media?.filename).thenReturn("Test file.jpg")
@ -68,16 +73,45 @@ class DeleteHelperTest {
/** /**
* Test a failed deletion * Test a failed deletion
*/ */
@Test @Test(expected = RuntimeException::class)
fun makeDeletionForNullToken() { fun makeDeletionForPrependEditFailure() {
`when`(mwApi?.editToken).thenReturn(null) `when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
`when`(sessionManager?.authCookie).thenReturn("Mock cookie") .thenReturn(Observable.just(false))
`when`(sessionManager?.currentAccount).thenReturn(Account("TestUser", "Test")) `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?.displayTitle).thenReturn("Test file")
`when`(media?.filename).thenReturn("Test file.jpg") `when`(media?.filename).thenReturn("Test file.jpg")
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet()
assertNotNull(makeDeletion) }
assertFalse(makeDeletion!!)
@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()
} }
} }

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.mwapi
import android.os.Build import android.os.Build
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.google.gson.Gson
import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.utils.ConfigUtils import fr.free.nrw.commons.utils.ConfigUtils
@ -38,7 +37,7 @@ class ApacheHttpClientMediaWikiApiTest {
wikidataServer = MockWebServer() wikidataServer = MockWebServer()
okHttpClient = OkHttpClient() okHttpClient = OkHttpClient()
sharedPreferences = mock(JsonKvStore::class.java) 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 @After
@ -46,181 +45,6 @@ class ApacheHttpClientMediaWikiApiTest {
server.shutdown() 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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"))
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"FAIL\" message=\"Incorrect password entered.&#10;Please try again.\" messagecode=\"wrongpassword\" /></api>"))
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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"))
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"PASS\" username=\"foo\" /></api>"))
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("<?xml version=\"1.0\"?><api batchcomplete=\"\"><query><tokens logintoken=\"baz\" /></query></api>"))
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><clientlogin status=\"PASS\" username=\"foo\" /></api>"))
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("<?xml version=\"1.0\"?><api><query><userinfo id=\"10\" name=\"foo\"/></query></api>"))
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("<?xml version=\"1.0\"?><api><query><userinfo id=\"0\" name=\"foo\"/></query></api>"))
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("<?xml version=\"1.0\"?><api><query><tokens csrftoken=\"baz\" /></query></api>"))
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("<?xml version=\"1.0\"?><api><centralauthtoken centralauthtoken=\"abc\" /></api>"))
wikidataServer.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><query><tokens csrftoken=\"baz\" /></query></api>"))
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 @Test
fun isUserBlockedFromCommonsForInfinitelyBlockedUser() { fun isUserBlockedFromCommonsForInfinitelyBlockedUser() {
server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><query><userinfo id=\"1000\" name=\"testusername\" blockid=\"3000\" blockedby=\"blockerusername\" blockedbyid=\"1001\" blockreason=\"testing\" blockedtimestamp=\"2018-05-24T15:32:09Z\" blockexpiry=\"infinite\"></userinfo></query></api>")) server.enqueue(MockResponse().setBody("<?xml version=\"1.0\"?><api><query><userinfo id=\"1000\" name=\"testusername\" blockid=\"3000\" blockedby=\"blockerusername\" blockedbyid=\"1001\" blockreason=\"testing\" blockedtimestamp=\"2018-05-24T15:32:09Z\" blockexpiry=\"infinite\"></userinfo></query></api>"))