Merge branch '(#696)-new-captcha-window' into (#696)-new-captcha-window-clover

# Conflicts:
#	Clover/app/build.gradle
#	Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
dev
k1rakishou 6 years ago
commit 1da9717ce9
  1. 3
      Clover/app/build.gradle
  2. 38
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  3. 8
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  4. 1
      Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java
  5. 18
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java
  6. 139
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaInfo.java
  7. 407
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java
  8. 327
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java
  9. 511
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java
  10. 139
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java
  11. 49
      Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java
  12. 51
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  13. 2
      Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
  14. 50
      Clover/app/src/main/java/org/floens/chan/ui/view/WrappingGridView.java
  15. 36
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  16. 7
      Clover/app/src/main/java/org/floens/chan/utils/BackgroundUtils.java
  17. 12
      Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml
  18. 40
      Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml
  19. 113
      Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml
  20. 17
      Clover/app/src/main/res/values/strings.xml
  21. 104
      Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java

@ -182,4 +182,7 @@ dependencies {
implementation 'me.xdrop:fuzzywuzzy:1.1.10' implementation 'me.xdrop:fuzzywuzzy:1.1.10'
implementation 'org.codejargon.feather:feather:1.0' implementation 'org.codejargon.feather:feather:1.0'
implementation 'io.sentry:sentry-android:1.7.24' implementation 'io.sentry:sentry-android:1.7.24'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.27.0'
} }

@ -37,6 +37,7 @@ import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse; import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
import org.floens.chan.ui.captcha.AuthenticationLayoutInterface; import org.floens.chan.ui.captcha.AuthenticationLayoutInterface;
import org.floens.chan.ui.captcha.v2.CaptchaNoJsLayoutV2;
import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -279,10 +280,22 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
public void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response) { public void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response) {
draft.captchaChallenge = challenge; draft.captchaChallenge = challenge;
draft.captchaResponse = response; draft.captchaResponse = response;
authenticationLayout.reset();
// we don't need this to be called for new captcha window.
// Otherwise "Request captcha request is already in progress" message will be shown
if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) {
// should this be called here?
authenticationLayout.reset();
}
makeSubmitCall(); makeSubmitCall();
} }
@Override
public void onFallbackToV1CaptchaView() {
callback.onFallbackToV1CaptchaView();
}
public void onCommentTextChanged(CharSequence text) { public void onCommentTextChanged(CharSequence text) {
int length = text.toString().getBytes(UTF_8).length; int length = text.toString().getBytes(UTF_8).length;
callback.updateCommentCount(length, board.maxCommentChars, length > board.maxCommentChars); callback.updateCommentCount(length, board.maxCommentChars, length > board.maxCommentChars);
@ -389,6 +402,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
callback.openSpoiler(false, false); callback.openSpoiler(false, false);
callback.openPreview(false, null); callback.openPreview(false, null);
callback.openPreviewMessage(false, null); callback.openPreviewMessage(false, null);
callback.destroyCurrentAuthentication();
} }
private void makeSubmitCall() { private void makeSubmitCall() {
@ -396,8 +410,12 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
switchPage(Page.LOADING, true); switchPage(Page.LOADING, true);
} }
private void switchPage(Page page, boolean animate) { public void switchPage(Page page, boolean animate) {
if (this.page != page) { switchPage(page, animate, ChanSettings.useNewCaptchaWindow.get());
}
public void switchPage(Page page, boolean animate, boolean useV2NoJsCaptcha) {
if (!useV2NoJsCaptcha || this.page != page) {
this.page = page; this.page = page;
switch (page) { switch (page) {
case LOADING: case LOADING:
@ -409,7 +427,9 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
case AUTHENTICATION: case AUTHENTICATION:
SiteAuthentication authentication = loadable.site.actions().postAuthenticate(); SiteAuthentication authentication = loadable.site.actions().postAuthenticate();
callback.initializeAuthentication(loadable.site, authentication, this); // cleanup resources tied to the new captcha layout/presenter
callback.destroyCurrentAuthentication();
callback.initializeAuthentication(loadable.site, authentication, this, useV2NoJsCaptcha);
callback.setPage(Page.AUTHENTICATION, true); callback.setPage(Page.AUTHENTICATION, true);
break; break;
@ -470,8 +490,10 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
void setPage(Page page, boolean animate); void setPage(Page page, boolean animate);
void initializeAuthentication(Site site, SiteAuthentication authentication, void initializeAuthentication(Site site,
AuthenticationLayoutCallback callback); SiteAuthentication authentication,
AuthenticationLayoutCallback callback,
boolean useV2NoJsCaptcha);
void resetAuthentication(); void resetAuthentication();
@ -518,5 +540,9 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
ChanThread getThread(); ChanThread getThread();
void focusComment(); void focusComment();
void onFallbackToV1CaptchaView();
void destroyCurrentAuthentication();
} }
} }

@ -160,6 +160,10 @@ public class ChanSettings {
public static final LongSetting updateCheckInterval; public static final LongSetting updateCheckInterval;
public static final BooleanSetting crashReporting; public static final BooleanSetting crashReporting;
public static final BooleanSetting useNewCaptchaWindow;
public static final BooleanSetting useRealGoogleCookies;
public static final StringSetting googleCookie;
public static final LongSetting lastGoogleCookieUpdateTime;
static { static {
SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences());
@ -250,6 +254,10 @@ public class ChanSettings {
updateCheckInterval = new LongSetting(p, "update_check_interval", UpdateManager.DEFAULT_UPDATE_CHECK_INTERVAL_MS); updateCheckInterval = new LongSetting(p, "update_check_interval", UpdateManager.DEFAULT_UPDATE_CHECK_INTERVAL_MS);
crashReporting = new BooleanSetting(p, "preference_crash_reporting", true); crashReporting = new BooleanSetting(p, "preference_crash_reporting", true);
useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true);
useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false);
googleCookie = new StringSetting(p, "google_cookie", "");
lastGoogleCookieUpdateTime = new LongSetting(p, "last_google_cookie_update_time", 0L);
// Old (but possibly still in some users phone) // Old (but possibly still in some users phone)
// preference_board_view_mode default "list" // preference_board_view_mode default "list"

@ -20,4 +20,5 @@ package org.floens.chan.ui.captcha;
public interface AuthenticationLayoutCallback { public interface AuthenticationLayoutCallback {
void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout,
String challenge, String response); String challenge, String response);
void onFallbackToV1CaptchaView();
} }

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.floens.chan.ui.captcha; package org.floens.chan.ui.captcha.v1;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
@ -32,6 +32,8 @@ import android.webkit.WebViewClient;
import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
import org.floens.chan.ui.captcha.AuthenticationLayoutInterface;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -48,7 +50,7 @@ import okhttp3.ResponseBody;
* It directly loads the captcha2 fallback url into a webview, and on each requests it executes * It directly loads the captcha2 fallback url into a webview, and on each requests it executes
* some javascript that will tell the callback if the token is there. * some javascript that will tell the callback if the token is there.
*/ */
public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutInterface { public class CaptchaNojsLayoutV1 extends WebView implements AuthenticationLayoutInterface {
private static final String TAG = "CaptchaNojsLayout"; private static final String TAG = "CaptchaNojsLayout";
private AuthenticationLayoutCallback callback; private AuthenticationLayoutCallback callback;
@ -59,15 +61,15 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn
private String webviewUserAgent; private String webviewUserAgent;
public CaptchaNojsLayout(Context context) { public CaptchaNojsLayoutV1(Context context) {
super(context); super(context);
} }
public CaptchaNojsLayout(Context context, AttributeSet attrs) { public CaptchaNojsLayoutV1(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
} }
public CaptchaNojsLayout(Context context, AttributeSet attrs, int defStyle) { public CaptchaNojsLayoutV1(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
} }
@ -114,7 +116,7 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn
return false; return false;
} }
if (host.equals(Uri.parse(CaptchaNojsLayout.this.baseUrl).getHost())) { if (host.equals(Uri.parse(CaptchaNojsLayoutV1.this.baseUrl).getHost())) {
return false; return false;
} else { } else {
AndroidUtils.openLink(url); AndroidUtils.openLink(url);
@ -172,9 +174,9 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn
} }
public static class CaptchaInterface { public static class CaptchaInterface {
private final CaptchaNojsLayout layout; private final CaptchaNojsLayoutV1 layout;
public CaptchaInterface(CaptchaNojsLayout layout) { public CaptchaInterface(CaptchaNojsLayoutV1 layout) {
this.layout = layout; this.layout = layout;
} }

@ -0,0 +1,139 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.captcha.v2;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class CaptchaInfo {
CaptchaType captchaType;
@NonNull
List<Integer> checkboxes;
@Nullable
String cParameter;
@NonNull
List<Bitmap> challengeImages;
@Nullable
CaptchaTitle captchaTitle;
public CaptchaInfo() {
captchaType = CaptchaType.Unknown;
checkboxes = new ArrayList<>();
cParameter = null;
challengeImages = null;
captchaTitle = null;
}
public void setCaptchaType(CaptchaType captchaType) {
this.captchaType = captchaType;
}
public void setCheckboxes(@NonNull List<Integer> checkboxes) {
this.checkboxes = checkboxes;
}
public void setcParameter(@Nullable String cParameter) {
this.cParameter = cParameter;
}
public void setChallengeImages(@NonNull List<Bitmap> challengeImages) {
this.challengeImages = challengeImages;
}
public void setCaptchaTitle(@Nullable CaptchaTitle captchaTitle) {
this.captchaTitle = captchaTitle;
}
public CaptchaType getCaptchaType() {
return captchaType;
}
@NonNull
public List<Bitmap> getChallengeImages() {
return challengeImages;
}
@NonNull
public List<Integer> getCheckboxes() {
return checkboxes;
}
@Nullable
public String getcParameter() {
return cParameter;
}
@Nullable
public CaptchaTitle getCaptchaTitle() {
return captchaTitle;
}
public enum CaptchaType {
Unknown,
// 3x3
Canonical,
// 2x4
NoCanonical;
public static CaptchaType fromCheckboxesCount(int count) {
if (count == 8) {
return NoCanonical;
} else if (count == 9) {
return Canonical;
}
return Unknown;
}
}
public static class CaptchaTitle {
private String title;
private int boldStart = -1;
private int boldEnd = -1;
public CaptchaTitle(String title, int boldStart, int boldEnd) {
this.title = title;
this.boldStart = boldStart;
this.boldEnd = boldEnd;
}
public boolean isEmpty() {
return title.isEmpty() && boldStart == -1 && boldEnd == -1;
}
public boolean hasBold() {
return boldStart != -1 && boldEnd != -1;
}
public String getTitle() {
return title;
}
public int getBoldStart() {
return boldStart;
}
public int getBoldEnd() {
return boldEnd;
}
}
}

@ -0,0 +1,407 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.captcha.v2;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.NonNull;
import android.support.v4.util.Pair;
import org.floens.chan.utils.BackgroundUtils;
import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class CaptchaNoJsHtmlParser {
private static final String TAG = "CaptchaNoJsHtmlParser";
private static final String googleBaseUrl = "https://www.google.com";
private static final Pattern checkboxesPattern =
Pattern.compile("<input class=\"fbc-imageselect-checkbox-\\d+\" type=\"checkbox\" name=\"response\" value=\"(\\d+)\">");
// FIXME: this pattern captures the C parameter as many times as it is in the HTML.
// Should match only the first occurrence instead.
private static final Pattern cParameterPattern =
Pattern.compile("<input type=\"hidden\" name=\"c\" value=\"(.*?)\"/>");
private static final Pattern challengeTitlePattern =
Pattern.compile("<div class=\"(rc-imageselect-desc-no-canonical|rc-imageselect-desc)\">(.*?)</div>");
private static final Pattern challengeImageUrlPattern =
Pattern.compile("<img class=\"fbc-imageselect-payload\" src=\"(.*?)&");
private static final Pattern challengeTitleBoldPartPattern =
Pattern.compile("<strong>(.*?)</strong>");
private static final Pattern verificationTokenPattern =
Pattern.compile("<div class=\"fbc-verification-token\"><textarea dir=\"ltr\" readonly>(.*?)</textarea></div>");
private static final String CHALLENGE_IMAGE_FILE_NAME = "challenge_image_file";
private static final int SUCCESS_STATUS_CODE = 200;
private OkHttpClient okHttpClient;
private Context context;
public CaptchaNoJsHtmlParser(Context context, OkHttpClient okHttpClient) {
this.context = context;
this.okHttpClient = okHttpClient;
}
@NonNull
public CaptchaInfo parseHtml(String responseHtml, String siteKey)
throws CaptchaNoJsV2ParsingError, IOException {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread!");
}
CaptchaInfo captchaInfo = new CaptchaInfo();
// parse challenge checkboxes' ids
parseCheckboxes(responseHtml, captchaInfo);
// parse captcha random key
parseCParameter(responseHtml, captchaInfo);
// parse title
parseChallengeTitle(responseHtml, captchaInfo);
// parse image url, download image and split it into list of separate images
parseAndDownloadChallengeImage(responseHtml, captchaInfo, siteKey);
return captchaInfo;
}
@NonNull
String parseVerificationToken(String responseHtml) throws CaptchaNoJsV2ParsingError {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread!");
}
Matcher matcher = verificationTokenPattern.matcher(responseHtml);
if (!matcher.find()) {
throw new CaptchaNoJsV2ParsingError("Could not parse verification token");
}
String token;
try {
token = matcher.group(1);
} catch (Throwable error) {
Logger.e(TAG, "Could not parse verification token", error);
throw error;
}
if (token == null || token.isEmpty()) {
throw new CaptchaNoJsV2ParsingError("Verification token is null or empty");
}
return token;
}
public void parseChallengeTitle(
String responseHtml,
CaptchaInfo captchaInfo
) throws CaptchaNoJsV2ParsingError {
Matcher matcher = challengeTitlePattern.matcher(responseHtml);
if (!matcher.find()) {
throw new CaptchaNoJsV2ParsingError("Could not parse challenge title " + responseHtml);
}
CaptchaInfo.CaptchaTitle captchaTitle;
try {
String title = matcher.group(2);
Matcher titleMatcher = challengeTitleBoldPartPattern.matcher(title);
if (titleMatcher.find()) {
// find the part of the title that should be bold
int start = title.indexOf("<strong>");
String firstPart = title.substring(0, start);
String boldPart = titleMatcher.group(1);
String resultTitle = firstPart + boldPart;
captchaTitle = new CaptchaInfo.CaptchaTitle(
resultTitle,
firstPart.length(),
firstPart.length() + boldPart.length());
} else {
// could not find it
captchaTitle = new CaptchaInfo.CaptchaTitle(title, -1, -1);
}
} catch (Throwable error) {
Logger.e(TAG, "Error while trying to parse challenge title", error);
throw error;
}
if (captchaTitle == null) {
throw new CaptchaNoJsV2ParsingError("challengeTitle is null");
}
if (captchaTitle.isEmpty()) {
throw new CaptchaNoJsV2ParsingError("challengeTitle is empty");
}
captchaInfo.setCaptchaTitle(captchaTitle);
}
private void parseAndDownloadChallengeImage(
String responseHtml,
CaptchaInfo captchaInfo,
String siteKey
) throws CaptchaNoJsV2ParsingError, IOException {
Matcher matcher = challengeImageUrlPattern.matcher(responseHtml);
if (!matcher.find()) {
throw new CaptchaNoJsV2ParsingError("Could not parse challenge image url");
}
String challengeImageUrl;
try {
challengeImageUrl = matcher.group(1);
} catch (Throwable error) {
Logger.e(TAG, "Error while trying to parse challenge image url", error);
throw error;
}
if (challengeImageUrl == null) {
throw new CaptchaNoJsV2ParsingError("challengeImageUrl is null");
}
if (challengeImageUrl.isEmpty()) {
throw new CaptchaNoJsV2ParsingError("challengeImageUrl is empty");
}
String fullUrl = googleBaseUrl + challengeImageUrl + "&k=" + siteKey;
File challengeImageFile = downloadAndStoreImage(fullUrl);
Pair<Integer, Integer> columnsAndRows = calculateColumnsAndRows(
captchaInfo.captchaType
);
Integer columns = columnsAndRows.first;
Integer rows = columnsAndRows.second;
if (columns == null) {
throw new CaptchaNoJsV2ParsingError("Could not calculate columns count");
}
if (rows == null) {
throw new CaptchaNoJsV2ParsingError("Could not calculate rows count");
}
List<Bitmap> challengeImages = decodeImagesFromFile(challengeImageFile, columns, rows);
captchaInfo.setChallengeImages(challengeImages);
}
@NonNull
private Pair<Integer, Integer> calculateColumnsAndRows(
CaptchaInfo.CaptchaType captchaType
) throws CaptchaNoJsV2ParsingError {
switch (captchaType) {
case Canonical: {
// 3x3 captcha with square images
return new Pair<>(3, 3);
}
case NoCanonical: {
// 2x4 captcha with rectangle images (store fronts)
return new Pair<>(2, 4);
}
default: {
throw new CaptchaNoJsV2ParsingError("Unknown captcha type");
}
}
}
public void parseCParameter(
String responseHtml,
CaptchaInfo captchaInfo
) throws CaptchaNoJsV2ParsingError {
Matcher matcher = cParameterPattern.matcher(responseHtml);
if (!matcher.find()) {
throw new CaptchaNoJsV2ParsingError("Could not parse c parameter");
}
String cParameter;
try {
cParameter = matcher.group(1);
} catch (Throwable error) {
Logger.e(TAG, "Error while trying to parse c parameter", error);
throw error;
}
if (cParameter == null) {
throw new CaptchaNoJsV2ParsingError("cParameter is null");
}
if (cParameter.isEmpty()) {
throw new CaptchaNoJsV2ParsingError("cParameter is empty");
}
captchaInfo.setcParameter(cParameter);
}
public void parseCheckboxes(
String responseHtml,
CaptchaInfo captchaInfo
) throws CaptchaNoJsV2ParsingError {
Matcher matcher = checkboxesPattern.matcher(responseHtml);
Set<Integer> checkboxesSet = new HashSet<>(matcher.groupCount());
int index = 0;
while (matcher.find()) {
try {
Integer checkboxId = Integer.parseInt(matcher.group(1));
checkboxesSet.add(checkboxId);
} catch (Throwable error) {
Logger.e(TAG, "Error while trying to parse checkbox with id (" + index + ")", error);
throw error;
}
++index;
}
if (checkboxesSet.isEmpty()) {
throw new CaptchaNoJsV2ParsingError("Could not parse any checkboxes!");
}
CaptchaInfo.CaptchaType captchaType;
try {
captchaType = CaptchaInfo.CaptchaType.fromCheckboxesCount(checkboxesSet.size());
} catch (Throwable error) {
Logger.e(TAG, "Error while trying to parse captcha type", error);
throw error;
}
if (captchaType == CaptchaInfo.CaptchaType.Unknown) {
throw new CaptchaNoJsV2ParsingError("Unknown captcha type");
}
captchaInfo.setCaptchaType(captchaType);
captchaInfo.setCheckboxes(new ArrayList<>(checkboxesSet));
}
@NonNull
private File downloadAndStoreImage(String fullUrl) throws IOException, CaptchaNoJsV2ParsingError {
Request request = new Request.Builder()
.url(fullUrl)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (response.code() != SUCCESS_STATUS_CODE) {
throw new CaptchaNoJsV2ParsingError(
"Could not download challenge image, status code = " + response.code());
}
if (response.body() == null) {
throw new CaptchaNoJsV2ParsingError("Captcha challenge image body is null");
}
try (InputStream is = response.body().byteStream()) {
File imageFile = getChallengeImageFile();
try (OutputStream os = new FileOutputStream(imageFile)) {
IOUtils.copy(is, os);
}
return imageFile;
}
}
}
private File getChallengeImageFile() throws CaptchaNoJsV2ParsingError, IOException {
File imageFile = new File(context.getCacheDir(), CHALLENGE_IMAGE_FILE_NAME);
if (imageFile.exists()) {
if (!imageFile.delete()) {
throw new CaptchaNoJsV2ParsingError(
"Could not delete old challenge image file \"" + imageFile.getAbsolutePath() + "\"");
}
}
if (!imageFile.createNewFile()) {
throw new CaptchaNoJsV2ParsingError(
"Could not create challenge image file \"" + imageFile.getAbsolutePath() + "\"");
}
if (!imageFile.canWrite()) {
throw new CaptchaNoJsV2ParsingError(
"Cannot write to the challenge image file \"" + imageFile.getAbsolutePath() + "\"");
}
return imageFile;
}
private List<Bitmap> decodeImagesFromFile(File challengeImageFile, int columns, int rows) {
Bitmap originalBitmap = BitmapFactory.decodeFile(challengeImageFile.getAbsolutePath());
List<Bitmap> resultImages = new ArrayList<>(columns * rows);
try {
int imageWidth = originalBitmap.getWidth() / columns;
int imageHeight = originalBitmap.getHeight() / rows;
for (int column = 0; column < columns; ++column) {
for (int row = 0; row < rows; ++row) {
Bitmap imagePiece = Bitmap.createBitmap(
originalBitmap,
column * imageWidth,
row * imageHeight,
imageWidth,
imageHeight);
resultImages.add(imagePiece);
}
}
return resultImages;
} catch (Throwable error) {
for (Bitmap bitmap : resultImages) {
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
}
resultImages.clear();
throw error;
} finally {
if (originalBitmap != null) {
originalBitmap.recycle();
}
}
}
public static class CaptchaNoJsV2ParsingError extends Exception {
public CaptchaNoJsV2ParsingError(String message) {
super(message);
}
}
}

@ -0,0 +1,327 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.captcha.v2;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Typeface;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.support.v7.widget.AppCompatButton;
import android.support.v7.widget.AppCompatTextView;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.ScrollView;
import android.widget.Toast;
import org.floens.chan.R;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
import org.floens.chan.ui.captcha.AuthenticationLayoutInterface;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger;
import java.util.List;
public class CaptchaNoJsLayoutV2 extends FrameLayout
implements AuthenticationLayoutInterface,
CaptchaNoJsPresenterV2.AuthenticationCallbacks, View.OnClickListener {
private static final String TAG = "CaptchaNoJsLayoutV2";
private AppCompatTextView captchaChallengeTitle;
private GridView captchaImagesGrid;
private AppCompatButton captchaVerifyButton;
private AppCompatButton useOldCaptchaButton;
private AppCompatButton reloadCaptchaButton;
private AppCompatButton refreshCookiesButton;
private ConstraintLayout buttonsHolder;
private ScrollView background;
private CaptchaNoJsV2Adapter adapter;
private CaptchaNoJsPresenterV2 presenter;
private Context context;
private AuthenticationLayoutCallback callback;
public CaptchaNoJsLayoutV2(@NonNull Context context) {
super(context);
init(context);
}
public CaptchaNoJsLayoutV2(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CaptchaNoJsLayoutV2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
this.context = context;
this.presenter = new CaptchaNoJsPresenterV2(this, context);
this.adapter = new CaptchaNoJsV2Adapter(context);
View view = inflate(context, R.layout.layout_captcha_nojs_v2, this);
captchaChallengeTitle = view.findViewById(R.id.captcha_layout_v2_title);
captchaImagesGrid = view.findViewById(R.id.captcha_layout_v2_images_grid);
captchaVerifyButton = view.findViewById(R.id.captcha_layout_v2_verify_button);
useOldCaptchaButton = view.findViewById(R.id.captcha_layout_v2_use_old_captcha_button);
reloadCaptchaButton = view.findViewById(R.id.captcha_layout_v2_reload_button);
refreshCookiesButton = view.findViewById(R.id.captcha_layout_v2_refresh_cookies);
buttonsHolder = view.findViewById(R.id.captcha_layout_v2_buttons_holder);
background = view.findViewById(R.id.captcha_layout_v2_background);
background.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor));
buttonsHolder.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor_secondary));
captchaChallengeTitle.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor_secondary));
captchaChallengeTitle.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
captchaVerifyButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
useOldCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
reloadCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
refreshCookiesButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary));
captchaVerifyButton.setOnClickListener(this);
useOldCaptchaButton.setOnClickListener(this);
reloadCaptchaButton.setOnClickListener(this);
if (ChanSettings.useRealGoogleCookies.get()) {
refreshCookiesButton.setVisibility(View.VISIBLE);
refreshCookiesButton.setOnClickListener(this);
}
captchaVerifyButton.setEnabled(false);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
reset();
}
@Override
public void initialize(Site site, AuthenticationLayoutCallback callback) {
this.callback = callback;
SiteAuthentication authentication = site.actions().postAuthenticate();
if (authentication.type != SiteAuthentication.Type.CAPTCHA2_NOJS) {
callback.onFallbackToV1CaptchaView();
return;
}
presenter.init(authentication.siteKey, authentication.baseUrl);
}
@Override
public void reset() {
hardReset();
}
@Override
public void hardReset() {
CaptchaNoJsPresenterV2.RequestCaptchaInfoError captchaInfoError = presenter.requestCaptchaInfo();
switch (captchaInfoError) {
case Ok:
break;
case HoldYourHorses:
showToast(getContext().getString(
R.string.captcha_layout_v2_you_are_requesting_captcha_too_fast));
break;
case AlreadyInProgress:
showToast(getContext().getString(
R.string.captcha_layout_v2_captcha_request_is_already_in_progress));
break;
case AlreadyShutdown:
// do nothing
break;
}
}
@Override
public void onCaptchaInfoParsed(CaptchaInfo captchaInfo) {
// called on a background thread
AndroidUtils.runOnUiThread(() -> {
captchaVerifyButton.setEnabled(true);
renderCaptchaWindow(captchaInfo);
});
}
@Override
public void onVerificationDone(String verificationToken) {
// called on a background thread
AndroidUtils.runOnUiThread(() -> {
captchaVerifyButton.setEnabled(true);
callback.onAuthenticationComplete(this, null, verificationToken);
});
}
@Override
public void onGoogleCookiesRefreshed() {
// called on a background thread
AndroidUtils.runOnUiThread(() -> {
showToast("Google cookies successfully refreshed");
// refresh the captcha as well
reset();
});
}
// Called when we could not get google cookies
@Override
public void onGetGoogleCookieError(boolean shouldFallback, Throwable error) {
// called on a background thread
handleError(shouldFallback, error);
}
// Called when we got response from re-captcha but could not parse some part of it
@Override
public void onCaptchaInfoParseError(Throwable error) {
// called on a background thread
handleError(true, error);
}
private void handleError(boolean shouldFallback, Throwable error) {
AndroidUtils.runOnUiThread(() -> {
Logger.e(TAG, "CaptchaV2 error", error);
String message = error.getMessage();
showToast(message);
captchaVerifyButton.setEnabled(true);
if (shouldFallback) {
callback.onFallbackToV1CaptchaView();
}
});
}
private void showToast(String message) {
AndroidUtils.runOnUiThread(() -> {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
});
}
@Override
public void onClick(View v) {
if (v == captchaVerifyButton) {
sendVerificationResponse();
} else if (v == useOldCaptchaButton) {
callback.onFallbackToV1CaptchaView();
} else if (v == reloadCaptchaButton) {
reset();
} else if (v == refreshCookiesButton) {
presenter.refreshCookies();
}
}
private void renderCaptchaWindow(CaptchaInfo captchaInfo) {
try {
setCaptchaTitle(captchaInfo);
captchaImagesGrid.setAdapter(null);
captchaImagesGrid.setAdapter(adapter);
int columnsCount = 0;
int imageSize = captchaImagesGrid.getWidth();
switch (captchaInfo.getCaptchaType()) {
case Canonical:
columnsCount = 3;
imageSize /= columnsCount;
break;
case NoCanonical:
columnsCount = 2;
imageSize /= columnsCount;
break;
default:
throw new IllegalStateException("Unknown captcha type");
}
captchaImagesGrid.setNumColumns(columnsCount);
adapter.setImageSize(imageSize);
adapter.setImages(captchaInfo.challengeImages);
captchaVerifyButton.setEnabled(true);
} catch (Throwable error) {
if (callback != null) {
callback.onFallbackToV1CaptchaView();
}
}
}
private void setCaptchaTitle(CaptchaInfo captchaInfo) {
if (captchaInfo.getCaptchaTitle().hasBold()) {
SpannableString spannableString = new SpannableString(captchaInfo.getCaptchaTitle().getTitle());
spannableString.setSpan(
new StyleSpan(Typeface.BOLD),
captchaInfo.getCaptchaTitle().getBoldStart(),
captchaInfo.getCaptchaTitle().getBoldEnd(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
captchaChallengeTitle.setText(spannableString);
} else {
captchaChallengeTitle.setText(captchaInfo.getCaptchaTitle().getTitle());
}
}
private void sendVerificationResponse() {
List<Integer> selectedIds = adapter.getCheckedImageIds();
try {
CaptchaNoJsPresenterV2.VerifyError verifyError = presenter.verify(selectedIds);
switch (verifyError) {
case Ok:
captchaVerifyButton.setEnabled(false);
break;
case NoImagesSelected:
showToast(getContext().getString(R.string.captcha_layout_v2_you_have_to_select_at_least_one_image));
break;
case AlreadyInProgress:
showToast(getContext().getString(R.string.captcha_layout_v2_verification_already_in_progress));
break;
case AlreadyShutdown:
// do nothing
break;
}
} catch (Throwable error) {
onCaptchaInfoParseError(error);
}
}
public void onDestroy() {
adapter.onDestroy();
presenter.onDestroy();
}
}

@ -0,0 +1,511 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.captcha.v2;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.utils.BackgroundUtils;
import org.floens.chan.utils.Logger;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class CaptchaNoJsPresenterV2 {
private static final String TAG = "CaptchaNoJsPresenterV2";
private static final String userAgentHeader = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36";
private static final String acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3";
private static final String acceptEncodingHeader = "deflate, br";
private static final String acceptLanguageHeader = "en-US";
private static final String recaptchaUrlBase = "https://www.google.com/recaptcha/api/fallback?k=";
private static final String googleBaseUrl = "https://www.google.com/";
private static final String encoding = "UTF-8";
private static final String mediaType = "application/x-www-form-urlencoded";
private static final String recaptchaChallengeString = "reCAPTCHA challenge";
private static final String verificationTokenString = "fbc-verification-token";
private static final String setCookieHeaderName = "set-cookie";
private static final int SUCCESS_STATUS_CODE = 200;
private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L;
private static final long THREE_MONTHS = TimeUnit.DAYS.toMillis(90);
// this cookie is taken from dashchan
private static final String defaultGoogleCookies = "NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf";
// TODO: inject this in the future when https://github.com/Floens/Clover/pull/678 is merged
private final OkHttpClient okHttpClient = new OkHttpClient();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final CaptchaNoJsHtmlParser parser;
@Nullable
private AuthenticationCallbacks callbacks;
@Nullable
private CaptchaInfo prevCaptchaInfo = null;
@NonNull
// either the default cookie or a real cookie
private volatile String googleCookie;
private AtomicBoolean verificationInProgress = new AtomicBoolean(false);
private AtomicBoolean captchaRequestInProgress = new AtomicBoolean(false);
private AtomicBoolean refreshCookiesRequestInProgress = new AtomicBoolean(false);
private String siteKey;
private String baseUrl;
private long lastTimeCaptchaRequest = 0L;
public CaptchaNoJsPresenterV2(@Nullable AuthenticationCallbacks callbacks, Context context) {
this.callbacks = callbacks;
this.parser = new CaptchaNoJsHtmlParser(context, okHttpClient);
this.googleCookie = ChanSettings.googleCookie.get();
}
public void init(String siteKey, String baseUrl) {
this.siteKey = siteKey;
this.baseUrl = baseUrl;
}
/**
* Send challenge solution back to the recaptcha
*/
public VerifyError verify(
List<Integer> selectedIds
) throws CaptchaNoJsV2Error {
if (!verificationInProgress.compareAndSet(false, true)) {
Logger.d(TAG, "Verify captcha request is already in progress");
return VerifyError.AlreadyInProgress;
}
if (executor.isShutdown()) {
verificationInProgress.set(false);
Logger.d(TAG, "Cannot verify, executor has been shut down");
return VerifyError.AlreadyShutdown;
}
try {
if (selectedIds.isEmpty()) {
verificationInProgress.set(false);
return VerifyError.NoImagesSelected;
}
if (prevCaptchaInfo == null) {
throw new CaptchaNoJsV2Error("prevCaptchaInfo is null");
}
if (prevCaptchaInfo.getcParameter() == null) {
throw new CaptchaNoJsV2Error("C parameter is null");
}
if (googleCookie.isEmpty()) {
throw new IllegalStateException("Google cookies are not supposed to be empty here");
}
executor.submit(() -> {
try {
String recaptchaUrl = recaptchaUrlBase + siteKey;
RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds);
Logger.d(TAG, "Verify called. Current cookie = " + googleCookie);
Request request = new Request.Builder()
.url(recaptchaUrl)
.post(body)
.header("Referer", recaptchaUrl)
.header("User-Agent", userAgentHeader)
.header("Accept", acceptHeader)
.header("Accept-Encoding", acceptEncodingHeader)
.header("Accept-Language", acceptLanguageHeader)
.header("Cookie", googleCookie)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
prevCaptchaInfo = handleGetRecaptchaResponse(response);
} finally {
verificationInProgress.set(false);
}
} catch (Throwable error) {
if (callbacks != null) {
try {
prevCaptchaInfo = null;
callbacks.onCaptchaInfoParseError(error);
} finally {
verificationInProgress.set(false);
}
}
}
});
return VerifyError.Ok;
} catch (Throwable error) {
verificationInProgress.set(false);
throw error;
}
}
/**
* Manually refreshes the google cookie
* */
public void refreshCookies() {
if (!refreshCookiesRequestInProgress.compareAndSet(false, true)) {
Logger.d(TAG, "Google cookie request is already in progress");
return;
}
if (executor.isShutdown()) {
refreshCookiesRequestInProgress.set(false);
Logger.d(TAG, "Cannot request google cookie, executor has been shut down");
return;
}
executor.submit(() -> {
try {
googleCookie = getGoogleCookies(true);
if (callbacks != null) {
callbacks.onGoogleCookiesRefreshed();
}
} catch (IOException e) {
if (callbacks != null) {
callbacks.onGetGoogleCookieError(false, e);
}
} finally {
refreshCookiesRequestInProgress.set(false);
}
});
}
/**
* Requests captcha data, parses it and then passes it to the render function
*/
public RequestCaptchaInfoError requestCaptchaInfo() {
if (!captchaRequestInProgress.compareAndSet(false, true)) {
Logger.d(TAG, "Request captcha request is already in progress");
return RequestCaptchaInfoError.AlreadyInProgress;
}
try {
// recaptcha may become very angry at you if your are fetching it too fast
if (System.currentTimeMillis() - lastTimeCaptchaRequest < CAPTCHA_REQUEST_THROTTLE_MS) {
captchaRequestInProgress.set(false);
Logger.d(TAG, "Requesting captcha info too fast");
return RequestCaptchaInfoError.HoldYourHorses;
}
if (executor.isShutdown()) {
captchaRequestInProgress.set(false);
Logger.d(TAG, "Cannot request captcha info, executor has been shut down");
return RequestCaptchaInfoError.AlreadyShutdown;
}
lastTimeCaptchaRequest = System.currentTimeMillis();
executor.submit(() -> {
try {
try {
googleCookie = getGoogleCookies(false);
} catch (Throwable error) {
if (callbacks != null) {
callbacks.onGetGoogleCookieError(true, error);
}
throw error;
}
try {
prevCaptchaInfo = getCaptchaInfo();
} catch (Throwable error) {
if (callbacks != null) {
callbacks.onCaptchaInfoParseError(error);
}
throw error;
}
} catch (Throwable error) {
Logger.e(TAG, "Error while executing captcha requests", error);
prevCaptchaInfo = null;
googleCookie = defaultGoogleCookies;
} finally {
captchaRequestInProgress.set(false);
}
});
return RequestCaptchaInfoError.Ok;
} catch (Throwable error) {
captchaRequestInProgress.set(false);
if (callbacks != null) {
callbacks.onCaptchaInfoParseError(error);
}
// return ok here too because we already handled this exception in the callback
return RequestCaptchaInfoError.Ok;
}
}
@NonNull
private String getGoogleCookies(boolean forced) throws IOException {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread");
}
if (!ChanSettings.useRealGoogleCookies.get()) {
Logger.d(TAG, "Google cookies request is disabled in the settings, using the default ones");
return defaultGoogleCookies;
}
boolean isItTimeToUpdateCookies =
((System.currentTimeMillis() - ChanSettings.lastGoogleCookieUpdateTime.get()) > THREE_MONTHS);
if (!forced && (!googleCookie.isEmpty() && !isItTimeToUpdateCookies)) {
Logger.d(TAG, "We already have google cookies");
return googleCookie;
}
Logger.d(TAG, "Time to update cookies: forced = " + forced + ", isCookieEmpty = " +
googleCookie.isEmpty() + ", last cookie expired = " + isItTimeToUpdateCookies);
Request request = new Request.Builder()
.url(googleBaseUrl)
.header("User-Agent", userAgentHeader)
.header("Accept", acceptHeader)
.header("Accept-Encoding", acceptEncodingHeader)
.header("Accept-Language", acceptLanguageHeader)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
String newCookie = handleGetGoogleCookiesResponse(response);
if (!newCookie.equalsIgnoreCase(defaultGoogleCookies)) {
ChanSettings.googleCookie.set(newCookie);
ChanSettings.lastGoogleCookieUpdateTime.set(System.currentTimeMillis());
Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie);
} else {
Logger.d(TAG, "Could not successfully handle google cookie response, " +
"using the default google cookies until the next request");
}
return newCookie;
}
}
@Nullable
private CaptchaInfo getCaptchaInfo() throws IOException {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread");
}
if (googleCookie.isEmpty()) {
throw new IllegalStateException("Google cookies are not supposed to be null here");
}
String recaptchaUrl = recaptchaUrlBase + siteKey;
Request request = new Request.Builder()
.url(recaptchaUrl)
.header("Referer", baseUrl)
.header("User-Agent", userAgentHeader)
.header("Accept", acceptHeader)
.header("Accept-Encoding", acceptEncodingHeader)
.header("Accept-Language", acceptLanguageHeader)
.header("Cookie", googleCookie)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
return handleGetRecaptchaResponse(response);
}
}
private RequestBody createResponseBody(
CaptchaInfo prevCaptchaInfo,
List<Integer> selectedIds
) throws UnsupportedEncodingException {
StringBuilder sb = new StringBuilder();
sb.append(URLEncoder.encode("c", encoding));
sb.append("=");
sb.append(URLEncoder.encode(prevCaptchaInfo.getcParameter(), encoding));
sb.append("&");
for (Integer selectedImageId : selectedIds) {
sb.append(URLEncoder.encode("response", encoding));
sb.append("=");
sb.append(URLEncoder.encode(String.valueOf(selectedImageId), encoding));
sb.append("&");
}
String resultBody;
if (selectedIds.size() > 0) {
resultBody = sb.deleteCharAt(sb.length() - 1).toString();
} else {
resultBody = sb.toString();
}
return MultipartBody.create(
MediaType.parse(mediaType),
resultBody);
}
@NonNull
private String handleGetGoogleCookiesResponse(Response response) {
if (response.code() != SUCCESS_STATUS_CODE) {
Logger.w(TAG, "Get google cookies request returned bad status code = " + response.code());
return defaultGoogleCookies;
}
Headers headers = response.headers();
for (String headerName : headers.names()) {
if (headerName.equalsIgnoreCase(setCookieHeaderName)) {
String setCookieHeader = headers.get(headerName);
if (setCookieHeader != null) {
String[] split = setCookieHeader.split(";");
for (String splitPart : split) {
if (splitPart.startsWith("NID")) {
return splitPart;
}
}
}
}
}
Logger.d(TAG, "Could not find the NID cookie in the headers");
return defaultGoogleCookies;
}
@Nullable
private CaptchaInfo handleGetRecaptchaResponse(Response response) {
try {
if (response.code() != SUCCESS_STATUS_CODE) {
if (callbacks != null) {
callbacks.onCaptchaInfoParseError(
new IOException("Bad status code for captcha request = " + response.code()));
}
return null;
}
ResponseBody body = response.body();
if (body == null) {
if (callbacks != null) {
callbacks.onCaptchaInfoParseError(
new IOException("Captcha response body is empty (null)"));
}
return null;
}
String bodyString = body.string();
if (!bodyString.contains(recaptchaChallengeString)) {
throw new IllegalStateException("Response body does not contain \"reCAPTCHA challenge\" string");
}
if (bodyString.contains(verificationTokenString)) {
// got the token
String verificationToken = parser.parseVerificationToken(bodyString);
Logger.d(TAG, "Got the verification token");
if (callbacks != null) {
callbacks.onVerificationDone(verificationToken);
}
return null;
} else {
// got the challenge
CaptchaInfo captchaInfo = parser.parseHtml(bodyString, siteKey);
Logger.d(TAG, "Got new challenge");
if (callbacks != null) {
callbacks.onCaptchaInfoParsed(captchaInfo);
} else {
// Return null when callbacks are null to reset prevCaptchaInfo so that we won't
// get stuck without captchaInfo and disabled buttons forever
return null;
}
return captchaInfo;
}
} catch (Throwable e) {
Logger.e(TAG, "Error while trying to parse captcha html data", e);
if (callbacks != null) {
callbacks.onCaptchaInfoParseError(e);
}
return null;
}
}
public void onDestroy() {
this.callbacks = null;
this.prevCaptchaInfo = null;
this.verificationInProgress.set(false);
this.captchaRequestInProgress.set(false);
executor.shutdown();
}
public enum VerifyError {
Ok,
NoImagesSelected,
AlreadyInProgress,
AlreadyShutdown
}
public enum RequestCaptchaInfoError {
Ok,
AlreadyInProgress,
HoldYourHorses,
AlreadyShutdown
}
public interface AuthenticationCallbacks {
void onGetGoogleCookieError(boolean shouldFallback, Throwable error);
void onGoogleCookiesRefreshed();
void onCaptchaInfoParsed(CaptchaInfo captchaInfo);
void onCaptchaInfoParseError(Throwable error);
void onVerificationDone(String verificationToken);
}
public static class CaptchaNoJsV2Error extends Exception {
public CaptchaNoJsV2Error(String message) {
super(message);
}
}
}

@ -0,0 +1,139 @@
package org.floens.chan.ui.captcha.v2;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.constraint.ConstraintLayout;
import android.support.v7.widget.AppCompatImageView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import org.floens.chan.R;
import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList;
import java.util.List;
public class CaptchaNoJsV2Adapter extends BaseAdapter {
private static final int ANIMATION_DURATION = 50;
private LayoutInflater inflater;
private int imageSize = 0;
private List<ImageChallengeInfo> imageList = new ArrayList<>();
public CaptchaNoJsV2Adapter(Context context) {
this.inflater = LayoutInflater.from(context);
}
public void setImages(List<Bitmap> imageList) {
cleanUpImages();
for (Bitmap bitmap : imageList) {
this.imageList.add(new ImageChallengeInfo(bitmap, false));
}
notifyDataSetChanged();
}
@Override
public int getCount() {
return imageList.size();
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = inflater.inflate(R.layout.layout_captcha_challenge_image, parent, false);
AppCompatImageView imageView = convertView.findViewById(R.id.captcha_challenge_image);
ConstraintLayout blueCheckmarkHolder = convertView.findViewById(R.id.captcha_challenge_blue_checkmark_holder);
ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(imageSize, imageSize);
imageView.setLayoutParams(layoutParams);
imageView.setOnClickListener((view) -> {
imageList.get(position).toggleChecked();
boolean isChecked = imageList.get(position).isChecked;
AndroidUtils.animateViewScale(imageView, isChecked, ANIMATION_DURATION);
blueCheckmarkHolder.setVisibility(isChecked ? View.VISIBLE : View.GONE);
});
if (position >= 0 && position <= imageList.size()) {
imageView.setImageBitmap(imageList.get(position).getBitmap());
}
}
return convertView;
}
public List<Integer> getCheckedImageIds() {
List<Integer> selectedList = new ArrayList<>();
for (int i = 0; i < imageList.size(); i++) {
ImageChallengeInfo info = imageList.get(i);
if (info.isChecked()) {
selectedList.add(i);
}
}
return selectedList;
}
public void setImageSize(int imageSize) {
this.imageSize = imageSize;
}
public void onDestroy() {
cleanUpImages();
}
private void cleanUpImages() {
for (ImageChallengeInfo imageChallengeInfo : imageList) {
imageChallengeInfo.recycle();
}
imageList.clear();
}
public static class ImageChallengeInfo {
private Bitmap bitmap;
private boolean isChecked;
public ImageChallengeInfo(Bitmap bitmap, boolean isChecked) {
this.bitmap = bitmap;
this.isChecked = isChecked;
}
public Bitmap getBitmap() {
return bitmap;
}
public boolean isChecked() {
return isChecked;
}
public void toggleChecked() {
this.isChecked = !this.isChecked;
}
public void recycle() {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
}

@ -39,6 +39,7 @@ import static org.floens.chan.Chan.injector;
public class BehaviourSettingsController extends SettingsController { public class BehaviourSettingsController extends SettingsController {
private SettingView forceEnglishSetting; private SettingView forceEnglishSetting;
private SettingView useNewCaptchaWindow;
public BehaviourSettingsController(Context context) { public BehaviourSettingsController(Context context) {
super(context); super(context);
@ -47,14 +48,10 @@ public class BehaviourSettingsController extends SettingsController {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
navigation.setTitle(R.string.settings_screen_behavior); navigation.setTitle(R.string.settings_screen_behavior);
setupLayout(); setupLayout();
rebuildPreferences();
populatePreferences();
buildPreferences();
} }
@Override @Override
@ -63,10 +60,33 @@ public class BehaviourSettingsController extends SettingsController {
if (item == forceEnglishSetting) { if (item == forceEnglishSetting) {
Toast.makeText(context, R.string.setting_force_english_locale_toggle_notice, Toast.makeText(context, R.string.setting_force_english_locale_toggle_notice,
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
} else if (item == useNewCaptchaWindow) {
// when user disables the new captcha window also disable the usage of the google cookies
if (!ChanSettings.useNewCaptchaWindow.get()) {
ChanSettings.useRealGoogleCookies.set(false);
// Reset the old google cookie
ChanSettings.googleCookie.set("");
// and cookie update time as well
ChanSettings.lastGoogleCookieUpdateTime.set(0L);
}
rebuildPreferences();
} }
} }
private void rebuildPreferences() {
populatePreferences();
buildPreferences();
}
private void populatePreferences() { private void populatePreferences() {
requiresUiRefresh.clear();
groups.clear();
requiresRestart.clear();
// General group // General group
{ {
SettingsGroup general = new SettingsGroup(R.string.settings_group_general); SettingsGroup general = new SettingsGroup(R.string.settings_group_general);
@ -152,6 +172,25 @@ public class BehaviourSettingsController extends SettingsController {
groups.add(post); groups.add(post);
} }
// Captcha group
{
SettingsGroup captcha = new SettingsGroup(R.string.settings_captcha_group);
useNewCaptchaWindow = captcha.add(new BooleanSettingView(this,
ChanSettings.useNewCaptchaWindow,
R.string.settings_use_new_captcha_window,
0));
if (ChanSettings.useNewCaptchaWindow.get()) {
captcha.add(new BooleanSettingView(this,
ChanSettings.useRealGoogleCookies,
R.string.settings_use_real_google_cookies,
R.string.settings_use_real_google_cookies_description));
}
groups.add(captcha);
}
// Proxy group // Proxy group
{ {
SettingsGroup proxy = new SettingsGroup(R.string.settings_group_proxy); SettingsGroup proxy = new SettingsGroup(R.string.settings_group_proxy);

@ -49,9 +49,10 @@ import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
import org.floens.chan.ui.captcha.AuthenticationLayoutInterface; import org.floens.chan.ui.captcha.AuthenticationLayoutInterface;
import org.floens.chan.ui.captcha.CaptchaLayout; import org.floens.chan.ui.captcha.CaptchaLayout;
import org.floens.chan.ui.captcha.CaptchaNojsLayout;
import org.floens.chan.ui.captcha.GenericWebViewAuthenticationLayout; import org.floens.chan.ui.captcha.GenericWebViewAuthenticationLayout;
import org.floens.chan.ui.captcha.LegacyCaptchaLayout; import org.floens.chan.ui.captcha.LegacyCaptchaLayout;
import org.floens.chan.ui.captcha.v1.CaptchaNojsLayoutV1;
import org.floens.chan.ui.captcha.v2.CaptchaNoJsLayoutV2;
import org.floens.chan.ui.drawable.DropdownArrowDrawable; import org.floens.chan.ui.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
@ -258,8 +259,10 @@ public class ReplyLayout extends LoadView implements
} }
@Override @Override
public void initializeAuthentication(Site site, SiteAuthentication authentication, public void initializeAuthentication(Site site,
AuthenticationLayoutCallback callback) { SiteAuthentication authentication,
AuthenticationLayoutCallback callback,
boolean useV2NoJsCaptcha) {
if (authenticationLayout == null) { if (authenticationLayout == null) {
switch (authentication.type) { switch (authentication.type) {
case CAPTCHA1: { case CAPTCHA1: {
@ -273,7 +276,25 @@ public class ReplyLayout extends LoadView implements
break; break;
} }
case CAPTCHA2_NOJS: case CAPTCHA2_NOJS:
authenticationLayout = new CaptchaNojsLayout(getContext()); if (useV2NoJsCaptcha) {
// new captcha window without webview
authenticationLayout = new CaptchaNoJsLayoutV2(getContext());
} else {
// default webview-based captcha view
authenticationLayout = new CaptchaNojsLayoutV1(getContext());
}
ImageView resetButton = captchaContainer.findViewById(R.id.reset);
if (resetButton != null) {
if (useV2NoJsCaptcha) {
// we don't need the default reset button because we have our own
resetButton.setVisibility(View.GONE);
} else {
// restore the button's visibility when using old v1 captcha view
resetButton.setVisibility(View.VISIBLE);
}
}
break; break;
case GENERIC_WEBVIEW: { case GENERIC_WEBVIEW: {
GenericWebViewAuthenticationLayout view = new GenericWebViewAuthenticationLayout(getContext()); GenericWebViewAuthenticationLayout view = new GenericWebViewAuthenticationLayout(getContext());
@ -338,6 +359,22 @@ public class ReplyLayout extends LoadView implements
authenticationLayout.reset(); authenticationLayout.reset();
} }
@Override
public void destroyCurrentAuthentication() {
if (authenticationLayout == null) {
return;
}
if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) {
return;
}
// cleanup resources when switching from the new to the old captcha view
((CaptchaNoJsLayoutV2) authenticationLayout).onDestroy();
captchaContainer.removeView((CaptchaNoJsLayoutV2) authenticationLayout);
authenticationLayout = null;
}
@Override @Override
public void loadDraftIntoViews(Reply draft) { public void loadDraftIntoViews(Reply draft) {
name.setText(draft.name); name.setText(draft.name);
@ -452,6 +489,12 @@ public class ReplyLayout extends LoadView implements
comment.post(() -> AndroidUtils.requestViewAndKeyboardFocus(comment)); comment.post(() -> AndroidUtils.requestViewAndKeyboardFocus(comment));
} }
@Override
public void onFallbackToV1CaptchaView() {
// fallback to v1 captcha window
presenter.switchPage(ReplyPresenter.Page.AUTHENTICATION, true, false);
}
@Override @Override
public void openPreview(boolean show, File previewFile) { public void openPreview(boolean show, File previewFile) {
if (show) { if (show) {

@ -140,6 +140,8 @@ public class SettingsController extends Controller implements AndroidUtils.OnMea
protected void buildPreferences() { protected void buildPreferences() {
LayoutInflater inf = LayoutInflater.from(context); LayoutInflater inf = LayoutInflater.from(context);
boolean firstGroup = true; boolean firstGroup = true;
content.removeAllViews();
for (SettingsGroup group : groups) { for (SettingsGroup group : groups) {
LinearLayout groupLayout = (LinearLayout) inf.inflate(R.layout.setting_group, content, false); LinearLayout groupLayout = (LinearLayout) inf.inflate(R.layout.setting_group, content, false);
((TextView) groupLayout.findViewById(R.id.header)).setText(group.name); ((TextView) groupLayout.findViewById(R.id.header)).setText(group.name);

@ -0,0 +1,50 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridView;
public class WrappingGridView extends GridView {
public WrappingGridView(Context context) {
super(context);
}
public WrappingGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WrappingGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec = heightMeasureSpec;
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
heightSpec = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
}
super.onMeasure(widthMeasureSpec, heightSpec);
}
}

@ -51,6 +51,8 @@ import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.ScaleAnimation;
import org.floens.chan.R; import org.floens.chan.R;
@ -458,6 +460,40 @@ public class AndroidUtils {
return networkInfo != null && networkInfo.isConnected(); return networkInfo != null && networkInfo.isConnected();
} }
public static void animateViewScale(View view, boolean zoomOut, int duration) {
ScaleAnimation scaleAnimation;
final float normalScale = 1.0f;
final float zoomOutScale = 0.8f;
if (zoomOut) {
scaleAnimation = new ScaleAnimation(
normalScale,
zoomOutScale,
normalScale,
zoomOutScale,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f);
} else {
scaleAnimation = new ScaleAnimation(
zoomOutScale,
normalScale,
zoomOutScale,
normalScale,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f);
}
scaleAnimation.setDuration(duration);
scaleAnimation.setFillAfter(true);
scaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
view.startAnimation(scaleAnimation);
}
public static boolean enableHighEndAnimations() { public static boolean enableHighEndAnimations() {
boolean lowRamDevice = ActivityManagerCompat.isLowRamDevice(activityManager); boolean lowRamDevice = ActivityManagerCompat.isLowRamDevice(activityManager);
return !lowRamDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; return !lowRamDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;

@ -17,11 +17,18 @@
*/ */
package org.floens.chan.utils; package org.floens.chan.utils;
import android.os.Looper;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class BackgroundUtils { public class BackgroundUtils {
public static boolean isMainThread() {
return Thread.currentThread() == Looper.getMainLooper().getThread();
}
public static <T> Cancelable runWithExecutor(Executor executor, final Callable<T> background, public static <T> Cancelable runWithExecutor(Executor executor, final Callable<T> background,
final BackgroundResult<T> result) { final BackgroundResult<T> result) {
final AtomicBoolean cancelled = new AtomicBoolean(false); final AtomicBoolean cancelled = new AtomicBoolean(false);

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF20A8C6"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
</vector>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/captcha_challenge_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.ConstraintLayout
android:id="@+id/captcha_challenge_blue_checkmark_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<android.support.v7.widget.AppCompatImageView
android:id="@+id/captcha_challenge_blue_checkmark"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_blue_checkmark_24dp"
app:layout_constraintBottom_toBottomOf="@+id/captcha_challenge_blue_checkmark_holder"
app:layout_constraintEnd_toEndOf="@+id/captcha_challenge_blue_checkmark_holder"
app:layout_constraintStart_toStartOf="@+id/captcha_challenge_blue_checkmark_holder"
app:layout_constraintTop_toTopOf="@+id/captcha_challenge_blue_checkmark_holder" />
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/captcha_layout_v2_background"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.AppCompatTextView
android:id="@+id/captcha_layout_v2_title"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/accent"
android:ellipsize="middle"
android:gravity="center"
android:maxLines="2"
android:padding="4dp"
android:text=""
android:textColor="#ffffff"
android:textSize="18sp"
tools:text="Select all images with something" />
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.floens.chan.ui.view.WrappingGridView
android:id="@+id/captcha_layout_v2_images_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:stretchMode="columnWidth" />
</android.support.v4.widget.NestedScrollView>
<android.support.constraint.ConstraintLayout
android:id="@+id/captcha_layout_v2_buttons_holder"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@color/accent"
tools:layout_editor_absoluteY="40dp">
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_use_old_captcha_button"
android:layout_width="112dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:text="@string/captcha_layout_v2_use_old_captcha"
android:textColor="#ffffff"
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_refresh_cookies"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_refresh_cookies"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:text="@string/captcha_layout_v2_refresh_cookies"
android:textColor="#ffffff"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_reload_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_use_old_captcha_button"
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_reload_button"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:text="@string/captcha_layout_v2_reload"
android:textColor="#ffffff"
app:layout_constraintBottom_toBottomOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintEnd_toStartOf="@+id/captcha_layout_v2_verify_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_refresh_cookies"
app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" />
<android.support.v7.widget.AppCompatButton
android:id="@+id/captcha_layout_v2_verify_button"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:text="@string/captcha_layout_v2_verify_button_text"
android:textColor="#ffffff"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/captcha_layout_v2_reload_button"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</LinearLayout>
</ScrollView>

@ -15,7 +15,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="close">Close</string> <string name="close">Close</string>
@ -579,4 +579,19 @@ Don't have a 4chan Pass?<br>
"</string> "</string>
<string name="settings_screen_theme">Themes</string> <string name="settings_screen_theme">Themes</string>
<!-- Captcha settings -->
<string name="settings_captcha_group" tools:ignore="MissingTranslation">Captcha</string>
<string name="settings_use_new_captcha_window" tools:ignore="MissingTranslation">Use new captcha window for no-js captcha</string>
<string name="settings_use_real_google_cookies" tools:ignore="MissingTranslation">Use real google cookies instead of hardcoded ones</string>
<string name="settings_use_real_google_cookies_description" tools:ignore="MissingTranslation">When this option is enabled a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (every three months). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row).</string>
<string name="captcha_layout_v2_verify_button_text" tools:ignore="MissingTranslation">Verify</string>
<string name="captcha_layout_v2_reload" tools:ignore="MissingTranslation">Reload</string>
<string name="captcha_layout_v2_use_old_captcha" tools:ignore="MissingTranslation">Use old captcha</string>
<string name="captcha_layout_v2_you_have_to_select_at_least_one_image" tools:ignore="MissingTranslation">You have to select at least one image (if you see a captcha that does not have any matching images then it is probably a bug fill an issue and use old captcha for a while)</string>
<string name="captcha_layout_v2_verification_already_in_progress" tools:ignore="MissingTranslation">Verification is already in progress</string>
<string name="captcha_layout_v2_captcha_request_is_already_in_progress" tools:ignore="MissingTranslation">Captcha request is already in progress</string>
<string name="captcha_layout_v2_you_are_requesting_captcha_too_fast" tools:ignore="MissingTranslation">You are requesting captcha too fast</string>
<string name="captcha_layout_v2_refresh_cookies" tools:ignore="MissingTranslation">Refresh cookies</string>
</resources> </resources>

@ -0,0 +1,104 @@
package org.floens.chan.ui.captcha.v2;
import android.content.Context;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
import okhttp3.OkHttpClient;
import static org.junit.Assert.assertEquals;
public class CaptchaNoJsHtmlParserTest {
private List<String> inputDataList = new ArrayList<>();
private List<List<Integer>> resultCheckboxList = new ArrayList<>();
private List<String> resultTitleList = new ArrayList<>();
private List<String> resultCParameterList = new ArrayList<>();
private Context contextMock = Mockito.mock(Context.class);
private OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class);
private CaptchaNoJsHtmlParser parser = new CaptchaNoJsHtmlParser(contextMock, okHttpClient);
@Before
public void setUp() {
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-candidates\"><div class=\"rc-canonical-car\"></div></div><div class=\"rc-imageselect-desc\">Select all images with <strong>cars</strong></div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSABiot1uRaxXzd4_PRTTGbxW4AV8NOI947nBNVIBwdfOC2DwB0gdrPnfe8YYLI3kkW_tzwTJbQwZk0_nKaIsfXLaGWnnrEhvRMstWDzRin27w_Yalo6OS8Yr5sIqYBUfI496UtsoDGSL8NVQAbxjXocz_E7DTNXVBMfdLPx093-9vUHKH4yt4Mo6HuKSXmBPC2KNL4j_yl78YQ_VG1mprDwUkQeW-MQZxBOQb4MyhWZMOmNp_aRgHKGpJYHpcVMljDUvwZRk_xLw-NoJAWsgCIAt6pfrUE9jvHxjE4fep8OCH2T-6g-_Lla_UovuyEVIfUi1MQa6KWQMypsavA2mVUjTvIe_NkkO8fJ6gteHqUc7a9FTIHHs1OR5uV7fD0SCn9G5ii\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLSABiot1uRaxXzd4_PRTTGbxW4AV8NOI947nBNVIBwdfOC2DwB0gdrPnfe8YYLI3kkW_tzwTJbQwZk0_nKaIsfXLaGWnnrEhvRMstWDzRin27w_Yalo6OS8Yr5sIqYBUfI496UtsoDGSL8NVQAbxjXocz_E7DTNXVBMfdLPx093-9vUHKH4yt4Mo6HuKSXmBPC2KNL4j_yl78YQ_VG1mprDwUkQeW-MQZxBOQb4MyhWZMOmNp_aRgHKGpJYHpcVMljDUvwZRk_xLw-NoJAWsgCIAt6pfrUE9jvHxjE4fep8OCH2T-6g-_Lla_UovuyEVIfUi1MQa6KWQMypsavA2mVUjTvIe_NkkO8fJ6gteHqUc7a9FTIHHs1OR5uV7fD0SCn9G5ii&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSABiot1uRaxXzd4_PRTTGbxW4AV8NOI947nBNVIBwdfOC2DwB0gdrPnfe8YYLI3kkW_tzwTJbQwZk0_nKaIsfXLaGWnnrEhvRMstWDzRin27w_Yalo6OS8Yr5sIqYBUfI496UtsoDGSL8NVQAbxjXocz_E7DTNXVBMfdLPx093-9vUHKH4yt4Mo6HuKSXmBPC2KNL4j_yl78YQ_VG1mprDwUkQeW-MQZxBOQb4MyhWZMOmNp_aRgHKGpJYHpcVMljDUvwZRk_xLw-NoJAWsgCIAt6pfrUE9jvHxjE4fep8OCH2T-6g-_Lla_UovuyEVIfUi1MQa6KWQMypsavA2mVUjTvIe_NkkO8fJ6gteHqUc7a9FTIHHs1OR5uV7fD0SCn9G5ii\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSABiot1uRaxXzd4_PRTTGbxW4AV8NOI947nBNVIBwdfOC2DwB0gdrPnfe8YYLI3kkW_tzwTJbQwZk0_nKaIsfXLaGWnnrEhvRMstWDzRin27w_Yalo6OS8Yr5sIqYBUfI496UtsoDGSL8NVQAbxjXocz_E7DTNXVBMfdLPx093-9vUHKH4yt4Mo6HuKSXmBPC2KNL4j_yl78YQ_VG1mprDwUkQeW-MQZxBOQb4MyhWZMOmNp_aRgHKGpJYHpcVMljDUvwZRk_xLw-NoJAWsgCIAt6pfrUE9jvHxjE4fep8OCH2T-6g-_Lla_UovuyEVIfUi1MQa6KWQMypsavA2mVUjTvIe_NkkO8fJ6gteHqUc7a9FTIHHs1OR5uV7fD0SCn9G5ii\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"qnaxKyOmAYxqk1TC5w5H7Q\">document.body.className += \" js-enabled\";</script></body></html>");
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-desc-no-canonical\">Select all images with <strong>mountains or hills</strong>.</div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLRv_ZKMPWAc0bBYTnC9wPIQElGB6H6SrifMnrj1yDeFnUPNpOPgGZHdaGPVzfieFHzQOh4GIKWUBPQ_aMs3gp0EJmgdP-LAGFBZIreVB-boqP1DyIblN-3ws1bxCyVvw4kSanc08rPVr7G2nwy7b0kkBV82x8ypU1QP1fQR1HxC8qUddQZSLvgmkFiRatbaOqMdsMPDwl1ENJVrzTkX0sQGmfqnf2aisqRbSsiH0BrW1_wTLPjiGv1SW-FrBYWKJZBWP_FanbTftUuhKLaYTjABDD2mcBg-7-QEpFbaFqi7cwSA4Y-SsCiJg7y0DBL8PjsDSxOqoY_Upc5I-0GI3X21KJLJMLxq9tRkWZbsyhHXNyFzzM_4PfOIiGErbTMzICa596q7\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLRv_ZKMPWAc0bBYTnC9wPIQElGB6H6SrifMnrj1yDeFnUPNpOPgGZHdaGPVzfieFHzQOh4GIKWUBPQ_aMs3gp0EJmgdP-LAGFBZIreVB-boqP1DyIblN-3ws1bxCyVvw4kSanc08rPVr7G2nwy7b0kkBV82x8ypU1QP1fQR1HxC8qUddQZSLvgmkFiRatbaOqMdsMPDwl1ENJVrzTkX0sQGmfqnf2aisqRbSsiH0BrW1_wTLPjiGv1SW-FrBYWKJZBWP_FanbTftUuhKLaYTjABDD2mcBg-7-QEpFbaFqi7cwSA4Y-SsCiJg7y0DBL8PjsDSxOqoY_Upc5I-0GI3X21KJLJMLxq9tRkWZbsyhHXNyFzzM_4PfOIiGErbTMzICa596q7&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLRv_ZKMPWAc0bBYTnC9wPIQElGB6H6SrifMnrj1yDeFnUPNpOPgGZHdaGPVzfieFHzQOh4GIKWUBPQ_aMs3gp0EJmgdP-LAGFBZIreVB-boqP1DyIblN-3ws1bxCyVvw4kSanc08rPVr7G2nwy7b0kkBV82x8ypU1QP1fQR1HxC8qUddQZSLvgmkFiRatbaOqMdsMPDwl1ENJVrzTkX0sQGmfqnf2aisqRbSsiH0BrW1_wTLPjiGv1SW-FrBYWKJZBWP_FanbTftUuhKLaYTjABDD2mcBg-7-QEpFbaFqi7cwSA4Y-SsCiJg7y0DBL8PjsDSxOqoY_Upc5I-0GI3X21KJLJMLxq9tRkWZbsyhHXNyFzzM_4PfOIiGErbTMzICa596q7\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLRv_ZKMPWAc0bBYTnC9wPIQElGB6H6SrifMnrj1yDeFnUPNpOPgGZHdaGPVzfieFHzQOh4GIKWUBPQ_aMs3gp0EJmgdP-LAGFBZIreVB-boqP1DyIblN-3ws1bxCyVvw4kSanc08rPVr7G2nwy7b0kkBV82x8ypU1QP1fQR1HxC8qUddQZSLvgmkFiRatbaOqMdsMPDwl1ENJVrzTkX0sQGmfqnf2aisqRbSsiH0BrW1_wTLPjiGv1SW-FrBYWKJZBWP_FanbTftUuhKLaYTjABDD2mcBg-7-QEpFbaFqi7cwSA4Y-SsCiJg7y0DBL8PjsDSxOqoY_Upc5I-0GI3X21KJLJMLxq9tRkWZbsyhHXNyFzzM_4PfOIiGErbTMzICa596q7\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"9mPanFXMj8uH+hihgCN2rQ\">document.body.className += \" js-enabled\";</script></body></html>");
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-desc-no-canonical\">Select all images with <strong>crosswalks</strong>.</div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLQdQO0rHKgYInXnNyPho7cEllKtboAokZsqQPHyE5VkAEXwvkvin540F_CAPIA872WSFqdlShtJvERZPgOP_DmuO4lAF7CxIRDeRzyMd2kKF-ivS3XADiISevh50q1qtolKOW6nhdktgo4JxW3nej539yRZxJ4X6SRde_29QsGgqtTUUB82CYWZ2vOa9yclvuMMLPTFfIIRQHy4DNQXqVOEKV0PfvBatjLhq4ekuYgEoHbni97ePTmjvfsL0gy6CR2SAmOf6m7oJm2snVyBBgxWfMnPue6wypmHco7JRLcEsRHOvUDBLa6rMT5p7fiRP3sxPQNcv_H6prK_EAtSPOB29SELUlf7G72UlcxBNOaKxsOY_xzyFHnyFPRTERh2xc7FEmDF\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLQdQO0rHKgYInXnNyPho7cEllKtboAokZsqQPHyE5VkAEXwvkvin540F_CAPIA872WSFqdlShtJvERZPgOP_DmuO4lAF7CxIRDeRzyMd2kKF-ivS3XADiISevh50q1qtolKOW6nhdktgo4JxW3nej539yRZxJ4X6SRde_29QsGgqtTUUB82CYWZ2vOa9yclvuMMLPTFfIIRQHy4DNQXqVOEKV0PfvBatjLhq4ekuYgEoHbni97ePTmjvfsL0gy6CR2SAmOf6m7oJm2snVyBBgxWfMnPue6wypmHco7JRLcEsRHOvUDBLa6rMT5p7fiRP3sxPQNcv_H6prK_EAtSPOB29SELUlf7G72UlcxBNOaKxsOY_xzyFHnyFPRTERh2xc7FEmDF&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLQdQO0rHKgYInXnNyPho7cEllKtboAokZsqQPHyE5VkAEXwvkvin540F_CAPIA872WSFqdlShtJvERZPgOP_DmuO4lAF7CxIRDeRzyMd2kKF-ivS3XADiISevh50q1qtolKOW6nhdktgo4JxW3nej539yRZxJ4X6SRde_29QsGgqtTUUB82CYWZ2vOa9yclvuMMLPTFfIIRQHy4DNQXqVOEKV0PfvBatjLhq4ekuYgEoHbni97ePTmjvfsL0gy6CR2SAmOf6m7oJm2snVyBBgxWfMnPue6wypmHco7JRLcEsRHOvUDBLa6rMT5p7fiRP3sxPQNcv_H6prK_EAtSPOB29SELUlf7G72UlcxBNOaKxsOY_xzyFHnyFPRTERh2xc7FEmDF\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLQdQO0rHKgYInXnNyPho7cEllKtboAokZsqQPHyE5VkAEXwvkvin540F_CAPIA872WSFqdlShtJvERZPgOP_DmuO4lAF7CxIRDeRzyMd2kKF-ivS3XADiISevh50q1qtolKOW6nhdktgo4JxW3nej539yRZxJ4X6SRde_29QsGgqtTUUB82CYWZ2vOa9yclvuMMLPTFfIIRQHy4DNQXqVOEKV0PfvBatjLhq4ekuYgEoHbni97ePTmjvfsL0gy6CR2SAmOf6m7oJm2snVyBBgxWfMnPue6wypmHco7JRLcEsRHOvUDBLa6rMT5p7fiRP3sxPQNcv_H6prK_EAtSPOB29SELUlf7G72UlcxBNOaKxsOY_xzyFHnyFPRTERh2xc7FEmDF\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"FBv2o/5cAreDSNeyDhdNxQ\">document.body.className += \" js-enabled\";</script></body></html>");
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-candidates\"><div class=\"rc-canonical-bridge\"></div></div><div class=\"rc-imageselect-desc\">Select all images with <strong>bridges</strong>.</div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLTkdQd0zGmSjFw9OCiKz18VT-h4lleJaBwtNMSDPFsUSR_9qepA1uCUW_FNAju-tgYzTOTjmjf0wWmvfLlwKCxEsyMKfYobnJXXYcOHyAwaOJRtEOAwn2SSmVEzlE1DYZ89lFH7XB5rw6tGpSJRJQkvitoVK-aLg_gzmYxbYJPH_VK9ofkfeGfhnZ6bsXoz1vnnhHYOXm7apmKjDEk6NIxKqB_FHDFclemX8rr7liJBaTcJ98I20a9wXGinBUEEyupoQ5gdnoYqLMu_XIQaRYtdmRsCgtm9RorpkJZxaqEpqfusfpFZUntIXkXF31dmx0hL-B5DpA4uFOBI6UmR2oPLAA2LhStMgKtlnl1IYwyrmaeYUybV0rPl0F5jMAr3GZxEa7Ld\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLTkdQd0zGmSjFw9OCiKz18VT-h4lleJaBwtNMSDPFsUSR_9qepA1uCUW_FNAju-tgYzTOTjmjf0wWmvfLlwKCxEsyMKfYobnJXXYcOHyAwaOJRtEOAwn2SSmVEzlE1DYZ89lFH7XB5rw6tGpSJRJQkvitoVK-aLg_gzmYxbYJPH_VK9ofkfeGfhnZ6bsXoz1vnnhHYOXm7apmKjDEk6NIxKqB_FHDFclemX8rr7liJBaTcJ98I20a9wXGinBUEEyupoQ5gdnoYqLMu_XIQaRYtdmRsCgtm9RorpkJZxaqEpqfusfpFZUntIXkXF31dmx0hL-B5DpA4uFOBI6UmR2oPLAA2LhStMgKtlnl1IYwyrmaeYUybV0rPl0F5jMAr3GZxEa7Ld&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLTkdQd0zGmSjFw9OCiKz18VT-h4lleJaBwtNMSDPFsUSR_9qepA1uCUW_FNAju-tgYzTOTjmjf0wWmvfLlwKCxEsyMKfYobnJXXYcOHyAwaOJRtEOAwn2SSmVEzlE1DYZ89lFH7XB5rw6tGpSJRJQkvitoVK-aLg_gzmYxbYJPH_VK9ofkfeGfhnZ6bsXoz1vnnhHYOXm7apmKjDEk6NIxKqB_FHDFclemX8rr7liJBaTcJ98I20a9wXGinBUEEyupoQ5gdnoYqLMu_XIQaRYtdmRsCgtm9RorpkJZxaqEpqfusfpFZUntIXkXF31dmx0hL-B5DpA4uFOBI6UmR2oPLAA2LhStMgKtlnl1IYwyrmaeYUybV0rPl0F5jMAr3GZxEa7Ld\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLTkdQd0zGmSjFw9OCiKz18VT-h4lleJaBwtNMSDPFsUSR_9qepA1uCUW_FNAju-tgYzTOTjmjf0wWmvfLlwKCxEsyMKfYobnJXXYcOHyAwaOJRtEOAwn2SSmVEzlE1DYZ89lFH7XB5rw6tGpSJRJQkvitoVK-aLg_gzmYxbYJPH_VK9ofkfeGfhnZ6bsXoz1vnnhHYOXm7apmKjDEk6NIxKqB_FHDFclemX8rr7liJBaTcJ98I20a9wXGinBUEEyupoQ5gdnoYqLMu_XIQaRYtdmRsCgtm9RorpkJZxaqEpqfusfpFZUntIXkXF31dmx0hL-B5DpA4uFOBI6UmR2oPLAA2LhStMgKtlnl1IYwyrmaeYUybV0rPl0F5jMAr3GZxEa7Ld\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"/dAVLmM2x00DwplNUqTwoA\">document.body.className += \" js-enabled\";</script></body></html>");
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-desc-no-canonical\">Select all images with <strong>bicycles</strong></div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLTAhnX3O2lESLZYL9y7w8txPisktNhNpI-h3hZpULhz_-yMEId_7-NW8RGCMbZ6_W8CRH6LnL7Icxdw3nISINh3DGcldAmF8jojjG17gPIVvCCk1WdKfgYSkC_oAThQDESoAOz-VGAahHroLUNSnCm1tHmExy5tRX3EGi0xO3ep-1iMSGuMb_aoo3hxIuEZE4_J37RGoE--t2yoU4pKDxfbnHNYYfl48KgIb_qBzUt9O-gOF5P3oMM7G5HpMHjq3lraD0Ep19tiq5c_P-JO21V7QiJBB8XiwqgIHsEwGaLsm3J2NdQjLHot1xy8_A8x04ZTqHw-d_LnpBOTz8Jry5ym2K-zfkqkK1VtbJUnayZ-i-e5_ErxE3HTesMtzJ6I-ZBu2h8T\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLTAhnX3O2lESLZYL9y7w8txPisktNhNpI-h3hZpULhz_-yMEId_7-NW8RGCMbZ6_W8CRH6LnL7Icxdw3nISINh3DGcldAmF8jojjG17gPIVvCCk1WdKfgYSkC_oAThQDESoAOz-VGAahHroLUNSnCm1tHmExy5tRX3EGi0xO3ep-1iMSGuMb_aoo3hxIuEZE4_J37RGoE--t2yoU4pKDxfbnHNYYfl48KgIb_qBzUt9O-gOF5P3oMM7G5HpMHjq3lraD0Ep19tiq5c_P-JO21V7QiJBB8XiwqgIHsEwGaLsm3J2NdQjLHot1xy8_A8x04ZTqHw-d_LnpBOTz8Jry5ym2K-zfkqkK1VtbJUnayZ-i-e5_ErxE3HTesMtzJ6I-ZBu2h8T&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLTAhnX3O2lESLZYL9y7w8txPisktNhNpI-h3hZpULhz_-yMEId_7-NW8RGCMbZ6_W8CRH6LnL7Icxdw3nISINh3DGcldAmF8jojjG17gPIVvCCk1WdKfgYSkC_oAThQDESoAOz-VGAahHroLUNSnCm1tHmExy5tRX3EGi0xO3ep-1iMSGuMb_aoo3hxIuEZE4_J37RGoE--t2yoU4pKDxfbnHNYYfl48KgIb_qBzUt9O-gOF5P3oMM7G5HpMHjq3lraD0Ep19tiq5c_P-JO21V7QiJBB8XiwqgIHsEwGaLsm3J2NdQjLHot1xy8_A8x04ZTqHw-d_LnpBOTz8Jry5ym2K-zfkqkK1VtbJUnayZ-i-e5_ErxE3HTesMtzJ6I-ZBu2h8T\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLTAhnX3O2lESLZYL9y7w8txPisktNhNpI-h3hZpULhz_-yMEId_7-NW8RGCMbZ6_W8CRH6LnL7Icxdw3nISINh3DGcldAmF8jojjG17gPIVvCCk1WdKfgYSkC_oAThQDESoAOz-VGAahHroLUNSnCm1tHmExy5tRX3EGi0xO3ep-1iMSGuMb_aoo3hxIuEZE4_J37RGoE--t2yoU4pKDxfbnHNYYfl48KgIb_qBzUt9O-gOF5P3oMM7G5HpMHjq3lraD0Ep19tiq5c_P-JO21V7QiJBB8XiwqgIHsEwGaLsm3J2NdQjLHot1xy8_A8x04ZTqHw-d_LnpBOTz8Jry5ym2K-zfkqkK1VtbJUnayZ-i-e5_ErxE3HTesMtzJ6I-ZBu2h8T\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"qBUhkppTt1rZFbcmcQcqhw\">document.body.className += \" js-enabled\";</script></body></html>");
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-candidates\"><div class=\"rc-canonical-car\"></div></div><div class=\"rc-imageselect-desc\">Select all images with <strong>cars</strong></div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSu6KJ0tMzrENsXJM2H5K8GDF_2PZOEYKew4UxN49JDmoXnrlOFLYbETkJfXyG-kXWdmiTIg3XgR8vVfIf8J4oR1DpEyi9HcPaV9kLNeq-0aK3P1Vw62wBm3OOhU_zeP49kiwmy4icv4ro9NQjfBxw1scZgYsj_R41sBDvlLkofoDMrWNzsAjwFAM2pmbRGKf6qThzQ6ijSAjHBAD_oIK4E4m7kgeUZyEvw9-2MN_8AJoGXtI_74LdvbkngnF1qqK5gIKYX05AyaI39M1-TXfT5Gq2ePu8XMKUR709pZPc22Pw9lni0a1I-UyfGMJJ1u0Pvh0HKO6atPREGFcXwvhOQsSHakAGuzHUE8V5ywUqpW3mNFE0xly-GK07BbGMicQZ-7F-9\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLSu6KJ0tMzrENsXJM2H5K8GDF_2PZOEYKew4UxN49JDmoXnrlOFLYbETkJfXyG-kXWdmiTIg3XgR8vVfIf8J4oR1DpEyi9HcPaV9kLNeq-0aK3P1Vw62wBm3OOhU_zeP49kiwmy4icv4ro9NQjfBxw1scZgYsj_R41sBDvlLkofoDMrWNzsAjwFAM2pmbRGKf6qThzQ6ijSAjHBAD_oIK4E4m7kgeUZyEvw9-2MN_8AJoGXtI_74LdvbkngnF1qqK5gIKYX05AyaI39M1-TXfT5Gq2ePu8XMKUR709pZPc22Pw9lni0a1I-UyfGMJJ1u0Pvh0HKO6atPREGFcXwvhOQsSHakAGuzHUE8V5ywUqpW3mNFE0xly-GK07BbGMicQZ-7F-9&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSu6KJ0tMzrENsXJM2H5K8GDF_2PZOEYKew4UxN49JDmoXnrlOFLYbETkJfXyG-kXWdmiTIg3XgR8vVfIf8J4oR1DpEyi9HcPaV9kLNeq-0aK3P1Vw62wBm3OOhU_zeP49kiwmy4icv4ro9NQjfBxw1scZgYsj_R41sBDvlLkofoDMrWNzsAjwFAM2pmbRGKf6qThzQ6ijSAjHBAD_oIK4E4m7kgeUZyEvw9-2MN_8AJoGXtI_74LdvbkngnF1qqK5gIKYX05AyaI39M1-TXfT5Gq2ePu8XMKUR709pZPc22Pw9lni0a1I-UyfGMJJ1u0Pvh0HKO6atPREGFcXwvhOQsSHakAGuzHUE8V5ywUqpW3mNFE0xly-GK07BbGMicQZ-7F-9\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSu6KJ0tMzrENsXJM2H5K8GDF_2PZOEYKew4UxN49JDmoXnrlOFLYbETkJfXyG-kXWdmiTIg3XgR8vVfIf8J4oR1DpEyi9HcPaV9kLNeq-0aK3P1Vw62wBm3OOhU_zeP49kiwmy4icv4ro9NQjfBxw1scZgYsj_R41sBDvlLkofoDMrWNzsAjwFAM2pmbRGKf6qThzQ6ijSAjHBAD_oIK4E4m7kgeUZyEvw9-2MN_8AJoGXtI_74LdvbkngnF1qqK5gIKYX05AyaI39M1-TXfT5Gq2ePu8XMKUR709pZPc22Pw9lni0a1I-UyfGMJJ1u0Pvh0HKO6atPREGFcXwvhOQsSHakAGuzHUE8V5ywUqpW3mNFE0xly-GK07BbGMicQZ-7F-9\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"/dM7smPRYX0+FKHz5eMxYw\">document.body.className += \" js-enabled\";</script></body></html>");
inputDataList.add("<!DOCTYPE HTML><html dir=\"ltr\"><head><meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>reCAPTCHA challenge</title><link rel=\"stylesheet\" href=\"https://www.gstatic.com/recaptcha/api2/v1557729121476/fallback__ltr.css\" type=\"text/css\" charset=\"utf-8\"></head><body><div class=\"fbc\"><div class=\"fbc-alert\"></div><div class=\"fbc-header\"><div class=\"fbc-logo\"><div class=\"fbc-logo-img\"></div><div class=\"fbc-logo-text\">reCAPTCHA</div></div><div class=\"fbc-imageselect-candidates\"></div><div class=\"fbc-imageselect-message-without-candidate-image\"><label for=\"response\" class=\"fbc-imageselect-message-text\"><div class=\"rc-imageselect-desc-no-canonical\">Select all images with a <strong>bus</strong>.</div></label></div></div><div><div class=\"fbc-imageselect-challenge\"><form method=\"POST\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSe4m_kEnKDI7Zg9oYjJ28Y1XKwS7uZ10NlWRoq-2e5tLG3r47ClAG3F_pTLqQC_krxnHkt8UBIV5EfW5goPvQQQN2W8ll3YYiMjWM_ge1_Z9lk_7M5KCriFBFhmPMB1MtMfeXS5LpcekGDSq9J2O20zySzNHXZ7xkvx5vJ327ioNoDEizwInEaLMG5tMqaWthqZE_-DZlpI7n3v35Glt3hNuT_utgSlBBqt7yipMuy-dunICsXT3Oxojeb-zcvPk3SFsWyMcTI-354eV1PfA-7q0mOGIADYuavzciBKjfZEHrsR9su48sHj_AH8RG4b_8yBzmR7WAj7Va-EhZzxDnlGdfBVqUo5wVUyJxlZe6NWkNkBwiaisxhC8As0VJIY0u_ugms\"/><div class=\"fbc-payload-imageselect\"><input class=\"fbc-imageselect-checkbox-1\" type=\"checkbox\" name=\"response\" value=\"0\"><input class=\"fbc-imageselect-checkbox-2\" type=\"checkbox\" name=\"response\" value=\"1\"><input class=\"fbc-imageselect-checkbox-3\" type=\"checkbox\" name=\"response\" value=\"2\"><input class=\"fbc-imageselect-checkbox-4\" type=\"checkbox\" name=\"response\" value=\"3\"><input class=\"fbc-imageselect-checkbox-5\" type=\"checkbox\" name=\"response\" value=\"4\"><input class=\"fbc-imageselect-checkbox-6\" type=\"checkbox\" name=\"response\" value=\"5\"><input class=\"fbc-imageselect-checkbox-7\" type=\"checkbox\" name=\"response\" value=\"6\"><input class=\"fbc-imageselect-checkbox-8\" type=\"checkbox\" name=\"response\" value=\"7\"><input class=\"fbc-imageselect-checkbox-9\" type=\"checkbox\" name=\"response\" value=\"8\"><img class=\"fbc-imageselect-payload\" src=\"/recaptcha/api2/payload?c=03AOLTBLSe4m_kEnKDI7Zg9oYjJ28Y1XKwS7uZ10NlWRoq-2e5tLG3r47ClAG3F_pTLqQC_krxnHkt8UBIV5EfW5goPvQQQN2W8ll3YYiMjWM_ge1_Z9lk_7M5KCriFBFhmPMB1MtMfeXS5LpcekGDSq9J2O20zySzNHXZ7xkvx5vJ327ioNoDEizwInEaLMG5tMqaWthqZE_-DZlpI7n3v35Glt3hNuT_utgSlBBqt7yipMuy-dunICsXT3Oxojeb-zcvPk3SFsWyMcTI-354eV1PfA-7q0mOGIADYuavzciBKjfZEHrsR9su48sHj_AH8RG4b_8yBzmR7WAj7Va-EhZzxDnlGdfBVqUo5wVUyJxlZe6NWkNkBwiaisxhC8As0VJIY0u_ugms&amp;k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc\" alt=\"reCAPTCHA challenge image\"/></div><div class=\"fbc-button-verify\"><input type=\"submit\" value=\"Verify\"/></div></form></div></div><div class=\"fbc-buttons\"><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSe4m_kEnKDI7Zg9oYjJ28Y1XKwS7uZ10NlWRoq-2e5tLG3r47ClAG3F_pTLqQC_krxnHkt8UBIV5EfW5goPvQQQN2W8ll3YYiMjWM_ge1_Z9lk_7M5KCriFBFhmPMB1MtMfeXS5LpcekGDSq9J2O20zySzNHXZ7xkvx5vJ327ioNoDEizwInEaLMG5tMqaWthqZE_-DZlpI7n3v35Glt3hNuT_utgSlBBqt7yipMuy-dunICsXT3Oxojeb-zcvPk3SFsWyMcTI-354eV1PfA-7q0mOGIADYuavzciBKjfZEHrsR9su48sHj_AH8RG4b_8yBzmR7WAj7Va-EhZzxDnlGdfBVqUo5wVUyJxlZe6NWkNkBwiaisxhC8As0VJIY0u_ugms\"/><input type=\"hidden\" name=\"reason\" value=\"r\"/><input class=\"fbc-button-reload fbc-button\" type=\"submit\" value=\"Get a new challenge\"/></form></div><div class=\"fbc-button-holder\"><form method=\"POST\" tabindex=\"-1\"><input type=\"hidden\" name=\"c\" value=\"03AOLTBLSe4m_kEnKDI7Zg9oYjJ28Y1XKwS7uZ10NlWRoq-2e5tLG3r47ClAG3F_pTLqQC_krxnHkt8UBIV5EfW5goPvQQQN2W8ll3YYiMjWM_ge1_Z9lk_7M5KCriFBFhmPMB1MtMfeXS5LpcekGDSq9J2O20zySzNHXZ7xkvx5vJ327ioNoDEizwInEaLMG5tMqaWthqZE_-DZlpI7n3v35Glt3hNuT_utgSlBBqt7yipMuy-dunICsXT3Oxojeb-zcvPk3SFsWyMcTI-354eV1PfA-7q0mOGIADYuavzciBKjfZEHrsR9su48sHj_AH8RG4b_8yBzmR7WAj7Va-EhZzxDnlGdfBVqUo5wVUyJxlZe6NWkNkBwiaisxhC8As0VJIY0u_ugms\"/><input type=\"hidden\" name=\"reason\" value=\"a\"/><input class=\"fbc-button-audio fbc-button\" type=\"submit\" value=\"Get an audio challenge\"/></form></div></div></div><div class=\"fbc-why-fallback\"><a href=\"https://support.google.com/recaptcha#6262736\" target=\"_blank\">Want an easier challenge?</a></div><div class=\"fbc-separator\"></div><div class=\"fbc-privacy\"><a href=\"https://www.google.com/intl/en/policies/privacy/\" target=\"_blank\">Privacy</a> - <a href=\"https://www.google.com/intl/en/policies/terms/\" target=\"_blank\">Terms</a></div></div><script nonce=\"AxGVM2LSHezkZMBRJNGTTQ\">document.body.className += \" js-enabled\";</script></body></html>");
for (int i = 0; i < inputDataList.size(); ++i) {
resultCheckboxList.add(new ArrayList<>());
resultCheckboxList.get(i).add(0);
resultCheckboxList.get(i).add(1);
resultCheckboxList.get(i).add(2);
resultCheckboxList.get(i).add(3);
resultCheckboxList.get(i).add(4);
resultCheckboxList.get(i).add(5);
resultCheckboxList.get(i).add(6);
resultCheckboxList.get(i).add(7);
resultCheckboxList.get(i).add(8);
}
resultTitleList.add("Select all images with cars");
resultTitleList.add("Select all images with mountains or hills");
resultTitleList.add("Select all images with crosswalks");
resultTitleList.add("Select all images with bridges");
resultTitleList.add("Select all images with bicycles");
resultTitleList.add("Select all images with cars");
resultTitleList.add("Select all images with a bus");
resultCParameterList.add("03AOLTBLSABiot1uRaxXzd4_PRTTGbxW4AV8NOI947nBNVIBwdfOC2DwB0gdrPnfe8YYLI3kkW_tzwTJbQwZk0_nKaIsfXLaGWnnrEhvRMstWDzRin27w_Yalo6OS8Yr5sIqYBUfI496UtsoDGSL8NVQAbxjXocz_E7DTNXVBMfdLPx093-9vUHKH4yt4Mo6HuKSXmBPC2KNL4j_yl78YQ_VG1mprDwUkQeW-MQZxBOQb4MyhWZMOmNp_aRgHKGpJYHpcVMljDUvwZRk_xLw-NoJAWsgCIAt6pfrUE9jvHxjE4fep8OCH2T-6g-_Lla_UovuyEVIfUi1MQa6KWQMypsavA2mVUjTvIe_NkkO8fJ6gteHqUc7a9FTIHHs1OR5uV7fD0SCn9G5ii");
resultCParameterList.add("03AOLTBLRv_ZKMPWAc0bBYTnC9wPIQElGB6H6SrifMnrj1yDeFnUPNpOPgGZHdaGPVzfieFHzQOh4GIKWUBPQ_aMs3gp0EJmgdP-LAGFBZIreVB-boqP1DyIblN-3ws1bxCyVvw4kSanc08rPVr7G2nwy7b0kkBV82x8ypU1QP1fQR1HxC8qUddQZSLvgmkFiRatbaOqMdsMPDwl1ENJVrzTkX0sQGmfqnf2aisqRbSsiH0BrW1_wTLPjiGv1SW-FrBYWKJZBWP_FanbTftUuhKLaYTjABDD2mcBg-7-QEpFbaFqi7cwSA4Y-SsCiJg7y0DBL8PjsDSxOqoY_Upc5I-0GI3X21KJLJMLxq9tRkWZbsyhHXNyFzzM_4PfOIiGErbTMzICa596q7");
resultCParameterList.add("03AOLTBLQdQO0rHKgYInXnNyPho7cEllKtboAokZsqQPHyE5VkAEXwvkvin540F_CAPIA872WSFqdlShtJvERZPgOP_DmuO4lAF7CxIRDeRzyMd2kKF-ivS3XADiISevh50q1qtolKOW6nhdktgo4JxW3nej539yRZxJ4X6SRde_29QsGgqtTUUB82CYWZ2vOa9yclvuMMLPTFfIIRQHy4DNQXqVOEKV0PfvBatjLhq4ekuYgEoHbni97ePTmjvfsL0gy6CR2SAmOf6m7oJm2snVyBBgxWfMnPue6wypmHco7JRLcEsRHOvUDBLa6rMT5p7fiRP3sxPQNcv_H6prK_EAtSPOB29SELUlf7G72UlcxBNOaKxsOY_xzyFHnyFPRTERh2xc7FEmDF");
resultCParameterList.add("03AOLTBLTkdQd0zGmSjFw9OCiKz18VT-h4lleJaBwtNMSDPFsUSR_9qepA1uCUW_FNAju-tgYzTOTjmjf0wWmvfLlwKCxEsyMKfYobnJXXYcOHyAwaOJRtEOAwn2SSmVEzlE1DYZ89lFH7XB5rw6tGpSJRJQkvitoVK-aLg_gzmYxbYJPH_VK9ofkfeGfhnZ6bsXoz1vnnhHYOXm7apmKjDEk6NIxKqB_FHDFclemX8rr7liJBaTcJ98I20a9wXGinBUEEyupoQ5gdnoYqLMu_XIQaRYtdmRsCgtm9RorpkJZxaqEpqfusfpFZUntIXkXF31dmx0hL-B5DpA4uFOBI6UmR2oPLAA2LhStMgKtlnl1IYwyrmaeYUybV0rPl0F5jMAr3GZxEa7Ld");
resultCParameterList.add("03AOLTBLTAhnX3O2lESLZYL9y7w8txPisktNhNpI-h3hZpULhz_-yMEId_7-NW8RGCMbZ6_W8CRH6LnL7Icxdw3nISINh3DGcldAmF8jojjG17gPIVvCCk1WdKfgYSkC_oAThQDESoAOz-VGAahHroLUNSnCm1tHmExy5tRX3EGi0xO3ep-1iMSGuMb_aoo3hxIuEZE4_J37RGoE--t2yoU4pKDxfbnHNYYfl48KgIb_qBzUt9O-gOF5P3oMM7G5HpMHjq3lraD0Ep19tiq5c_P-JO21V7QiJBB8XiwqgIHsEwGaLsm3J2NdQjLHot1xy8_A8x04ZTqHw-d_LnpBOTz8Jry5ym2K-zfkqkK1VtbJUnayZ-i-e5_ErxE3HTesMtzJ6I-ZBu2h8T");
resultCParameterList.add("03AOLTBLSu6KJ0tMzrENsXJM2H5K8GDF_2PZOEYKew4UxN49JDmoXnrlOFLYbETkJfXyG-kXWdmiTIg3XgR8vVfIf8J4oR1DpEyi9HcPaV9kLNeq-0aK3P1Vw62wBm3OOhU_zeP49kiwmy4icv4ro9NQjfBxw1scZgYsj_R41sBDvlLkofoDMrWNzsAjwFAM2pmbRGKf6qThzQ6ijSAjHBAD_oIK4E4m7kgeUZyEvw9-2MN_8AJoGXtI_74LdvbkngnF1qqK5gIKYX05AyaI39M1-TXfT5Gq2ePu8XMKUR709pZPc22Pw9lni0a1I-UyfGMJJ1u0Pvh0HKO6atPREGFcXwvhOQsSHakAGuzHUE8V5ywUqpW3mNFE0xly-GK07BbGMicQZ-7F-9");
resultCParameterList.add("03AOLTBLSe4m_kEnKDI7Zg9oYjJ28Y1XKwS7uZ10NlWRoq-2e5tLG3r47ClAG3F_pTLqQC_krxnHkt8UBIV5EfW5goPvQQQN2W8ll3YYiMjWM_ge1_Z9lk_7M5KCriFBFhmPMB1MtMfeXS5LpcekGDSq9J2O20zySzNHXZ7xkvx5vJ327ioNoDEizwInEaLMG5tMqaWthqZE_-DZlpI7n3v35Glt3hNuT_utgSlBBqt7yipMuy-dunICsXT3Oxojeb-zcvPk3SFsWyMcTI-354eV1PfA-7q0mOGIADYuavzciBKjfZEHrsR9su48sHj_AH8RG4b_8yBzmR7WAj7Va-EhZzxDnlGdfBVqUo5wVUyJxlZe6NWkNkBwiaisxhC8As0VJIY0u_ugms");
}
@Test
public void testCheckBoxParsing() throws CaptchaNoJsHtmlParser.CaptchaNoJsV2ParsingError {
for (int i = 0; i < inputDataList.size(); ++i) {
String inputData = inputDataList.get(i);
CaptchaInfo captchaInfo = new CaptchaInfo();
parser.parseCheckboxes(inputData, captchaInfo);
for (int j = 0; j < captchaInfo.checkboxes.size(); j++) {
Integer checkbox = captchaInfo.checkboxes.get(j);
assertEquals(checkbox, resultCheckboxList.get(i).get(j));
}
}
}
@Test
public void testTitleParsing() throws CaptchaNoJsHtmlParser.CaptchaNoJsV2ParsingError {
for (int i = 0; i < inputDataList.size(); ++i) {
String inputData = inputDataList.get(i);
CaptchaInfo captchaInfo = new CaptchaInfo();
parser.parseChallengeTitle(inputData, captchaInfo);
String title = captchaInfo.captchaTitle.getTitle();
assertEquals(resultTitleList.get(i), title);
}
}
@Test
public void testCValueParsing() throws CaptchaNoJsHtmlParser.CaptchaNoJsV2ParsingError {
for (int i = 0; i < inputDataList.size(); ++i) {
String inputData = inputDataList.get(i);
CaptchaInfo captchaInfo = new CaptchaInfo();
parser.parseCParameter(inputData, captchaInfo);
String cParameter = captchaInfo.cParameter;
assertEquals(resultCParameterList.get(i), cParameter);
}
}
}
Loading…
Cancel
Save