Consolidated media wiki api calls in a single place

This commit is contained in:
Paul Hawke 2017-07-04 14:24:08 -05:00
parent 5396fc6ed0
commit 599e7bb453
18 changed files with 467 additions and 376 deletions

View file

@ -8,41 +8,30 @@ import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.util.LruCache; import android.support.v4.util.LruCache;
import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho; import com.facebook.stetho.Stetho;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.LeakCanary;
import org.acra.ACRA; import org.acra.ACRA;
import org.acra.ReportingInteractionMode; import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes; import org.acra.annotation.ReportsCrashes;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.utils.FileUtils; import fr.free.nrw.commons.utils.FileUtils;
import timber.log.Timber; import timber.log.Timber;
@ -76,9 +65,8 @@ public class CommonsApplication extends Application {
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
private static CommonsApplication instance = null; private static CommonsApplication instance = null;
private AbstractHttpClient httpClient = null; private MediaWikiApi api = null;
private MWApi api = null; private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
private CacheController cacheData = null; private CacheController cacheData = null;
private DBOpenHelper dbOpenHelper = null; private DBOpenHelper dbOpenHelper = null;
private NearbyPlaces nearbyPlaces = null; private NearbyPlaces nearbyPlaces = null;
@ -98,35 +86,13 @@ public class CommonsApplication extends Application {
return instance; return instance;
} }
public AbstractHttpClient getHttpClient() { public MediaWikiApi getMWApi() {
if (httpClient == null) {
httpClient = newHttpClient();
}
return httpClient;
}
private AbstractHttpClient newHttpClient() {
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
return new DefaultHttpClient(cm, params);
}
public MWApi getMWApi() {
if (api == null) { if (api == null) {
api = newMWApi(); api = new ApacheHttpClientMediaWikiApi(API_URL);
} }
return api; return api;
} }
private MWApi newMWApi() {
return new MWApi(API_URL, getHttpClient());
}
public CacheController getCacheData() { public CacheController getCacheData() {
if (cacheData == null) { if (cacheData == null) {
cacheData = new CacheController(); cacheData = new CacheController();
@ -174,9 +140,6 @@ public class CommonsApplication extends Application {
Fresco.initialize(this); Fresco.initialize(this);
// Initialize EventLogging
EventLog.setApp(this);
//For caching area -> categories //For caching area -> categories
cacheData = new CacheController(); cacheData = new CacheController();
} }

View file

@ -1,130 +0,0 @@
package fr.free.nrw.commons;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import org.apache.http.HttpResponse;
import org.apache.http.impl.client.AbstractHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import fr.free.nrw.commons.settings.Prefs;
import in.yuvi.http.fluent.Http;
import timber.log.Timber;
public class EventLog {
private static CommonsApplication app;
private static class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
@Override
protected Boolean doInBackground(LogBuilder... logBuilders) {
boolean allSuccess = true;
// Not using the default URL connection, since that seems to have different behavior than the rest of the code
for(LogBuilder logBuilder: logBuilders) {
try {
URL url = logBuilder.toUrl();
AbstractHttpClient httpClient = CommonsApplication.getInstance().getHttpClient();
HttpResponse response = Http.get(url.toString()).use(httpClient).asResponse();
if(response.getStatusLine().getStatusCode() != 204) {
allSuccess = false;
}
Timber.d("EventLog hit %s", url);
} catch (IOException e) {
// Probably just ignore for now. Can be much more robust with a service, etc later on.
Timber.d("IO Error, EventLog hit skipped");
}
}
return allSuccess;
}
}
private static final String DEVICE;
static {
if (Build.MODEL.startsWith(Build.MANUFACTURER)) {
DEVICE = Utils.capitalize(Build.MODEL);
} else {
DEVICE = Utils.capitalize(Build.MANUFACTURER) + " " + Build.MODEL;
}
}
public static void setApp(CommonsApplication app) {
EventLog.app = app;
}
public static class LogBuilder {
private JSONObject data;
private long rev;
private String schema;
private LogBuilder(String schema, long revision) {
data = new JSONObject();
this.schema = schema;
this.rev = revision;
}
public LogBuilder param(String key, Object value) {
try {
data.put(key, value);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return this;
}
private URL toUrl() {
JSONObject fullData = new JSONObject();
try {
fullData.put("schema", schema);
fullData.put("revision", rev);
fullData.put("wiki", CommonsApplication.EVENTLOG_WIKI);
data.put("device", DEVICE);
data.put("platform", "Android/" + Build.VERSION.RELEASE);
data.put("appversion", "Android/" + BuildConfig.VERSION_NAME);
fullData.put("event", data);
return new URL(CommonsApplication.EVENTLOG_URL + "?" + Utils.urlEncode(fullData.toString()) + ";");
} catch (MalformedURLException | JSONException e) {
throw new RuntimeException(e);
}
}
// force param disregards user preference
// Use *only* for tracking the user preference change for EventLogging
// Attempting to use anywhere else will cause kitten explosions
public void log(boolean force) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(app);
if(!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
return; // User has disabled tracking
}
LogTask logTask = new LogTask();
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
}
public void log() {
log(false);
}
}
public static LogBuilder schema(String schema, long revision) {
return new LogBuilder(schema, revision);
}
public static LogBuilder schema(Object[] scid) {
if(scid.length != 2) {
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
}
return schema((String)scid[0], (Long)scid[1]);
}
}

View file

@ -1,94 +0,0 @@
package fr.free.nrw.commons;
import java.io.IOException;
import org.apache.http.impl.client.AbstractHttpClient;
import org.mediawiki.api.ApiResult;
/**
* @author Addshore
*/
public class MWApi extends org.mediawiki.api.MWApi {
/** We don't actually use this but need to pass it in requests */
private static String LOGIN_RETURN_TO_URL = "https://commons.wikimedia.org";
public MWApi(String apiURL, AbstractHttpClient client) {
super(apiURL, client);
}
/**
* @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 token = this.getLoginToken();
ApiResult loginApiResult = this.action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", token)
.param("loginreturnurl", LOGIN_RETURN_TO_URL)
.post();
return this.getErrorCodeToReturn( loginApiResult );
}
/**
* @param username String
* @param password String
* @param twoFactorCode String
* @return String as returned by this.getErrorCodeToReturn()
* @throws IOException On api request IO issue
*/
public String login(String username, String password, String twoFactorCode) throws IOException {
String token = this.getLoginToken();//TODO cache this instead of calling again when 2FAing
ApiResult loginApiResult = this.action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", token)
.param("logincontinue", "1")
.param("OATHToken", twoFactorCode)
.post();
return this.getErrorCodeToReturn( loginApiResult );
}
private String getLoginToken() throws IOException {
ApiResult tokenResult = this.action("query")
.param("action", "query")
.param("meta", "tokens")
.param("type", "login")
.post();
return tokenResult.getString("/api/query/tokens/@logintoken");
}
/**
* @param loginApiResult ApiResult Any clientlogin api result
* @return String On success: "PASS"
* continue: "2FA" (More information required for 2FA)
* failure: A failure message code (defined by mediawiki)
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
*/
private String getErrorCodeToReturn( ApiResult loginApiResult ) {
String status = loginApiResult.getString("/api/clientlogin/@status");
if (status.equals("PASS")) {
this.isLoggedIn = true;
return status;
} else if (status.equals("FAIL")) {
return loginApiResult.getString("/api/clientlogin/@messagecode");
} else if (
status.equals("UI")
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
) {
return "2FA";
}
// UI, REDIRECT, RESTART
return "genericerror-" + status;
}
}

View file

@ -21,6 +21,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -62,16 +63,8 @@ public class MediaDataExtractor {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
} }
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result = api.action("query") processResult(api.fetchMediaByFilename(filename));
.param("prop", "revisions")
.param("titles", filename)
.param("rvprop", "content")
.param("rvlimit", 1)
.param("rvgeneratexml", 1)
.get();
processResult(result);
fetched = true; fetched = true;
} }

