diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aeb672aabacb..1618857848b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,6 +64,14 @@ The Android build tools now allow drawable resources to be provided exclusively From the [release notes](http://tools.android.com/tech-docs/new-build-system): `PNGs are generated for every vector drawable found in a resource directory that does not specify an API version (or specifies a version lower than 21).` +# Subtree'd projects + +The [WordPress-HealthCheck-Common][healthcheck] project is used in the tests and loaded from `assets` on tests run. Use the following command to pull in newer commits from the external project: + + $ git subtree pull --prefix=WordPress/src/androidTest/assets/health-check/ https://github.com/wordpress-mobile/WordPress-HealthCheck-Common.git develop + +[healthcheck]: https://github.com/wordpress-mobile/WordPress-HealthCheck-Common + # Contribute to translations We use a tool called GlotPress to manage translations. The WordPress-Android GlotPress instance lives here: http://translate.wordpress.org/projects/apps/android/dev. To add new translations or fix existing ones, create an account over at GlotPress and submit your changes over at the GlotPress site. diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 679001c32e63..35b0b065ec2e 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -95,6 +95,8 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0' androidTestCompile 'org.objenesis:objenesis:2.1' androidTestCompile 'org.mockito:mockito-core:+' + androidTestCompile 'com.squareup.okhttp:mockwebserver:2.7.5' + // Provided by the WordPress-Android Repository compile 'org.wordpress:drag-sort-listview:0.6.1' // not found in maven central diff --git a/WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json b/WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json new file mode 100644 index 000000000000..49cb953e8e13 --- /dev/null +++ b/WordPress/src/androidTest/assets/health-check/health-check-xplat-testcases.json @@ -0,0 +1,229 @@ +{ + "testcases": [ + { + "comment": "testing empty url", + "realm": "URL_CANONICALIZATION", + "setup": { + "input": { + "siteUrl": "" + }, + "output": { + "error": { + "type": "SITE_URL_CANNOT_BE_EMPTY", + "message": "Check that the site URL entered is valid" + } + } + } + }, + { + "comment": "testing whitespace url", + "realm": "URL_CANONICALIZATION", + "setup": { + "input": { + "siteUrl": " " + }, + "output": { + "error": { + "type": "SITE_URL_CANNOT_BE_EMPTY", + "message": "Check that the site URL entered is valid" + } + } + } + }, + { + "comment": "testing null url", + "realm": "URL_CANONICALIZATION", + "setup": { + "input": { + "siteUrl": null + }, + "output": { + "error": { + "type": "SITE_URL_CANNOT_BE_EMPTY", + "message": "Check that the site URL entered is valid" + } + } + } + }, + { + "comment": "testing padding whitespace", + "realm": "URL_CANONICALIZATION", + "setup": { + "input": { + "siteUrl": " \t http://wordpress.com \t " + }, + "output": { + "siteUrl": "http://wordpress.com" + } + } + }, + { + "comment": "testing xmlrpc.php missing", + "realm": "XMLPRC_DISCOVERY", + "setup": { + "input": { + "serverMock": [ + { + "request": { + "method": "GET", + "path": "/" + }, + "response": { + "statusCode": 404 + } + }, + { + "request": { + "method": "GET", + "path": "/xmlrpc.php" + }, + "response": { + "statusCode": 404 + } + }, + { + "request": { + "method": "POST", + "path": "/" + }, + "response": { + "statusCode": 404 + } + }, + { + "request": { + "method": "POST", + "path": "/xmlrpc.php" + }, + "response": { + "statusCode": 404 + } + } + ] + }, + "output": { + "error": { + "type": "XMLRPC_MISSING", + "message": "Couldn't connect to the WordPress site" + } + } + } + }, + { + "comment": "testing xmlrpc.php found", + "realm": "XMLPRC_DISCOVERY", + "setup": { + "input": { + "serverMock": [ + { + "request": { + "method": "POST", + "path": "/xmlrpc.php" + }, + "response": { + "statusCode": 200, + "body": "asset:listMethodsResponse.xml" + } + } + ] + }, + "output": { + "xmlrpcEndpoint": "http://mockserver/xmlrpc.php" + } + } + }, + { + "comment": "testing xmlrpc.php discovered after redirect", + "realm": "XMLPRC_DISCOVERY", + "setup": { + "input": { + "siteUrl": "http://mockserver/wp", + "serverMock": [ + { + "request": { + "method": "GET", + "path": "/wp" + }, + "response": { + "statusCode": 301, + "headers": { + "Location": "/wpnew/" + }, + "body": "Page has moved!" + } + }, + { + "request": { + "method": "POST", + "path": "/wp" + }, + "response": { + "statusCode": 307, + "headers": { + "Location": "/wpnew/" + }, + "body": "Page has moved! POST to new address!" + } + }, + { + "request": { + "method": "POST", + "path": "/wp/xmlrpc.php" + }, + "response": { + "statusCode": 307, + "headers": { + "Location": "/wpnew/xmlrpc.php" + }, + "body": "Page has moved! POST to new address!" + } + }, + { + "request": { + "method": "GET", + "path": "/wpnew/" + }, + "response": { + "statusCode": 200, + "body": "asset:index_with_redirect.html" + } + }, + { + "request": { + "method": "GET", + "path": "/wpnew/xmlrpc.php?rsd" + }, + "response": { + "statusCode": 200, + "body": "asset:rsd_with_redirect.xml" + } + }, + { + "request": { + "method": "GET", + "path": "/wpnew/xmlrpc.php" + }, + "response": { + "statusCode": 405, + "body": "XML-RPC server accepts POST requests only." + } + }, + { + "request": { + "method": "POST", + "path": "/wpnew/xmlrpc.php" + }, + "response": { + "statusCode": 200, + "body": "asset:listMethodsResponse.xml" + } + } + ] + }, + "output": { + "xmlrpcEndpoint": "http://mockserver/wpnew/xmlrpc.php" + } + } + } + ] +} diff --git a/WordPress/src/androidTest/assets/health-check/index.html b/WordPress/src/androidTest/assets/health-check/index.html new file mode 100644 index 000000000000..e4c7f314bb12 --- /dev/null +++ b/WordPress/src/androidTest/assets/health-check/index.html @@ -0,0 +1,153 @@ + + + + + + + + + + wplogin – Just another WordPress site + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + + + + + +
+
+ + +
+ + + +
+ + + + + + + + diff --git a/WordPress/src/androidTest/assets/health-check/index_with_redirect.html b/WordPress/src/androidTest/assets/health-check/index_with_redirect.html new file mode 100644 index 000000000000..7c5ea729ee33 --- /dev/null +++ b/WordPress/src/androidTest/assets/health-check/index_with_redirect.html @@ -0,0 +1,153 @@ + + + + + + + + + + wplogin – Just another WordPress site + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + + + + + +
+
+ + +
+ + + +
+ + + + + + + + diff --git a/WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml b/WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml new file mode 100644 index 000000000000..d7b363def971 --- /dev/null +++ b/WordPress/src/androidTest/assets/health-check/listMethodsResponse.xml @@ -0,0 +1,91 @@ + + + + + + + system.multicall + system.listMethods + system.getCapabilities + demo.addTwoNumbers + demo.sayHello + pingback.extensions.getPingbacks + pingback.ping + mt.publishPost + mt.getTrackbackPings + mt.supportedTextFilters + mt.supportedMethods + mt.setPostCategories + mt.getPostCategories + mt.getRecentPostTitles + mt.getCategoryList + metaWeblog.getUsersBlogs + metaWeblog.deletePost + metaWeblog.newMediaObject + metaWeblog.getCategories + metaWeblog.getRecentPosts + metaWeblog.getPost + metaWeblog.editPost + metaWeblog.newPost + blogger.deletePost + blogger.editPost + blogger.newPost + blogger.getRecentPosts + blogger.getPost + blogger.getUserInfo + blogger.getUsersBlogs + wp.restoreRevision + wp.getRevisions + wp.getPostTypes + wp.getPostType + wp.getPostFormats + wp.getMediaLibrary + wp.getMediaItem + wp.getCommentStatusList + wp.newComment + wp.editComment + wp.deleteComment + wp.getComments + wp.getComment + wp.setOptions + wp.getOptions + wp.getPageTemplates + wp.getPageStatusList + wp.getPostStatusList + wp.getCommentCount + wp.deleteFile + wp.uploadFile + wp.suggestCategories + wp.deleteCategory + wp.newCategory + wp.getTags + wp.getCategories + wp.getAuthors + wp.getPageList + wp.editPage + wp.deletePage + wp.newPage + wp.getPages + wp.getPage + wp.editProfile + wp.getProfile + wp.getUsers + wp.getUser + wp.getTaxonomies + wp.getTaxonomy + wp.getTerms + wp.getTerm + wp.deleteTerm + wp.editTerm + wp.newTerm + wp.getPosts + wp.getPost + wp.deletePost + wp.editPost + wp.newPost + wp.getUsersBlogs + + + + + \ No newline at end of file diff --git a/WordPress/src/androidTest/assets/health-check/rsd.xml b/WordPress/src/androidTest/assets/health-check/rsd.xml new file mode 100644 index 000000000000..3ce3273938bd --- /dev/null +++ b/WordPress/src/androidTest/assets/health-check/rsd.xml @@ -0,0 +1,14 @@ + + + WordPress + https://wordpress.org/ + http://mockserver + + + + + + + + + diff --git a/WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml b/WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml new file mode 100644 index 000000000000..4de183be479b --- /dev/null +++ b/WordPress/src/androidTest/assets/health-check/rsd_with_redirect.xml @@ -0,0 +1,14 @@ + + + WordPress + https://wordpress.org/ + http://mockserver + + + + + + + + + diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java b/WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java new file mode 100644 index 000000000000..6f0d6a63b857 --- /dev/null +++ b/WordPress/src/androidTest/java/org/wordpress/android/util/HealthCheckTest.java @@ -0,0 +1,228 @@ +package org.wordpress.android.util; + +import com.squareup.okhttp.Headers; +import com.squareup.okhttp.mockwebserver.Dispatcher; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.TestUtils; +import org.wordpress.android.WordPress; +import org.wordpress.android.ui.accounts.helpers.FetchBlogListWPOrg; +import org.xmlrpc.android.LoggedInputStream; +import org.xmlrpc.android.XMLRPCUtils; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.test.InstrumentationTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +public class HealthCheckTest extends InstrumentationTestCase { + private static final String sAssetPathBase = "health-check/"; + private static final String sServerAddressMagicString = "mockserver"; + private static final String sServerResponsesMagicScheme = "asset:"; + + private void setLocale(String language, String country) { + Locale locale = new Locale(language, country); + Locale.setDefault(locale); + Resources res = getInstrumentation().getTargetContext().getResources(); + Configuration config = res.getConfiguration(); + config.locale = locale; + res.updateConfiguration(config, res.getDisplayMetrics()); + } + + @Override + protected void setUp() { + WordPress.setupVolleyQueue(); + + // set the app locale to english since the tests only support English for now + setLocale("en", "US"); + } + + @Override + protected void tearDown() { + } + + private static String stringFromAsset(Context context, String assetFilename) throws IOException { + LoggedInputStream mLoggedInputStream = new LoggedInputStream(context.getAssets().open(assetFilename)); + return TestUtils.convertStreamToString(mLoggedInputStream); + } + + private static JSONObject jsonFromAsset(Context context, String assetFilename) throws IOException, JSONException { + return new JSONObject(stringFromAsset(context, assetFilename)); + } + + public void testHealthCheckXplat() throws JSONException, IOException { + JSONArray testCases = jsonFromAsset(getInstrumentation().getContext(), sAssetPathBase + + "health-check-xplat-testcases.json").getJSONArray("testcases"); + + for (int i = 0; i < testCases.length(); i++) { + final JSONObject testCase = testCases.getJSONObject(i); + final String testCaseComment = testCase.getString("comment"); + + final JSONObject testSetup = testCase.getJSONObject("setup"); + final String realm = testCase.getString("realm"); + + switch (realm) { + case "URL_CANONICALIZATION": + runUrlCanonicalization(testCaseComment, testSetup); + break; + case "XMLPRC_DISCOVERY": + runXmlrpcDiscovery(testCaseComment, testSetup); + break; + default: + // fail the testsuite + assertTrue("health-check realm " + realm + " is not supported!", false); + break; + } + } + } + + private void runUrlCanonicalization(String testCaseComment, JSONObject testSetup) throws JSONException { + final JSONObject input = testSetup.getJSONObject("input"); + + final String inputUrl = input.isNull("siteUrl") ? null : input.getString("siteUrl"); + + final JSONObject output = testSetup.getJSONObject("output"); + + final String outputUrl = output.optString("siteUrl", null); + final JSONObject error = output.optJSONObject("error"); + + String canonicalizedUrl = null; + try { + canonicalizedUrl = XMLRPCUtils.sanitizeSiteUrl(inputUrl); + + // if we reached this point, it means that no error occurred + assertNull(testCaseMessage("Testcase defines an error but no error occurred!", testCaseComment), error); + } catch (XMLRPCUtils.XMLRPCUtilsException hce) { + assertNotNull(testCaseMessage("Error occurred but testcase does not define an error!", testCaseComment), + error); + + assertEquals(testCaseMessage("Error message does not match the defined one!", testCaseComment), error + .getString("message"), getInstrumentation().getTargetContext().getString(hce.errorMsgId)); + } + + assertEquals(testCaseMessage("Canonicalized URL does not match the defined one!", testCaseComment), + outputUrl, canonicalizedUrl); + } + + private void runXmlrpcDiscovery(String testCaseComment, JSONObject testSetup) throws JSONException, IOException { + final MockWebServer server = new MockWebServer(); + + testSetup = new JSONObject(replaceServerMagicName(server, testSetup.toString())); + + final JSONObject input = testSetup.getJSONObject("input"); + + setupMockHttpServer(server, input); + + final String inputUrl = input.isNull("siteUrl") ? server.url("").toString() : input.getString("siteUrl"); + + final JSONObject output = testSetup.getJSONObject("output"); + + final String outputUrl = output.optString("xmlrpcEndpoint", null); + final JSONObject error = output.optJSONObject("error"); + + String xmlrpcUrl = null; + try { + xmlrpcUrl = XMLRPCUtils.verifyOrDiscoverXmlRpcUrl(inputUrl, input.optString("username", null), input + .optString("username", null)); + + // if we reached this point, it means that no error occurred + assertNull(testCaseMessage("Testcase defines an error but no error occurred!", testCaseComment), error); + } catch (XMLRPCUtils.XMLRPCUtilsException hce) { + assertNotNull(testCaseMessage("Error occurred but testcase does not define an error!", testCaseComment), + error); + + assertEquals(testCaseMessage("Error message does not match the defined one!", testCaseComment), error + .getString("message"), getInstrumentation().getTargetContext().getString(hce.errorMsgId)); + } + + assertEquals(testCaseMessage("XMLRPC URL does not match the defined one!", testCaseComment), outputUrl, + xmlrpcUrl); + + server.shutdown(); + } + + private MockWebServer setupMockHttpServer(MockWebServer server, JSONObject requestResponsesJson) throws + JSONException, IOException { + final Map mockRequestResponses = new HashMap<>(); + + final JSONArray serverMock = requestResponsesJson.getJSONArray("serverMock"); + for (int i = 0; i < serverMock.length(); i++) { + final JSONObject reqRespJson = serverMock.getJSONObject(i); + + final JSONObject reqJson = reqRespJson.getJSONObject("request"); + Headers reqHeaders = json2Headers(reqJson.optJSONObject("headers")); + + RecordedRequest recordedRequest = new RecordedRequest(reqJson.getString("method") + " " + reqJson + .getString("path") + " HTTP/1.1", reqHeaders, null, 0, null, 0, null); + + final JSONObject respJson = reqRespJson.getJSONObject("response"); + Headers respHeaders = json2Headers(respJson.optJSONObject("headers")); + + String body = respJson.optString("body"); + if (body.startsWith(sServerResponsesMagicScheme)) { + body = stringFromAsset(getInstrumentation().getContext(), sAssetPathBase + body.substring + (sServerResponsesMagicScheme.length())); + } + + body = replaceServerMagicName(server, body); + + final MockResponse resp = new MockResponse() + .setResponseCode(respJson.getInt("statusCode")) + .setHeaders(respHeaders) + .setBody(body); + + mockRequestResponses.put(recordedRequest, resp); + } + + server.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + + for (Map.Entry reqResp : mockRequestResponses.entrySet()) { + final RecordedRequest mockRequest = reqResp.getKey(); + if (mockRequest.getRequestLine().equals(request.getRequestLine())) { + return reqResp.getValue(); + } + } + return new MockResponse().setResponseCode(404).setBody(""); + } + }); + + return server; + } + + private String replaceServerMagicName(MockWebServer server, String str) { + return str.replaceAll(sServerAddressMagicString, server.getHostName() + ":" + server.getPort()); + + } + + private Headers json2Headers(JSONObject headersJson) throws JSONException { + if (headersJson != null) { + Headers.Builder headBuilder = new Headers.Builder(); + Iterator headerKeys = headersJson.keys(); + while (headerKeys.hasNext()) { + final String headerName = headerKeys.next(); + headBuilder.add(headerName, headersJson.getString(headerName)); + } + + return headBuilder.build(); + } + + return new Headers.Builder().build(); + } + + private String testCaseMessage(String message, String testCaseComment) { + return message + " (on testCase: '" + testCaseComment + "')"; + } +} diff --git a/WordPress/src/future/res/PluginsCheckerWPOrg.java b/WordPress/src/future/res/PluginsCheckerWPOrg.java new file mode 100644 index 000000000000..ade442b88a63 --- /dev/null +++ b/WordPress/src/future/res/PluginsCheckerWPOrg.java @@ -0,0 +1,202 @@ +package org.wordpress.android.ui.accounts.helpers; + +import android.text.TextUtils; +import android.webkit.URLUtil; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.util.AppLog; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * + * This class can test a WordPress installation for plugins/themes that cause problems with WordPress for Android connecting correctly. + * + * The tool is a black box scanner, it allows remote testing of a WordPress installation. + * Find problematic plugins and themes, configuration issues and other glitches that can cause problems with our apps. + * + */ +public class PluginsCheckerWPOrg { + private final static String BB_PLUGINS_LIST_URL = "https://raw.githubusercontent.com/wordpress-mobile/app-blocking-plugins/master/xmlrpc-plugins.json"; + + // Do not use the WP-APP user agent. Requests could be blocked if made from our app UA. + private final static String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36"; + + /** Socket timeout in milliseconds for the requests */ + private static final int REQUEST_TIMEOUT_MS = 30000; + + String mOriginalURL; + + public PluginsCheckerWPOrg(String url) { + mOriginalURL = url; + } + + /** + * This routine does a black box scanning on the plugis folder of the remote host, and tries to find plugins that cause + * problems connecting to the host from one of our mobile apps. + */ + public List checkForPlugins() { + String responseHTML = downloadPluginsList(); + if (TextUtils.isEmpty(responseHTML)) { + AppLog.w(AppLog.T.NUX, "Without the list we can't check if the host has some BB plugins installed on it."); + return null; + } + + JSONArray listOfPlugins; + try { + listOfPlugins = new JSONArray(responseHTML); + } catch (JSONException e) { + AppLog.e(AppLog.T.NUX, "Error while parsing the list of plugins returned from the server.", e); + return null; + } + + // we have the list. Start the process of checking for plugins + String baseURL = getBaseURL(mOriginalURL); + + if (!baseURL.contains("/plugins")) { + baseURL = baseURL + "/wp-content/plugins/"; + } + + AppLog.i(AppLog.T.NUX, "The calculated plugins URL is the following: " + baseURL); + + if (!URLUtil.isValidUrl(baseURL)) { + AppLog.w(AppLog.T.NUX, "The calculated plugins URL isn't a valid URL. Returning now."); + return null; + } + + int respCode = openConnection(baseURL); + if (respCode != HttpURLConnection.HTTP_OK && respCode != 401 && respCode != 403) { + AppLog.w(AppLog.T.NUX, "The request to plugins URL returned with an unexpected HTTP error code. Returning now."); + return null; + } + + AppLog.i(AppLog.T.NUX, "Start checking the plugins list.."); + ArrayList listOfBBPlugins = new ArrayList<>(); + for (int i=0; i 0) { + url = url.substring(0, url.indexOf(prefix)); + } + + if (!URLUtil.isValidUrl(url)) { + throw new IllegalArgumentException("The new URL " + url + " is not valid!"); + } + + return url; + } + + public class Plugin { + String name; + String url; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java index 6e9ec246cc3e..11531bb4ce7a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/helpers/FetchBlogListWPOrg.java @@ -1,43 +1,22 @@ package org.wordpress.android.ui.accounts.helpers; -import android.os.AsyncTask; -import android.webkit.URLUtil; - import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.analytics.AnalyticsTracker.Stat; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.BlogUtils; -import org.wordpress.android.util.CrashlyticsUtils; -import org.wordpress.android.util.CrashlyticsUtils.ExceptionType; -import org.wordpress.android.util.CrashlyticsUtils.ExtraKey; -import org.wordpress.android.util.UrlUtils; -import org.wordpress.android.util.WPUrlUtils; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlrpc.android.ApiHelper; -import org.xmlrpc.android.ApiHelper.Method; -import org.xmlrpc.android.XMLRPCClientInterface; -import org.xmlrpc.android.XMLRPCException; -import org.xmlrpc.android.XMLRPCFactory; -import org.xmlrpc.android.XMLRPCFault; -import java.io.IOException; +import org.xmlrpc.android.XMLRPCUtils; +import org.xmlrpc.android.XMLRPCUtils.XMLRPCUtilsException; + +import android.os.AsyncTask; + import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; - public class FetchBlogListWPOrg extends FetchBlogListAbstract { private String mSelfHostedUrl; private String mHttpUsername; private String mHttpPassword; - private boolean mHttpAuthRequired; - private boolean mErroneousSslCertificate; - private int mErrorMsgId; public FetchBlogListWPOrg(String username, String password, String selfHostedUrl) { super(username, password); @@ -49,246 +28,42 @@ public void setHttpCredentials(String username, String password) { mHttpPassword = password; } - private void handleXmlRpcFault(XMLRPCFault xmlRpcFault) { - AppLog.e(T.NUX, "XMLRPCFault received from XMLRPC call wp.getUsersBlogs", xmlRpcFault); - switch (xmlRpcFault.getFaultCode()) { - case 403: - mErrorMsgId = org.wordpress.android.R.string.username_or_password_incorrect; - break; - case 404: - mErrorMsgId = org.wordpress.android.R.string.xmlrpc_error; - break; - case 425: - mErrorMsgId = org.wordpress.android.R.string.account_two_step_auth_enabled; - break; - default: - mErrorMsgId = org.wordpress.android.R.string.no_site_error; - break; - } - } - public void fetchBlogList(Callback callback) { (new FetchBlogListTask(callback)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - private String getRsdUrl(String baseUrl) throws SSLHandshakeException { - String rsdUrl; - rsdUrl = ApiHelper.getRSDMetaTagHrefRegEx(baseUrl); - if (rsdUrl == null) { - rsdUrl = ApiHelper.getRSDMetaTagHref(baseUrl); - } - return rsdUrl; - } - - private boolean isHTTPAuthErrorMessage(Exception e) { - if (e != null && e.getMessage() != null && e.getMessage().contains("401")) { - mHttpAuthRequired = true; - return true; - } - return false; - } - - private String getXmlrpcByUserEnteredPath(String baseUrl) { - String xmlRpcUrl; - if (!UrlUtils.isValidUrlAndHostNotNull(baseUrl)) { - AppLog.e(T.NUX, "invalid URL: " + baseUrl); - mErrorMsgId = org.wordpress.android.R.string.invalid_site_url_message; - return null; - } - URI uri = URI.create(baseUrl); - XMLRPCClientInterface client = XMLRPCFactory.instantiate(uri, mHttpUsername, mHttpPassword); - try { - client.call(Method.LIST_METHODS); - xmlRpcUrl = baseUrl; - return xmlRpcUrl; - } catch (XMLRPCException e) { - AppLog.i(T.NUX, "system.listMethods failed on: " + baseUrl); - if (isHTTPAuthErrorMessage(e)) { - return null; - } - } catch (SSLHandshakeException e) { - if (!WPUrlUtils.isWordPressCom(baseUrl)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); - return null; - } catch (SSLPeerUnverifiedException e) { - if (!WPUrlUtils.isWordPressCom(baseUrl)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLPeerUnverifiedException failed. Erroneous SSL certificate detected."); - return null; - } catch (IOException e) { - AppLog.i(T.NUX, "system.listMethods failed on: " + baseUrl); - if (isHTTPAuthErrorMessage(e)) { - return null; - } - } catch (XmlPullParserException e) { - AppLog.i(T.NUX, "system.listMethods failed on: " + baseUrl); - if (isHTTPAuthErrorMessage(e)) { - return null; - } - } catch (IllegalArgumentException e) { - // TODO: Hopefully a temporary log - remove it if we find a pattern of failing URLs - CrashlyticsUtils.setString(ExtraKey.ENTERED_URL, baseUrl); - CrashlyticsUtils.logException(e, ExceptionType.SPECIFIC, T.NUX); - mErrorMsgId = org.wordpress.android.R.string.invalid_site_url_message; - return null; - } - - // Guess the xmlrpc path - String guessURL = baseUrl; - if (guessURL.substring(guessURL.length() - 1, guessURL.length()).equals("/")) { - guessURL = guessURL.substring(0, guessURL.length() - 1); - } - guessURL += "/xmlrpc.php"; - uri = URI.create(guessURL); - client = XMLRPCFactory.instantiate(uri, mHttpUsername, mHttpPassword); - try { - client.call(Method.LIST_METHODS); - xmlRpcUrl = guessURL; - return xmlRpcUrl; - } catch (XMLRPCException e) { - AnalyticsTracker.track(Stat.LOGIN_FAILED_TO_GUESS_XMLRPC); - AppLog.e(T.NUX, "system.listMethods failed on: " + guessURL, e); - } catch (SSLHandshakeException e) { - if (!WPUrlUtils.isWordPressCom(baseUrl)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); - return null; - } catch (SSLPeerUnverifiedException e) { - if (!WPUrlUtils.isWordPressCom(baseUrl)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLPeerUnverifiedException failed. Erroneous SSL certificate detected."); - return null; - } catch (IOException e) { - AnalyticsTracker.track(Stat.LOGIN_FAILED_TO_GUESS_XMLRPC); - AppLog.e(T.NUX, "system.listMethods failed on: " + guessURL, e); - } catch (XmlPullParserException e) { - AnalyticsTracker.track(Stat.LOGIN_FAILED_TO_GUESS_XMLRPC); - AppLog.e(T.NUX, "system.listMethods failed on: " + guessURL, e); - } - - return null; - } - - // Attempts to retrieve the xmlrpc url for a self-hosted site, in this order: - // 1: Try to retrieve it by finding the ?rsd url in the site's header - // 2: Take whatever URL the user entered to see if that returns a correct response - // 3: Finally, just guess as to what the xmlrpc url should be - private String getSelfHostedXmlrpcUrl(String url) { - String xmlrpcUrl; - - // Convert IDN names to punycode if necessary - url = UrlUtils.convertUrlToPunycodeIfNeeded(url); - - // Add http to the beginning of the URL if needed - url = UrlUtils.addUrlSchemeIfNeeded(url, false); - - if (!URLUtil.isValidUrl(url)) { - mErrorMsgId = org.wordpress.android.R.string.invalid_site_url_message; - return null; - } - - // Attempt to get the XMLRPC URL via RSD - String rsdUrl; - try { - rsdUrl = UrlUtils.addUrlSchemeIfNeeded(getRsdUrl(url), false); - } catch (SSLHandshakeException e) { - if (!WPUrlUtils.isWordPressCom(url)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); - return null; - } - - try { - if (rsdUrl != null) { - xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(ApiHelper.getXMLRPCUrl(rsdUrl), false); - if (xmlrpcUrl == null) { - xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(rsdUrl.replace("?rsd", ""), false); - } - } else { - xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXmlrpcByUserEnteredPath(url), false); - } - } catch (SSLHandshakeException e) { - if (!WPUrlUtils.isWordPressCom(url)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); - return null; - } - - return xmlrpcUrl; - } - public class FetchBlogListTask extends AsyncTask>> { private final Callback mCallback; + private boolean mHttpAuthRequired; + private boolean mErroneousSslCertificate; + private int mErrorMsgId; private String mClientResponse = ""; public FetchBlogListTask(Callback callback) { mCallback = callback; } + private void trackInvalidInsertedURL(String url){ + Map properties = new HashMap<>(); + properties.put("user_inserted_url", url); + AnalyticsTracker.track(Stat.LOGIN_INSERTED_INVALID_URL, properties); + } + @Override protected List> doInBackground(Void... notUsed) { - String xmlrpcUrl = null; - if (mSelfHostedUrl != null && mSelfHostedUrl.length() != 0) { - xmlrpcUrl = getSelfHostedXmlrpcUrl(mSelfHostedUrl); - } - - if (xmlrpcUrl == null) { - if (!mHttpAuthRequired && mErrorMsgId == 0) { - mErrorMsgId = org.wordpress.android.R.string.no_site_error; - } - return null; - } - - // Validate the URL found before calling the client. Prevent a crash that can occur - // during the setup of self-hosted sites. - URI xmlrpcUri; - xmlrpcUri = URI.create(xmlrpcUrl); - XMLRPCClientInterface client = XMLRPCFactory.instantiate(xmlrpcUri, mHttpUsername, mHttpPassword); - Object[] params = {mUsername, mPassword}; try { - Object[] userBlogs = (Object[]) client.call(Method.GET_BLOGS, params); - if (userBlogs == null) { - // Could happen if the returned server response is truncated - mErrorMsgId = org.wordpress.android.R.string.xmlrpc_error; - mClientResponse = client.getResponse(); - return null; - } - Arrays.sort(userBlogs, BlogUtils.BlogNameComparator); - List> userBlogList = new ArrayList>(); - for (Object blog : userBlogs) { - try { - userBlogList.add((Map) blog); - } catch (ClassCastException e) { - AppLog.e(T.NUX, "invalid data received from XMLRPC call wp.getUsersBlogs"); - } - } - return userBlogList; - } catch (XmlPullParserException parserException) { - mErrorMsgId = org.wordpress.android.R.string.xmlrpc_error; - AppLog.e(T.NUX, "invalid data received from XMLRPC call wp.getUsersBlogs", parserException); - } catch (XMLRPCFault xmlRpcFault) { - handleXmlRpcFault(xmlRpcFault); - } catch (XMLRPCException xmlRpcException) { - AppLog.e(T.NUX, "XMLRPCException received from XMLRPC call wp.getUsersBlogs", xmlRpcException); - mErrorMsgId = org.wordpress.android.R.string.no_site_error; - } catch (SSLHandshakeException e) { - if (WPUrlUtils.isWordPressCom(xmlrpcUri)) { - mErroneousSslCertificate = true; - } - AppLog.w(T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); - } catch (IOException e) { - AppLog.e(T.NUX, "Exception received from XMLRPC call wp.getUsersBlogs", e); - mErrorMsgId = org.wordpress.android.R.string.no_site_error; + String xmlrpcUrl = XMLRPCUtils.verifyOrDiscoverXmlRpcUrl(mSelfHostedUrl, mHttpUsername, mHttpPassword); + + // The XML-RPC address is now available. Call wp.getUsersBlogs and load the sites. + return XMLRPCUtils.getUserBlogsList(URI.create(xmlrpcUrl), mUsername, mPassword, mHttpUsername, + mHttpPassword); + } catch (XMLRPCUtilsException hce) { + mErrorMsgId = hce.errorMsgId; + mHttpAuthRequired = (hce.kind == XMLRPCUtilsException.Kind.HTTP_AUTH_REQUIRED); + mErroneousSslCertificate = (hce.kind == XMLRPCUtilsException.Kind.ERRONEOUS_SSL_CERTIFICATE); + trackInvalidInsertedURL(hce.failedUrl); + return null; } - mClientResponse = client.getResponse(); - return null; } protected void onPostExecute(List> userBlogList) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java index 590994f1f7e9..cefbb98ce113 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanFragment.java @@ -2,6 +2,7 @@ import android.app.Fragment; import android.os.Bundle; +import android.support.annotation.NonNull; import android.text.Html; import android.text.TextUtils; import android.view.LayoutInflater; @@ -21,15 +22,14 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.HtmlUtils; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; public class PlanFragment extends Fragment { private static final String SITE_PLAN = "SITE_PLAN"; - private static final String PLAN_DETAILS = "PLAN_DETAILS"; private ViewGroup mPlanContainerView; - private SitePlan mSitePlan; private Plan mPlanDetails; @@ -44,10 +44,10 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { if (savedInstanceState.containsKey(SITE_PLAN)) { - mSitePlan = (SitePlan) savedInstanceState.getSerializable(SITE_PLAN); - } - if (savedInstanceState.containsKey(PLAN_DETAILS)) { - mPlanDetails = (Plan) savedInstanceState.getSerializable(PLAN_DETAILS); + Serializable serial = savedInstanceState.getSerializable(SITE_PLAN); + if (serial instanceof SitePlan) { + setSitePlan((SitePlan) serial); + } } } } @@ -70,7 +70,6 @@ public void onResume() { @Override public void onSaveInstanceState(Bundle outState) { outState.putSerializable(SITE_PLAN, mSitePlan); - outState.putSerializable(PLAN_DETAILS, mPlanDetails); super.onSaveInstanceState(outState); } @@ -169,7 +168,7 @@ private void addFeature(Feature feature) { mPlanContainerView.addView(view); } - private void setSitePlan(SitePlan sitePlan) { + private void setSitePlan(@NonNull SitePlan sitePlan) { mSitePlan = sitePlan; mPlanDetails = PlansUtils.getGlobalPlan(mSitePlan.getProductID()); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java index 5a79a6ce8d62..1cc753185132 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/models/Plan.java @@ -9,12 +9,11 @@ import org.wordpress.android.util.JSONUtils; import org.wordpress.android.util.StringUtils; -import java.io.Serializable; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; -public class Plan implements Serializable { +public class Plan { private long mProductID; private String mProductName; private final Hashtable mPrices = new Hashtable<>(); diff --git a/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java b/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java index c83967b911f1..8426cd8f230a 100644 --- a/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java +++ b/WordPress/src/main/java/org/xmlrpc/android/ApiHelper.java @@ -6,12 +6,12 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; -import android.util.Xml; import android.webkit.URLUtil; import com.android.volley.DefaultRetryPolicy; import com.android.volley.NetworkResponse; import com.android.volley.RedirectError; +import com.android.volley.TimeoutError; import com.android.volley.toolbox.RequestFuture; import com.android.volley.toolbox.StringRequest; import com.google.gson.Gson; @@ -35,13 +35,11 @@ import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.MapUtils; import org.wordpress.android.util.helpers.MediaFile; -import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -50,8 +48,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.net.ssl.SSLHandshakeException; @@ -988,33 +984,13 @@ protected void onPostExecute(FeatureSet result) { } - /** - * Discover the XML-RPC endpoint for the WordPress API associated with the specified blog URL. - * - * @param urlString URL of the blog to get the XML-RPC endpoint for. - * @return XML-RPC endpoint for the specified blog, or null if unable to discover endpoint. - */ - public static String getXMLRPCUrl(String urlString) throws SSLHandshakeException { - Pattern xmlrpcLink = Pattern.compile(" future = RequestFuture.newFuture(); StringRequest request = new StringRequest(stringUrl, future, future); request.setRetryPolicy(new DefaultRetryPolicy(XMLRPCClient.DEFAULT_SOCKET_TIMEOUT_MS, 0, 1)); @@ -1068,99 +1044,16 @@ public static String getResponse(final String stringUrl, int numberOfRedirects) AppLog.i(T.API, "Follow redirect from " + stringUrl + " to " + newURL); return getResponse(newURL, numberOfRedirects + 1); } + } else if (e.getCause() != null && e.getCause() instanceof com.android.volley.TimeoutError) { + AppLog.e(T.API, e); + throw (com.android.volley.TimeoutError) e.getCause(); } else { AppLog.e(T.API, e); } - - } catch (TimeoutException e) { - AppLog.e(T.API, e); } return null; } - /** - * Regex pattern for matching the RSD link found in most WordPress sites. - */ - private static final Pattern rsdLink = Pattern.compile( - " urlsToTry = new ArrayList<>(); + + // start by adding the url with 'xmlrpc.php'. This will be the first url to try. + urlsToTry.add(XMLRPCUtils.appendXMLRPCPath(sanitizedSiteUrl)); + + // add the sanitized URL without the '/xmlrpc.php' suffix added to it + if (!urlsToTry.contains(sanitizedSiteUrl)) { + urlsToTry.add(sanitizedSiteUrl); + } + + // add the user provided URL as well + if (!urlsToTry.contains(siteUrl)) { + urlsToTry.add(siteUrl); + } + + AppLog.i(AppLog.T.NUX, "The app will call system.listMethods on the following URLs: " + urlsToTry); + for (String url : urlsToTry) { + if (XMLRPCUtils.checkXMLRPCEndpointValidity(url, httpUsername, httpPassword)) { + // Endpoint found and works fine. + return url; + } + } + + // input url was not verified to be working + return null; + } + + // Attempts to retrieve the xmlrpc url for a self-hosted site. + // See diagrams here https://github.com/wordpress-mobile/WordPress-Android/issues/3805 for details about the + // whole process. + private static String discoverSelfHostedXmlrpcUrl(String siteUrl, String httpUsername, String httpPassword) throws + XMLRPCUtilsException { + // Array of Strings that contains the URLs we want to try + final List urlsToTry = new ArrayList<>(); + + // add the url as provided by the user + urlsToTry.add(siteUrl); + + // add a sanitized version of the url + final String sanitizedURL = sanitizeSiteUrl(siteUrl); + if (!urlsToTry.contains(sanitizedURL)) { + urlsToTry.add(sanitizedURL); + } + + String appendedXmlrpcUrl = appendXMLRPCPath(sanitizedURL); + if (!urlsToTry.contains(appendedXmlrpcUrl)) { + appendedXmlrpcUrl += "?rsd"; + urlsToTry.add(appendedXmlrpcUrl); + } + + AppLog.i(AppLog.T.NUX, "The app will call the RSD discovery process on the following URLs: " + urlsToTry); + + String xmlrpcUrl = null; + for (String currentURL : urlsToTry) { + try { + // Download the HTML content + AppLog.i(AppLog.T.NUX, "Downloading the HTML content at the following URL: " + currentURL); + String responseHTML = ApiHelper.getResponse(currentURL); + if (TextUtils.isEmpty(responseHTML)) { + AppLog.w(AppLog.T.NUX, "Content downloaded but it's empty or null. Skipping this URL"); + continue; + } + + // Try to find the RSD tag with a regex + String rsdUrl = getRSDMetaTagHrefRegEx(responseHTML); + // If the regex approach fails try to parse the HTML doc and retrieve the RSD tag. + if (rsdUrl == null) { + rsdUrl = getRSDMetaTagHref(responseHTML); + } + rsdUrl = UrlUtils.addUrlSchemeIfNeeded(rsdUrl, false); + + // if the RSD URL is empty here, try to see if there is already the pingback or the Apilink in the doc + // the user could have inserted a direct link to the xml-rpc endpoint + if (rsdUrl == null) { + AppLog.i(AppLog.T.NUX, "Can't find the RSD endpoint in the HTML document. Try to check the " + + "pingback tag, and the apiLink tag."); + xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXMLRPCPingback(responseHTML), false); + if (xmlrpcUrl == null) { + xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXMLRPCApiLink(responseHTML), false); + } + } else { + AppLog.i(AppLog.T.NUX, "RSD endpoint found at the following address: " + rsdUrl); + AppLog.i(AppLog.T.NUX, "Downloading the RSD document..."); + String rsdEndpointDocument = ApiHelper.getResponse(rsdUrl); + if (TextUtils.isEmpty(rsdEndpointDocument)) { + AppLog.w(AppLog.T.NUX, "Content downloaded but it's empty or null. Skipping this RSD document" + + " URL."); + continue; + } + AppLog.i(AppLog.T.NUX, "Extracting the XML-RPC Endpoint address from the RSD document"); + xmlrpcUrl = UrlUtils.addUrlSchemeIfNeeded(getXMLRPCApiLink(rsdEndpointDocument), false); + } + if (xmlrpcUrl != null) { + AppLog.i(AppLog.T.NUX, "Found the XML-RPC endpoint in the HTML document!!!"); + break; + } else { + AppLog.i(AppLog.T.NUX, "XML-RPC endpoint NOT found"); + } + } catch (SSLHandshakeException e) { + if (!WPUrlUtils.isWordPressCom(currentURL)) { + throw new XMLRPCUtilsException(Kind.ERRONEOUS_SSL_CERTIFICATE, 0, currentURL, null); + } + AppLog.w(AppLog.T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); + return null; + } catch (TimeoutError | TimeoutException e) { + AppLog.w(AppLog.T.NUX, "Timeout error while connecting to the site: " + currentURL); + throw new XMLRPCUtilsException(Kind.SITE_TIME_OUT, org.wordpress.android.R + .string.site_timeout_error, currentURL, null); + } + } + + if (URLUtil.isValidUrl(xmlrpcUrl)) { + if (checkXMLRPCEndpointValidity(xmlrpcUrl, httpUsername, httpPassword)) { + // Endpoint found and works fine. + return xmlrpcUrl; + } + } + + throw new XMLRPCUtilsException(Kind.NO_SITE_ERROR, org.wordpress.android.R.string.no_site_error, null, null); + } + + public static List> getUserBlogsList(URI xmlrpcUri, String username, String password, String + httpUsername, String httpPassword) throws XMLRPCUtilsException { + XMLRPCClientInterface client = XMLRPCFactory.instantiate(xmlrpcUri, httpUsername, httpPassword); + Object[] params = { username, password }; + try { + Object[] userBlogs = (Object[]) client.call(ApiHelper.Method.GET_BLOGS, params); + if (userBlogs == null) { + // Could happen if the returned server response is truncated + throw new XMLRPCUtilsException(Kind.XMLRPC_MALFORMED_RESPONSE, R.string.xmlrpc_malformed_response_error, + xmlrpcUri.toString(), client.getResponse()); + } + Arrays.sort(userBlogs, BlogUtils.BlogNameComparator); + List> userBlogList = new ArrayList<>(); + for (Object blog : userBlogs) { + try { + userBlogList.add((Map) blog); + } catch (ClassCastException e) { + AppLog.e(AppLog.T.NUX, "invalid data received from XMLRPC call wp.getUsersBlogs"); + } + } + return userBlogList; + } catch (XmlPullParserException parserException) { + AppLog.e(AppLog.T.NUX, "invalid data received from XMLRPC call wp.getUsersBlogs", parserException); + throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.xmlrpc_error, xmlrpcUri.toString(), client + .getResponse()); + } catch (XMLRPCFault xmlRpcFault) { + AppLog.e(AppLog.T.NUX, "XMLRPCFault received from XMLRPC call wp.getUsersBlogs", xmlRpcFault); + throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, handleXmlRpcFault(xmlRpcFault), xmlrpcUri.toString() + , client.getResponse()); + } catch (XMLRPCException xmlRpcException) { + AppLog.e(AppLog.T.NUX, "XMLRPCException received from XMLRPC call wp.getUsersBlogs", xmlRpcException); + throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.no_site_error, xmlrpcUri.toString(), client + .getResponse()); + } catch (SSLHandshakeException e) { + if (!WPUrlUtils.isWordPressCom(xmlrpcUri.toString())) { + throw new XMLRPCUtilsException(Kind.ERRONEOUS_SSL_CERTIFICATE, 0, xmlrpcUri.toString(), null); + } + AppLog.w(AppLog.T.NUX, "SSLHandshakeException failed. Erroneous SSL certificate detected."); + } catch (ConnectTimeoutException e) { + AppLog.e(AppLog.T.NUX, "Timeout exception when calling wp.getUsersBlogs", e); + throw new XMLRPCUtilsException(Kind.SITE_TIME_OUT, R.string.site_timeout_error, + xmlrpcUri.toString(), client.getResponse()); + } catch (IOException e) { + AppLog.e(AppLog.T.NUX, "Exception received from XMLRPC call wp.getUsersBlogs", e); + throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.no_site_error, xmlrpcUri.toString(), client + .getResponse()); + } + + throw new XMLRPCUtilsException(Kind.XMLRPC_ERROR, R.string.no_site_error, xmlrpcUri.toString(), client + .getResponse()); + } + + /** + * Regex pattern for matching the RSD link found in most WordPress sites. + */ + private static final Pattern rsdLink = Pattern.compile( + " 0) { + data = data.substring(indexOfFirstXML); + } + StringReader stringReader = new StringReader(data); + XmlPullParser parser = Xml.newPullParser(); + try { + // auto-detect the encoding from the stream + parser.setInput(stringReader); + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + String name; + String rel = ""; + String type = ""; + String href = ""; + switch (eventType) { + case XmlPullParser.START_TAG: + name = parser.getName(); + if (name.equalsIgnoreCase("link")) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + String attrName = parser.getAttributeName(i); + String attrValue = parser.getAttributeValue(i); + if (attrName.equals("rel")) { + rel = attrValue; + } else if (attrName.equals("type")) + type = attrValue; + else if (attrName.equals("href")) + href = attrValue; + } + + if (rel.equals("EditURI") && type.equals("application/rsd+xml")) { + return href; + } + // currentMessage.setLink(parser.nextText()); + } + break; + } + eventType = parser.next(); + } + } catch (XmlPullParserException e) { + AppLog.e(AppLog.T.API, e); + return null; + } catch (IOException e) { + AppLog.e(AppLog.T.API, e); + return null; + } + } + return null; // never found the rsd tag + } + + /** + * Find the XML-RPC endpoint for the WordPress API. + * + * @return XML-RPC endpoint for the specified blog, or null if unable to discover endpoint. + */ + private static String getXMLRPCApiLink(String html) { + Pattern xmlrpcLink = Pattern.compile(" Couldn\'t connect. Enter the full path to xmlrpc.php on your site and try again. + Couldn\'t connect. Required XML-RPC methods are missing on the server. + Couldn\'t connect. The WordPress installation responded with an invalid XML-RPC document. + Couldn\'t connect to the WordPress site due to Timeout error. No network available There is no network available Disconnecting your account will remove all of @%s’s WordPress.com data from this device, including local drafts and local changes. diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 024e82afade2..8697fe04efa8 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -116,6 +116,7 @@ public enum Stat { SUPPORT_SENT_REPLY_TO_SUPPORT_MESSAGE, LOGIN_FAILED, LOGIN_FAILED_TO_GUESS_XMLRPC, + LOGIN_INSERTED_INVALID_URL, PUSH_AUTHENTICATION_APPROVED, PUSH_AUTHENTICATION_EXPIRED, PUSH_AUTHENTICATION_FAILED, diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index ed70140e719e..5b3c87271707 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -363,6 +363,9 @@ public void track(AnalyticsTracker.Stat stat, Map properties) { case LOGIN_FAILED_TO_GUESS_XMLRPC: eventName = "login_failed_to_guess_xmlrpc"; break; + case LOGIN_INSERTED_INVALID_URL: + eventName = "login_inserted_invalid_url"; + break; case PUSH_AUTHENTICATION_APPROVED: eventName = "push_authentication_approved"; break;