From d8aef6e3330c94d283cc85cfa435cba36cf591d7 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 28 Apr 2019 19:51:02 +0300 Subject: [PATCH 01/19] Implement new captcha window --- .../chan/core/presenter/ReplyPresenter.java | 24 +- .../captcha/AuthenticationLayoutCallback.java | 1 + .../CaptchaNojsLayoutV1.java} | 18 +- .../chan/ui/captcha/v2/CaptchaInfo.java | 139 ++++++ .../ui/captcha/v2/CaptchaNoJsHtmlParser.java | 398 ++++++++++++++++++ .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 274 ++++++++++++ .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 347 +++++++++++++++ .../ui/captcha/v2/CaptchaNoJsV2Adapter.java | 137 ++++++ .../floens/chan/ui/layout/ReplyLayout.java | 51 ++- .../org/floens/chan/utils/AndroidUtils.java | 36 ++ .../res/drawable/background_rectangle.xml | 4 + .../res/drawable/ic_blue_checkmark_24dp.xml | 5 + .../layout/layout_captcha_challenge_image.xml | 31 ++ .../res/layout/layout_captcha_nojs_v2.xml | 93 ++++ Clover/app/src/main/res/values/strings.xml | 2 +- 15 files changed, 1543 insertions(+), 17 deletions(-) rename Clover/app/src/main/java/org/floens/chan/ui/captcha/{CaptchaNojsLayout.java => v1/CaptchaNojsLayoutV1.java} (89%) create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaInfo.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java create mode 100644 Clover/app/src/main/res/drawable/background_rectangle.xml create mode 100644 Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml create mode 100644 Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml create mode 100644 Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 28e8d427..6901551b 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -283,6 +283,11 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe makeSubmitCall(); } + @Override + public void onFallbackToV1CaptchaView() { + callback.onFallbackToV1CaptchaView(); + } + public void onCommentTextChanged(CharSequence text) { int length = text.toString().getBytes(UTF_8).length; callback.updateCommentCount(length, board.maxCommentChars, length > board.maxCommentChars); @@ -396,8 +401,13 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe switchPage(Page.LOADING, true); } - private void switchPage(Page page, boolean animate) { - if (this.page != page) { + public void switchPage(Page page, boolean animate) { + // by default try to use the new nojs captcha view + switchPage(page, animate, true); + } + + public void switchPage(Page page, boolean animate, boolean useV2NoJsCaptcha) { + if (!useV2NoJsCaptcha || this.page != page) { this.page = page; switch (page) { case LOADING: @@ -470,8 +480,10 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe void setPage(Page page, boolean animate); - void initializeAuthentication(Site site, SiteAuthentication authentication, - AuthenticationLayoutCallback callback); + void initializeAuthentication(Site site, + SiteAuthentication authentication, + AuthenticationLayoutCallback callback, + boolean useV2NoJsCaptcha); void resetAuthentication(); @@ -518,5 +530,9 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe ChanThread getThread(); void focusComment(); + + void onFallbackToV1CaptchaView(); + + void destroyCurrentAuthentication(); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java index 019f85f8..10e6cc0e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java @@ -20,4 +20,5 @@ package org.floens.chan.ui.captcha; public interface AuthenticationLayoutCallback { void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response); + void onFallbackToV1CaptchaView(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java similarity index 89% rename from Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java rename to Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java index 254b0daa..819e51e9 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.floens.chan.ui.captcha; +package org.floens.chan.ui.captcha.v1; import android.annotation.SuppressLint; 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.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.Logger; @@ -48,7 +50,7 @@ import okhttp3.ResponseBody; * 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. */ -public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutInterface { +public class CaptchaNojsLayoutV1 extends WebView implements AuthenticationLayoutInterface { private static final String TAG = "CaptchaNojsLayout"; private AuthenticationLayoutCallback callback; @@ -59,15 +61,15 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn private String webviewUserAgent; - public CaptchaNojsLayout(Context context) { + public CaptchaNojsLayoutV1(Context context) { super(context); } - public CaptchaNojsLayout(Context context, AttributeSet attrs) { + public CaptchaNojsLayoutV1(Context context, AttributeSet attrs) { super(context, attrs); } - public CaptchaNojsLayout(Context context, AttributeSet attrs, int defStyle) { + public CaptchaNojsLayoutV1(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @@ -114,7 +116,7 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn return false; } - if (host.equals(Uri.parse(CaptchaNojsLayout.this.baseUrl).getHost())) { + if (host.equals(Uri.parse(CaptchaNojsLayoutV1.this.baseUrl).getHost())) { return false; } else { AndroidUtils.openLink(url); @@ -172,9 +174,9 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn } public static class CaptchaInterface { - private final CaptchaNojsLayout layout; + private final CaptchaNojsLayoutV1 layout; - public CaptchaInterface(CaptchaNojsLayout layout) { + public CaptchaInterface(CaptchaNojsLayoutV1 layout) { this.layout = layout; } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaInfo.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaInfo.java new file mode 100644 index 00000000..1a878ab5 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaInfo.java @@ -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 . + */ +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 checkboxes; + @Nullable + String cParameter; + @NonNull + List 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 checkboxes) { + this.checkboxes = checkboxes; + } + + public void setcParameter(@Nullable String cParameter) { + this.cParameter = cParameter; + } + + public void setChallengeImages(@NonNull List challengeImages) { + this.challengeImages = challengeImages; + } + + public void setCaptchaTitle(@Nullable CaptchaTitle captchaTitle) { + this.captchaTitle = captchaTitle; + } + + public CaptchaType getCaptchaType() { + return captchaType; + } + + @NonNull + public List getChallengeImages() { + return challengeImages; + } + + @NonNull + public List 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; + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java new file mode 100644 index 00000000..19663588 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java @@ -0,0 +1,398 @@ +/* + * 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 . + */ +package org.floens.chan.ui.captcha.v2; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.support.annotation.NonNull; +import android.support.v4.util.Pair; + +import org.floens.chan.utils.BackgroundUtils; +import org.floens.chan.utils.IOUtils; +import org.floens.chan.utils.Logger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class CaptchaNoJsHtmlParser { + private static final String TAG = "CaptchaNoJsHtmlParser"; + private static final String googleBaseUrl = "https://www.google.com"; + private static final Pattern checkboxesPattern = + Pattern.compile(""); + + // 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(""); + private static final Pattern challengeTitlePattern = + Pattern.compile("
(.*?)
"); + private static final Pattern challengeImageUrlPattern = + Pattern.compile("(.*?)"); + private static final Pattern verificationTokenPattern = + Pattern.compile("
"); + private static final String CHALLENGE_IMAGE_FILE_NAME = "challenge_image_file"; + private static final int SUCCESS_STATUS_CODE = 200; + + private OkHttpClient okHttpClient; + private Context context; + + public CaptchaNoJsHtmlParser(Context context, OkHttpClient okHttpClient) { + this.context = context; + this.okHttpClient = okHttpClient; + } + + @NonNull + public CaptchaInfo parseHtml(String responseHtml, String siteKey) + throws CaptchaNoJsV2ParsingError, IOException { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Must not be executed on the main thread!"); + } + + CaptchaInfo captchaInfo = new CaptchaInfo(); + + // parse challenge checkboxes' ids + parseCheckboxes(responseHtml, captchaInfo); + + // parse captcha random key + parseCParameter(responseHtml, captchaInfo); + + // parse title + parseChallengeTitle(responseHtml, captchaInfo); + + // parse image url, download image and split it into list of separate images + parseAndDownloadChallengeImage(responseHtml, captchaInfo, siteKey); + + return captchaInfo; + } + + @NonNull + String parseVerificationToken(String responseHtml) throws CaptchaNoJsV2ParsingError { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Must not be executed on the main thread!"); + } + + Matcher matcher = verificationTokenPattern.matcher(responseHtml); + if (!matcher.find()) { + throw new CaptchaNoJsV2ParsingError("Could not parse verification token"); + } + + String token; + + try { + token = matcher.group(1); + } catch (Throwable error) { + Logger.e(TAG, "Could not parse verification token", error); + throw error; + } + + if (token == null || token.isEmpty()) { + throw new CaptchaNoJsV2ParsingError("Verification token is null or empty"); + } + + return token; + } + + private 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(""); + + String firstPart = title.substring(0, start); + String boldPart = titleMatcher.group(1); + String resultTitle = firstPart + boldPart; + + captchaTitle = new CaptchaInfo.CaptchaTitle( + resultTitle, + firstPart.length(), + firstPart.length() + boldPart.length()); + } else { + // could not find it + captchaTitle = new CaptchaInfo.CaptchaTitle(title, -1, -1); + } + } catch (Throwable error) { + Logger.e(TAG, "Error while trying to parse challenge title", error); + throw error; + } + + if (captchaTitle == null) { + throw new CaptchaNoJsV2ParsingError("challengeTitle is null"); + } + + if (captchaTitle.isEmpty()) { + throw new CaptchaNoJsV2ParsingError("challengeTitle is empty"); + } + + captchaInfo.setCaptchaTitle(captchaTitle); + } + + private void parseAndDownloadChallengeImage( + String responseHtml, + CaptchaInfo captchaInfo, + String siteKey + ) throws CaptchaNoJsV2ParsingError, IOException { + Matcher matcher = challengeImageUrlPattern.matcher(responseHtml); + if (!matcher.find()) { + throw new CaptchaNoJsV2ParsingError("Could not parse challenge image url"); + } + + String challengeImageUrl; + + try { + challengeImageUrl = matcher.group(1); + } catch (Throwable error) { + Logger.e(TAG, "Error while trying to parse challenge image url", error); + throw error; + } + + if (challengeImageUrl == null) { + throw new CaptchaNoJsV2ParsingError("challengeImageUrl is null"); + } + + if (challengeImageUrl.isEmpty()) { + throw new CaptchaNoJsV2ParsingError("challengeImageUrl is empty"); + } + + String fullUrl = googleBaseUrl + challengeImageUrl + "&k=" + siteKey; + File challengeImageFile = downloadAndStoreImage(fullUrl); + + Pair 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 challengeImages = decodeImagesFromFile(challengeImageFile, columns, rows); + captchaInfo.setChallengeImages(challengeImages); + } + + @NonNull + private Pair 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"); + } + } + } + + private void parseCParameter( + String responseHtml, + CaptchaInfo captchaInfo + ) throws CaptchaNoJsV2ParsingError { + Matcher matcher = cParameterPattern.matcher(responseHtml); + if (!matcher.find()) { + throw new CaptchaNoJsV2ParsingError("Could not parse c parameter"); + } + + String cParameter; + + try { + cParameter = matcher.group(1); + } catch (Throwable error) { + Logger.e(TAG, "Error while trying to parse c parameter", error); + throw error; + } + + if (cParameter == null) { + throw new CaptchaNoJsV2ParsingError("cParameter is null"); + } + + if (cParameter.isEmpty()) { + throw new CaptchaNoJsV2ParsingError("cParameter is empty"); + } + + captchaInfo.setcParameter(cParameter); + } + + private void parseCheckboxes( + String responseHtml, + CaptchaInfo captchaInfo + ) throws CaptchaNoJsV2ParsingError { + Matcher matcher = checkboxesPattern.matcher(responseHtml); + Set checkboxesSet = new HashSet<>(matcher.groupCount()); + int index = 0; + + while (matcher.find()) { + try { + Integer checkboxId = Integer.parseInt(matcher.group(1)); + checkboxesSet.add(checkboxId); + } catch (Throwable error) { + Logger.e(TAG, "Error while trying to parse checkbox with id (" + index + ")", error); + throw error; + } + + ++index; + } + + if (checkboxesSet.isEmpty()) { + throw new CaptchaNoJsV2ParsingError("Could not parse any checkboxes!"); + } + + CaptchaInfo.CaptchaType captchaType; + + try { + captchaType = CaptchaInfo.CaptchaType.fromCheckboxesCount(checkboxesSet.size()); + } catch (Throwable error) { + Logger.e(TAG, "Error while trying to parse captcha type", error); + throw error; + } + + if (captchaType == CaptchaInfo.CaptchaType.Unknown) { + throw new CaptchaNoJsV2ParsingError("Unknown captcha type"); + } + + captchaInfo.setCaptchaType(captchaType); + captchaInfo.setCheckboxes(new ArrayList<>(checkboxesSet)); + } + + @NonNull + private File downloadAndStoreImage(String fullUrl) throws IOException, CaptchaNoJsV2ParsingError { + Request request = new Request.Builder() + .url(fullUrl) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (response.code() != SUCCESS_STATUS_CODE) { + throw new CaptchaNoJsV2ParsingError( + "Could not download challenge image, status code = " + response.code()); + } + + if (response.body() == null) { + throw new CaptchaNoJsV2ParsingError("Captcha challenge image body is null"); + } + + try (InputStream is = response.body().byteStream()) { + File imageFile = getChallengeImageFile(); + + try (OutputStream os = new FileOutputStream(imageFile)) { + IOUtils.copy(is, os); + } + + return imageFile; + } + } + } + + private File getChallengeImageFile() throws CaptchaNoJsV2ParsingError, IOException { + File imageFile = new File(context.getCacheDir(), CHALLENGE_IMAGE_FILE_NAME); + + if (imageFile.exists()) { + if (!imageFile.delete()) { + throw new CaptchaNoJsV2ParsingError( + "Could not delete old challenge image file \"" + imageFile.getAbsolutePath() + "\""); + } + } + + if (!imageFile.createNewFile()) { + throw new CaptchaNoJsV2ParsingError( + "Could not create challenge image file \"" + imageFile.getAbsolutePath() + "\""); + } + + if (!imageFile.canWrite()) { + throw new CaptchaNoJsV2ParsingError( + "Cannot write to the challenge image file \"" + imageFile.getAbsolutePath() + "\""); + } + + return imageFile; + } + + private List decodeImagesFromFile(File challengeImageFile, int columns, int rows) { + Bitmap originalBitmap = BitmapFactory.decodeFile(challengeImageFile.getAbsolutePath()); + List resultImages = new ArrayList<>(columns * rows); + + try { + int imageWidth = originalBitmap.getWidth() / columns; + int imageHeight = originalBitmap.getHeight() / rows; + + for (int column = 0; column < columns; ++column) { + for (int row = 0; row < rows; ++row) { + Bitmap imagePiece = Bitmap.createBitmap( + originalBitmap, + column * imageWidth, + row * imageHeight, + imageWidth, + imageHeight); + + resultImages.add(imagePiece); + } + } + + return resultImages; + } finally { + if (originalBitmap != null) { + originalBitmap.recycle(); + } + } + } + + public static class CaptchaNoJsV2ParsingError extends Exception { + public CaptchaNoJsV2ParsingError(String message) { + super(message); + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java new file mode 100644 index 00000000..b9537de8 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -0,0 +1,274 @@ +/* + * 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 . + */ +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.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 ConstraintLayout buttonsHolder; + private ConstraintLayout background; + + private CaptchaNoJsV2Adapter adapter; + private CaptchaNoJsPresenterV2 presenter; + private Context context; + private AuthenticationLayoutCallback callback; + + public CaptchaNoJsLayoutV2(@NonNull Context context) { + super(context); + init(context); + } + + public CaptchaNoJsLayoutV2(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public CaptchaNoJsLayoutV2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + this.context = context; + this.presenter = new CaptchaNoJsPresenterV2(this, context); + this.adapter = new CaptchaNoJsV2Adapter(context); + + View view = inflate(context, R.layout.layout_captcha_nojs_v2, this); + + captchaChallengeTitle = view.findViewById(R.id.captcha_layout_v2_title); + captchaImagesGrid = view.findViewById(R.id.captcha_layout_v2_images_grid); + captchaVerifyButton = view.findViewById(R.id.captcha_layout_v2_verify_button); + useOldCaptchaButton = view.findViewById(R.id.captcha_layout_v2_use_old_captcha_button); + reloadCaptchaButton = view.findViewById(R.id.captcha_layout_v2_reload_button); + buttonsHolder = view.findViewById(R.id.captcha_layout_v2_buttons_holder); + background = view.findViewById(R.id.captcha_layout_v2_background); + + background.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor)); + buttonsHolder.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor_secondary)); + captchaChallengeTitle.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor_secondary)); + captchaChallengeTitle.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); + captchaVerifyButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); + useOldCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); + reloadCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); + + captchaVerifyButton.setOnClickListener(this); + useOldCaptchaButton.setOnClickListener(this); + reloadCaptchaButton.setOnClickListener(this); + } + + @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; + } + } + + @Override + public void onCaptchaInfoParsed(CaptchaInfo captchaInfo) { + // called on a background thread + + AndroidUtils.runOnUiThread(() -> { + renderCaptchaWindow(captchaInfo); + }); + } + + @Override + public void onVerificationDone(String verificationToken) { + // called on a background thread + + AndroidUtils.runOnUiThread(() -> { + callback.onAuthenticationComplete(this, null, verificationToken); + }); + } + + @Override + public void onCaptchaInfoParseError(Throwable error) { + // called on a background thread + + AndroidUtils.runOnUiThread(() -> { + Logger.e(TAG, "CaptchaV2 error", error); + + String message = error.getMessage(); + showToast(message); + + 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 = 0; + int imageSize = captchaImagesGrid.getWidth(); + + switch (captchaInfo.getCaptchaType()) { + case Canonical: + columnsCount = 3; + imageSize /= columnsCount; + break; + case NoCanonical: + columnsCount = 2; + imageSize /= columnsCount; + break; + default: + throw new IllegalStateException("Unknown captcha type"); + } + + captchaImagesGrid.setNumColumns(columnsCount); + + adapter.setImageSize(imageSize); + adapter.setImages(captchaInfo.challengeImages); + } catch (Throwable error) { + if (callback != null) { + callback.onFallbackToV1CaptchaView(); + } + } + } + + private void setCaptchaTitle(CaptchaInfo captchaInfo) { + if (captchaInfo.getCaptchaTitle().hasBold()) { + SpannableString spannableString = new SpannableString(captchaInfo.getCaptchaTitle().getTitle()); + spannableString.setSpan( + new StyleSpan(Typeface.BOLD), + captchaInfo.getCaptchaTitle().getBoldStart(), + captchaInfo.getCaptchaTitle().getBoldEnd(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + captchaChallengeTitle.setText(spannableString); + } else { + captchaChallengeTitle.setText(captchaInfo.getCaptchaTitle().getTitle()); + } + } + + private void sendVerificationResponse() { + List selectedIds = adapter.getCheckedImageIds(); + + try { + + CaptchaNoJsPresenterV2.VerifyError verifyError = presenter.verify(selectedIds); + switch (verifyError) { + case Ok: + 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; + } + } catch (Throwable error) { + onCaptchaInfoParseError(error); + } + } + + public void onDestroy() { + adapter.onDestroy(); + presenter.onDestroy(); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java new file mode 100644 index 00000000..2cfe7392 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -0,0 +1,347 @@ +/* + * 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 . + */ +package org.floens.chan.ui.captcha.v2; + +import android.content.Context; +import android.support.annotation.Nullable; + +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.Call; +import okhttp3.Callback; +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"; + // TODO: change useragent? + private static final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"; + 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 should be updated once in 3 months IIRC + private static final String googleCookies = + "SID=NwcOCOnxA7PVDPUY_InSzz6saQ93BmWzg7N2276OPwUvtwXkLpfqbs1tYNkxNP0xC_Awmg.; " + + "HSID=AoZX0-3xQ7lN9EMEm; " + + "SSID=AWwVsHei8i4VlX3H4; " + + "NID=182=ikM1ZwJ0tMSqCiJc8gdretZpOHCnUSsZ2oM7I681KmnkL0DKFOBmFU6zlmTMC_mDnPhBB5mQxsaB0ipTICW9WyCa9nLUbo1Bx-H9jFmOLNhr1E5EezqlyFZcBKEyZgjFYmaY79_5jHMa4uE6v_Vb8HR1Uk9CXbNfw_yIPEsZXZ0t0CURkV-CdxdOIiZ4BAy0oE6w60GJ9kLpOWWSeNZ_lBcn0PkWRj6vmRH6kKrl2exOKnk"; + + // 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 lastTimeCaptchRequest = 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 + */ + public VerifyError verify( + List selectedIds + ) throws CaptchaNoJsV2Error, UnsupportedEncodingException { + if (!verificationInProgress.compareAndSet(false, true)) { + return VerifyError.AlreadyInProgress; + } + + 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"); + } + + String recaptchaUrl = recaptchaUrlBase + siteKey; + RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds); + + Request request = new Request.Builder() + .url(recaptchaUrl) + .post(body) + .header("User-Agent", userAgent) + .header("Referer", recaptchaUrl) + .header("Accept-Language", "en-US") + .header("Cookie", googleCookies) + .build(); + + okHttpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + if (callbacks != null) { + try { + callbacks.onCaptchaInfoParseError(e); + } finally { + verificationInProgress.set(false); + } + } + } + + @Override + public void onResponse(Call call, Response response) { + executor.execute(() -> { + // to avoid okhttp's threads to hang + + try { + prevCaptchaInfo = handleGetRecaptchaResponse(response); + } finally { + verificationInProgress.set(false); + response.close(); + } + }); + } + }); + + 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)) { + return RequestCaptchaInfoError.AlreadyInProgress; + } + + try { + // recaptcha may become very angry at you if your are fetching it too fast + if (System.currentTimeMillis() - lastTimeCaptchRequest < CAPTCHA_REQUEST_THROTTLE_MS) { + captchaRequestInProgress.set(false); + return RequestCaptchaInfoError.HoldYourHorses; + } + + lastTimeCaptchRequest = System.currentTimeMillis(); + String recaptchaUrl = recaptchaUrlBase + siteKey; + + Request request = new Request.Builder() + .url(recaptchaUrl) + .header("User-Agent", userAgent) + .header("Referer", baseUrl) + .header("Accept-Language", "en-US") + .header("Cookie", googleCookies) + .build(); + + okHttpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + if (callbacks != null) { + try { + callbacks.onCaptchaInfoParseError(e); + } finally { + captchaRequestInProgress.set(false); + } + } + } + + @Override + public void onResponse(Call call, Response response) { + executor.execute(() -> { + // to avoid okhttp's threads to hang + + try { + prevCaptchaInfo = handleGetRecaptchaResponse(response); + } finally { + captchaRequestInProgress.set(false); + response.close(); + } + }); + } + }); + + 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; + } + } + + private RequestBody createResponseBody( + CaptchaInfo prevCaptchaInfo, + List 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); + + if (callbacks != null) { + callbacks.onVerificationDone(verificationToken); + } + + return null; + } else { + // got the challenge + CaptchaInfo captchaInfo = parser.parseHtml(bodyString, siteKey); + + if (callbacks != null) { + callbacks.onCaptchaInfoParsed(captchaInfo); + } + + return captchaInfo; + } + } catch (Throwable e) { + Logger.e(TAG, "Error while trying to parse captcha html data", e); + + if (callbacks != null) { + callbacks.onCaptchaInfoParseError(e); + } + + return null; + } + } + + public void onDestroy() { + this.callbacks = null; + this.prevCaptchaInfo = null; + this.verificationInProgress.set(false); + this.captchaRequestInProgress.set(false); + + executor.shutdown(); + } + + public enum VerifyError { + Ok, + NoImagesSelected, + AlreadyInProgress + } + + public enum RequestCaptchaInfoError { + Ok, + AlreadyInProgress, + HoldYourHorses + } + + 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); + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java new file mode 100644 index 00000000..7bade2e6 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java @@ -0,0 +1,137 @@ +package org.floens.chan.ui.captcha.v2; + +import android.content.Context; +import android.graphics.Bitmap; +import android.support.constraint.ConstraintLayout; +import android.support.v7.widget.AppCompatImageView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import org.floens.chan.R; +import org.floens.chan.utils.AndroidUtils; + +import java.util.ArrayList; +import java.util.List; + +public class CaptchaNoJsV2Adapter extends BaseAdapter { + private LayoutInflater inflater; + private int imageSize = 0; + + private List imageList = new ArrayList<>(); + + public CaptchaNoJsV2Adapter(Context context) { + this.inflater = LayoutInflater.from(context); + } + + public void setImages(List 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); + AppCompatImageView blueCheckmark = convertView.findViewById(R.id.captcha_challenge_blue_checkmark); + + ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(imageSize, imageSize); + imageView.setLayoutParams(layoutParams); + + imageView.setOnClickListener((view) -> { + imageList.get(position).toggleChecked(); + boolean isChecked = imageList.get(position).isChecked; + + AndroidUtils.animateViewScale(imageView, isChecked); + blueCheckmark.setVisibility(isChecked ? View.VISIBLE : View.GONE); + }); + + if (position >= 0 && position <= imageList.size()) { + imageView.setImageBitmap(imageList.get(position).getBitmap()); + } + } + + return convertView; + } + + public List getCheckedImageIds() { + List 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(); + } + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index 0286f130..62d9421e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -49,9 +49,10 @@ import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; import org.floens.chan.ui.captcha.AuthenticationLayoutInterface; 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.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.helper.ImagePickDelegate; import org.floens.chan.ui.view.LoadView; @@ -254,8 +255,10 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply } @Override - public void initializeAuthentication(Site site, SiteAuthentication authentication, - AuthenticationLayoutCallback callback) { + public void initializeAuthentication(Site site, + SiteAuthentication authentication, + AuthenticationLayoutCallback callback, + boolean useV2NoJsCaptcha) { if (authenticationLayout == null) { switch (authentication.type) { case CAPTCHA1: { @@ -269,7 +272,25 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply break; } case CAPTCHA2_NOJS: - authenticationLayout = new CaptchaNojsLayout(getContext()); + if (!useV2NoJsCaptcha) { + // default webview-based captcha view + authenticationLayout = new CaptchaNojsLayoutV1(getContext()); + } else { + // new captcha window without webview + authenticationLayout = new CaptchaNoJsLayoutV2(getContext()); + } + + ImageView resetButton = captchaContainer.findViewById(R.id.reset); + if (resetButton != null) { + if (!useV2NoJsCaptcha) { + // restore the button's visibility when using old v1 captcha view + resetButton.setVisibility(View.VISIBLE); + } else { + // we don't need the default reset button because we have our own + resetButton.setVisibility(View.GONE); + } + } + break; case GENERIC_WEBVIEW: { GenericWebViewAuthenticationLayout view = new GenericWebViewAuthenticationLayout(getContext()); @@ -334,6 +355,22 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply authenticationLayout.reset(); } + @Override + public void destroyCurrentAuthentication() { + if (authenticationLayout == null) { + return; + } + + if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) { + return; + } + + // cleanup resources when switching from the new to the old captcha view + ((CaptchaNoJsLayoutV2) authenticationLayout).onDestroy(); + captchaContainer.removeView((CaptchaNoJsLayoutV2) authenticationLayout); + authenticationLayout = null; + } + @Override public void loadDraftIntoViews(Reply draft) { name.setText(draft.name); @@ -448,6 +485,12 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply comment.post(() -> AndroidUtils.requestViewAndKeyboardFocus(comment)); } + @Override + public void onFallbackToV1CaptchaView() { + // fallback to v1 captcha window + presenter.switchPage(ReplyPresenter.Page.AUTHENTICATION, true, false); + } + @Override public void openPreview(boolean show, File previewFile) { if (show) { diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java index b408942d..07b8bb06 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -49,6 +49,8 @@ import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; import android.widget.Toast; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.ScaleAnimation; import org.floens.chan.R; @@ -452,4 +454,38 @@ public class AndroidUtils { NetworkInfo networkInfo = connectivityManager.getNetworkInfo(type); return networkInfo != null && networkInfo.isConnected(); } + + public static void animateViewScale(View view, boolean zoomOut) { + ScaleAnimation scaleAnimation; + final float normalScale = 1.0f; + final float zoomOutScale = 0.8f; + + if (zoomOut) { + scaleAnimation = new ScaleAnimation( + normalScale, + zoomOutScale, + normalScale, + zoomOutScale, + ScaleAnimation.RELATIVE_TO_SELF, + 0.5f, + ScaleAnimation.RELATIVE_TO_SELF, + 0.5f); + } else { + scaleAnimation = new ScaleAnimation( + zoomOutScale, + normalScale, + zoomOutScale, + normalScale, + ScaleAnimation.RELATIVE_TO_SELF, + 0.5f, + ScaleAnimation.RELATIVE_TO_SELF, + 0.5f); + } + + scaleAnimation.setDuration(50); + scaleAnimation.setFillAfter(true); + scaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); + + view.startAnimation(scaleAnimation); + } } diff --git a/Clover/app/src/main/res/drawable/background_rectangle.xml b/Clover/app/src/main/res/drawable/background_rectangle.xml new file mode 100644 index 00000000..89e226bc --- /dev/null +++ b/Clover/app/src/main/res/drawable/background_rectangle.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml b/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml new file mode 100644 index 00000000..9e2adec1 --- /dev/null +++ b/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml new file mode 100644 index 00000000..cb54eaa3 --- /dev/null +++ b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml b/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml new file mode 100644 index 00000000..a8d933f4 --- /dev/null +++ b/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 71002459..b3d9994d 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ 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 . --> - + Cancel Add Close From 110912006124e7b8c06f8ef0ffb5e53d76c0af96 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 28 Apr 2019 19:55:16 +0300 Subject: [PATCH 02/19] Minor fixes --- .../chan/core/presenter/ReplyPresenter.java | 5 +- .../floens/chan/ui/view/WrappingGridView.java | 50 +++++++++++++++++++ .../floens/chan/utils/BackgroundUtils.java | 7 +++ Clover/app/src/main/res/values/strings.xml | 7 +++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/view/WrappingGridView.java diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 6901551b..1d32dc20 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -394,6 +394,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe callback.openSpoiler(false, false); callback.openPreview(false, null); callback.openPreviewMessage(false, null); + callback.destroyCurrentAuthentication(); } private void makeSubmitCall() { @@ -419,7 +420,9 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe case AUTHENTICATION: 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); break; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/WrappingGridView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/WrappingGridView.java new file mode 100644 index 00000000..7e681c07 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/WrappingGridView.java @@ -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 . + */ +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); + } + +} \ No newline at end of file diff --git a/Clover/app/src/main/java/org/floens/chan/utils/BackgroundUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/BackgroundUtils.java index 6062d117..db57cc5b 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/BackgroundUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/BackgroundUtils.java @@ -17,11 +17,18 @@ */ package org.floens.chan.utils; +import android.os.Looper; + import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; public class BackgroundUtils { + + public static boolean isMainThread() { + return Thread.currentThread() == Looper.getMainLooper().getThread(); + } + public static Cancelable runWithExecutor(Executor executor, final Callable background, final BackgroundResult result) { final AtomicBoolean cancelled = new AtomicBoolean(false); diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index b3d9994d..18685f89 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -577,4 +577,11 @@ Don't have a 4chan Pass?
" Themes + Verify + Reload + Use old captcha + You have to selected 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) + Verification is already in progress + Captcha request is already in progress + You are requesting captcha too fast
From 6b865c246e111156a9fe3d54f93f27aa094b4e40 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 28 Apr 2019 20:34:45 +0300 Subject: [PATCH 03/19] Post review fixes --- .../java/org/floens/chan/core/presenter/ReplyPresenter.java | 2 ++ .../org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java | 4 +++- .../app/src/main/java/org/floens/chan/utils/AndroidUtils.java | 4 ++-- Clover/app/src/main/res/drawable/background_rectangle.xml | 4 ---- .../src/main/res/layout/layout_captcha_challenge_image.xml | 2 -- Clover/app/src/main/res/values/strings.xml | 2 +- 6 files changed, 8 insertions(+), 10 deletions(-) delete mode 100644 Clover/app/src/main/res/drawable/background_rectangle.xml diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 1d32dc20..06a053a0 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -279,6 +279,8 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe public void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response) { draft.captchaChallenge = challenge; draft.captchaResponse = response; + + // should this be called here? authenticationLayout.reset(); makeSubmitCall(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java index 7bade2e6..72f7c46e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java @@ -16,6 +16,8 @@ import java.util.ArrayList; import java.util.List; public class CaptchaNoJsV2Adapter extends BaseAdapter { + private static final int ANIMATION_DURATION = 50; + private LayoutInflater inflater; private int imageSize = 0; @@ -65,7 +67,7 @@ public class CaptchaNoJsV2Adapter extends BaseAdapter { imageList.get(position).toggleChecked(); boolean isChecked = imageList.get(position).isChecked; - AndroidUtils.animateViewScale(imageView, isChecked); + AndroidUtils.animateViewScale(imageView, isChecked, ANIMATION_DURATION); blueCheckmark.setVisibility(isChecked ? View.VISIBLE : View.GONE); }); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java index 07b8bb06..8cd83a67 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -455,7 +455,7 @@ public class AndroidUtils { return networkInfo != null && networkInfo.isConnected(); } - public static void animateViewScale(View view, boolean zoomOut) { + public static void animateViewScale(View view, boolean zoomOut, int duration) { ScaleAnimation scaleAnimation; final float normalScale = 1.0f; final float zoomOutScale = 0.8f; @@ -482,7 +482,7 @@ public class AndroidUtils { 0.5f); } - scaleAnimation.setDuration(50); + scaleAnimation.setDuration(duration); scaleAnimation.setFillAfter(true); scaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); diff --git a/Clover/app/src/main/res/drawable/background_rectangle.xml b/Clover/app/src/main/res/drawable/background_rectangle.xml deleted file mode 100644 index 89e226bc..00000000 --- a/Clover/app/src/main/res/drawable/background_rectangle.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml index cb54eaa3..855b01bd 100644 --- a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml +++ b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml @@ -9,8 +9,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY" - android:layout_margin="2dp" - android:background="@drawable/background_rectangle" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 18685f89..95d6505c 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -583,5 +583,5 @@ Don't have a 4chan Pass?
You have to selected 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) Verification is already in progress Captcha request is already in progress - You are requesting captcha too fast + You are requesting captcha too fast
From 2820d23ff787e7d97cb69c0536ea5e2d08c8e3fb Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Tue, 30 Apr 2019 20:14:16 +0300 Subject: [PATCH 04/19] Review fixes Also update google cookies to use dashchan ones --- .../ui/captcha/v2/CaptchaNoJsHtmlParser.java | 9 +++++ .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 9 ++++- .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 14 +++++--- .../ui/captcha/v2/CaptchaNoJsV2Adapter.java | 4 +-- .../floens/chan/ui/layout/ReplyLayout.java | 16 ++++----- .../layout/layout_captcha_challenge_image.xml | 33 +++++++++++++++---- Clover/app/src/main/res/values/strings.xml | 2 +- 7 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java index 19663588..bc22ae89 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java @@ -383,6 +383,15 @@ public class CaptchaNoJsHtmlParser { } return resultImages; + } catch (Throwable error) { + for (Bitmap bitmap : resultImages) { + if (!bitmap.isRecycled()) { + bitmap.recycle(); + } + } + + resultImages.clear(); + throw error; } finally { if (originalBitmap != null) { originalBitmap.recycle(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java index b9537de8..69fe843e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -103,6 +103,8 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaVerifyButton.setOnClickListener(this); useOldCaptchaButton.setOnClickListener(this); reloadCaptchaButton.setOnClickListener(this); + + captchaVerifyButton.setEnabled(false); } @Override @@ -153,6 +155,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout // called on a background thread AndroidUtils.runOnUiThread(() -> { + captchaVerifyButton.setEnabled(true); renderCaptchaWindow(captchaInfo); }); } @@ -162,6 +165,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout // called on a background thread AndroidUtils.runOnUiThread(() -> { + captchaVerifyButton.setEnabled(true); callback.onAuthenticationComplete(this, null, verificationToken); }); } @@ -176,6 +180,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout String message = error.getMessage(); showToast(message); + captchaVerifyButton.setEnabled(true); callback.onFallbackToV1CaptchaView(); }); } @@ -224,6 +229,8 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout adapter.setImageSize(imageSize); adapter.setImages(captchaInfo.challengeImages); + + captchaVerifyButton.setEnabled(true); } catch (Throwable error) { if (callback != null) { callback.onFallbackToV1CaptchaView(); @@ -250,10 +257,10 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout List 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)); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index 2cfe7392..026b6f79 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -54,10 +54,10 @@ public class CaptchaNoJsPresenterV2 { // this should be updated once in 3 months IIRC private static final String googleCookies = - "SID=NwcOCOnxA7PVDPUY_InSzz6saQ93BmWzg7N2276OPwUvtwXkLpfqbs1tYNkxNP0xC_Awmg.; " + - "HSID=AoZX0-3xQ7lN9EMEm; " + - "SSID=AWwVsHei8i4VlX3H4; " + - "NID=182=ikM1ZwJ0tMSqCiJc8gdretZpOHCnUSsZ2oM7I681KmnkL0DKFOBmFU6zlmTMC_mDnPhBB5mQxsaB0ipTICW9WyCa9nLUbo1Bx-H9jFmOLNhr1E5EezqlyFZcBKEyZgjFYmaY79_5jHMa4uE6v_Vb8HR1Uk9CXbNfw_yIPEsZXZ0t0CURkV-CdxdOIiZ4BAy0oE6w60GJ9kLpOWWSeNZ_lBcn0PkWRj6vmRH6kKrl2exOKnk"; + "SID=gjaHjfFJPAN5HO3MVVZpjHFKa_249dsfjHa9klsiaflsd99.asHqjsM2lAS; " + + "HSID=j7m0aFJ82lPF7Hd9d; " + + "SSID=nJKpa81jOskq7Jsps; " + + "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(); @@ -127,6 +127,7 @@ public class CaptchaNoJsPresenterV2 { public void onFailure(Call call, IOException e) { if (callbacks != null) { try { + prevCaptchaInfo = null; callbacks.onCaptchaInfoParseError(e); } finally { verificationInProgress.set(false); @@ -187,6 +188,7 @@ public class CaptchaNoJsPresenterV2 { public void onFailure(Call call, IOException e) { if (callbacks != null) { try { + prevCaptchaInfo = null; callbacks.onCaptchaInfoParseError(e); } finally { captchaRequestInProgress.set(false); @@ -295,6 +297,10 @@ public class CaptchaNoJsPresenterV2 { 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; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java index 72f7c46e..96efb515 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java @@ -58,7 +58,7 @@ public class CaptchaNoJsV2Adapter extends BaseAdapter { convertView = inflater.inflate(R.layout.layout_captcha_challenge_image, parent, false); AppCompatImageView imageView = convertView.findViewById(R.id.captcha_challenge_image); - AppCompatImageView blueCheckmark = convertView.findViewById(R.id.captcha_challenge_blue_checkmark); + ConstraintLayout blueCheckmarkHolder = convertView.findViewById(R.id.captcha_challenge_blue_checkmark_holder); ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(imageSize, imageSize); imageView.setLayoutParams(layoutParams); @@ -68,7 +68,7 @@ public class CaptchaNoJsV2Adapter extends BaseAdapter { boolean isChecked = imageList.get(position).isChecked; AndroidUtils.animateViewScale(imageView, isChecked, ANIMATION_DURATION); - blueCheckmark.setVisibility(isChecked ? View.VISIBLE : View.GONE); + blueCheckmarkHolder.setVisibility(isChecked ? View.VISIBLE : View.GONE); }); if (position >= 0 && position <= imageList.size()) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index 62d9421e..73c53ebf 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -272,22 +272,22 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply break; } case CAPTCHA2_NOJS: - if (!useV2NoJsCaptcha) { - // default webview-based captcha view - authenticationLayout = new CaptchaNojsLayoutV1(getContext()); - } else { + 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) { - // restore the button's visibility when using old v1 captcha view - resetButton.setVisibility(View.VISIBLE); - } else { + 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); } } diff --git a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml index 855b01bd..de13d603 100644 --- a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml +++ b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml @@ -14,16 +14,37 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintTop_toTopOf="@+id/captcha_challenge_image"> + + + + + + + \ No newline at end of file diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 95d6505c..ed7e8ba4 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -580,7 +580,7 @@ Don't have a 4chan Pass?
Verify Reload Use old captcha - You have to selected 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) + 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) Verification is already in progress Captcha request is already in progress You are requesting captcha too fast From 055de6c4bd41c208cad980e5019be604aa3c460c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Tue, 30 Apr 2019 20:23:20 +0300 Subject: [PATCH 05/19] Fix captcha view not being scrollable when using portrait orientation --- .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 3 ++- .../res/layout/layout_captcha_nojs_v2.xml | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java index 69fe843e..c082712a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -32,6 +32,7 @@ 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; @@ -55,7 +56,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout private AppCompatButton useOldCaptchaButton; private AppCompatButton reloadCaptchaButton; private ConstraintLayout buttonsHolder; - private ConstraintLayout background; + private ScrollView background; private CaptchaNoJsV2Adapter adapter; private CaptchaNoJsPresenterV2 presenter; diff --git a/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml b/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml index a8d933f4..bf5e2c98 100644 --- a/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml +++ b/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml @@ -1,5 +1,5 @@ - - + android:layout_height="wrap_content"> + + + + + android:background="@color/accent" + tools:layout_editor_absoluteY="40dp"> - \ No newline at end of file + \ No newline at end of file From d10b8a0e36649f693deeb5f7fa6d3c862aa35c31 Mon Sep 17 00:00:00 2001 From: Adamantcheese Date: Wed, 8 May 2019 20:23:48 -0700 Subject: [PATCH 06/19] Fixes captcha check not being centered and makes the checkmark white instead of transparent --- .../res/drawable/ic_blue_checkmark_24dp.xml | 15 +++++++--- .../layout/layout_captcha_challenge_image.xml | 30 +++++++------------ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml b/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml index 9e2adec1..0ade221a 100644 --- a/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml +++ b/Clover/app/src/main/res/drawable/ic_blue_checkmark_24dp.xml @@ -1,5 +1,12 @@ - - + + + diff --git a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml index de13d603..22da5cb4 100644 --- a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml +++ b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml @@ -16,33 +16,23 @@ - - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" > + 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" /> From 6fdc7b74c248c59cbb87cdbc8cc99f83d17f449d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 9 May 2019 14:26:40 +0300 Subject: [PATCH 07/19] Add settings for usage of the new captcha window and google cookies --- .../chan/core/settings/ChanSettings.java | 4 ++ .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 1 - .../BehaviourSettingsController.java | 43 ++++++++++++++++--- .../chan/ui/settings/SettingsController.java | 2 + Clover/app/src/main/res/values/strings.xml | 7 +++ 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index cb1624c0..638e0585 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -160,6 +160,8 @@ public class ChanSettings { public static final LongSetting updateCheckInterval; public static final BooleanSetting crashReporting; + public static final BooleanSetting useNewCaptchaWindow; + public static final BooleanSetting useRealGoogleCookies; static { SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); @@ -250,6 +252,8 @@ public class ChanSettings { updateCheckInterval = new LongSetting(p, "update_check_interval", UpdateManager.DEFAULT_UPDATE_CHECK_INTERVAL_MS); crashReporting = new BooleanSetting(p, "preference_crash_reporting", true); + useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true); + useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false); // Old (but possibly still in some users phone) // preference_board_view_mode default "list" diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index 026b6f79..c0f57b53 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -52,7 +52,6 @@ public class CaptchaNoJsPresenterV2 { private static final int SUCCESS_STATUS_CODE = 200; private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L; - // this should be updated once in 3 months IIRC private static final String googleCookies = "SID=gjaHjfFJPAN5HO3MVVZpjHFKa_249dsfjHa9klsiaflsd99.asHqjsM2lAS; " + "HSID=j7m0aFJ82lPF7Hd9d; " + diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java index 5dfb4b3c..5e88615e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java @@ -39,6 +39,7 @@ import static org.floens.chan.Chan.injector; public class BehaviourSettingsController extends SettingsController { private SettingView forceEnglishSetting; + private SettingView useNewCaptchaWindow; public BehaviourSettingsController(Context context) { super(context); @@ -47,14 +48,10 @@ public class BehaviourSettingsController extends SettingsController { @Override public void onCreate() { super.onCreate(); - navigation.setTitle(R.string.settings_screen_behavior); setupLayout(); - - populatePreferences(); - - buildPreferences(); + rebuildPreferences(); } @Override @@ -63,10 +60,27 @@ public class BehaviourSettingsController extends SettingsController { if (item == forceEnglishSetting) { Toast.makeText(context, R.string.setting_force_english_locale_toggle_notice, Toast.LENGTH_LONG).show(); + } else if (item == useNewCaptchaWindow) { + // when user disables the new captcha window also disable the usage of the google cookies + if (!ChanSettings.useNewCaptchaWindow.get()) { + ChanSettings.useRealGoogleCookies.set(false); + } + + rebuildPreferences(); } } + private void rebuildPreferences() { + populatePreferences(); + buildPreferences(); + } + private void populatePreferences() { + requiresUiRefresh.clear(); + groups.clear(); + requiresRestart.clear(); + + // General group { SettingsGroup general = new SettingsGroup(R.string.settings_group_general); @@ -152,6 +166,25 @@ public class BehaviourSettingsController extends SettingsController { groups.add(post); } + // Captcha group + { + SettingsGroup captcha = new SettingsGroup(R.string.settings_captcha_group); + + useNewCaptchaWindow = captcha.add(new BooleanSettingView(this, + ChanSettings.useNewCaptchaWindow, + R.string.settings_use_new_captcha_window, + 0)); + + if (ChanSettings.useNewCaptchaWindow.get()) { + captcha.add(new BooleanSettingView(this, + ChanSettings.useRealGoogleCookies, + R.string.settings_use_real_google_cookies, + R.string.settings_use_real_google_cookies_description)); + } + + groups.add(captcha); + } + // Proxy group { SettingsGroup proxy = new SettingsGroup(R.string.settings_group_proxy); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java index 63f7a3a8..0c80b5ba 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java @@ -140,6 +140,8 @@ public class SettingsController extends Controller implements AndroidUtils.OnMea protected void buildPreferences() { LayoutInflater inf = LayoutInflater.from(context); boolean firstGroup = true; + content.removeAllViews(); + for (SettingsGroup group : groups) { LinearLayout groupLayout = (LinearLayout) inf.inflate(R.layout.setting_group, content, false); ((TextView) groupLayout.findViewById(R.id.header)).setText(group.name); diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index ed7e8ba4..7978578b 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -577,6 +577,13 @@ Don't have a 4chan Pass?
" Themes + + + Captcha + Use new captcha window for no-js captcha + Use real google cookies instead of hardcoded ones + By using the real google cookies a GET request to the google.com will be executed to get the google cookies (SID, HSID, SSID, NID) which will be stored on the device and used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (once in a month). But it will also be possible to update them manually + Verify Reload Use old captcha From 2e1005654674b0f5ae66b3514a31df455d71d054 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 9 May 2019 18:21:10 +0300 Subject: [PATCH 08/19] Add ability to manually refresh google captcha --- .../chan/core/presenter/ReplyPresenter.java | 13 +- .../chan/core/settings/ChanSettings.java | 2 + .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 47 ++- .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 293 +++++++++++++----- .../BehaviourSettingsController.java | 3 + .../res/layout/layout_captcha_nojs_v2.xml | 35 ++- Clover/app/src/main/res/values/strings.xml | 3 +- 7 files changed, 303 insertions(+), 93 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 06a053a0..0f6532ac 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -37,6 +37,7 @@ import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.ReplyResponse; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; import org.floens.chan.ui.captcha.AuthenticationLayoutInterface; +import org.floens.chan.ui.captcha.v2.CaptchaNoJsLayoutV2; import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.utils.Logger; @@ -280,8 +281,13 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe draft.captchaChallenge = challenge; draft.captchaResponse = response; - // should this be called here? - authenticationLayout.reset(); + // we don't need this to be called for new captcha window. + // Otherwise "Request captcha request is already in progress" message will be shown + if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) { + // should this be called here? + authenticationLayout.reset(); + } + makeSubmitCall(); } @@ -405,8 +411,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe } public void switchPage(Page page, boolean animate) { - // by default try to use the new nojs captcha view - switchPage(page, animate, true); + switchPage(page, animate, ChanSettings.useNewCaptchaWindow.get()); } public void switchPage(Page page, boolean animate, boolean useV2NoJsCaptcha) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 638e0585..d1789fff 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -162,6 +162,7 @@ public class ChanSettings { public static final BooleanSetting crashReporting; public static final BooleanSetting useNewCaptchaWindow; public static final BooleanSetting useRealGoogleCookies; + public static final StringSetting googleCookie; static { SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); @@ -254,6 +255,7 @@ public class ChanSettings { crashReporting = new BooleanSetting(p, "preference_crash_reporting", true); useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true); useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false); + googleCookie = new StringSetting(p, "google_cookie", ""); // Old (but possibly still in some users phone) // preference_board_view_mode default "list" diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java index c082712a..68e2af69 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -36,6 +36,7 @@ import android.widget.ScrollView; import android.widget.Toast; import org.floens.chan.R; +import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.site.Site; import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; @@ -55,6 +56,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout private AppCompatButton captchaVerifyButton; private AppCompatButton useOldCaptchaButton; private AppCompatButton reloadCaptchaButton; + private AppCompatButton refreshCookiesButton; private ConstraintLayout buttonsHolder; private ScrollView background; @@ -90,6 +92,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaVerifyButton = view.findViewById(R.id.captcha_layout_v2_verify_button); useOldCaptchaButton = view.findViewById(R.id.captcha_layout_v2_use_old_captcha_button); reloadCaptchaButton = view.findViewById(R.id.captcha_layout_v2_reload_button); + refreshCookiesButton = view.findViewById(R.id.captcha_layout_v2_refresh_cookies); buttonsHolder = view.findViewById(R.id.captcha_layout_v2_buttons_holder); background = view.findViewById(R.id.captcha_layout_v2_background); @@ -100,11 +103,17 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaVerifyButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); useOldCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); reloadCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); + refreshCookiesButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); captchaVerifyButton.setOnClickListener(this); useOldCaptchaButton.setOnClickListener(this); reloadCaptchaButton.setOnClickListener(this); + if (ChanSettings.useRealGoogleCookies.get()) { + refreshCookiesButton.setVisibility(View.VISIBLE); + refreshCookiesButton.setOnClickListener(this); + } + captchaVerifyButton.setEnabled(false); } @@ -148,6 +157,9 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout showToast(getContext().getString( R.string.captcha_layout_v2_captcha_request_is_already_in_progress)); break; + case AlreadyShutdown: + // do nothing + break; } } @@ -171,10 +183,35 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout }); } + @Override + public void onGoogleCookiesRefreshed() { + // called on a background thread + + AndroidUtils.runOnUiThread(() -> { + showToast("Google cookies successfully refreshed"); + + // refresh the captcha as well + reset(); + }); + } + + // Called when we could not get google cookies + @Override + public void onGetGoogleCookieError(boolean shouldFallback, Throwable error) { + // called on a background thread + + handleError(shouldFallback, error); + } + + // Called when we got response from re-captcha but could not parse some part of it @Override public void onCaptchaInfoParseError(Throwable error) { // called on a background thread + handleError(true, error); + } + + private void handleError(boolean shouldFallback, Throwable error) { AndroidUtils.runOnUiThread(() -> { Logger.e(TAG, "CaptchaV2 error", error); @@ -182,7 +219,10 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout showToast(message); captchaVerifyButton.setEnabled(true); - callback.onFallbackToV1CaptchaView(); + + if (shouldFallback) { + callback.onFallbackToV1CaptchaView(); + } }); } @@ -200,6 +240,8 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout callback.onFallbackToV1CaptchaView(); } else if (v == reloadCaptchaButton) { reset(); + } else if (v == refreshCookiesButton) { + presenter.refreshCookies(); } } @@ -269,6 +311,9 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout 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); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index c0f57b53..fcd4fee5 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -18,8 +18,11 @@ package org.floens.chan.ui.captcha.v2; import android.content.Context; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.utils.BackgroundUtils; import org.floens.chan.utils.Logger; import java.io.IOException; @@ -30,8 +33,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; -import okhttp3.Call; -import okhttp3.Callback; +import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; @@ -42,21 +44,23 @@ import okhttp3.ResponseBody; public class CaptchaNoJsPresenterV2 { private static final String TAG = "CaptchaNoJsPresenterV2"; - // TODO: change useragent? - private static final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"; + private static final String userAgentHeader = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"; + private static final String acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"; + private static final String acceptEncodingHeader = "deflate, br"; + private static final String acceptLanguageHeader = "en-US"; + private static final String recaptchaUrlBase = "https://www.google.com/recaptcha/api/fallback?k="; + private static final String googleBaseUrl = "https://www.google.com/"; private static final String encoding = "UTF-8"; private static final String mediaType = "application/x-www-form-urlencoded"; private static final String recaptchaChallengeString = "reCAPTCHA challenge"; private static final String verificationTokenString = "fbc-verification-token"; + private static final String setCookieHeaderName = "set-cookie"; private static final int SUCCESS_STATUS_CODE = 200; private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L; - private static final String googleCookies = - "SID=gjaHjfFJPAN5HO3MVVZpjHFKa_249dsfjHa9klsiaflsd99.asHqjsM2lAS; " + - "HSID=j7m0aFJ82lPF7Hd9d; " + - "SSID=nJKpa81jOskq7Jsps; " + - "NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf"; + // 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(); @@ -68,16 +72,22 @@ public class CaptchaNoJsPresenterV2 { private AuthenticationCallbacks callbacks; @Nullable private CaptchaInfo prevCaptchaInfo = null; + @NonNull + // either the default cookie or a real cookie + private volatile String googleCookie; + private AtomicBoolean verificationInProgress = new AtomicBoolean(false); private AtomicBoolean captchaRequestInProgress = new AtomicBoolean(false); - + private AtomicBoolean refreshCookiesRequestInProgress = new AtomicBoolean(false); private String siteKey; private String baseUrl; - private long lastTimeCaptchRequest = 0L; + private long lastTimeCaptchaRequest = 0L; public CaptchaNoJsPresenterV2(@Nullable AuthenticationCallbacks callbacks, Context context) { this.callbacks = callbacks; this.parser = new CaptchaNoJsHtmlParser(context, okHttpClient); + + this.googleCookie = ChanSettings.googleCookie.get(); } public void init(String siteKey, String baseUrl) { @@ -86,15 +96,22 @@ public class CaptchaNoJsPresenterV2 { } /** - * Send challenge solution back + * Send challenge solution back to the recaptcha */ public VerifyError verify( List selectedIds - ) throws CaptchaNoJsV2Error, UnsupportedEncodingException { + ) 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 is already shutdown"); + return VerifyError.AlreadyShutdown; + } + try { if (selectedIds.isEmpty()) { verificationInProgress.set(false); @@ -109,44 +126,41 @@ public class CaptchaNoJsPresenterV2 { throw new CaptchaNoJsV2Error("C parameter is null"); } - String recaptchaUrl = recaptchaUrlBase + siteKey; - RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds); - - Request request = new Request.Builder() - .url(recaptchaUrl) - .post(body) - .header("User-Agent", userAgent) - .header("Referer", recaptchaUrl) - .header("Accept-Language", "en-US") - .header("Cookie", googleCookies) - .build(); - - okHttpClient.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { + if (googleCookie.isEmpty()) { + throw new IllegalStateException("Google cookies are not supposed to be empty here"); + } + + executor.submit(() -> { + try { + String recaptchaUrl = recaptchaUrlBase + siteKey; + RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds); + + Request request = new Request.Builder() + .url(recaptchaUrl) + .post(body) + .header("Referer", recaptchaUrl) + .header("User-Agent", userAgentHeader) + .header("Accept", acceptHeader) + .header("Accept-Encoding", acceptEncodingHeader) + .header("Accept-Language", acceptLanguageHeader) + .header("Cookie", googleCookie) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + prevCaptchaInfo = handleGetRecaptchaResponse(response); + } finally { + verificationInProgress.set(false); + } + } catch (Throwable error) { if (callbacks != null) { try { prevCaptchaInfo = null; - callbacks.onCaptchaInfoParseError(e); + callbacks.onCaptchaInfoParseError(error); } finally { verificationInProgress.set(false); } } } - - @Override - public void onResponse(Call call, Response response) { - executor.execute(() -> { - // to avoid okhttp's threads to hang - - try { - prevCaptchaInfo = handleGetRecaptchaResponse(response); - } finally { - verificationInProgress.set(false); - response.close(); - } - }); - } }); return VerifyError.Ok; @@ -156,57 +170,89 @@ public class CaptchaNoJsPresenterV2 { } } + /** + * Manually refreshes the google cookie + * */ + public void refreshCookies() { + if (!refreshCookiesRequestInProgress.compareAndSet(false, true)) { + return; + } + + if (executor.isShutdown()) { + refreshCookiesRequestInProgress.set(false); + return; + } + + executor.submit(() -> { + try { + googleCookie = getGoogleCookies(true); + + if (callbacks != null) { + callbacks.onGoogleCookiesRefreshed(); + } + } catch (IOException e) { + if (callbacks != null) { + callbacks.onGetGoogleCookieError(false, e); + } + } finally { + refreshCookiesRequestInProgress.set(false); + } + }); + } + /** * Requests captcha data, parses it and then passes it to the render function */ public RequestCaptchaInfoError requestCaptchaInfo() { if (!captchaRequestInProgress.compareAndSet(false, true)) { + Logger.d(TAG, "Request captcha request is already in progress"); return RequestCaptchaInfoError.AlreadyInProgress; } try { // recaptcha may become very angry at you if your are fetching it too fast - if (System.currentTimeMillis() - lastTimeCaptchRequest < CAPTCHA_REQUEST_THROTTLE_MS) { + if (System.currentTimeMillis() - lastTimeCaptchaRequest < CAPTCHA_REQUEST_THROTTLE_MS) { captchaRequestInProgress.set(false); + Logger.d(TAG, "Requesting captcha info too fast"); return RequestCaptchaInfoError.HoldYourHorses; } - lastTimeCaptchRequest = System.currentTimeMillis(); - String recaptchaUrl = recaptchaUrlBase + siteKey; + if (executor.isShutdown()) { + captchaRequestInProgress.set(false); + Logger.d(TAG, "Cannot request captcha info, executor is already shutdown"); + return RequestCaptchaInfoError.AlreadyShutdown; + } - Request request = new Request.Builder() - .url(recaptchaUrl) - .header("User-Agent", userAgent) - .header("Referer", baseUrl) - .header("Accept-Language", "en-US") - .header("Cookie", googleCookies) - .build(); + lastTimeCaptchaRequest = System.currentTimeMillis(); - okHttpClient.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - if (callbacks != null) { - try { - prevCaptchaInfo = null; - callbacks.onCaptchaInfoParseError(e); - } finally { - captchaRequestInProgress.set(false); + executor.submit(() -> { + try { + try { + googleCookie = getGoogleCookies(false); + } catch (Throwable error) { + if (callbacks != null) { + callbacks.onGetGoogleCookieError(true, error); } - } - } - @Override - public void onResponse(Call call, Response response) { - executor.execute(() -> { - // to avoid okhttp's threads to hang + throw error; + } - try { - prevCaptchaInfo = handleGetRecaptchaResponse(response); - } finally { - captchaRequestInProgress.set(false); - response.close(); + try { + prevCaptchaInfo = getCaptchaInfo(); + } catch (Throwable error) { + if (callbacks != null) { + callbacks.onCaptchaInfoParseError(error); } - }); + + throw error; + } + } catch (Throwable error) { + Logger.e(TAG, "Error while executing captcha requests", error); + + prevCaptchaInfo = null; + googleCookie = defaultGoogleCookies; + } finally { + captchaRequestInProgress.set(false); } }); @@ -223,6 +269,66 @@ public class CaptchaNoJsPresenterV2 { } } + @NonNull + private String getGoogleCookies(boolean forced) throws IOException { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Must not be executed on the main thread"); + } + + if (!ChanSettings.useRealGoogleCookies.get()) { + Logger.d(TAG, "Google cookies request is disabled in the settings, using the default ones"); + return defaultGoogleCookies; + } + + if (!forced && !googleCookie.isEmpty()) { + Logger.d(TAG, "We already have google cookies"); + return googleCookie; + } + + Request request = new Request.Builder() + .url(googleBaseUrl) + .header("User-Agent", userAgentHeader) + .header("Accept", acceptHeader) + .header("Accept-Encoding", acceptEncodingHeader) + .header("Accept-Language", acceptLanguageHeader) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + String newCookie = handleGetGoogleCookiesResponse(response); + ChanSettings.googleCookie.set(newCookie); + + Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie); + return newCookie; + } + } + + @Nullable + private CaptchaInfo getCaptchaInfo() throws IOException { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Must not be executed on the main thread"); + } + + if (googleCookie.isEmpty()) { + throw new IllegalStateException("Google cookies are not supposed to be null here"); + } + + String recaptchaUrl = recaptchaUrlBase + siteKey; + + Request request = new Request.Builder() + .url(recaptchaUrl) + .header("Referer", baseUrl) + .header("User-Agent", userAgentHeader) + .header("Accept", acceptHeader) + .header("Accept-Encoding", acceptEncodingHeader) + .header("Accept-Language", acceptLanguageHeader) + .header("Cookie", googleCookie) + .build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + return handleGetRecaptchaResponse(response); + } + } + private RequestBody createResponseBody( CaptchaInfo prevCaptchaInfo, List selectedIds @@ -254,6 +360,33 @@ public class CaptchaNoJsPresenterV2 { resultBody); } + @NonNull + private String handleGetGoogleCookiesResponse(Response response) { + if (response.code() != SUCCESS_STATUS_CODE) { + Logger.w(TAG, "Get google cookies request returned bad status code = " + response.code()); + return defaultGoogleCookies; + } + + Headers headers = response.headers(); + + for (String headerName : headers.names()) { + if (headerName.equalsIgnoreCase(setCookieHeaderName)) { + String setCookieHeader = headers.get(headerName); + if (setCookieHeader != null) { + String[] split = setCookieHeader.split(";"); + for (String splitPart : split) { + if (splitPart.startsWith("NID")) { + return splitPart; + } + } + } + } + } + + Logger.d(TAG, "Could not find the NID cookie in the headers"); + return defaultGoogleCookies; + } + @Nullable private CaptchaInfo handleGetRecaptchaResponse(Response response) { try { @@ -284,6 +417,7 @@ public class CaptchaNoJsPresenterV2 { 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); @@ -293,6 +427,7 @@ public class CaptchaNoJsPresenterV2 { } else { // got the challenge CaptchaInfo captchaInfo = parser.parseHtml(bodyString, siteKey); + Logger.d(TAG, "Got new challenge"); if (callbacks != null) { callbacks.onCaptchaInfoParsed(captchaInfo); @@ -327,16 +462,22 @@ public class CaptchaNoJsPresenterV2 { public enum VerifyError { Ok, NoImagesSelected, - AlreadyInProgress + AlreadyInProgress, + AlreadyShutdown } public enum RequestCaptchaInfoError { Ok, AlreadyInProgress, - HoldYourHorses + HoldYourHorses, + AlreadyShutdown } public interface AuthenticationCallbacks { + void onGetGoogleCookieError(boolean shouldFallback, Throwable error); + + void onGoogleCookiesRefreshed(); + void onCaptchaInfoParsed(CaptchaInfo captchaInfo); void onCaptchaInfoParseError(Throwable error); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java index 5e88615e..0a5ff7a9 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java @@ -64,6 +64,9 @@ public class BehaviourSettingsController extends SettingsController { // when user disables the new captcha window also disable the usage of the google cookies if (!ChanSettings.useNewCaptchaWindow.get()) { ChanSettings.useRealGoogleCookies.set(false); + + // reset the old google cookie as well + ChanSettings.googleCookie.set(""); } rebuildPreferences(); diff --git a/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml b/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml index bf5e2c98..c42d5fb8 100644 --- a/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml +++ b/Clover/app/src/main/res/layout/layout_captcha_nojs_v2.xml @@ -47,26 +47,38 @@ + app:layout_constraintTop_toTopOf="@+id/captcha_layout_v2_verify_button" /> + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 7978578b..e568791f 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -582,7 +582,7 @@ Don't have a 4chan Pass?
Captcha Use new captcha window for no-js captcha Use real google cookies instead of hardcoded ones - By using the real google cookies a GET request to the google.com will be executed to get the google cookies (SID, HSID, SSID, NID) which will be stored on the device and used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (once in a month). But it will also be possible to update them manually + When the real google cookies a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (when posting the first time after the app start). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row). Verify Reload @@ -591,4 +591,5 @@ Don't have a 4chan Pass?
Verification is already in progress Captcha request is already in progress You are requesting captcha too fast + Refresh cookies From 3259c797aad04cb55d62f86994438c291c50ff51 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 9 May 2019 18:28:57 +0300 Subject: [PATCH 09/19] Add forgotten comments --- .../floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index fcd4fee5..8f391c1c 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -108,7 +108,7 @@ public class CaptchaNoJsPresenterV2 { if (executor.isShutdown()) { verificationInProgress.set(false); - Logger.d(TAG, "Cannot verify, executor is already shutdown"); + Logger.d(TAG, "Cannot verify, executor has been shut down"); return VerifyError.AlreadyShutdown; } @@ -175,11 +175,13 @@ public class CaptchaNoJsPresenterV2 { * */ public void refreshCookies() { if (!refreshCookiesRequestInProgress.compareAndSet(false, true)) { + Logger.d(TAG, "Google cookie request is already in progress"); return; } if (executor.isShutdown()) { refreshCookiesRequestInProgress.set(false); + Logger.d(TAG, "Cannot request google cookie, executor has been shut down"); return; } @@ -219,7 +221,7 @@ public class CaptchaNoJsPresenterV2 { if (executor.isShutdown()) { captchaRequestInProgress.set(false); - Logger.d(TAG, "Cannot request captcha info, executor is already shutdown"); + Logger.d(TAG, "Cannot request captcha info, executor has been shut down"); return RequestCaptchaInfoError.AlreadyShutdown; } From 8fec9c894ec07d45c70f56c9c06ba0c66939ff5a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 18 May 2019 17:42:51 +0300 Subject: [PATCH 10/19] Add captchaInfo parsing tests Store real google cookie up to three months instead of until the next app restart --- Clover/app/build.gradle | 2 + .../chan/core/settings/ChanSettings.java | 2 + .../ui/captcha/v2/CaptchaNoJsHtmlParser.java | 6 +- .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 24 +++- .../BehaviourSettingsController.java | 5 +- Clover/app/src/main/res/values/strings.xml | 2 +- .../captcha/v2/CaptchaNoJsHtmlParserTest.java | 104 ++++++++++++++++++ 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index 8eb4f098..ad8ff1fd 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -158,4 +158,6 @@ dependencies { implementation 'org.codejargon.feather:feather:1.0' releaseImplementation 'ch.acra:acra-http:5.1.3' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.27.0' } diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index d1789fff..cf2793c8 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -163,6 +163,7 @@ public class ChanSettings { public static final BooleanSetting useNewCaptchaWindow; public static final BooleanSetting useRealGoogleCookies; public static final StringSetting googleCookie; + public static final LongSetting lastGoogleCookieUpdateTime; static { SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); @@ -256,6 +257,7 @@ public class ChanSettings { useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true); useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false); googleCookie = new StringSetting(p, "google_cookie", ""); + lastGoogleCookieUpdateTime = new LongSetting(p, "last_google_cookie_update_time", 0L); // Old (but possibly still in some users phone) // preference_board_view_mode default "list" diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java index bc22ae89..bd063398 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java @@ -123,7 +123,7 @@ public class CaptchaNoJsHtmlParser { return token; } - private void parseChallengeTitle( + public void parseChallengeTitle( String responseHtml, CaptchaInfo captchaInfo ) throws CaptchaNoJsV2ParsingError { @@ -240,7 +240,7 @@ public class CaptchaNoJsHtmlParser { } } - private void parseCParameter( + public void parseCParameter( String responseHtml, CaptchaInfo captchaInfo ) throws CaptchaNoJsV2ParsingError { @@ -269,7 +269,7 @@ public class CaptchaNoJsHtmlParser { captchaInfo.setcParameter(cParameter); } - private void parseCheckboxes( + public void parseCheckboxes( String responseHtml, CaptchaInfo captchaInfo ) throws CaptchaNoJsV2ParsingError { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index 8f391c1c..256ca2c1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -31,6 +31,7 @@ import java.net.URLEncoder; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import okhttp3.Headers; @@ -48,7 +49,6 @@ public class CaptchaNoJsPresenterV2 { private static final String acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"; private static final String acceptEncodingHeader = "deflate, br"; private static final String acceptLanguageHeader = "en-US"; - private static final String recaptchaUrlBase = "https://www.google.com/recaptcha/api/fallback?k="; private static final String googleBaseUrl = "https://www.google.com/"; private static final String encoding = "UTF-8"; @@ -58,6 +58,7 @@ public class CaptchaNoJsPresenterV2 { private static final String setCookieHeaderName = "set-cookie"; private static final int SUCCESS_STATUS_CODE = 200; private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L; + private static final long THREE_MONTHS = TimeUnit.DAYS.toMillis(90); // this cookie is taken from dashchan private static final String defaultGoogleCookies = "NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf"; @@ -135,6 +136,8 @@ public class CaptchaNoJsPresenterV2 { String recaptchaUrl = recaptchaUrlBase + siteKey; RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds); + Logger.d(TAG, "Verify called. Current cookie = " + googleCookie); + Request request = new Request.Builder() .url(recaptchaUrl) .post(body) @@ -282,11 +285,17 @@ public class CaptchaNoJsPresenterV2 { return defaultGoogleCookies; } - if (!forced && !googleCookie.isEmpty()) { + boolean isItTimeToUpdateCookies = + ((System.currentTimeMillis() - ChanSettings.lastGoogleCookieUpdateTime.get()) > THREE_MONTHS); + + if (!forced && (!googleCookie.isEmpty() && !isItTimeToUpdateCookies)) { Logger.d(TAG, "We already have google cookies"); return googleCookie; } + Logger.d(TAG, "Time to update cookies: forced = " + forced + ", isCookieEmpty = " + + googleCookie.isEmpty() + ", last cookie expired = " + isItTimeToUpdateCookies); + Request request = new Request.Builder() .url(googleBaseUrl) .header("User-Agent", userAgentHeader) @@ -297,9 +306,16 @@ public class CaptchaNoJsPresenterV2 { try (Response response = okHttpClient.newCall(request).execute()) { String newCookie = handleGetGoogleCookiesResponse(response); - ChanSettings.googleCookie.set(newCookie); + if (!newCookie.equalsIgnoreCase(defaultGoogleCookies)) { + ChanSettings.googleCookie.set(newCookie); + ChanSettings.lastGoogleCookieUpdateTime.set(System.currentTimeMillis()); + + Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie); + } else { + Logger.d(TAG, "Could not successfully handle google cookie response, " + + "using the default google cookies until the next request"); + } - Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie); return newCookie; } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java index 0a5ff7a9..e2f83be1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java @@ -65,8 +65,11 @@ public class BehaviourSettingsController extends SettingsController { if (!ChanSettings.useNewCaptchaWindow.get()) { ChanSettings.useRealGoogleCookies.set(false); - // reset the old google cookie as well + // Reset the old google cookie ChanSettings.googleCookie.set(""); + + // and cookie update time as well + ChanSettings.lastGoogleCookieUpdateTime.set(0L); } rebuildPreferences(); diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index e568791f..93a1c29a 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -582,7 +582,7 @@ Don't have a 4chan Pass?
Captcha Use new captcha window for no-js captcha Use real google cookies instead of hardcoded ones - When the real google cookies a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (when posting the first time after the app start). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row). + When this option is enabled a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (every three months). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row). Verify Reload diff --git a/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java b/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java new file mode 100644 index 00000000..37a2f32a --- /dev/null +++ b/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java @@ -0,0 +1,104 @@ +package org.floens.chan.ui.captcha.v2; + +import android.content.Context; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import okhttp3.OkHttpClient; + +import static org.junit.Assert.assertEquals; + +public class CaptchaNoJsHtmlParserTest { + private List inputDataList = new ArrayList<>(); + private List> resultCheckboxList = new ArrayList<>(); + private List resultTitleList = new ArrayList<>(); + private List 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("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + + 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); + } + } +} \ No newline at end of file From 802605a48ec56787d81c3c8ca3471b1199178677 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 25 Jul 2019 20:19:57 +0300 Subject: [PATCH 11/19] (#696) Captcha haptic feedback --- .../org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java index 96efb515..51a1780b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java @@ -4,6 +4,7 @@ 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; @@ -64,6 +65,8 @@ public class CaptchaNoJsV2Adapter extends BaseAdapter { imageView.setLayoutParams(layoutParams); imageView.setOnClickListener((view) -> { + view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + imageList.get(position).toggleChecked(); boolean isChecked = imageList.get(position).isChecked; From 92866d325b206bf65510df07684a0c22b5710dc8 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 25 Jul 2019 20:22:37 +0300 Subject: [PATCH 12/19] (#696) Image parsing fix --- .../org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java index bd063398..9a6caafe 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java @@ -373,8 +373,8 @@ public class CaptchaNoJsHtmlParser { for (int row = 0; row < rows; ++row) { Bitmap imagePiece = Bitmap.createBitmap( originalBitmap, - column * imageWidth, - row * imageHeight, + row * imageWidth, + column * imageHeight, imageWidth, imageHeight); From b0df3049a395d57dde8fe2cca6fc6a96bbb58ed0 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 25 Jul 2019 21:43:12 +0300 Subject: [PATCH 13/19] (#696) Remove everything related to google cookies fetching --- .../chan/core/settings/ChanSettings.java | 2 - .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 47 +----- .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 143 +----------------- .../BehaviourSettingsController.java | 20 --- .../captcha/v2/CaptchaNoJsHtmlParserTest.java | 2 + 5 files changed, 13 insertions(+), 201 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index cca835ad..8f6f16dc 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -161,7 +161,6 @@ public class ChanSettings { public static final BooleanSetting crashReporting; public static final BooleanSetting useNewCaptchaWindow; - public static final BooleanSetting useRealGoogleCookies; public static final StringSetting googleCookie; public static final LongSetting lastGoogleCookieUpdateTime; @@ -255,7 +254,6 @@ public class ChanSettings { crashReporting = new BooleanSetting(p, "preference_crash_reporting", true); useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true); - useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false); googleCookie = new StringSetting(p, "google_cookie", ""); lastGoogleCookieUpdateTime = new LongSetting(p, "last_google_cookie_update_time", 0L); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java index 68e2af69..daa6179d 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -36,7 +36,6 @@ import android.widget.ScrollView; import android.widget.Toast; import org.floens.chan.R; -import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.site.Site; import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; @@ -56,9 +55,6 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout private AppCompatButton captchaVerifyButton; private AppCompatButton useOldCaptchaButton; private AppCompatButton reloadCaptchaButton; - private AppCompatButton refreshCookiesButton; - private ConstraintLayout buttonsHolder; - private ScrollView background; private CaptchaNoJsV2Adapter adapter; private CaptchaNoJsPresenterV2 presenter; @@ -92,9 +88,8 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaVerifyButton = view.findViewById(R.id.captcha_layout_v2_verify_button); useOldCaptchaButton = view.findViewById(R.id.captcha_layout_v2_use_old_captcha_button); reloadCaptchaButton = view.findViewById(R.id.captcha_layout_v2_reload_button); - refreshCookiesButton = view.findViewById(R.id.captcha_layout_v2_refresh_cookies); - buttonsHolder = view.findViewById(R.id.captcha_layout_v2_buttons_holder); - background = view.findViewById(R.id.captcha_layout_v2_background); + 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)); @@ -103,17 +98,11 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaVerifyButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); useOldCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); reloadCaptchaButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); - refreshCookiesButton.setTextColor(AndroidUtils.getAttrColor(getContext(), R.attr.text_color_primary)); captchaVerifyButton.setOnClickListener(this); useOldCaptchaButton.setOnClickListener(this); reloadCaptchaButton.setOnClickListener(this); - if (ChanSettings.useRealGoogleCookies.get()) { - refreshCookiesButton.setVisibility(View.VISIBLE); - refreshCookiesButton.setOnClickListener(this); - } - captchaVerifyButton.setEnabled(false); } @@ -183,26 +172,6 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout }); } - @Override - public void onGoogleCookiesRefreshed() { - // called on a background thread - - AndroidUtils.runOnUiThread(() -> { - showToast("Google cookies successfully refreshed"); - - // refresh the captcha as well - reset(); - }); - } - - // Called when we could not get google cookies - @Override - public void onGetGoogleCookieError(boolean shouldFallback, Throwable error) { - // called on a background thread - - handleError(shouldFallback, error); - } - // Called when we got response from re-captcha but could not parse some part of it @Override public void onCaptchaInfoParseError(Throwable error) { @@ -227,9 +196,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout } private void showToast(String message) { - AndroidUtils.runOnUiThread(() -> { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - }); + AndroidUtils.runOnUiThread(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show()); } @Override @@ -240,8 +207,6 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout callback.onFallbackToV1CaptchaView(); } else if (v == reloadCaptchaButton) { reset(); - } else if (v == refreshCookiesButton) { - presenter.refreshCookies(); } } @@ -252,7 +217,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaImagesGrid.setAdapter(null); captchaImagesGrid.setAdapter(adapter); - int columnsCount = 0; + int columnsCount; int imageSize = captchaImagesGrid.getWidth(); switch (captchaInfo.getCaptchaType()) { @@ -282,6 +247,10 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout } private void setCaptchaTitle(CaptchaInfo captchaInfo) { + if (captchaInfo.getCaptchaTitle() == null) { + return; + } + if (captchaInfo.getCaptchaTitle().hasBold()) { SpannableString spannableString = new SpannableString(captchaInfo.getCaptchaTitle().getTitle()); spannableString.setSpan( diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index 256ca2c1..bf20c518 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -50,15 +50,12 @@ public class CaptchaNoJsPresenterV2 { private static final String acceptEncodingHeader = "deflate, br"; private static final String acceptLanguageHeader = "en-US"; private static final String recaptchaUrlBase = "https://www.google.com/recaptcha/api/fallback?k="; - private static final String googleBaseUrl = "https://www.google.com/"; private static final String encoding = "UTF-8"; private static final String mediaType = "application/x-www-form-urlencoded"; private static final String recaptchaChallengeString = "reCAPTCHA challenge"; private static final String verificationTokenString = "fbc-verification-token"; - private static final String setCookieHeaderName = "set-cookie"; private static final int SUCCESS_STATUS_CODE = 200; private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L; - private static final long THREE_MONTHS = TimeUnit.DAYS.toMillis(90); // this cookie is taken from dashchan private static final String defaultGoogleCookies = "NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf"; @@ -73,13 +70,9 @@ public class CaptchaNoJsPresenterV2 { private AuthenticationCallbacks callbacks; @Nullable private CaptchaInfo prevCaptchaInfo = null; - @NonNull - // either the default cookie or a real cookie - private volatile String googleCookie; private AtomicBoolean verificationInProgress = new AtomicBoolean(false); private AtomicBoolean captchaRequestInProgress = new AtomicBoolean(false); - private AtomicBoolean refreshCookiesRequestInProgress = new AtomicBoolean(false); private String siteKey; private String baseUrl; private long lastTimeCaptchaRequest = 0L; @@ -87,8 +80,6 @@ public class CaptchaNoJsPresenterV2 { public CaptchaNoJsPresenterV2(@Nullable AuthenticationCallbacks callbacks, Context context) { this.callbacks = callbacks; this.parser = new CaptchaNoJsHtmlParser(context, okHttpClient); - - this.googleCookie = ChanSettings.googleCookie.get(); } public void init(String siteKey, String baseUrl) { @@ -127,16 +118,12 @@ public class CaptchaNoJsPresenterV2 { throw new CaptchaNoJsV2Error("C parameter is null"); } - if (googleCookie.isEmpty()) { - throw new IllegalStateException("Google cookies are not supposed to be empty here"); - } - executor.submit(() -> { try { String recaptchaUrl = recaptchaUrlBase + siteKey; RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds); - Logger.d(TAG, "Verify called. Current cookie = " + googleCookie); + Logger.d(TAG, "Verify called"); Request request = new Request.Builder() .url(recaptchaUrl) @@ -146,7 +133,7 @@ public class CaptchaNoJsPresenterV2 { .header("Accept", acceptHeader) .header("Accept-Encoding", acceptEncodingHeader) .header("Accept-Language", acceptLanguageHeader) - .header("Cookie", googleCookie) + .header("Cookie", defaultGoogleCookies) .build(); try (Response response = okHttpClient.newCall(request).execute()) { @@ -173,38 +160,6 @@ public class CaptchaNoJsPresenterV2 { } } - /** - * Manually refreshes the google cookie - * */ - public void refreshCookies() { - if (!refreshCookiesRequestInProgress.compareAndSet(false, true)) { - Logger.d(TAG, "Google cookie request is already in progress"); - return; - } - - if (executor.isShutdown()) { - refreshCookiesRequestInProgress.set(false); - Logger.d(TAG, "Cannot request google cookie, executor has been shut down"); - return; - } - - executor.submit(() -> { - try { - googleCookie = getGoogleCookies(true); - - if (callbacks != null) { - callbacks.onGoogleCookiesRefreshed(); - } - } catch (IOException e) { - if (callbacks != null) { - callbacks.onGetGoogleCookieError(false, e); - } - } finally { - refreshCookiesRequestInProgress.set(false); - } - }); - } - /** * Requests captcha data, parses it and then passes it to the render function */ @@ -232,16 +187,6 @@ public class CaptchaNoJsPresenterV2 { executor.submit(() -> { try { - try { - googleCookie = getGoogleCookies(false); - } catch (Throwable error) { - if (callbacks != null) { - callbacks.onGetGoogleCookieError(true, error); - } - - throw error; - } - try { prevCaptchaInfo = getCaptchaInfo(); } catch (Throwable error) { @@ -255,7 +200,6 @@ public class CaptchaNoJsPresenterV2 { Logger.e(TAG, "Error while executing captcha requests", error); prevCaptchaInfo = null; - googleCookie = defaultGoogleCookies; } finally { captchaRequestInProgress.set(false); } @@ -274,62 +218,12 @@ public class CaptchaNoJsPresenterV2 { } } - @NonNull - private String getGoogleCookies(boolean forced) throws IOException { - if (BackgroundUtils.isMainThread()) { - throw new RuntimeException("Must not be executed on the main thread"); - } - - if (!ChanSettings.useRealGoogleCookies.get()) { - Logger.d(TAG, "Google cookies request is disabled in the settings, using the default ones"); - return defaultGoogleCookies; - } - - boolean isItTimeToUpdateCookies = - ((System.currentTimeMillis() - ChanSettings.lastGoogleCookieUpdateTime.get()) > THREE_MONTHS); - - if (!forced && (!googleCookie.isEmpty() && !isItTimeToUpdateCookies)) { - Logger.d(TAG, "We already have google cookies"); - return googleCookie; - } - - Logger.d(TAG, "Time to update cookies: forced = " + forced + ", isCookieEmpty = " + - googleCookie.isEmpty() + ", last cookie expired = " + isItTimeToUpdateCookies); - - Request request = new Request.Builder() - .url(googleBaseUrl) - .header("User-Agent", userAgentHeader) - .header("Accept", acceptHeader) - .header("Accept-Encoding", acceptEncodingHeader) - .header("Accept-Language", acceptLanguageHeader) - .build(); - - try (Response response = okHttpClient.newCall(request).execute()) { - String newCookie = handleGetGoogleCookiesResponse(response); - if (!newCookie.equalsIgnoreCase(defaultGoogleCookies)) { - ChanSettings.googleCookie.set(newCookie); - ChanSettings.lastGoogleCookieUpdateTime.set(System.currentTimeMillis()); - - Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie); - } else { - Logger.d(TAG, "Could not successfully handle google cookie response, " + - "using the default google cookies until the next request"); - } - - return newCookie; - } - } - @Nullable private CaptchaInfo getCaptchaInfo() throws IOException { if (BackgroundUtils.isMainThread()) { throw new RuntimeException("Must not be executed on the main thread"); } - if (googleCookie.isEmpty()) { - throw new IllegalStateException("Google cookies are not supposed to be null here"); - } - String recaptchaUrl = recaptchaUrlBase + siteKey; Request request = new Request.Builder() @@ -339,7 +233,7 @@ public class CaptchaNoJsPresenterV2 { .header("Accept", acceptHeader) .header("Accept-Encoding", acceptEncodingHeader) .header("Accept-Language", acceptLanguageHeader) - .header("Cookie", googleCookie) + .header("Cookie", defaultGoogleCookies) .build(); try (Response response = okHttpClient.newCall(request).execute()) { @@ -378,33 +272,6 @@ public class CaptchaNoJsPresenterV2 { resultBody); } - @NonNull - private String handleGetGoogleCookiesResponse(Response response) { - if (response.code() != SUCCESS_STATUS_CODE) { - Logger.w(TAG, "Get google cookies request returned bad status code = " + response.code()); - return defaultGoogleCookies; - } - - Headers headers = response.headers(); - - for (String headerName : headers.names()) { - if (headerName.equalsIgnoreCase(setCookieHeaderName)) { - String setCookieHeader = headers.get(headerName); - if (setCookieHeader != null) { - String[] split = setCookieHeader.split(";"); - for (String splitPart : split) { - if (splitPart.startsWith("NID")) { - return splitPart; - } - } - } - } - } - - Logger.d(TAG, "Could not find the NID cookie in the headers"); - return defaultGoogleCookies; - } - @Nullable private CaptchaInfo handleGetRecaptchaResponse(Response response) { try { @@ -492,10 +359,6 @@ public class CaptchaNoJsPresenterV2 { } public interface AuthenticationCallbacks { - void onGetGoogleCookieError(boolean shouldFallback, Throwable error); - - void onGoogleCookiesRefreshed(); - void onCaptchaInfoParsed(CaptchaInfo captchaInfo); void onCaptchaInfoParseError(Throwable error); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java index e2f83be1..dabaf0ec 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java @@ -60,19 +60,6 @@ public class BehaviourSettingsController extends SettingsController { if (item == forceEnglishSetting) { Toast.makeText(context, R.string.setting_force_english_locale_toggle_notice, Toast.LENGTH_LONG).show(); - } else if (item == useNewCaptchaWindow) { - // when user disables the new captcha window also disable the usage of the google cookies - if (!ChanSettings.useNewCaptchaWindow.get()) { - ChanSettings.useRealGoogleCookies.set(false); - - // Reset the old google cookie - ChanSettings.googleCookie.set(""); - - // and cookie update time as well - ChanSettings.lastGoogleCookieUpdateTime.set(0L); - } - - rebuildPreferences(); } } @@ -181,13 +168,6 @@ public class BehaviourSettingsController extends SettingsController { R.string.settings_use_new_captcha_window, 0)); - if (ChanSettings.useNewCaptchaWindow.get()) { - captcha.add(new BooleanSettingView(this, - ChanSettings.useRealGoogleCookies, - R.string.settings_use_real_google_cookies, - R.string.settings_use_real_google_cookies_description)); - } - groups.add(captcha); } diff --git a/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java b/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java index 37a2f32a..62faa58b 100644 --- a/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java +++ b/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java @@ -2,6 +2,7 @@ package org.floens.chan.ui.captcha.v2; import android.content.Context; + import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -13,6 +14,7 @@ import okhttp3.OkHttpClient; import static org.junit.Assert.assertEquals; + public class CaptchaNoJsHtmlParserTest { private List inputDataList = new ArrayList<>(); private List> resultCheckboxList = new ArrayList<>(); From c9b8100ead30c93e5d9c0cc957763bc99aa1fab9 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 25 Jul 2019 23:01:27 +0300 Subject: [PATCH 14/19] (#696) checkstyle fixes --- .../org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index bf20c518..05c11246 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -18,10 +18,8 @@ package org.floens.chan.ui.captcha.v2; import android.content.Context; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.utils.BackgroundUtils; import org.floens.chan.utils.Logger; @@ -31,10 +29,8 @@ import java.net.URLEncoder; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; From f55d0ab3c3f89bb30ca873f7826acf7970e4d0dc Mon Sep 17 00:00:00 2001 From: Floens Date: Fri, 26 Jul 2019 17:35:05 +0200 Subject: [PATCH 15/19] strings: tell tools that this is our default locale (en) --- Clover/app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 590da38d..c50172ac 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ 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 . --> - + Cancel Add Close From aa27d1c3cb9fae9be142aa9d821345263f3026c8 Mon Sep 17 00:00:00 2001 From: Floens Date: Fri, 26 Jul 2019 17:36:51 +0200 Subject: [PATCH 16/19] captchanojs2: set up view for each getView call it's for when a view is given that you should recycle, it sets up the view every time. use viewpropertyanimator for animating the image and checkmark. --- .../ui/captcha/v2/CaptchaNoJsV2Adapter.java | 54 +++++++++++-------- .../org/floens/chan/utils/AndroidUtils.java | 36 ------------- .../layout/layout_captcha_challenge_image.xml | 7 +-- 3 files changed, 36 insertions(+), 61 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java index 51a1780b..74a1f9b4 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsV2Adapter.java @@ -8,17 +8,15 @@ 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 org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; import java.util.List; public class CaptchaNoJsV2Adapter extends BaseAdapter { - private static final int ANIMATION_DURATION = 50; - private LayoutInflater inflater; private int imageSize = 0; @@ -57,26 +55,38 @@ public class CaptchaNoJsV2Adapter extends BaseAdapter { public View getView(final int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.layout_captcha_challenge_image, parent, false); + } - AppCompatImageView imageView = convertView.findViewById(R.id.captcha_challenge_image); - ConstraintLayout blueCheckmarkHolder = convertView.findViewById(R.id.captcha_challenge_blue_checkmark_holder); - - ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(imageSize, imageSize); - imageView.setLayoutParams(layoutParams); - - imageView.setOnClickListener((view) -> { - view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - - imageList.get(position).toggleChecked(); - boolean isChecked = imageList.get(position).isChecked; - - AndroidUtils.animateViewScale(imageView, isChecked, ANIMATION_DURATION); - blueCheckmarkHolder.setVisibility(isChecked ? View.VISIBLE : View.GONE); - }); - - if (position >= 0 && position <= imageList.size()) { - imageView.setImageBitmap(imageList.get(position).getBitmap()); - } + 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; diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java index d4e950a0..cc8aa1f3 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -51,8 +51,6 @@ import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; import android.widget.Toast; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.ScaleAnimation; import org.floens.chan.R; @@ -460,40 +458,6 @@ public class AndroidUtils { return networkInfo != null && networkInfo.isConnected(); } - public static void animateViewScale(View view, boolean zoomOut, int duration) { - ScaleAnimation scaleAnimation; - final float normalScale = 1.0f; - final float zoomOutScale = 0.8f; - - if (zoomOut) { - scaleAnimation = new ScaleAnimation( - normalScale, - zoomOutScale, - normalScale, - zoomOutScale, - ScaleAnimation.RELATIVE_TO_SELF, - 0.5f, - ScaleAnimation.RELATIVE_TO_SELF, - 0.5f); - } else { - scaleAnimation = new ScaleAnimation( - zoomOutScale, - normalScale, - zoomOutScale, - normalScale, - ScaleAnimation.RELATIVE_TO_SELF, - 0.5f, - ScaleAnimation.RELATIVE_TO_SELF, - 0.5f); - } - - scaleAnimation.setDuration(duration); - scaleAnimation.setFillAfter(true); - scaleAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); - - view.startAnimation(scaleAnimation); - } - public static boolean enableHighEndAnimations() { boolean lowRamDevice = ActivityManagerCompat.isLowRamDevice(activityManager); return !lowRamDevice && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; diff --git a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml index 22da5cb4..760bfb3d 100644 --- a/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml +++ b/Clover/app/src/main/res/layout/layout_captcha_challenge_image.xml @@ -2,7 +2,8 @@ + android:layout_height="match_parent" + android:padding="1dp"> + app:layout_constraintTop_toTopOf="parent"> Date: Fri, 26 Jul 2019 17:37:53 +0200 Subject: [PATCH 17/19] captchanojs2: use the parse exception everywhere does this by adding a constructor to the exception class, to allow linking a cause-throwable. reduce some catches from throwable to exception. --- .../ui/captcha/v2/CaptchaNoJsHtmlParser.java | 41 ++++++++----------- .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 1 + .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 2 +- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java index 9a6caafe..17edaee3 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java @@ -25,7 +25,6 @@ import android.support.v4.util.Pair; import org.floens.chan.utils.BackgroundUtils; import org.floens.chan.utils.IOUtils; -import org.floens.chan.utils.Logger; import java.io.File; import java.io.FileOutputStream; @@ -111,9 +110,8 @@ public class CaptchaNoJsHtmlParser { try { token = matcher.group(1); - } catch (Throwable error) { - Logger.e(TAG, "Could not parse verification token", error); - throw error; + } catch (Exception error) { + throw new CaptchaNoJsV2ParsingError("Could not parse verification token", error); } if (token == null || token.isEmpty()) { @@ -154,13 +152,8 @@ public class CaptchaNoJsHtmlParser { // could not find it captchaTitle = new CaptchaInfo.CaptchaTitle(title, -1, -1); } - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to parse challenge title", error); - throw error; - } - - if (captchaTitle == null) { - throw new CaptchaNoJsV2ParsingError("challengeTitle is null"); + } catch (Exception error) { + throw new CaptchaNoJsV2ParsingError("Error while trying to parse challenge title", error); } if (captchaTitle.isEmpty()) { @@ -184,9 +177,8 @@ public class CaptchaNoJsHtmlParser { try { challengeImageUrl = matcher.group(1); - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to parse challenge image url", error); - throw error; + } catch (Exception error) { + throw new CaptchaNoJsV2ParsingError("Error while trying to parse challenge image url", error); } if (challengeImageUrl == null) { @@ -253,9 +245,8 @@ public class CaptchaNoJsHtmlParser { try { cParameter = matcher.group(1); - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to parse c parameter", error); - throw error; + } catch (Exception error) { + throw new CaptchaNoJsV2ParsingError("Error while trying to parse c parameter", error); } if (cParameter == null) { @@ -281,9 +272,8 @@ public class CaptchaNoJsHtmlParser { try { Integer checkboxId = Integer.parseInt(matcher.group(1)); checkboxesSet.add(checkboxId); - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to parse checkbox with id (" + index + ")", error); - throw error; + } catch (Exception error) { + throw new CaptchaNoJsV2ParsingError("Error while trying to parse checkbox with id (" + index + ")", error); } ++index; @@ -297,9 +287,8 @@ public class CaptchaNoJsHtmlParser { try { captchaType = CaptchaInfo.CaptchaType.fromCheckboxesCount(checkboxesSet.size()); - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to parse captcha type", error); - throw error; + } catch (Exception error) { + throw new CaptchaNoJsV2ParsingError("Error while trying to parse captcha type", error); } if (captchaType == CaptchaInfo.CaptchaType.Unknown) { @@ -383,7 +372,7 @@ public class CaptchaNoJsHtmlParser { } return resultImages; - } catch (Throwable error) { + } catch (Exception error) { for (Bitmap bitmap : resultImages) { if (!bitmap.isRecycled()) { bitmap.recycle(); @@ -403,5 +392,9 @@ public class CaptchaNoJsHtmlParser { public CaptchaNoJsV2ParsingError(String message) { super(message); } + + public CaptchaNoJsV2ParsingError(String message, Throwable cause) { + super(message, cause); + } } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java index daa6179d..943a3b0a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -240,6 +240,7 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout captchaVerifyButton.setEnabled(true); } catch (Throwable error) { + Logger.e(TAG, "renderCaptchaWindow", error); if (callback != null) { callback.onFallbackToV1CaptchaView(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index 05c11246..a8069a28 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -320,7 +320,7 @@ public class CaptchaNoJsPresenterV2 { return captchaInfo; } - } catch (Throwable e) { + } catch (Exception e) { Logger.e(TAG, "Error while trying to parse captcha html data", e); if (callbacks != null) { From ee7ffb6ad12f8e4c2759a38b10b0242375eda2c6 Mon Sep 17 00:00:00 2001 From: Floens Date: Fri, 26 Jul 2019 17:57:51 +0200 Subject: [PATCH 18/19] captcha: replace instance check for reset and destroy to interface methods. --- .../chan/core/presenter/ReplyPresenter.java | 4 +--- .../captcha/AuthenticationLayoutInterface.java | 4 ++++ .../floens/chan/ui/captcha/CaptchaLayout.java | 9 +++++++++ .../GenericWebViewAuthenticationLayout.java | 10 ++++++++++ .../chan/ui/captcha/LegacyCaptchaLayout.java | 10 ++++++++++ .../ui/captcha/v1/CaptchaNojsLayoutV1.java | 9 +++++++++ .../ui/captcha/v2/CaptchaNoJsLayoutV2.java | 18 ++++++++++++------ .../org/floens/chan/ui/layout/ReplyLayout.java | 7 +------ 8 files changed, 56 insertions(+), 15 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 0f6532ac..f2c2a8b2 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -37,7 +37,6 @@ import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.ReplyResponse; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; import org.floens.chan.ui.captcha.AuthenticationLayoutInterface; -import org.floens.chan.ui.captcha.v2.CaptchaNoJsLayoutV2; import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.utils.Logger; @@ -283,8 +282,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe // we don't need this to be called for new captcha window. // Otherwise "Request captcha request is already in progress" message will be shown - if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) { - // should this be called here? + if (authenticationLayout.requireResetAfterComplete()) { authenticationLayout.reset(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java index a3f2c10a..ce6203dd 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java @@ -24,5 +24,9 @@ public interface AuthenticationLayoutInterface { void reset(); + boolean requireResetAfterComplete(); + void hardReset(); + + void onDestroy(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java index a3c0af4b..601595ad 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java @@ -121,6 +121,15 @@ public class CaptchaLayout extends WebView implements AuthenticationLayoutInterf loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null); } + @Override + public boolean requireResetAfterComplete() { + return true; + } + + @Override + public void onDestroy() { + } + private void onCaptchaLoaded() { } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java index 21473b0a..bad7d1d0 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java @@ -80,6 +80,16 @@ public class GenericWebViewAuthenticationLayout extends WebView implements Authe public void hardReset() { } + @Override + public boolean requireResetAfterComplete() { + return true; + } + + @Override + public void onDestroy() { + + } + private void checkText() { loadUrl("javascript:WebInterface.onAllText(document.documentElement.textContent)"); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java index e7e6e9b3..34485940 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java @@ -142,6 +142,16 @@ public class LegacyCaptchaLayout extends LinearLayout implements AuthenticationL input.requestFocus(); } + @Override + public boolean requireResetAfterComplete() { + return true; + } + + @Override + public void onDestroy() { + + } + private void submitCaptcha() { AndroidUtils.hideKeyboard(this); callback.onAuthenticationComplete(this, challenge, input.getText().toString()); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java index 819e51e9..8fb5f1f8 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v1/CaptchaNojsLayoutV1.java @@ -133,11 +133,20 @@ public class CaptchaNojsLayoutV1 extends WebView implements AuthenticationLayout hardReset(); } + @Override + public boolean requireResetAfterComplete() { + return true; + } + @Override public void hardReset() { loadRecaptchaAndSetWebViewData(); } + @Override + public void onDestroy() { + } + private void loadRecaptchaAndSetWebViewData() { final String recaptchaUrl = "https://www.google.com/recaptcha/api/fallback?k=" + siteKey; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java index 943a3b0a..cfefbb52 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsLayoutV2.java @@ -152,6 +152,17 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout } } + @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 @@ -171,8 +182,8 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout 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 @@ -289,9 +300,4 @@ public class CaptchaNoJsLayoutV2 extends FrameLayout onCaptchaInfoParseError(error); } } - - public void onDestroy() { - adapter.onDestroy(); - presenter.onDestroy(); - } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index 3fd4eb67..9ff48145 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -365,12 +365,7 @@ public class ReplyLayout extends LoadView implements return; } - if (!(authenticationLayout instanceof CaptchaNoJsLayoutV2)) { - return; - } - - // cleanup resources when switching from the new to the old captcha view - ((CaptchaNoJsLayoutV2) authenticationLayout).onDestroy(); + authenticationLayout.onDestroy(); captchaContainer.removeView((CaptchaNoJsLayoutV2) authenticationLayout); authenticationLayout = null; } From afee7c7ba2f627fcbaf147e2cdde8c5ed41ef7fd Mon Sep 17 00:00:00 2001 From: Floens Date: Fri, 26 Jul 2019 18:07:03 +0200 Subject: [PATCH 19/19] captcha: remove leftover strings and settings for defunct google cookie functionality --- .../main/java/org/floens/chan/core/settings/ChanSettings.java | 4 ---- Clover/app/src/main/res/values/strings.xml | 2 -- 2 files changed, 6 deletions(-) diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index 8f6f16dc..2dd373e9 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -161,8 +161,6 @@ public class ChanSettings { public static final BooleanSetting crashReporting; public static final BooleanSetting useNewCaptchaWindow; - public static final StringSetting googleCookie; - public static final LongSetting lastGoogleCookieUpdateTime; static { SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); @@ -254,8 +252,6 @@ public class ChanSettings { crashReporting = new BooleanSetting(p, "preference_crash_reporting", true); useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true); - googleCookie = new StringSetting(p, "google_cookie", ""); - lastGoogleCookieUpdateTime = new LongSetting(p, "last_google_cookie_update_time", 0L); // Old (but possibly still in some users phone) // preference_board_view_mode default "list" diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index c50172ac..af0ff99c 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -583,8 +583,6 @@ Don't have a 4chan Pass?
Captcha Use new captcha window for no-js captcha - Use real google cookies instead of hardcoded ones - When this option is enabled a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (every three months). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row). Verify Reload