From 2e1005654674b0f5ae66b3514a31df455d71d054 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 9 May 2019 18:21:10 +0300 Subject: [PATCH] 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