View file

@ -3,10 +3,9 @@ package fr.free.nrw.commons;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.mediawiki.api.ApiResult; import fr.free.nrw.commons.mwapi.MediaWikiApi;
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> { class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
private static final String THUMB_SIZE = "640";
protected final Media media; protected final Media media;
public MediaThumbnailFetchTask(@NonNull Media media) { public MediaThumbnailFetchTask(@NonNull Media media) {
@ -16,15 +15,8 @@ class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
@Override @Override
protected String doInBackground(String... params) { protected String doInBackground(String... params) {
try { try {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result =api.action("query") return api.findThumbnailByFilename(params[0]);
.param("format", "xml")
.param("prop", "imageinfo")
.param("iiprop", "url")
.param("iiurlwidth", THUMB_SIZE)
.param("titles", params[0])
.get();
return result.getString("/api/query/pages/page/imageinfo/ii/@thumburl");
} catch (Exception e) { } catch (Exception e) {
// Do something better! // Do something better!
} }

View file

@ -8,13 +8,13 @@ import android.accounts.NetworkErrorException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
@ -75,7 +75,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
} }
private String getAuthCookie(String username, String password) throws IOException { private String getAuthCookie(String username, String password) throws IOException {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
//TODO add 2fa support here //TODO add 2fa support here
String result = api.login(username, password); String result = api.login(username, password);
if(result.equals("PASS")) { if(result.equals("PASS")) {

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.category;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.view.View; import android.view.View;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
@ -12,6 +11,7 @@ import java.util.Calendar;
import java.util.Iterator; import java.util.Iterator;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -79,20 +79,13 @@ public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
protected ArrayList<String> doInBackground(Void... voids) { protected ArrayList<String> doInBackground(Void... voids) {
//otherwise if user has typed something in that isn't in cache, search API for matching categories //otherwise if user has typed something in that isn't in cache, search API for matching categories
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result; ApiResult result;
ArrayList<String> categories = new ArrayList<>(); ArrayList<String> categories = new ArrayList<>();
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch= //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try { try {
result = api.action("query") result = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter);
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", catFragment.SEARCH_CATS_LIMIT)
.param("srsearch", filter)
.get();
Timber.d("Method A URL filter %s", result); Timber.d("Method A URL filter %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");

View file

@ -4,7 +4,6 @@ import android.os.AsyncTask;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
@ -13,6 +12,7 @@ import java.util.Calendar;
import java.util.Iterator; import java.util.Iterator;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -95,15 +95,11 @@ public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
//otherwise if user has typed something in that isn't in cache, search API for matching categories //otherwise if user has typed something in that isn't in cache, search API for matching categories
//URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25 //URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result; ApiResult result;
ArrayList<String> categories = new ArrayList<>(); ArrayList<String> categories = new ArrayList<>();
try { try {
result = api.action("query") result = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter);
.param("list", "allcategories")
.param("acprefix", filter)
.param("aclimit", catFragment.SEARCH_CATS_LIMIT)
.get();
Timber.d("Prefix URL filter %s", result); Timber.d("Prefix URL filter %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");

View file

@ -2,13 +2,13 @@ package fr.free.nrw.commons.category;
import android.os.AsyncTask; import android.os.AsyncTask;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -34,20 +34,13 @@ public class TitleCategories extends AsyncTask<Void, Void, ArrayList<String>> {
@Override @Override
protected ArrayList<String> doInBackground(Void... voids) { protected ArrayList<String> doInBackground(Void... voids) {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result; ApiResult result;
ArrayList<String> items = new ArrayList<>(); ArrayList<String> items = new ArrayList<>();
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch= //URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try { try {
result = api.action("query") result = api.searchTitles(SEARCH_CATS_LIMIT, this.title);
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", SEARCH_CATS_LIMIT)
.param("srsearch", title)
.get();
Timber.d("Searching for cats for title: %s", result); Timber.d("Searching for cats for title: %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");

View file

@ -19,8 +19,8 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MWApi;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
@ -61,7 +61,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
String user = account.name; String user = account.name;
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE); SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", ""); String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date(); Date curTime = new Date();
@ -71,19 +71,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
while(!done) { while(!done) {
try { try {
MWApi.RequestBuilder builder = api.action("query") result = api.logEvents(user, lastModified, queryContinue, getLimit());
.param("list", "logevents")
.param("letype", "upload")
.param("leprop", "title|timestamp|ids")
.param("leuser", user)
.param("lelimit", getLimit());
if(!TextUtils.isEmpty(lastModified)) {
builder.param("leend", lastModified);
}
if(!TextUtils.isEmpty(queryContinue)) {
builder.param("lestart", queryContinue);
}
result = builder.get();
} catch (IOException e) { } catch (IOException e) {
// There isn't really much we can do, eh? // There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging? // FIXME: Perhaps add EventLogging?

View file

@ -12,7 +12,6 @@ import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import fr.free.nrw.commons.MWApi;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import java.io.IOException; import java.io.IOException;
@ -21,6 +20,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@ -61,7 +61,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return; return;
} }
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
api.setAuthCookie(authCookie); api.setAuthCookie(authCookie);
String editToken; String editToken;
@ -98,11 +98,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
if(contrib.getState() == Contribution.STATE_COMPLETED) { if(contrib.getState() == Contribution.STATE_COMPLETED) {
try { try {
requestResult = api.action("query") requestResult = api.revisionsByFilename(contrib.getFilename());
.param("prop", "revisions")
.param("rvprop", "timestamp|content")
.param("titles", contrib.getFilename())
.get();
} catch (IOException e) { } catch (IOException e) {
Timber.d("Network fuckup on modifications sync!"); Timber.d("Network fuckup on modifications sync!");
continue; continue;
@ -113,12 +109,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
try { try {
responseResult = api.action("edit") responseResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
.param("title", contrib.getFilename())
.param("token", editToken)
.param("text", processedPageContent)
.param("summary", sequence.getEditSummary())
.post();
} catch (IOException e) { } catch (IOException e) {
Timber.d("Network fuckup on modifications sync!"); Timber.d("Network fuckup on modifications sync!");
continue; continue;

View file

@ -0,0 +1,258 @@
package fr.free.nrw.commons.mwapi;
import android.os.Build;
import android.text.TextUtils;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.mediawiki.api.ApiResult;
import java.io.IOException;
import java.net.URL;
import fr.free.nrw.commons.BuildConfig;
import in.yuvi.http.fluent.Http;
import timber.log.Timber;
/**
* @author Addshore
*/
public class ApacheHttpClientMediaWikiApi extends org.mediawiki.api.MWApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private static AbstractHttpClient httpClient;
public ApacheHttpClientMediaWikiApi(String apiURL) {
super(apiURL, getHttpClient());
}
private static AbstractHttpClient getHttpClient() {
if (httpClient == null) {
httpClient = newHttpClient();
}
return httpClient;
}
private static AbstractHttpClient newHttpClient() {
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
return new DefaultHttpClient(cm, params);
}
/**
* @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 {
return getErrorCodeToReturn(action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", this.getLoginToken())
.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 {
return getErrorCodeToReturn(action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", getLoginToken())
.param("logincontinue", "1")
.param("OATHToken", twoFactorCode)
.post());
}
private String getLoginToken() throws IOException {
return this.action("query")
.param("action", "query")
.param("meta", "tokens")
.param("type", "login")
.post()
.getString("/api/query/tokens/@logintoken");
}
/**
* @param loginApiResult ApiResult Any clientlogin api result
* @return String On success: "PASS"
* continue: "2FA" (More information required for 2FA)
* failure: A failure message code (defined by mediawiki)
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
*/
private String getErrorCodeToReturn(ApiResult loginApiResult) {
String status = loginApiResult.getString("/api/clientlogin/@status");
if (status.equals("PASS")) {
this.isLoggedIn = true;
return status;
} else if (status.equals("FAIL")) {
return loginApiResult.getString("/api/clientlogin/@messagecode");
} else if (
status.equals("UI")
&& loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
&& loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
) {
return "2FA";
}
// UI, REDIRECT, RESTART
return "genericerror-" + status;
}
// Moved / consolidated methods
@Override
public boolean fileExistsWithName(String fileName) throws IOException {
return action("query")
.param("prop", "imageinfo")
.param("titles", "File:" + fileName)
.get()
.getNodes("/api/query/pages/page/imageinfo").size() > 0;
}
@Override
public ApiResult edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
return action("edit")
.param("title", filename)
.param("token", editToken)
.param("text", processedPageContent)
.param("summary", summary)
.post();
}
@Override
public String findThumbnailByFilename(String filename) throws IOException {
return action("query")
.param("format", "xml")
.param("prop", "imageinfo")
.param("iiprop", "url")
.param("iiurlwidth", THUMB_SIZE)
.param("titles", filename)
.get()
.getString("/api/query/pages/page/imageinfo/ii/@thumburl");
}
@Override
public ApiResult fetchMediaByFilename(String filename) throws IOException {
return action("query")
.param("prop", "revisions")
.param("titles", filename)
.param("rvprop", "content")
.param("rvlimit", 1)
.param("rvgeneratexml", 1)
.get();
}
@Override
public ApiResult searchCategories(int searchCatsLimit, String filterValue) throws IOException {
return action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", filterValue)
.get();
}
@Override
public ApiResult allCategories(int searchCatsLimit, String filterValue) throws IOException {
return action("query")
.param("list", "allcategories")
.param("acprefix", filterValue)
.param("aclimit", searchCatsLimit)
.get();
}
@Override
public ApiResult searchTitles(int searchCatsLimit, String title) throws IOException {
return action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", title)
.get();
}
@Override
public ApiResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException {
org.mediawiki.api.MWApi.RequestBuilder builder = action("query")
.param("list", "logevents")
.param("letype", "upload")
.param("leprop", "title|timestamp|ids")
.param("leuser", user)
.param("lelimit", limit);
if (!TextUtils.isEmpty(lastModified)) {
builder.param("leend", lastModified);
}
if (!TextUtils.isEmpty(queryContinue)) {
builder.param("lestart", queryContinue);
}
return builder.get();
}
@Override
public ApiResult revisionsByFilename(String filename) throws IOException {
return action("query")
.param("prop", "revisions")
.param("rvprop", "timestamp|content")
.param("titles", filename)
.get();
}
@Override
public ApiResult existingFile(String fileSha1) throws IOException {
return action("query")
.param("format", "xml")
.param("list", "allimages")
.param("aisha1", fileSha1)
.get();
}
@Override
public boolean logEvents(LogBuilder[] logBuilders) {
boolean allSuccess = true;
// Not using the default URL connection, since that seems to have different behavior than the rest of the code
for (LogBuilder logBuilder : logBuilders) {
try {
URL url = logBuilder.toUrl();
HttpResponse response = Http.get(url.toString()).use(httpClient).asResponse();
if (response.getStatusLine().getStatusCode() != 204) {
allSuccess = false;
}
Timber.d("EventLog hit %s", url);
} catch (IOException e) {
// Probably just ignore for now. Can be much more robust with a service, etc later on.
Timber.d("IO Error, EventLog hit skipped");
}
}
return allSuccess;
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.mwapi;
import android.os.Build;
import fr.free.nrw.commons.Utils;
public class EventLog {
static final String DEVICE;
static {
if (Build.MODEL.startsWith(Build.MANUFACTURER)) {
DEVICE = Utils.capitalize(Build.MODEL);
} else {
DEVICE = Utils.capitalize(Build.MANUFACTURER) + " " + Build.MODEL;
}
}
private static LogBuilder schema(String schema, long revision) {
return new LogBuilder(schema, revision);
}
public static LogBuilder schema(Object[] scid) {
if (scid.length != 2) {
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
}
return schema((String) scid[0], (Long) scid[1]);
}
}

View file

@ -0,0 +1,71 @@
package fr.free.nrw.commons.mwapi;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.MalformedURLException;
import java.net.URL;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
public class LogBuilder {
private JSONObject data;
private long rev;
private String schema;
LogBuilder(String schema, long revision) {
data = new JSONObject();
this.schema = schema;
this.rev = revision;
}
public LogBuilder param(String key, Object value) {
try {
data.put(key, value);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return this;
}
URL toUrl() {
JSONObject fullData = new JSONObject();
try {
fullData.put("schema", schema);
fullData.put("revision", rev);
fullData.put("wiki", CommonsApplication.EVENTLOG_WIKI);
data.put("device", EventLog.DEVICE);
data.put("platform", "Android/" + Build.VERSION.RELEASE);
data.put("appversion", "Android/" + BuildConfig.VERSION_NAME);
fullData.put("event", data);
return new URL(CommonsApplication.EVENTLOG_URL + "?" + Utils.urlEncode(fullData.toString()) + ";");
} catch (MalformedURLException | JSONException e) {
throw new RuntimeException(e);
}
}
// force param disregards user preference
// Use *only* for tracking the user preference change for EventLogging
// Attempting to use anywhere else will cause kitten explosions
public void log(boolean force) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
return; // User has disabled tracking
}
LogTask logTask = new LogTask();
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
}
public void log() {
log(false);
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi;
import android.os.AsyncTask;
import fr.free.nrw.commons.CommonsApplication;
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
@Override
protected Boolean doInBackground(LogBuilder... logBuilders) {
return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders);
}
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.mwapi;
import org.mediawiki.api.ApiResult;
import java.io.IOException;
import java.io.InputStream;
import in.yuvi.http.fluent.ProgressListener;
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;
ApiResult upload(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
boolean fileExistsWithName(String fileName) throws IOException;
ApiResult edit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
String findThumbnailByFilename(String filename) throws IOException;
ApiResult fetchMediaByFilename(String filename) throws IOException;
ApiResult searchCategories(int searchCatsLimit, String filterValue) throws IOException;
ApiResult allCategories(int searchCatsLimit, String filter) throws IOException;
ApiResult searchTitles(int searchCatsLimit, String title) throws IOException;
ApiResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
ApiResult revisionsByFilename(String filename) throws IOException;
ApiResult existingFile(String fileSha1) throws IOException;
boolean logEvents(LogBuilder[] logBuilders);
}

View file

@ -12,9 +12,9 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MWApi;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -49,16 +49,13 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
MWApi api = CommonsApplication.getInstance().getMWApi(); MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
ApiResult result; ApiResult result;
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
try { try {
result = api.action("query") String fileSha1 = this.fileSha1;
.param("format", "xml") result = api.existingFile(fileSha1);
.param("list", "allimages")
.param("aisha1", fileSha1)
.get();
Timber.d("Searching Commons API for existing file: %s", result); Timber.d("Searching Commons API for existing file: %s", result);
} catch (IOException e) { } catch (IOException e) {
Timber.e(e, "IO Exception: "); Timber.e(e, "IO Exception: ");

View file

@ -13,7 +13,6 @@ import android.support.v4.app.NotificationCompat;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
import fr.free.nrw.commons.*;
import org.mediawiki.api.ApiResult; import org.mediawiki.api.ApiResult;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -25,10 +24,16 @@ import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import in.yuvi.http.fluent.ProgressListener; import in.yuvi.http.fluent.ProgressListener;
import timber.log.Timber; import timber.log.Timber;
@ -176,7 +181,7 @@ public class UploadService extends HandlerService<Contribution> {
} }
private void uploadContribution(Contribution contribution) { private void uploadContribution(Contribution contribution) {
MWApi api = app.getMWApi(); MediaWikiApi api = app.getMWApi();
ApiResult result; ApiResult result;
InputStream file = null; InputStream file = null;
@ -304,7 +309,7 @@ public class UploadService extends HandlerService<Contribution> {
} }
private String findUniqueFilename(String fileName) throws IOException { private String findUniqueFilename(String fileName) throws IOException {
MWApi api = app.getMWApi(); MediaWikiApi api = app.getMWApi();
String sequenceFileName; String sequenceFileName;
for ( int sequenceNumber = 1; true; sequenceNumber++ ) { for ( int sequenceNumber = 1; true; sequenceNumber++ ) {
if (sequenceNumber == 1) { if (sequenceNumber == 1) {
@ -320,7 +325,7 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
} }
} }
if ( fileExistsWithName(api, sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) { if ( api.fileExistsWithName(sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) {
continue; continue;
} else { } else {
break; break;
@ -328,15 +333,4 @@ public class UploadService extends HandlerService<Contribution> {
} }
return sequenceFileName; return sequenceFileName;
} }
private static boolean fileExistsWithName(MWApi api, String fileName) throws IOException {
ApiResult result;
result = api.action("query")
.param("prop", "imageinfo")
.param("titles", "File:" + fileName)
.get();
return result.getNodes("/api/query/pages/page/imageinfo").size() > 0;
}
} }