package cgeo.geocaching.connector.gc;
import cgeo.geocaching.R;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.cgeoapplication;
import cgeo.geocaching.enumerations.StatusCode;
import cgeo.geocaching.network.Cookies;
import cgeo.geocaching.network.HtmlImage;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.network.Parameters;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.MatcherWrapper;
import cgeo.geocaching.utils.TextUtils;
import ch.boye.httpclientandroidlib.HttpResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import android.graphics.drawable.BitmapDrawable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public abstract class Login {
private final static String ENGLISH = "English▼";
// false = not logged in
private static boolean actualLoginStatus = false;
private static String actualUserName = "";
private static int actualCachesFound = -1;
private static String actualStatus = "";
private final static Map gcCustomDateFormats;
public static final String LANGUAGE_CHANGE_URI = "http://www.geocaching.com/my/souvenirs.aspx";
static {
final String[] formats = new String[] {
"MM/dd/yyyy",
"yyyy-MM-dd",
"yyyy/MM/dd",
"dd/MMM/yyyy",
"MMM/dd/yyyy",
"dd MMM yy",
"dd/MM/yyyy"
};
final Map map = new HashMap();
for (final String format : formats) {
map.put(format, new SimpleDateFormat(format, Locale.ENGLISH));
}
gcCustomDateFormats = Collections.unmodifiableMap(map);
}
public static StatusCode login() {
return login(true);
}
private static StatusCode login(boolean retry) {
final ImmutablePair login = Settings.getGcLogin();
if (login == null || StringUtils.isEmpty(login.left) || StringUtils.isEmpty(login.right)) {
Login.setActualStatus(cgeoapplication.getInstance().getString(R.string.err_login));
Log.e("Login.login: No login information stored");
return StatusCode.NO_LOGIN_INFO_STORED;
}
Login.setActualStatus(cgeoapplication.getInstance().getString(R.string.init_login_popup_working));
HttpResponse loginResponse = Network.getRequest("https://www.geocaching.com/login/default.aspx");
String loginData = Network.getResponseData(loginResponse);
if (loginResponse != null && loginResponse.getStatusLine().getStatusCode() == 503 && TextUtils.matches(loginData, GCConstants.PATTERN_MAINTENANCE)) {
return StatusCode.MAINTENANCE;
}
if (StringUtils.isBlank(loginData)) {
Log.e("Login.login: Failed to retrieve login page (1st)");
return StatusCode.CONNECTION_FAILED; // no loginpage
}
if (Login.getLoginStatus(loginData)) {
Log.i("Already logged in Geocaching.com as " + login.left + " (" + Settings.getMemberStatus() + ')');
Login.switchToEnglish(loginData);
return StatusCode.NO_ERROR; // logged in
}
Cookies.clearCookies();
Settings.setCookieStore(null);
final Parameters params = new Parameters(
"__EVENTTARGET", "",
"__EVENTARGUMENT", "",
"ctl00$ContentBody$tbUsername", login.left,
"ctl00$ContentBody$tbPassword", login.right,
"ctl00$ContentBody$cbRememberMe", "on",
"ctl00$ContentBody$btnSignIn", "Login");
final String[] viewstates = Login.getViewstates(loginData);
if (isEmpty(viewstates)) {
Log.e("Login.login: Failed to find viewstates");
return StatusCode.LOGIN_PARSE_ERROR; // no viewstates
}
Login.putViewstates(params, viewstates);
loginResponse = Network.postRequest("https://www.geocaching.com/login/default.aspx", params);
loginData = Network.getResponseData(loginResponse);
if (StringUtils.isBlank(loginData)) {
Log.e("Login.login: Failed to retrieve login page (2nd)");
// FIXME: should it be CONNECTION_FAILED to match the first attempt?
return StatusCode.COMMUNICATION_ERROR; // no login page
}
if (Login.getLoginStatus(loginData)) {
Log.i("Successfully logged in Geocaching.com as " + login.left + " (" + Settings.getMemberStatus() + ')');
Login.switchToEnglish(loginData);
Settings.setCookieStore(Cookies.dumpCookieStore());
return StatusCode.NO_ERROR; // logged in
}
if (loginData.contains("Your username/password combination does not match.")) {
Log.i("Failed to log in Geocaching.com as " + login.left + " because of wrong username/password");
return StatusCode.WRONG_LOGIN_DATA; // wrong login
}
if (loginData.contains("You must validate your account before you can log in.")) {
Log.i("Failed to log in Geocaching.com as " + login.left + " because account needs to be validated first");
return StatusCode.UNVALIDATED_ACCOUNT;
}
Log.i("Failed to log in Geocaching.com as " + login.left + " for some unknown reason");
if (retry) {
Login.switchToEnglish(loginData);
return login(false);
}
return StatusCode.UNKNOWN_ERROR; // can't login
}
public static StatusCode logout() {
final HttpResponse logoutResponse = Network.getRequest("https://www.geocaching.com/login/default.aspx?RESET=Y&redir=http%3a%2f%2fwww.geocaching.com%2fdefault.aspx%3f");
final String logoutData = Network.getResponseData(logoutResponse);
if (logoutResponse != null && logoutResponse.getStatusLine().getStatusCode() == 503 && TextUtils.matches(logoutData, GCConstants.PATTERN_MAINTENANCE)) {
return StatusCode.MAINTENANCE;
}
Cookies.clearCookies();
Settings.setCookieStore(null);
return StatusCode.NO_ERROR;
}
static void setActualCachesFound(final int found) {
actualCachesFound = found;
}
public static String getActualStatus() {
return actualStatus;
}
private static void setActualStatus(final String status) {
actualStatus = status;
}
public static boolean isActualLoginStatus() {
return actualLoginStatus;
}
private static void setActualLoginStatus(boolean loginStatus) {
actualLoginStatus = loginStatus;
}
public static String getActualUserName() {
return actualUserName;
}
private static void setActualUserName(String userName) {
actualUserName = userName;
}
public static int getActualCachesFound() {
return actualCachesFound;
}
/**
* Check if the user has been logged in when he retrieved the data.
*
* @param page
* @return true if user is logged in, false otherwise
*/
public static boolean getLoginStatus(final String page) {
if (StringUtils.isBlank(page)) {
Log.e("Login.checkLogin: No page given");
return false;
}
setActualStatus(cgeoapplication.getInstance().getString(R.string.init_login_popup_ok));
// on every page except login page
setActualLoginStatus(TextUtils.matches(page, GCConstants.PATTERN_LOGIN_NAME));
if (isActualLoginStatus()) {
setActualUserName(TextUtils.getMatch(page, GCConstants.PATTERN_LOGIN_NAME, true, "???"));
int cachesCount = 0;
try {
cachesCount = Integer.parseInt(TextUtils.getMatch(page, GCConstants.PATTERN_CACHES_FOUND, true, "0").replaceAll("[,.]", ""));
} catch (final NumberFormatException e) {
Log.e("getLoginStatus: bad cache count", e);
}
setActualCachesFound(cachesCount);
Settings.setMemberStatus(TextUtils.getMatch(page, GCConstants.PATTERN_MEMBER_STATUS, true, null));
if ( page.contains(GCConstants.MEMBER_STATUS_RENEW) ) {
Settings.setMemberStatus(GCConstants.MEMBER_STATUS_PM);
}
return true;
}
// login page
setActualLoginStatus(TextUtils.matches(page, GCConstants.PATTERN_LOGIN_NAME_LOGIN_PAGE));
if (isActualLoginStatus()) {
setActualUserName(Settings.getUsername());
// number of caches found is not part of this page
return true;
}
setActualStatus(cgeoapplication.getInstance().getString(R.string.init_login_popup_failed));
return false;
}
private static void switchToEnglish(String previousPage) {
if (previousPage != null && previousPage.contains(ENGLISH)) {
Log.i("Geocaching.com language already set to English");
// get find count
getLoginStatus(Network.getResponseData(Network.getRequest("http://www.geocaching.com/email/")));
} else {
final String page = Network.getResponseData(Network.getRequest(LANGUAGE_CHANGE_URI));
getLoginStatus(page);
if (page == null) {
Log.e("Failed to read viewstates to set geocaching.com language");
}
final Parameters params = new Parameters(
"__EVENTTARGET", "ctl00$uxLocaleList$uxLocaleList$ctl00$uxLocaleItem", // switch to english
"__EVENTARGUMENT", "");
Login.transferViewstates(page, params);
final HttpResponse response = Network.postRequest(LANGUAGE_CHANGE_URI, params, new Parameters("Referer", LANGUAGE_CHANGE_URI));
if (Network.isSuccess(response)) {
Log.i("changed language on geocaching.com to English");
} else {
Log.e("Failed to set geocaching.com language to English");
}
}
}
public static BitmapDrawable downloadAvatarAndGetMemberStatus() {
try {
final String profile = TextUtils.replaceWhitespace(Network.getResponseData(Network.getRequest("http://www.geocaching.com/my/")));
Settings.setMemberStatus(TextUtils.getMatch(profile, GCConstants.PATTERN_MEMBER_STATUS, true, null));
if (profile.contains(GCConstants.MEMBER_STATUS_RENEW)) {
Settings.setMemberStatus(GCConstants.MEMBER_STATUS_PM);
}
setActualCachesFound(Integer.parseInt(TextUtils.getMatch(profile, GCConstants.PATTERN_CACHES_FOUND, true, "-1").replaceAll("[,.]", "")));
final String avatarURL = TextUtils.getMatch(profile, GCConstants.PATTERN_AVATAR_IMAGE_PROFILE_PAGE, false, null);
if (null != avatarURL) {
final HtmlImage imgGetter = new HtmlImage("", false, 0, false);
return imgGetter.getDrawable(avatarURL);
}
// No match? There may be no avatar set by user.
Log.d("No avatar set for user");
} catch (final Exception e) {
Log.w("Error when retrieving user avatar", e);
}
return null;
}
/**
* Detect user date settings on geocaching.com
*/
public static void detectGcCustomDate() {
final String result = Network.getResponseData(Network.getRequest("http://www.geocaching.com/account/ManagePreferences.aspx"));
if (null == result) {
Log.w("Login.detectGcCustomDate: result is null");
return;
}
final String customDate = TextUtils.getMatch(result, GCConstants.PATTERN_CUSTOMDATE, true, null);
if (null != customDate) {
Settings.setGcCustomDate(customDate);
}
}
public static Date parseGcCustomDate(final String input, final String format) throws ParseException {
if (StringUtils.isBlank(input)) {
throw new ParseException("Input is null", 0);
}
final String trimmed = input.trim();
if (gcCustomDateFormats.containsKey(format)) {
try {
return gcCustomDateFormats.get(format).parse(trimmed);
} catch (final ParseException e) {
}
}
for (final SimpleDateFormat sdf : gcCustomDateFormats.values()) {
try {
return sdf.parse(trimmed);
} catch (final ParseException e) {
}
}
throw new ParseException("No matching pattern", 0);
}
public static Date parseGcCustomDate(final String input) throws ParseException {
return parseGcCustomDate(input, Settings.getGcCustomDate());
}
public static SimpleDateFormat getCustomGcDateFormat() {
final String format = Settings.getGcCustomDate();
if (gcCustomDateFormats.containsKey(format)) {
return gcCustomDateFormats.get(format);
}
return gcCustomDateFormats.get("MM/dd/yyyy");
}
/**
* checks if an Array of Strings is empty or not. Empty means:
* - Array is null
* - or all elements are null or empty strings
*/
public static boolean isEmpty(String[] a) {
if (a == null) {
return true;
}
for (final String s : a) {
if (StringUtils.isNotEmpty(s)) {
return false;
}
}
return true;
}
/**
* read all viewstates from page
*
* @return String[] with all view states
*/
public static String[] getViewstates(String page) {
// Get the number of viewstates.
// If there is only one viewstate, __VIEWSTATEFIELDCOUNT is not present
if (page == null) { // no network access
return null;
}
int count = 1;
final MatcherWrapper matcherViewstateCount = new MatcherWrapper(GCConstants.PATTERN_VIEWSTATEFIELDCOUNT, page);
if (matcherViewstateCount.find()) {
try {
count = Integer.parseInt(matcherViewstateCount.group(1));
} catch (final NumberFormatException e) {
Log.e("getViewStates", e);
}
}
final String[] viewstates = new String[count];
// Get the viewstates
final MatcherWrapper matcherViewstates = new MatcherWrapper(GCConstants.PATTERN_VIEWSTATES, page);
while (matcherViewstates.find()) {
final String sno = matcherViewstates.group(1); // number of viewstate
int no;
if (StringUtils.isEmpty(sno)) {
no = 0;
} else {
try {
no = Integer.parseInt(sno);
} catch (final NumberFormatException e) {
Log.e("getViewStates", e);
no = 0;
}
}
viewstates[no] = matcherViewstates.group(2);
}
if (viewstates.length != 1 || viewstates[0] != null) {
return viewstates;
}
// no viewstates were present
return null;
}
/**
* put viewstates into request parameters
*/
public static void putViewstates(final Parameters params, final String[] viewstates) {
if (ArrayUtils.isEmpty(viewstates)) {
return;
}
params.put("__VIEWSTATE", viewstates[0]);
if (viewstates.length > 1) {
for (int i = 1; i < viewstates.length; i++) {
params.put("__VIEWSTATE" + i, viewstates[i]);
}
params.put("__VIEWSTATEFIELDCOUNT", String.valueOf(viewstates.length));
}
}
/**
* transfers the viewstates variables from a page (response) to parameters
* (next request)
*/
public static void transferViewstates(final String page, final Parameters params) {
putViewstates(params, getViewstates(page));
}
/**
* POST HTTP request. Do the request a second time if the user is not logged in
*
* @param uri
* @return
*/
public static String postRequestLogged(final String uri, final Parameters params) {
final String data = Network.getResponseData(Network.postRequest(uri, params));
if (getLoginStatus(data)) {
return data;
}
if (login() == StatusCode.NO_ERROR) {
return Network.getResponseData(Network.postRequest(uri, params));
}
Log.i("Working as guest.");
return data;
}
/**
* GET HTTP request. Do the request a second time if the user is not logged in
*
* @param uri
* @param params
* @return
*/
public static String getRequestLogged(final String uri, final Parameters params) {
final String data = Network.getResponseData(Network.getRequest(uri, params), canRemoveWhitespace(uri));
if (getLoginStatus(data)) {
return data;
}
if (login() == StatusCode.NO_ERROR) {
return Network.getResponseData(Network.getRequest(uri, params), canRemoveWhitespace(uri));
}
Log.w("Working as guest.");
return data;
}
/**
* Unfortunately the cache details page contains user generated whitespace in the personal note, therefore we cannot
* remove the white space from cache details pages.
*
* @param uri
* @return
*/
private static boolean canRemoveWhitespace(final String uri) {
return !StringUtils.contains(uri, "cache_details");
}
/** Get user session & session token from the Live Map. Needed for following requests */
public static String[] getMapTokens() {
final HttpResponse response = Network.getRequest(GCConstants.URL_LIVE_MAP);
final String data = Network.getResponseData(response);
final String userSession = TextUtils.getMatch(data, GCConstants.PATTERN_USERSESSION, "");
final String sessionToken = TextUtils.getMatch(data, GCConstants.PATTERN_SESSIONTOKEN, "");
return new String[] { userSession, sessionToken };
}
}