diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java index 8d661702bd..42cfba2bfe 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -1,10 +1,15 @@ package fr.free.nrw.commons.auth; +import android.accounts.Account; +import android.accounts.AccountManager; import android.content.Context; +import android.support.annotation.Nullable; + +import fr.free.nrw.commons.BuildConfig; +import timber.log.Timber; public class AccountUtil { - public static final String ACCOUNT_TYPE = "fr.free.nrw.commons"; public static final String AUTH_COOKIE = "authCookie"; public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; private final Context context; @@ -13,4 +18,36 @@ public AccountUtil(Context context) { this.context = context; } + /** + * @return Account|null + */ + @Nullable + public static Account account(Context context) { + try { + Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE); + if (accounts.length > 0) { + return accounts[0]; + } + } catch (SecurityException e) { + Timber.e(e); + } + return null; + } + + @Nullable + public static String getUserName(Context context) { + Account account = account(context); + return account == null ? null : account.name; + } + + @Nullable + public static String getPassword(Context context) { + Account account = account(context); + return account == null ? null : accountManager(context).getPassword(account); + } + + private static AccountManager accountManager(Context context) { + return AccountManager.get(context); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 35e5f4c943..9cdd933525 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -55,7 +55,6 @@ import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class LoginActivity extends AccountAuthenticatorActivity { @@ -242,7 +241,7 @@ private void handlePassResult(String username, String password) { if (response != null) { Bundle authResult = new Bundle(); authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); - authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); + authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE); response.onResult(authResult); } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index 4a22b88c90..cd23d12829 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -76,10 +76,6 @@ public void createAccount(@Nullable AccountAuthenticatorResponse response, ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! } - private AccountManager accountManager() { - return AccountManager.get(context); - } - /** * @return Account|null */ @@ -95,6 +91,22 @@ public Account getCurrentAccount() { return currentAccount; } + @Nullable + public String getUserName() { + Account account = getCurrentAccount(); + return account == null ? null : account.name; + } + + @Nullable + public String getPassword() { + Account account = getCurrentAccount(); + return account == null ? null : accountManager().getPassword(account); + } + + private AccountManager accountManager() { + return AccountManager.get(context); + } + public Boolean revalidateAuthToken() { AccountManager accountManager = AccountManager.get(context); Account curAccount = getCurrentAccount(); @@ -103,12 +115,13 @@ public Boolean revalidateAuthToken() { return false; // This should never happen } - accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); + accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, null); String authCookie = getAuthCookie(); if (authCookie == null) { return false; } + mediaWikiApi.setAuthCookie(authCookie); return true; } 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 7a0980d80f..2f71a69b42 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 @@ -13,10 +13,7 @@ import android.support.annotation.Nullable; import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; -import fr.free.nrw.commons.modifications.ModificationsContentProvider; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { @@ -99,7 +96,7 @@ public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, } private boolean supportedAccountType(@Nullable String type) { - return ACCOUNT_TYPE.equals(type); + return BuildConfig.ACCOUNT_TYPE.equals(type); } private Bundle addAccount(AccountAuthenticatorResponse response) { diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 7ca10b05c7..2eb3f9b71e 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -25,8 +25,6 @@ import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; import org.json.JSONObject; -import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -37,7 +35,6 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; @@ -48,6 +45,7 @@ import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.PageTitle; +import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.category.CategoryImageUtils; import fr.free.nrw.commons.category.QueryContinue; import fr.free.nrw.commons.notification.Notification; @@ -72,8 +70,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; private AbstractHttpClient httpClient; - private MWApi api; - private MWApi wikidataApi; + private CustomMwApi api; + private CustomMwApi wikidataApi; private Context context; private SharedPreferences defaultPreferences; private SharedPreferences categoryPreferences; @@ -97,8 +95,8 @@ public ApacheHttpClientMediaWikiApi(Context context, if (BuildConfig.DEBUG) { httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor()); } - api = new MWApi(apiURL, httpClient); - wikidataApi = new MWApi(wikidatApiURL, httpClient); + api = new CustomMwApi(apiURL, httpClient); + wikidataApi = new CustomMwApi(wikidatApiURL, httpClient); this.defaultPreferences = defaultPreferences; this.categoryPreferences = categoryPreferences; this.gson = gson; @@ -163,25 +161,25 @@ private String getLoginToken() throws IOException { } /** - * @param loginApiResult ApiResult Any clientlogin api result + * @param loginCustomApiResult CustomApiResult Any clientlogin api result * @return String On success: "PASS" * continue: "2FA" (More information required for 2FA) * failure: A failure message code (defined by mediawiki) * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART */ - private String getErrorCodeToReturn(ApiResult loginApiResult) { - String status = loginApiResult.getString("/api/clientlogin/@status"); + private String getErrorCodeToReturn(CustomApiResult loginCustomApiResult) { + String status = loginCustomApiResult.getString("/api/clientlogin/@status"); if (status.equals("PASS")) { api.isLoggedIn = true; setAuthCookieOnLogin(true); return status; } else if (status.equals("FAIL")) { setAuthCookieOnLogin(false); - return loginApiResult.getString("/api/clientlogin/@messagecode"); + return loginCustomApiResult.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).") + && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") + && loginCustomApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") ) { setAuthCookieOnLogin(false); return "2FA"; @@ -211,16 +209,26 @@ public String getAuthCookie() { @Override public void setAuthCookie(String authCookie) { api.setAuthCookie(authCookie); + + Timber.d("Mediawiki auth cookie is %s", api.getAuthCookie()); } @Override public boolean validateLogin() throws IOException { - return api.validateLogin(); + boolean validateLoginResp = api.validateLogin(); + Timber.d("Validate login response is %s", validateLoginResp); + return validateLoginResp; } @Override public String getEditToken() throws IOException { - return api.getEditToken(); + String editToken = api.action("query") + .param("centralauthtoken", getCentralAuthToken()) + .param("meta", "tokens") + .post() + .getString("/api/query/tokens/@csrftoken"); + Timber.d("MediaWiki edit token is %s", editToken); + return editToken; } @Override @@ -229,6 +237,14 @@ public String getCentralAuthToken() throws IOException { .get() .getString("/api/centralauthtoken/@centralauthtoken"); Timber.d("MediaWiki Central auth token is %s", centralAuthToken); + + if(centralAuthToken == null || centralAuthToken.isEmpty()) { + api.removeAllCookies(); + String login = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context)); + if(login.equals("PASS")) { + return getCentralAuthToken(); + } + } return centralAuthToken; } @@ -301,7 +317,7 @@ public String findThumbnailByFilename(String filename) throws IOException { @Override @NonNull public MediaResult fetchMediaByFilename(String filename) throws IOException { - ApiResult apiResult = api.action("query") + CustomApiResult apiResult = api.action("query") .param("prop", "revisions") .param("titles", filename) .param("rvprop", "content") @@ -318,7 +334,7 @@ public MediaResult fetchMediaByFilename(String filename) throws IOException { @NonNull public Observable searchCategories(String filterValue, int searchCatsLimit) { return Single.fromCallable(() -> { - List categoryNodes = null; + List categoryNodes = null; try { categoryNodes = api.action("query") .param("format", "xml") @@ -338,7 +354,7 @@ public Observable searchCategories(String filterValue, int searchCatsLim } List categories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { String cat = categoryNode.getDocument().getTextContent(); String catString = cat.replace("Category:", ""); categories.add(catString); @@ -352,7 +368,7 @@ public Observable searchCategories(String filterValue, int searchCatsLim @NonNull public Observable allCategories(String filterValue, int searchCatsLimit) { return Single.fromCallable(() -> { - ArrayList categoryNodes = null; + ArrayList categoryNodes = null; try { categoryNodes = api.action("query") .param("list", "allcategories") @@ -369,7 +385,7 @@ public Observable allCategories(String filterValue, int searchCatsLimit) } List categories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { categories.add(categoryNode.getDocument().getTextContent()); } @@ -377,16 +393,6 @@ public Observable allCategories(String filterValue, int searchCatsLimit) }).flatMapObservable(Observable::fromIterable); } - /** - * Get the edit token for making wiki data edits - * https://www.mediawiki.org/wiki/API:Tokens - * @return - * @throws IOException - */ - private String getWikidataEditToken() throws IOException { - return wikidataApi.getEditToken(); - } - @Override public String getWikidataCsrfToken() throws IOException { String wikidataCsrfToken = wikidataApi.action("query") @@ -413,7 +419,7 @@ public String getWikidataCsrfToken() throws IOException { @Override public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { Timber.d("Filename is %s", value); - ApiResult result = wikidataApi.action("wbcreateclaim") + CustomApiResult result = wikidataApi.action("wbcreateclaim") .param("entity", entityId) .param("centralauthtoken", getCentralAuthToken()) .param("token", getWikidataCsrfToken()) @@ -446,7 +452,7 @@ public String wikidatCreateClaim(String entityId, String property, String snakty @Nullable @Override public boolean addWikidataEditTag(String revisionId) throws IOException { - ApiResult result = wikidataApi.action("tag") + CustomApiResult result = wikidataApi.action("tag") .param("revid", revisionId) .param("centralauthtoken", getCentralAuthToken()) .param("token", getWikidataCsrfToken()) @@ -473,7 +479,7 @@ public boolean addWikidataEditTag(String revisionId) throws IOException { @NonNull public Observable searchTitles(String title, int searchCatsLimit) { return Single.fromCallable((Callable>) () -> { - ArrayList categoryNodes; + ArrayList categoryNodes; try { categoryNodes = api.action("query") @@ -495,7 +501,7 @@ public Observable searchTitles(String title, int searchCatsLimit) { } List titleCategories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { String cat = categoryNode.getDocument().getTextContent(); String catString = cat.replace("Category:", ""); titleCategories.add(catString); @@ -508,7 +514,7 @@ public Observable searchTitles(String title, int searchCatsLimit) { @Override @NonNull public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException { - org.mediawiki.api.MWApi.RequestBuilder builder = api.action("query") + CustomMwApi.RequestBuilder builder = api.action("query") .param("list", "logevents") .param("letype", "upload") .param("leprop", "title|timestamp|ids") @@ -520,7 +526,7 @@ public LogEventResult logEvents(String user, String lastModified, String queryCo if (!TextUtils.isEmpty(queryContinue)) { builder.param("lestart", queryContinue); } - ApiResult result = builder.get(); + CustomApiResult result = builder.get(); return new LogEventResult( getLogEventsFromResult(result), @@ -528,11 +534,11 @@ public LogEventResult logEvents(String user, String lastModified, String queryCo } @NonNull - private ArrayList getLogEventsFromResult(ApiResult result) { - ArrayList uploads = result.getNodes("/api/query/logevents/item"); + private ArrayList getLogEventsFromResult(CustomApiResult result) { + ArrayList uploads = result.getNodes("/api/query/logevents/item"); Timber.d("%d results!", uploads.size()); ArrayList logEvents = new ArrayList<>(); - for (ApiResult image : uploads) { + for (CustomApiResult image : uploads) { logEvents.add(new LogEventResult.LogEvent( image.getString("@pageid"), image.getString("@title"), @@ -556,7 +562,7 @@ public String revisionsByFilename(String filename) throws IOException { @Override @NonNull public List getNotifications() { - ApiResult notificationNode = null; + CustomApiResult notificationNode = null; try { notificationNode = api.action("query") .param("notprop", "list") @@ -591,9 +597,9 @@ public List getNotifications() { @Override @NonNull public List getSubCategoryList(String categoryName) { - ApiResult apiResult = null; + CustomApiResult apiResult = null; try { - MWApi.RequestBuilder requestBuilder = api.action("query") + CustomMwApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categorymembers") .param("format", "xml") .param("gcmtype","subcat") @@ -611,7 +617,7 @@ public List getSubCategoryList(String categoryName) { return new ArrayList<>(); } - ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); if (categoryImagesNode == null || categoryImagesNode.getDocument() == null || categoryImagesNode.getDocument().getChildNodes() == null @@ -632,9 +638,9 @@ public List getSubCategoryList(String categoryName) { @Override @NonNull public List getParentCategoryList(String categoryName) { - ApiResult apiResult = null; + CustomApiResult apiResult = null; try { - MWApi.RequestBuilder requestBuilder = api.action("query") + CustomMwApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categories") .param("format", "xml") .param("titles", categoryName) @@ -651,7 +657,7 @@ public List getParentCategoryList(String categoryName) { return new ArrayList<>(); } - ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); if (categoryImagesNode == null || categoryImagesNode.getDocument() == null || categoryImagesNode.getDocument().getChildNodes() == null @@ -674,9 +680,9 @@ public List getParentCategoryList(String categoryName) { @Override @NonNull public List getCategoryImages(String categoryName) { - ApiResult apiResult = null; + CustomApiResult apiResult = null; try { - MWApi.RequestBuilder requestBuilder = api.action("query") + CustomMwApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categorymembers") .param("format", "xml") .param("gcmtype", "file") @@ -701,7 +707,7 @@ public List getCategoryImages(String categoryName) { return new ArrayList<>(); } - ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); if (categoryImagesNode == null || categoryImagesNode.getDocument() == null || categoryImagesNode.getDocument().getChildNodes() == null @@ -729,7 +735,7 @@ public List getCategoryImages(String categoryName) { @Override @NonNull public List searchImages(String query, int offset) { - List imageNodes = null; + List imageNodes = null; try { imageNodes = api.action("query") .param("format", "xml") @@ -750,7 +756,7 @@ public List searchImages(String query, int offset) { } List images = new ArrayList<>(); - for (ApiResult imageNode : imageNodes) { + for (CustomApiResult imageNode : imageNodes) { String imgName = imageNode.getDocument().getTextContent(); images.add(new Media(imgName)); } @@ -767,7 +773,7 @@ public List searchImages(String query, int offset) { @Override @NonNull public List searchCategory(String query, int offset) { - List categoryNodes = null; + List categoryNodes = null; try { categoryNodes = api.action("query") .param("format", "xml") @@ -788,7 +794,7 @@ public List searchCategory(String query, int offset) { } List categories = new ArrayList<>(); - for (ApiResult categoryNode : categoryNodes) { + for (CustomApiResult categoryNode : categoryNodes) { String catName = categoryNode.getDocument().getTextContent(); categories.add(catName); } @@ -860,11 +866,11 @@ public UploadResult uploadFile(String filename, long dataLength, String pageContents, String editSummary, - final ProgressListener progressListener, Uri fileUri, - Uri contentProviderUri) throws IOException { + Uri contentProviderUri, + final ProgressListener progressListener) throws IOException { - ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress); + CustomApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, getCentralAuthToken(), getEditToken(), progressListener::onProgress); Log.e("WTF", "Result: " + result.toString()); @@ -912,7 +918,7 @@ public Single getUploadCount(String userName) { public boolean isUserBlockedFromCommons() { boolean userBlocked = false; try { - ApiResult result = api.action("query") + CustomApiResult result = api.action("query") .param("action", "query") .param("format", "xml") .param("meta", "userinfo") diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java new file mode 100644 index 0000000000..a35270150f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java @@ -0,0 +1,123 @@ +package fr.free.nrw.commons.mwapi; + +import org.apache.http.client.HttpClient; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.IOError; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import in.yuvi.http.fluent.Http; +import timber.log.Timber; + +public class CustomApiResult { + private Node doc; + private XPath evaluator; + + CustomApiResult(Node doc) { + this.doc = doc; + this.evaluator = XPathFactory.newInstance().newXPath(); + } + + static CustomApiResult fromRequestBuilder(Http.HttpRequestBuilder builder, HttpClient client) throws IOException { + + Timber.d("API request is %s", builder.toString()); + Timber.d("API params are %s", client.getParams()); + + try { + DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = docBuilder.parse(builder.use(client).charset("utf-8").data("format", "xml").asResponse().getEntity().getContent()); + printStringFromDocument(doc); + return new CustomApiResult(doc); + } catch (ParserConfigurationException e) { + // I don't know wtf I can do about this on... + throw new RuntimeException(e); + } catch (IllegalStateException e) { + // So, this should never actually happen - since we assume MediaWiki always generates valid json + // So the only thing causing this would be a network truncation + // Sooo... I can throw IOError + // Thanks Java, for making me spend significant time on shit that happens once in a bluemoon + // I surely am writing Nuclear Submarine controller code + throw new IOError(e); + } catch (SAXException e) { + // See Rant above + throw new IOError(e); + } + } + + public static void printStringFromDocument(Document doc) + { + try + { + DOMSource domSource = new DOMSource(doc); + StringWriter writer = new StringWriter(); + StreamResult result = new StreamResult(writer); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(domSource, result); + Timber.d("API response is\n %s", writer.toString()); + } + catch(TransformerException ex) + { + Timber.d("Error occurred in transforming", ex); + } + } + + public Node getDocument() { + return doc; + } + + public ArrayList getNodes(String xpath) { + try { + ArrayList results = new ArrayList(); + NodeList nodes = (NodeList) evaluator.evaluate(xpath, doc, XPathConstants.NODESET); + for(int i = 0; i < nodes.getLength(); i++) { + results.add(new CustomApiResult(nodes.item(i))); + } + return results; + } catch (XPathExpressionException e) { + return null; + } + + } + public CustomApiResult getNode(String xpath) { + try { + return new CustomApiResult((Node) evaluator.evaluate(xpath, doc, XPathConstants.NODE)); + } catch (XPathExpressionException e) { + return null; + } + } + + public Double getNumber(String xpath) { + try { + return (Double) evaluator.evaluate(xpath, doc, XPathConstants.NUMBER); + } catch (XPathExpressionException e) { + return null; + } + } + + public String getString(String xpath) { + try { + return (String) evaluator.evaluate(xpath, doc, XPathConstants.STRING); + } catch (XPathExpressionException e) { + return null; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java new file mode 100644 index 0000000000..1b9e93fa9c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java @@ -0,0 +1,183 @@ +package fr.free.nrw.commons.mwapi; + +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.mediawiki.api.ApiResult; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; + +import in.yuvi.http.fluent.Http; +import in.yuvi.http.fluent.ProgressListener; +import timber.log.Timber; + +public class CustomMwApi { + public class RequestBuilder { + private HashMap params; + private CustomMwApi api; + + RequestBuilder(CustomMwApi api) { + params = new HashMap(); + this.api = api; + } + + public RequestBuilder param(String key, Object value) { + params.put(key, value); + return this; + } + + public CustomApiResult get() throws IOException { + return api.makeRequest("GET", params); + } + + public CustomApiResult post() throws IOException { + return api.makeRequest("POST", params); + } + } + + private AbstractHttpClient client; + private String apiURL; + public boolean isLoggedIn; + private String authCookie = null; + private String userName = null; + private String userID = null; + + public CustomMwApi(String apiURL, AbstractHttpClient client) { + this.apiURL = apiURL; + this.client = client; + } + + public RequestBuilder action(String action) { + RequestBuilder builder = new RequestBuilder(this); + builder.param("action", action); + return builder; + } + + public String getAuthCookie() { + if(authCookie == null){ + authCookie = ""; + List cookies = client.getCookieStore().getCookies(); + for(Cookie cookie: cookies) { + authCookie += cookie.getName() + "=" + cookie.getValue() + ";"; + } + } + return authCookie; + } + + public void setAuthCookie(String authCookie) { + this.authCookie = authCookie; + this.isLoggedIn = true; + String[] cookies = authCookie.split(";"); + String domain; + try { + domain = new URL(apiURL).getHost(); + } catch (MalformedURLException e) { + // Mighty well better not happen! + e.printStackTrace(); + throw new RuntimeException(e); + } + // This works because I know which cookies are going to be set by MediaWiki, and they don't contain a = or ; in them :D + for(String cookie: cookies) { + String[] parts = cookie.split("="); + BasicClientCookie c = new BasicClientCookie(parts[0], parts[1]); + c.setDomain(domain); + client.getCookieStore().addCookie(c); + } + } + + public void removeAllCookies() { + client.getCookieStore().clear(); + } + + public boolean validateLogin() throws IOException { + CustomApiResult userMeta = this.action("query").param("meta", "userinfo").get(); + this.userID = userMeta.getString("/api/query/userinfo/@id"); + this.userName = userMeta.getString("/api/query/userinfo/@name"); + Timber.d("User id is %s and user name is %s", userID, userName); + return !userID.equals("0"); + } + + public String getUserID() throws IOException { + if(this.userID == null || this.userID.equals("0")) { + this.validateLogin(); + } + return userID; + } + + public String getUserName() throws IOException { + if(this.userID == null || this.userID.equals("0")) { + this.validateLogin(); + } + return userName; + } + + public String login(String username, String password) throws IOException { + CustomApiResult tokenData = this.action("login").param("lgname", username).param("lgpassword", password).post(); + String result = tokenData.getString("/api/login/@result"); + if (result.equals("NeedToken")) { + String token = tokenData.getString("/api/login/@token"); + CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post(); + String finalResult = confirmData.getString("/api/login/@result"); + if(finalResult.equals("Success")) { + isLoggedIn = true; + } + return finalResult; + } else { + return result; + } + } + + public CustomApiResult upload(String filename, InputStream file, long length, String text, String comment, String centralAuthToken, String token) throws IOException { + return this.upload(filename, file, length, text, comment,centralAuthToken, token, null); + } + + public CustomApiResult upload(String filename, InputStream file, String text, String comment, String centralAuthToken, String token) throws IOException { + return this.upload(filename, file, -1, text, comment,centralAuthToken, token, null); + } + + public CustomApiResult upload(String filename, InputStream file, long length, String text, String comment, String centralAuthToken, String token, ProgressListener uploadProgressListener) throws IOException { + Timber.d("Token being used is %s", token); + + Http.HttpRequestBuilder builder = Http.multipart(apiURL) + .data("action", "upload") + .data("token", token) + .data("centralauthtoken", centralAuthToken) + .data("text", text) + .data("ignorewarnings", "1") + .data("comment", comment) + .data("filename", filename) + .sendProgressListener(uploadProgressListener); + if(length != -1) { + builder.file("file", filename, file, length); + } else { + builder.file("file", filename, file); + } + + Timber.d("Final cookies are %s", client.getCookieStore().getCookies().toString()); + + return CustomApiResult.fromRequestBuilder(builder, client); + } + + public void logout() throws IOException { + // I should be doing more validation here, but meh + isLoggedIn = false; + this.action("logout").post(); + } + + private CustomApiResult makeRequest(String method, HashMap params) throws IOException { + Http.HttpRequestBuilder builder; + if (method.equals("POST")) { + builder = Http.post(apiURL); + } else { + builder = Http.get(apiURL); + } + builder.data(params); + return CustomApiResult.fromRequestBuilder(builder, client); + } +} +; \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index a40f04bffa..d96e9b177a 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -55,7 +55,7 @@ public interface MediaWikiApi { List searchCategory(String title, int offset); @NonNull - UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener, Uri fileUri, Uri contentProviderUri) throws IOException; + UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, Uri fileUri, Uri contentProviderUri, ProgressListener progressListener) throws IOException; @Nullable String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 8107a961a9..2933d4c192 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -71,7 +71,7 @@ private void setUserName() { View navHeaderView = navigationView.getHeaderView(0); TextView username = navHeaderView.findViewById(R.id.username); AccountManager accountManager = AccountManager.get(this); - Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE); + Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); if (allAccounts.length != 0) { username.setText(allAccounts[0].name); } 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 505811ab1e..306c7272f8 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 @@ -249,7 +249,7 @@ private void uploadContribution(Contribution contribution) { getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), contribution ); - UploadResult uploadResult = mwApi.uploadFile(filename, fileInputStream, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater, contribution.getLocalUri(), contribution.getContentProviderUri()); + UploadResult uploadResult = mwApi.uploadFile(filename, fileInputStream, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), contribution.getLocalUri(), contribution.getContentProviderUri(), notificationUpdater); Timber.d("Response is %s", uploadResult.toString()); diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt index fd6e93fab6..b070aed2a0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -182,15 +182,23 @@ class ApacheHttpClientMediaWikiApiTest { @Test fun editToken() { - server.enqueue(MockResponse().setBody("")) + server.enqueue(MockResponse().setBody("")) + server.enqueue(MockResponse().setBody("")) val result = testObject.editToken - assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> - parseQueryParams(loginTokenRequest).let { params -> + assertBasicRequestParameters(server, "GET").let { centralAuthTokenRequest -> + parseQueryParams(centralAuthTokenRequest).let { params -> assertEquals("xml", params["format"]) - assertEquals("tokens", params["action"]) - assertEquals("edit", params["type"]) + assertEquals("centralauthtoken", params["action"]) + } + } + + assertBasicRequestParameters(server, "POST").let { editTokenRequest -> + parseBody(editTokenRequest.body.readUtf8()).let { body -> + assertEquals("query", body["action"]) + assertEquals("abc", body["centralauthtoken"]) + assertEquals("tokens", body["meta"]) } }