mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-28 21:33:53 +01:00
Refactor CSRF token API to move it into the main commons code base (#5472)
* Remove redundent constructor parameter * Converted the CsrfTokenClient and test to kotlin * Moved getCsrfTokenCall() out of the data client
This commit is contained in:
parent
e8e87b1d1c
commit
97a208dcfa
14 changed files with 263 additions and 348 deletions
|
|
@ -25,7 +25,7 @@ class PageEditClient(
|
||||||
*/
|
*/
|
||||||
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
||||||
return try {
|
return try {
|
||||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking)
|
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
|
|
@ -41,7 +41,7 @@ class PageEditClient(
|
||||||
*/
|
*/
|
||||||
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
||||||
return try {
|
return try {
|
||||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking)
|
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
|
|
@ -57,7 +57,7 @@ class PageEditClient(
|
||||||
*/
|
*/
|
||||||
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
||||||
return try {
|
return try {
|
||||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking)
|
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
|
|
@ -76,7 +76,8 @@ class PageEditClient(
|
||||||
language: String, value: String) : Observable<Int>{
|
language: String, value: String) : Observable<Int>{
|
||||||
return try {
|
return try {
|
||||||
pageEditInterface.postCaptions(summary, title, language,
|
pageEditInterface.postCaptions(summary, title, language,
|
||||||
value, csrfTokenClient.tokenBlocking).map { it.success }
|
value, csrfTokenClient.getTokenBlocking()
|
||||||
|
).map { it.success }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(0)
|
Observable.just(0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class ThanksClient @Inject constructor(
|
||||||
service.thank(
|
service.thank(
|
||||||
revisionId.toString(), // Rev
|
revisionId.toString(), // Rev
|
||||||
null, // Log
|
null, // Log
|
||||||
csrfTokenClient.tokenBlocking, // Token
|
csrfTokenClient.getTokenBlocking(), // Token
|
||||||
CommonsApplication.getInstance().userAgent // Source
|
CommonsApplication.getInstance().userAgent // Source
|
||||||
).map {
|
).map {
|
||||||
mwThankPostResponse -> mwThankPostResponse.result?.success == 1
|
mwThankPostResponse -> mwThankPostResponse.result?.success == 1
|
||||||
|
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth.csrf;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.dataclient.Service;
|
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
|
||||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import org.wikipedia.login.LoginClient;
|
|
||||||
import org.wikipedia.login.LoginResult;
|
|
||||||
import org.wikipedia.util.log.L;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Response;
|
|
||||||
|
|
||||||
public class CsrfTokenClient {
|
|
||||||
private static final String ANON_TOKEN = "+\\";
|
|
||||||
private static final int MAX_RETRIES = 1;
|
|
||||||
private static final int MAX_RETRIES_OF_LOGIN_BLOCKING = 2;
|
|
||||||
@NonNull private final WikiSite csrfWikiSite;
|
|
||||||
@NonNull private final WikiSite loginWikiSite;
|
|
||||||
private int retries = 0;
|
|
||||||
|
|
||||||
@Nullable private Call<MwQueryResponse> csrfTokenCall;
|
|
||||||
@NonNull private LoginClient loginClient = new LoginClient();
|
|
||||||
|
|
||||||
public CsrfTokenClient(@NonNull WikiSite csrfWikiSite, @NonNull WikiSite loginWikiSite) {
|
|
||||||
this.csrfWikiSite = csrfWikiSite;
|
|
||||||
this.loginWikiSite = loginWikiSite;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void request(@NonNull final Callback callback) {
|
|
||||||
request(false, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void request(boolean forceLogin, @NonNull final Callback callback) {
|
|
||||||
cancel();
|
|
||||||
if (forceLogin) {
|
|
||||||
retryWithLogin(new RuntimeException("Forcing login..."), callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
csrfTokenCall = request(ServiceFactory.get(csrfWikiSite), callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancel() {
|
|
||||||
loginClient.cancel();
|
|
||||||
if (csrfTokenCall != null) {
|
|
||||||
csrfTokenCall.cancel();
|
|
||||||
csrfTokenCall = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
@NonNull
|
|
||||||
Call<MwQueryResponse> request(@NonNull Service service, @NonNull final Callback cb) {
|
|
||||||
return requestToken(service, new CsrfTokenClient.Callback() {
|
|
||||||
@Override public void success(@NonNull String token) {
|
|
||||||
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
|
|
||||||
retryWithLogin(new RuntimeException("App believes we're logged in, but got anonymous token."), cb);
|
|
||||||
} else {
|
|
||||||
cb.success(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void failure(@NonNull Throwable caught) {
|
|
||||||
retryWithLogin(caught, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void twoFactorPrompt() {
|
|
||||||
cb.twoFactorPrompt();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void retryWithLogin(@NonNull Throwable caught, @NonNull final Callback callback) {
|
|
||||||
if (retries < MAX_RETRIES
|
|
||||||
&& !TextUtils.isEmpty(AppAdapter.get().getUserName())
|
|
||||||
&& !TextUtils.isEmpty(AppAdapter.get().getPassword())) {
|
|
||||||
retries++;
|
|
||||||
|
|
||||||
SharedPreferenceCookieManager.getInstance().clearAllCookies();
|
|
||||||
|
|
||||||
login(AppAdapter.get().getUserName(), AppAdapter.get().getPassword(), () -> {
|
|
||||||
L.i("retrying...");
|
|
||||||
request(callback);
|
|
||||||
}, callback);
|
|
||||||
} else {
|
|
||||||
callback.failure(caught);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void login(@NonNull final String username, @NonNull final String password,
|
|
||||||
@NonNull final RetryCallback retryCallback,
|
|
||||||
@NonNull final Callback callback) {
|
|
||||||
new LoginClient().request(loginWikiSite, username, password,
|
|
||||||
new LoginClient.LoginCallback() {
|
|
||||||
@Override
|
|
||||||
public void success(@NonNull LoginResult loginResult) {
|
|
||||||
if (loginResult.pass()) {
|
|
||||||
AppAdapter.get().updateAccount(loginResult);
|
|
||||||
retryCallback.retry();
|
|
||||||
} else {
|
|
||||||
callback.failure(new LoginClient.LoginFailedException(loginResult.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
|
|
||||||
callback.twoFactorPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void passwordResetPrompt(@Nullable String token) {
|
|
||||||
// Should not happen here, but call the callback just in case.
|
|
||||||
callback.failure(new LoginClient.LoginFailedException("Logged in with temporary password."));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void error(@NonNull Throwable caught) {
|
|
||||||
callback.failure(caught);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull public String getTokenBlocking() throws Throwable {
|
|
||||||
String token = "";
|
|
||||||
Service service = ServiceFactory.get(csrfWikiSite);
|
|
||||||
|
|
||||||
for (int retry = 0; retry < MAX_RETRIES_OF_LOGIN_BLOCKING; retry++) {
|
|
||||||
try {
|
|
||||||
if (retry > 0) {
|
|
||||||
// Log in explicitly
|
|
||||||
new LoginClient().loginBlocking(loginWikiSite, AppAdapter.get().getUserName(),
|
|
||||||
AppAdapter.get().getPassword(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get CSRFToken response off the main thread.
|
|
||||||
Response<MwQueryResponse> response = Executors.newSingleThreadExecutor().submit(new CsrfTokenCallExecutor(service)).get();
|
|
||||||
|
|
||||||
if (response.body() == null || response.body().query() == null
|
|
||||||
|| TextUtils.isEmpty(response.body().query().csrfToken())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
token = response.body().query().csrfToken();
|
|
||||||
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
|
|
||||||
throw new RuntimeException("App believes we're logged in, but got anonymous token.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} catch (Throwable t) {
|
|
||||||
L.w(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(token) || token.equals(ANON_TOKEN)) {
|
|
||||||
throw new IOException("Invalid token, or login failure.");
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting @NonNull Call<MwQueryResponse> requestToken(@NonNull Service service,
|
|
||||||
@NonNull final Callback cb) {
|
|
||||||
Call<MwQueryResponse> call = service.getCsrfTokenCall();
|
|
||||||
call.enqueue(new retrofit2.Callback<MwQueryResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<MwQueryResponse> call, @NonNull Response<MwQueryResponse> response) {
|
|
||||||
if (call.isCanceled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cb.success(response.body().query().csrfToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable t) {
|
|
||||||
if (call.isCanceled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cb.failure(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return call;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
void success(@NonNull String token);
|
|
||||||
void failure(@NonNull Throwable caught);
|
|
||||||
void twoFactorPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class DefaultCallback implements Callback {
|
|
||||||
@Override
|
|
||||||
public void success(@NonNull String token) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void failure(@NonNull Throwable caught) {
|
|
||||||
L.e(caught);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void twoFactorPrompt() {
|
|
||||||
// TODO:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface RetryCallback {
|
|
||||||
void retry();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class CsrfTokenCallExecutor which implement callable interface to get CsrfTokenCall.
|
|
||||||
*/
|
|
||||||
class CsrfTokenCallExecutor implements Callable<Response<MwQueryResponse>> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for token call.
|
|
||||||
*/
|
|
||||||
private Service service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Constructor.
|
|
||||||
* @param service
|
|
||||||
*/
|
|
||||||
public CsrfTokenCallExecutor(Service service){
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes a result, or throws an exception if unable to do so.
|
|
||||||
*
|
|
||||||
* @return computed result
|
|
||||||
* @throws Exception if unable to compute a result
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Response<MwQueryResponse> call() throws Exception {
|
|
||||||
return service.getCsrfTokenCall().execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import org.wikipedia.AppAdapter
|
||||||
|
import org.wikipedia.dataclient.ServiceFactory
|
||||||
|
import org.wikipedia.dataclient.SharedPreferenceCookieManager
|
||||||
|
import org.wikipedia.dataclient.WikiSite
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||||
|
import org.wikipedia.login.LoginClient
|
||||||
|
import org.wikipedia.login.LoginClient.LoginCallback
|
||||||
|
import org.wikipedia.login.LoginClient.LoginFailedException
|
||||||
|
import org.wikipedia.login.LoginResult
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.Callable
|
||||||
|
import java.util.concurrent.Executors.newSingleThreadExecutor
|
||||||
|
|
||||||
|
class CsrfTokenClient(private val csrfWikiSite: WikiSite) {
|
||||||
|
private var retries = 0
|
||||||
|
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
private val loginClient = LoginClient()
|
||||||
|
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun getTokenBlocking(): String {
|
||||||
|
var token = ""
|
||||||
|
val service = ServiceFactory.get(csrfWikiSite, CsrfTokenInterface::class.java)
|
||||||
|
val userName = AppAdapter.get().getUserName()
|
||||||
|
val password = AppAdapter.get().getPassword()
|
||||||
|
|
||||||
|
for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) {
|
||||||
|
try {
|
||||||
|
if (retry > 0) {
|
||||||
|
// Log in explicitly
|
||||||
|
LoginClient().loginBlocking(csrfWikiSite, userName, password, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get CSRFToken response off the main thread.
|
||||||
|
val response = newSingleThreadExecutor().submit(Callable {
|
||||||
|
service.getCsrfTokenCall().execute()
|
||||||
|
}).get()
|
||||||
|
|
||||||
|
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
token = response.body()!!.query()!!.csrfToken()!!
|
||||||
|
if (AppAdapter.get().isLoggedIn() && token == ANON_TOKEN) {
|
||||||
|
throw RuntimeException("App believes we're logged in, but got anonymous token.")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.w(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.isEmpty() || token == ANON_TOKEN) {
|
||||||
|
throw IOException("Invalid token, or login failure.")
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> =
|
||||||
|
requestToken(service, object : Callback {
|
||||||
|
override fun success(token: String?) {
|
||||||
|
if (AppAdapter.get().isLoggedIn() && token == ANON_TOKEN) {
|
||||||
|
retryWithLogin(cb) {
|
||||||
|
RuntimeException("App believes we're logged in, but got anonymous token.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.success(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||||
|
|
||||||
|
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||||
|
})
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> {
|
||||||
|
val call = service.getCsrfTokenCall()
|
||||||
|
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.success(response.body()!!.query()!!.csrfToken())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.failure(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return call
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) {
|
||||||
|
val userName = AppAdapter.get().getUserName()
|
||||||
|
val password = AppAdapter.get().getPassword()
|
||||||
|
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||||
|
retries++
|
||||||
|
SharedPreferenceCookieManager.getInstance().clearAllCookies()
|
||||||
|
login(userName, password, callback) {
|
||||||
|
Timber.i("retrying...")
|
||||||
|
cancel()
|
||||||
|
csrfTokenCall = request(ServiceFactory.get(csrfWikiSite, CsrfTokenInterface::class.java), callback)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.failure(caught())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
callback: Callback,
|
||||||
|
retryCallback: () -> Unit
|
||||||
|
) = LoginClient().request(csrfWikiSite, username, password, object : LoginCallback {
|
||||||
|
override fun success(loginResult: LoginResult) {
|
||||||
|
if (loginResult.pass()) {
|
||||||
|
AppAdapter.get().updateAccount(loginResult)
|
||||||
|
retryCallback()
|
||||||
|
} else {
|
||||||
|
callback.failure(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun twoFactorPrompt(caught: Throwable, token: String?) =
|
||||||
|
callback.twoFactorPrompt()
|
||||||
|
|
||||||
|
// Should not happen here, but call the callback just in case.
|
||||||
|
override fun passwordResetPrompt(token: String?) =
|
||||||
|
callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||||
|
|
||||||
|
override fun error(caught: Throwable) = callback.failure(caught)
|
||||||
|
})
|
||||||
|
|
||||||
|
private fun cancel() {
|
||||||
|
loginClient.cancel()
|
||||||
|
if (csrfTokenCall != null) {
|
||||||
|
csrfTokenCall!!.cancel()
|
||||||
|
csrfTokenCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun success(token: String?)
|
||||||
|
fun failure(caught: Throwable?)
|
||||||
|
fun twoFactorPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ANON_TOKEN = "+\\"
|
||||||
|
private const val MAX_RETRIES = 1
|
||||||
|
private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import org.wikipedia.dataclient.Service
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
|
||||||
|
interface CsrfTokenInterface {
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@GET(Service.MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||||
|
fun getCsrfTokenCall(): Call<MwQueryResponse?>
|
||||||
|
}
|
||||||
|
|
@ -106,7 +106,7 @@ public class NetworkingModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||||
return new CsrfTokenClient(commonsWikiSite, commonsWikiSite);
|
return new CsrfTokenClient(commonsWikiSite);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class NotificationClient @Inject constructor(
|
||||||
fun markNotificationAsRead(notificationId: String?): Observable<Boolean> {
|
fun markNotificationAsRead(notificationId: String?): Observable<Boolean> {
|
||||||
return try {
|
return try {
|
||||||
service.markRead(
|
service.markRead(
|
||||||
token = csrfTokenClient.tokenBlocking,
|
token = csrfTokenClient.getTokenBlocking(),
|
||||||
readList = notificationId,
|
readList = notificationId,
|
||||||
unreadList = ""
|
unreadList = ""
|
||||||
).map(MwQueryResponse::success)
|
).map(MwQueryResponse::success)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class PageEditClientTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testEdit() {
|
fun testEdit() {
|
||||||
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
|
Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test")
|
||||||
pageEditClient.edit("test", "test", "test")
|
pageEditClient.edit("test", "test", "test")
|
||||||
verify(pageEditInterface).postEdit(eq("test"), eq("test"), eq("test"), eq("test"))
|
verify(pageEditInterface).postEdit(eq("test"), eq("test"), eq("test"), eq("test"))
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +51,7 @@ class PageEditClientTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testAppendEdit() {
|
fun testAppendEdit() {
|
||||||
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
|
Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test")
|
||||||
Mockito.`when`(
|
Mockito.`when`(
|
||||||
pageEditInterface.postAppendEdit(
|
pageEditInterface.postAppendEdit(
|
||||||
ArgumentMatchers.anyString(),
|
ArgumentMatchers.anyString(),
|
||||||
|
|
@ -65,7 +65,7 @@ class PageEditClientTest {
|
||||||
Mockito.`when`(edit.edit()).thenReturn(editResult)
|
Mockito.`when`(edit.edit()).thenReturn(editResult)
|
||||||
Mockito.`when`(editResult.editSucceeded()).thenReturn(true)
|
Mockito.`when`(editResult.editSucceeded()).thenReturn(true)
|
||||||
pageEditClient.appendEdit("test", "test", "test").test()
|
pageEditClient.appendEdit("test", "test", "test").test()
|
||||||
verify(csrfTokenClient).tokenBlocking
|
verify(csrfTokenClient).getTokenBlocking()
|
||||||
verify(pageEditInterface).postAppendEdit(eq("test"), eq("test"), eq("test"), eq("test"))
|
verify(pageEditInterface).postAppendEdit(eq("test"), eq("test"), eq("test"), eq("test"))
|
||||||
verify(edit).edit()
|
verify(edit).edit()
|
||||||
verify(editResult).editSucceeded()
|
verify(editResult).editSucceeded()
|
||||||
|
|
@ -76,7 +76,7 @@ class PageEditClientTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testPrependEdit() {
|
fun testPrependEdit() {
|
||||||
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
|
Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test")
|
||||||
pageEditClient.prependEdit("test", "test", "test")
|
pageEditClient.prependEdit("test", "test", "test")
|
||||||
verify(pageEditInterface).postPrependEdit(eq("test"), eq("test"), eq("test"), eq("test"))
|
verify(pageEditInterface).postPrependEdit(eq("test"), eq("test"), eq("test"), eq("test"))
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ class PageEditClientTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testSetCaptions() {
|
fun testSetCaptions() {
|
||||||
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
|
Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test")
|
||||||
pageEditClient.setCaptions("test", "test", "en", "test")
|
pageEditClient.setCaptions("test", "test", "en", "test")
|
||||||
verify(pageEditInterface).postCaptions(eq("test"), eq("test"), eq("en"),
|
verify(pageEditInterface).postCaptions(eq("test"), eq("test"), eq("en"),
|
||||||
eq("test"), eq("test"))
|
eq("test"), eq("test"))
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class ThanksClientTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testThanks() {
|
fun testThanks() {
|
||||||
`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
|
`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test")
|
||||||
`when`(commonsApplication.userAgent).thenReturn("test")
|
`when`(commonsApplication.userAgent).thenReturn("test")
|
||||||
thanksClient.thank(1L)
|
thanksClient.thank(1L)
|
||||||
verify(service).thank(ArgumentMatchers.anyString(), ArgumentMatchers.any(), eq("test"), eq("test"))
|
verify(service).thank(ArgumentMatchers.anyString(), ArgumentMatchers.any(), eq("test"), eq("test"))
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth.csrf;
|
|
||||||
|
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import com.google.gson.stream.MalformedJsonException;
|
|
||||||
import fr.free.nrw.commons.MockWebServerTest;
|
|
||||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient.Callback;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.mockito.ArgumentMatchers;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.wikipedia.dataclient.Service;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwException;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import org.wikipedia.dataclient.okhttp.HttpStatusException;
|
|
||||||
import retrofit2.Call;
|
|
||||||
|
|
||||||
public class CsrfTokenClientTest extends MockWebServerTest {
|
|
||||||
private static final WikiSite TEST_WIKI = new WikiSite("test.wikipedia.org");
|
|
||||||
@NonNull private final CsrfTokenClient subject = new CsrfTokenClient(TEST_WIKI, TEST_WIKI);
|
|
||||||
|
|
||||||
@Test public void testRequestSuccess() throws Throwable {
|
|
||||||
String expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\";
|
|
||||||
enqueueFromFile("csrf_token.json");
|
|
||||||
|
|
||||||
Callback cb = Mockito.mock(Callback.class);
|
|
||||||
request(cb);
|
|
||||||
|
|
||||||
server().takeRequest();
|
|
||||||
assertCallbackSuccess(cb, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void testRequestResponseApiError() throws Throwable {
|
|
||||||
enqueueFromFile("api_error.json");
|
|
||||||
|
|
||||||
Callback cb = Mockito.mock(Callback.class);
|
|
||||||
request(cb);
|
|
||||||
|
|
||||||
server().takeRequest();
|
|
||||||
assertCallbackFailure(cb, MwException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void testRequestResponseFailure() throws Throwable {
|
|
||||||
enqueue404();
|
|
||||||
|
|
||||||
Callback cb = Mockito.mock(Callback.class);
|
|
||||||
request(cb);
|
|
||||||
|
|
||||||
server().takeRequest();
|
|
||||||
assertCallbackFailure(cb, HttpStatusException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void testRequestResponseMalformed() throws Throwable {
|
|
||||||
enqueueMalformed();
|
|
||||||
|
|
||||||
Callback cb = Mockito.mock(Callback.class);
|
|
||||||
request(cb);
|
|
||||||
|
|
||||||
server().takeRequest();
|
|
||||||
assertCallbackFailure(cb, MalformedJsonException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertCallbackSuccess(@NonNull Callback cb,
|
|
||||||
@NonNull String expected) {
|
|
||||||
verify(cb).success(ArgumentMatchers.eq(expected));
|
|
||||||
//noinspection unchecked
|
|
||||||
verify(cb, never()).failure(ArgumentMatchers.any(Throwable.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertCallbackFailure(@NonNull Callback cb,
|
|
||||||
@NonNull Class<? extends Throwable> throwable) {
|
|
||||||
//noinspection unchecked
|
|
||||||
verify(cb, never()).success(ArgumentMatchers.any(String.class));
|
|
||||||
verify(cb).failure(ArgumentMatchers.isA(throwable));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Call<MwQueryResponse> request(@NonNull Callback cb) {
|
|
||||||
return subject.request(service(Service.class), cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import com.google.gson.stream.MalformedJsonException
|
||||||
|
import fr.free.nrw.commons.MockWebServerTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers.any
|
||||||
|
import org.mockito.ArgumentMatchers.eq
|
||||||
|
import org.mockito.ArgumentMatchers.isA
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.wikipedia.dataclient.Service
|
||||||
|
import org.wikipedia.dataclient.WikiSite
|
||||||
|
import org.wikipedia.dataclient.mwapi.MwException
|
||||||
|
import org.wikipedia.dataclient.okhttp.HttpStatusException
|
||||||
|
|
||||||
|
class CsrfTokenClientTest : MockWebServerTest() {
|
||||||
|
private val wikiSite = WikiSite("test.wikipedia.org")
|
||||||
|
private val subject = CsrfTokenClient(wikiSite)
|
||||||
|
private val cb = mock(CsrfTokenClient.Callback::class.java)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRequestSuccess() {
|
||||||
|
val expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\"
|
||||||
|
enqueueFromFile("csrf_token.json")
|
||||||
|
|
||||||
|
performRequest()
|
||||||
|
|
||||||
|
verify(cb).success(eq(expected))
|
||||||
|
verify(cb, never()).failure(any(Throwable::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRequestResponseApiError() {
|
||||||
|
enqueueFromFile("api_error.json")
|
||||||
|
|
||||||
|
performRequest()
|
||||||
|
|
||||||
|
verify(cb, never()).success(any(String::class.java))
|
||||||
|
verify(cb).failure(isA(MwException::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRequestResponseFailure() {
|
||||||
|
enqueue404()
|
||||||
|
|
||||||
|
performRequest()
|
||||||
|
|
||||||
|
verify(cb, never()).success(any(String::class.java))
|
||||||
|
verify(cb).failure(isA(HttpStatusException::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRequestResponseMalformed() {
|
||||||
|
enqueueMalformed()
|
||||||
|
|
||||||
|
performRequest()
|
||||||
|
|
||||||
|
verify(cb, never()).success(any(String::class.java))
|
||||||
|
verify(cb).failure(isA(MalformedJsonException::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performRequest() {
|
||||||
|
subject.request(service(CsrfTokenInterface::class.java), cb)
|
||||||
|
server().takeRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ class NotificationClientTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun markNotificationAsReadTest() {
|
fun markNotificationAsReadTest() {
|
||||||
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
|
Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test")
|
||||||
Mockito.`when`(service.markRead(anyString(), anyString(), anyString()))
|
Mockito.`when`(service.markRead(anyString(), anyString(), anyString()))
|
||||||
.thenReturn(Observable.just(mQueryResponse))
|
.thenReturn(Observable.just(mQueryResponse))
|
||||||
Mockito.`when`(mQueryResponse.success()).thenReturn(true)
|
Mockito.`when`(mQueryResponse.success()).thenReturn(true)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class WikiBaseClientUnitTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.openMocks(this)
|
MockitoAnnotations.openMocks(this)
|
||||||
Mockito.`when`(csrfTokenClient!!.tokenBlocking)
|
Mockito.`when`(csrfTokenClient!!.getTokenBlocking())
|
||||||
.thenReturn("test")
|
.thenReturn("test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,10 +192,6 @@ public interface Service {
|
||||||
|
|
||||||
// ------- CSRF, Login, and Create Account -------
|
// ------- CSRF, Login, and Create Account -------
|
||||||
|
|
||||||
@Headers("Cache-Control: no-cache")
|
|
||||||
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
|
||||||
@NonNull Call<MwQueryResponse> getCsrfTokenCall();
|
|
||||||
|
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||||
@NonNull Observable<MwQueryResponse> getCsrfToken();
|
@NonNull Observable<MwQueryResponse> getCsrfToken();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue