Merge pull request #733 from chandevel/(#696)-new-captcha-window-clover

(#696) New captcha window
dev
Florens 6 years ago committed by GitHub
commit 0c58c619ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Clover/app/build.gradle
  2. 34
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  3. 2
      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. 4
      Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java
  6. 9
      Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java
  7. 10
      Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java
  8. 10
      Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java
  9. 27
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java
  10. 139
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaInfo.java
  11. 400
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java
  12. 303
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java
  13. 370
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java
  14. 152
      Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java
  15. 29
      Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java
  16. 46
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  17. 2
      Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
  18. 50
      Clover/app/src/main/java/org/floens/chan/ui/view/WrappingGridView.java
  19. 7
      Clover/app/src/main/java/org/floens/chan/utils/BackgroundUtils.java
  20. 12
      Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml
  21. 41
      Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml
  22. 113
      Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml
  23. 15
      Clover/app/src/main/res/values/strings.xml
  24. 106
      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'
} }

@ -279,10 +279,21 @@ 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;
// 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.requireResetAfterComplete()) {
authenticationLayout.reset(); 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 +400,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 +408,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 +425,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 +488,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 +538,9 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
ChanThread getThread(); ChanThread getThread();
void focusComment(); void focusComment();
void onFallbackToV1CaptchaView();
void destroyCurrentAuthentication();
} }
} }

@ -160,6 +160,7 @@ 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;
static { static {
SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences());
@ -250,6 +251,7 @@ 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);
// 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();
} }

@ -24,5 +24,9 @@ public interface AuthenticationLayoutInterface {
void reset(); void reset();
boolean requireResetAfterComplete();
void hardReset(); void hardReset();
void onDestroy();
} }

@ -121,6 +121,15 @@ public class CaptchaLayout extends WebView implements AuthenticationLayoutInterf
loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null); loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null);
} }
@Override
public boolean requireResetAfterComplete() {
return true;
}
@Override
public void onDestroy() {
}
private void onCaptchaLoaded() { private void onCaptchaLoaded() {
} }

@ -80,6 +80,16 @@ public class GenericWebViewAuthenticationLayout extends WebView implements Authe
public void hardReset() { public void hardReset() {
} }
@Override
public boolean requireResetAfterComplete() {
return true;
}
@Override
public void onDestroy() {
}
private void checkText() { private void checkText() {
loadUrl("javascript:WebInterface.onAllText(document.documentElement.textContent)"); loadUrl("javascript:WebInterface.onAllText(document.documentElement.textContent)");
} }

@ -142,6 +142,16 @@ public class LegacyCaptchaLayout extends LinearLayout implements AuthenticationL
input.requestFocus(); input.requestFocus();
} }
@Override
public boolean requireResetAfterComplete() {
return true;
}
@Override
public void onDestroy() {
}
private void submitCaptcha() { private void submitCaptcha() {
AndroidUtils.hideKeyboard(this); AndroidUtils.hideKeyboard(this);
callback.onAuthenticationComplete(this, challenge, input.getText().toString()); callback.onAuthenticationComplete(this, challenge, input.getText().toString());

@ -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);
@ -131,11 +133,20 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn
hardReset(); hardReset();
} }
@Override
public boolean requireResetAfterComplete() {
return true;
}
@Override @Override
public void hardReset() { public void hardReset() {
loadRecaptchaAndSetWebViewData(); loadRecaptchaAndSetWebViewData();
} }
@Override
public void onDestroy() {
}
private void loadRecaptchaAndSetWebViewData() { private void loadRecaptchaAndSetWebViewData() {
final String recaptchaUrl = "https://www.google.com/recaptcha/api/fallback?k=" + siteKey; final String recaptchaUrl = "https://www.google.com/recaptcha/api/fallback?k=" + siteKey;
@ -172,9 +183,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,400 @@
/*
* 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 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 (Exception error) {
throw new CaptchaNoJsV2ParsingError("Could not parse verification token", 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 (Exception error) {
throw new CaptchaNoJsV2ParsingError("Error while trying to parse challenge title", error);
}
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 (Exception error) {
throw new CaptchaNoJsV2ParsingError("Error while trying to parse challenge image url", 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 (Exception error) {
throw new CaptchaNoJsV2ParsingError("Error while trying to parse c parameter", 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 (Exception error) {
throw new CaptchaNoJsV2ParsingError("Error while trying to parse checkbox with id (" + index + ")", error);
}
++index;
}
if (checkboxesSet.isEmpty()) {
throw new CaptchaNoJsV2ParsingError("Could not parse any checkboxes!");
}
CaptchaInfo.CaptchaType captchaType;
try {
captchaType = CaptchaInfo.CaptchaType.fromCheckboxesCount(checkboxesSet.size());
} catch (Exception error) {
throw new CaptchaNoJsV2ParsingError("Error while trying to parse captcha type", 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,
row * imageWidth,
column * imageHeight,
imageWidth,
imageHeight);
resultImages.add(imagePiece);
}
}
return resultImages;
} catch (Exception 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);
}
public CaptchaNoJsV2ParsingError(String message, Throwable cause) {
super(message, cause);
}
}
}

@ -0,0 +1,303 @@
/*
* 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.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 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);
ConstraintLayout buttonsHolder = view.findViewById(R.id.captcha_layout_v2_buttons_holder);
ScrollView 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));
captchaVerifyButton.setOnClickListener(this);
useOldCaptchaButton.setOnClickListener(this);
reloadCaptchaButton.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 boolean requireResetAfterComplete() {
return false;
}
@Override
public void onDestroy() {
adapter.onDestroy();
presenter.onDestroy();
}
@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);
});
}
// 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();
}
}
private void renderCaptchaWindow(CaptchaInfo captchaInfo) {
try {
setCaptchaTitle(captchaInfo);
captchaImagesGrid.setAdapter(null);
captchaImagesGrid.setAdapter(adapter);
int columnsCount;
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) {
Logger.e(TAG, "renderCaptchaWindow", error);
if (callback != null) {
callback.onFallbackToV1CaptchaView();
}
}
}
private void setCaptchaTitle(CaptchaInfo captchaInfo) {
if (captchaInfo.getCaptchaTitle() == null) {
return;
}
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);
}
}
}

@ -0,0 +1,370 @@
/*
* 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.Nullable;
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.atomic.AtomicBoolean;
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 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 int SUCCESS_STATUS_CODE = 200;
private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L;
// 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;
private AtomicBoolean verificationInProgress = new AtomicBoolean(false);
private AtomicBoolean captchaRequestInProgress = 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);
}
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");
}
executor.submit(() -> {
try {
String recaptchaUrl = recaptchaUrlBase + siteKey;
RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds);
Logger.d(TAG, "Verify called");
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", defaultGoogleCookies)
.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;
}
}
/**
* 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 {
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;
} 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;
}
}
@Nullable
private CaptchaInfo getCaptchaInfo() throws IOException {
if (BackgroundUtils.isMainThread()) {
throw new RuntimeException("Must not be executed on the main thread");
}
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", defaultGoogleCookies)
.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);
}
@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 (Exception 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 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,152 @@
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.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.BaseAdapter;
import org.floens.chan.R;
import java.util.ArrayList;
import java.util.List;
public class CaptchaNoJsV2Adapter extends BaseAdapter {
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);
imageView.setScaleX(1.0f);
imageView.setScaleY(1.0f);
ConstraintLayout blueCheckmarkHolder = convertView.findViewById(R.id.captcha_challenge_blue_checkmark_holder);
blueCheckmarkHolder.setAlpha(0.0f);
ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(imageSize, imageSize);
imageView.setLayoutParams(layoutParams);
imageView.setOnClickListener((view) -> {
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
imageList.get(position).toggleChecked();
boolean isChecked = imageList.get(position).isChecked;
imageView.animate()
.scaleX(isChecked ? 0.8f : 1.0f)
.scaleY(isChecked ? 0.8f : 1.0f)
.setInterpolator(new DecelerateInterpolator(2.0f))
.setDuration(200)
.start();
blueCheckmarkHolder.animate()
.alpha(isChecked ? 1.0f : 0.0f)
.setInterpolator(new DecelerateInterpolator(2.0f))
.setDuration(200)
.start();
});
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
@ -66,7 +63,17 @@ public class BehaviourSettingsController extends SettingsController {
} }
} }
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 +159,18 @@ 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));
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,17 @@ public class ReplyLayout extends LoadView implements
authenticationLayout.reset(); authenticationLayout.reset();
} }
@Override
public void destroyCurrentAuthentication() {
if (authenticationLayout == null) {
return;
}
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 +484,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);
}
}

@ -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,41 @@
<?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:padding="1dp">
<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:alpha="0"
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" tools:locale="en">
<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,17 @@ 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="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,106 @@
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