From 599e7bb4531edf832789d2ca1ef799fe669b4531 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 4 Jul 2017 14:24:08 -0500 Subject: [PATCH] Consolidated media wiki api calls in a single place --- .../free/nrw/commons/CommonsApplication.java | 63 +---- .../java/fr/free/nrw/commons/EventLog.java | 130 --------- .../main/java/fr/free/nrw/commons/MWApi.java | 94 ------- .../free/nrw/commons/MediaDataExtractor.java | 13 +- .../nrw/commons/MediaThumbnailFetchTask.java | 14 +- .../auth/WikiAccountAuthenticator.java | 8 +- .../nrw/commons/category/MethodAUpdater.java | 13 +- .../nrw/commons/category/PrefixUpdater.java | 10 +- .../nrw/commons/category/TitleCategories.java | 13 +- .../ContributionsSyncAdapter.java | 18 +- .../ModificationsSyncAdapter.java | 17 +- .../mwapi/ApacheHttpClientMediaWikiApi.java | 258 ++++++++++++++++++ .../fr/free/nrw/commons/mwapi/EventLog.java | 28 ++ .../fr/free/nrw/commons/mwapi/LogBuilder.java | 71 +++++ .../fr/free/nrw/commons/mwapi/LogTask.java | 12 + .../free/nrw/commons/mwapi/MediaWikiApi.java | 46 ++++ .../nrw/commons/upload/ExistingFileAsync.java | 11 +- .../nrw/commons/upload/UploadService.java | 24 +- 18 files changed, 467 insertions(+), 376 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/EventLog.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/MWApi.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 46f74810e..b78bfdc6f 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -8,41 +8,30 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.os.Build; import android.database.sqlite.SQLiteDatabase; import android.preference.PreferenceManager; import android.support.v4.util.LruCache; import com.facebook.drawee.backends.pipeline.Fresco; 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 org.acra.ACRA; import org.acra.ReportingInteractionMode; 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.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 timber.log.Timber; @@ -76,9 +65,8 @@ public class CommonsApplication extends Application { public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; private static CommonsApplication instance = null; - private AbstractHttpClient httpClient = null; - private MWApi api = null; - LruCache thumbnailUrlCache = new LruCache<>(1024); + private MediaWikiApi api = null; + private LruCache thumbnailUrlCache = new LruCache<>(1024); private CacheController cacheData = null; private DBOpenHelper dbOpenHelper = null; private NearbyPlaces nearbyPlaces = null; @@ -98,35 +86,13 @@ public class CommonsApplication extends Application { return instance; } - public AbstractHttpClient getHttpClient() { - 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() { + public MediaWikiApi getMWApi() { if (api == null) { - api = newMWApi(); + api = new ApacheHttpClientMediaWikiApi(API_URL); } return api; } - private MWApi newMWApi() { - return new MWApi(API_URL, getHttpClient()); - } - public CacheController getCacheData() { if (cacheData == null) { cacheData = new CacheController(); @@ -174,9 +140,6 @@ public class CommonsApplication extends Application { Fresco.initialize(this); - // Initialize EventLogging - EventLog.setApp(this); - //For caching area -> categories cacheData = new CacheController(); } diff --git a/app/src/main/java/fr/free/nrw/commons/EventLog.java b/app/src/main/java/fr/free/nrw/commons/EventLog.java deleted file mode 100644 index 5cb36450b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/EventLog.java +++ /dev/null @@ -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 { - - @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]); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/MWApi.java b/app/src/main/java/fr/free/nrw/commons/MWApi.java deleted file mode 100644 index 785ea7886..000000000 --- a/app/src/main/java/fr/free/nrw/commons/MWApi.java +++ /dev/null @@ -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; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 8bbc03f4d..c6c12c33c 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -21,6 +21,7 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -62,16 +63,8 @@ public class MediaDataExtractor { throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); } - MWApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result = api.action("query") - .param("prop", "revisions") - .param("titles", filename) - .param("rvprop", "content") - .param("rvlimit", 1) - .param("rvgeneratexml", 1) - .get(); - - processResult(result); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); + processResult(api.fetchMediaByFilename(filename)); fetched = true; } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java index fb73b5a2e..5ccc80c06 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java @@ -3,10 +3,9 @@ package fr.free.nrw.commons; import android.os.AsyncTask; import android.support.annotation.NonNull; -import org.mediawiki.api.ApiResult; +import fr.free.nrw.commons.mwapi.MediaWikiApi; class MediaThumbnailFetchTask extends AsyncTask { - private static final String THUMB_SIZE = "640"; protected final Media media; public MediaThumbnailFetchTask(@NonNull Media media) { @@ -16,15 +15,8 @@ class MediaThumbnailFetchTask extends AsyncTask { @Override protected String doInBackground(String... params) { try { - MWApi api = CommonsApplication.getInstance().getMWApi(); - ApiResult result =api.action("query") - .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"); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); + return api.findThumbnailByFilename(params[0]); } catch (Exception e) { // Do something better! } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index ebbd6c285..8ecfc67cb 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -8,13 +8,13 @@ import android.accounts.NetworkErrorException; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.io.IOException; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MWApi; +import fr.free.nrw.commons.mwapi.MediaWikiApi; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { @@ -75,7 +75,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { } private String getAuthCookie(String username, String password) throws IOException { - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); //TODO add 2fa support here String result = api.login(username, password); if(result.equals("PASS")) { diff --git a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java index 9300f640d..95f77bc27 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; import android.view.View; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; @@ -12,6 +11,7 @@ import java.util.Calendar; import java.util.Iterator; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -79,20 +79,13 @@ public class MethodAUpdater extends AsyncTask> { protected ArrayList doInBackground(Void... voids) { //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; ArrayList 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= try { - result = api.action("query") - .param("format", "xml") - .param("list", "search") - .param("srwhat", "text") - .param("srnamespace", "14") - .param("srlimit", catFragment.SEARCH_CATS_LIMIT) - .param("srsearch", filter) - .get(); + result = api.searchCategories(CategorizationFragment.SEARCH_CATS_LIMIT, filter); Timber.d("Method A URL filter %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java index 05770081d..426a8eb37 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java @@ -4,7 +4,6 @@ import android.os.AsyncTask; import android.text.TextUtils; import android.view.View; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; @@ -13,6 +12,7 @@ import java.util.Calendar; import java.util.Iterator; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -95,15 +95,11 @@ public class PrefixUpdater extends AsyncTask> { //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 - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList categories = new ArrayList<>(); try { - result = api.action("query") - .param("list", "allcategories") - .param("acprefix", filter) - .param("aclimit", catFragment.SEARCH_CATS_LIMIT) - .get(); + result = api.allCategories(CategorizationFragment.SEARCH_CATS_LIMIT, this.filter); Timber.d("Prefix URL filter %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java index 6ca46be0f..62d0f5d16 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java +++ b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java @@ -2,13 +2,13 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; import java.io.IOException; import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -34,20 +34,13 @@ public class TitleCategories extends AsyncTask> { @Override protected ArrayList doInBackground(Void... voids) { - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList 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= try { - result = api.action("query") - .param("format", "xml") - .param("list", "search") - .param("srwhat", "text") - .param("srnamespace", "14") - .param("srlimit", SEARCH_CATS_LIMIT) - .param("srsearch", title) - .get(); + result = api.searchTitles(SEARCH_CATS_LIMIT, this.title); Timber.d("Searching for cats for title: %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index d7ce9df9d..8f531ea2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -19,8 +19,8 @@ import java.util.ArrayList; import java.util.Date; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; 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) { // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! String user = account.name; - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); Date curTime = new Date(); @@ -71,19 +71,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { while(!done) { try { - MWApi.RequestBuilder builder = api.action("query") - .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(); + result = api.logEvents(user, lastModified, queryContinue, getLimit()); } catch (IOException e) { // There isn't really much we can do, eh? // FIXME: Perhaps add EventLogging? diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index 898c41f7d..a307f36e3 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -12,7 +12,6 @@ import android.database.Cursor; import android.os.Bundle; import android.os.RemoteException; -import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; 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.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @@ -61,7 +61,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { return; } - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); api.setAuthCookie(authCookie); String editToken; @@ -98,11 +98,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { if(contrib.getState() == Contribution.STATE_COMPLETED) { try { - requestResult = api.action("query") - .param("prop", "revisions") - .param("rvprop", "timestamp|content") - .param("titles", contrib.getFilename()) - .get(); + requestResult = api.revisionsByFilename(contrib.getFilename()); } catch (IOException e) { Timber.d("Network fuckup on modifications sync!"); continue; @@ -113,12 +109,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); try { - responseResult = api.action("edit") - .param("title", contrib.getFilename()) - .param("token", editToken) - .param("text", processedPageContent) - .param("summary", sequence.getEditSummary()) - .post(); + responseResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); } catch (IOException e) { Timber.d("Network fuckup on modifications sync!"); continue; diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java new file mode 100644 index 000000000..c43f4f64e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java new file mode 100644 index 000000000..d3ba7c0d5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java @@ -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]); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java new file mode 100644 index 000000000..b512f9647 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java @@ -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); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java new file mode 100644 index 000000000..ee947afbc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi; + +import android.os.AsyncTask; + +import fr.free.nrw.commons.CommonsApplication; + +class LogTask extends AsyncTask { + @Override + protected Boolean doInBackground(LogBuilder... logBuilders) { + return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java new file mode 100644 index 000000000..2f2f5dc4f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -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); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index 85e71860d..08b17c4c1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -12,9 +12,9 @@ import java.io.IOException; import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; /** @@ -49,16 +49,13 @@ public class ExistingFileAsync extends AsyncTask { @Override protected Boolean doInBackground(Void... voids) { - MWApi api = CommonsApplication.getInstance().getMWApi(); + MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba try { - result = api.action("query") - .param("format", "xml") - .param("list", "allimages") - .param("aisha1", fileSha1) - .get(); + String fileSha1 = this.fileSha1; + result = api.existingFile(fileSha1); Timber.d("Searching Commons API for existing file: %s", result); } catch (IOException e) { Timber.e(e, "IO Exception: "); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 853952fae..eba1dd586 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -13,7 +13,6 @@ import android.support.v4.app.NotificationCompat; import android.webkit.MimeTypeMap; import android.widget.Toast; -import fr.free.nrw.commons.*; import org.mediawiki.api.ApiResult; import java.io.FileNotFoundException; @@ -25,10 +24,16 @@ import java.util.Set; import java.util.regex.Matcher; 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.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; 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 timber.log.Timber; @@ -176,7 +181,7 @@ public class UploadService extends HandlerService { } private void uploadContribution(Contribution contribution) { - MWApi api = app.getMWApi(); + MediaWikiApi api = app.getMWApi(); ApiResult result; InputStream file = null; @@ -304,7 +309,7 @@ public class UploadService extends HandlerService { } private String findUniqueFilename(String fileName) throws IOException { - MWApi api = app.getMWApi(); + MediaWikiApi api = app.getMWApi(); String sequenceFileName; for ( int sequenceNumber = 1; true; sequenceNumber++ ) { if (sequenceNumber == 1) { @@ -320,7 +325,7 @@ public class UploadService extends HandlerService { sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); } } - if ( fileExistsWithName(api, sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) { + if ( api.fileExistsWithName(sequenceFileName) || unfinishedUploads.contains(sequenceFileName) ) { continue; } else { break; @@ -328,15 +333,4 @@ public class UploadService extends HandlerService { } 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; - } }