From 9137f5f32f74c837afbcdf5533ffbe202fb2c1ee Mon Sep 17 00:00:00 2001 From: Floens Date: Thu, 28 Dec 2017 13:48:11 +0100 Subject: [PATCH] authentication work, 8ch captcha works --- .../chan/core/database/DatabaseHelper.java | 5 - .../chan/core/database/DatabaseManager.java | 87 +++++++--- .../chan/core/presenter/ReplyPresenter.java | 54 +++--- .../floens/chan/core/site/Authentication.java | 68 ++++++++ .../java/org/floens/chan/core/site/Site.java | 11 +- .../chan/core/site/SiteAuthentication.java | 10 -- .../floens/chan/core/site/http/HttpCall.java | 5 +- .../chan/core/site/http/ReplyResponse.java | 1 + .../chan/core/site/sites/chan4/Chan4.java | 34 ++-- .../chan/core/site/sites/chan8/Chan8.java | 46 +++-- .../site/sites/chan8/Chan8ReplyHttpCall.java | 52 +++++- ...java => AuthenticationLayoutCallback.java} | 7 +- ...ava => AuthenticationLayoutInterface.java} | 6 +- .../floens/chan/ui/captcha/CaptchaLayout.java | 24 ++- .../GenericWebViewAuthenticationLayout.java | 158 ++++++++++++++++++ .../chan/ui/captcha/LegacyCaptchaLayout.java | 17 +- .../floens/chan/ui/layout/ReplyLayout.java | 69 +++++--- 17 files changed, 494 insertions(+), 160 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java delete mode 100644 Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java rename Clover/app/src/main/java/org/floens/chan/ui/captcha/{CaptchaCallback.java => AuthenticationLayoutCallback.java} (73%) rename Clover/app/src/main/java/org/floens/chan/ui/captcha/{CaptchaLayoutInterface.java => AuthenticationLayoutInterface.java} (83%) create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java index e8a4d272..1e1c99b6 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java @@ -60,8 +60,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { private final Context context; - public boolean isUpgrading = false; - public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -101,7 +99,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { @Override public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) { Logger.i(TAG, "Upgrading database from " + oldVersion + " to " + newVersion); - isUpgrading = true; if (oldVersion < 12) { try { @@ -235,8 +232,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { getGraph().get(SiteManager.class).addSiteForLegacy(); } - - isUpgrading = false; } public void reset() { diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java index bd073bb5..8b275257 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java @@ -20,6 +20,7 @@ package org.floens.chan.core.database; import android.content.Context; import android.os.Handler; import android.os.Looper; +import android.support.annotation.NonNull; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.misc.TransactionManager; @@ -40,6 +41,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.inject.Inject; import javax.inject.Singleton; @@ -62,6 +65,7 @@ public class DatabaseManager { private static final long THREAD_HIDE_TRIM_COUNT = 50; private final ExecutorService backgroundExecutor; + private Thread executorThread; private final DatabaseHelper helper; private final List threadHides = new ArrayList<>(); @@ -281,35 +285,70 @@ public class DatabaseManager { } private Future executeTask(final Callable taskCallable, final TaskResult taskResult) { - if (helper.isUpgrading) { + if (Thread.currentThread() == executorThread) { + DatabaseCallable databaseCallable = new DatabaseCallable<>(taskCallable, taskResult); + T result = databaseCallable.call(); + + return new Future() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + return result; + } + + @Override + public T get(long timeout, @NonNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return result; + } + }; + } else { + return backgroundExecutor.submit(new DatabaseCallable<>(taskCallable, taskResult)); + } + } + + private class DatabaseCallable implements Callable { + private final Callable taskCallable; + private final TaskResult taskResult; + + public DatabaseCallable(Callable taskCallable, TaskResult taskResult) { + this.taskCallable = taskCallable; + this.taskResult = taskResult; + } + + @Override + public T call() { + executorThread = Thread.currentThread(); + try { - taskCallable.call(); + final T result = TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); + if (taskResult != null) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + taskResult.onComplete(result); + } + }); + } + return result; } catch (Exception e) { + Logger.e(TAG, "executeTask", e); throw new RuntimeException(e); } - return null; } - - return backgroundExecutor.submit(new Callable() { - @Override - public T call() { - try { - final T result = TransactionManager.callInTransaction(helper.getConnectionSource(), taskCallable); - if (taskResult != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - taskResult.onComplete(result); - } - }); - } - return result; - } catch (Exception e) { - Logger.e(TAG, "executeTask", e); - throw new RuntimeException(e); - } - } - }); } public interface TaskResult { 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 26c073ff..5bc93a6d 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 @@ -20,7 +20,6 @@ package org.floens.chan.core.presenter; import android.text.TextUtils; import org.floens.chan.R; -import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.WatchManager; @@ -30,13 +29,13 @@ import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.SavedReply; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.Site; -import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.ReplyResponse; -import org.floens.chan.ui.captcha.CaptchaCallback; -import org.floens.chan.ui.captcha.CaptchaLayoutInterface; +import org.floens.chan.ui.captcha.AuthenticationLayoutCallback; +import org.floens.chan.ui.captcha.AuthenticationLayoutInterface; import org.floens.chan.ui.helper.ImagePickDelegate; import java.io.File; @@ -50,7 +49,7 @@ import static org.floens.chan.utils.AndroidUtils.getReadableFileSize; import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.getString; -public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImagePickCallback, Site.PostListener { +public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDelegate.ImagePickCallback, Site.PostListener { public enum Page { INPUT, AUTHENTICATION, @@ -75,7 +74,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP private boolean moreOpen; private boolean previewOpen; private boolean pickingFile; - private boolean captchaInited; + private boolean authenticationInited; private int selectedQuote = -1; @Inject @@ -98,8 +97,6 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP bound = true; this.loadable = loadable; - callback.setCaptchaVersion(ChanSettings.postNewCaptcha.get()); - this.board = loadable.board; draft = replyManager.getReply(loadable); @@ -117,8 +114,8 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP showPreview(draft.fileName, draft.file); } - if (captchaInited) { - callback.resetCaptcha(); + if (authenticationInited) { + callback.resetAuthentication(); } switchPage(Page.INPUT, false); @@ -207,7 +204,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP draft.spoilerImage = draft.spoilerImage && board.spoilers; draft.captchaResponse = null; - if (loadable.getSite().authentication().requireAuthentication(SiteAuthentication.AuthenticationRequestType.POSTING)) { + if (false) { switchPage(Page.AUTHENTICATION, true); } else { makeSubmitCall(); @@ -226,7 +223,8 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP SavedReply savedReply = SavedReply.fromSiteBoardNoPassword( loadable.site, loadable.board, replyResponse.postNo, replyResponse.password); - databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager().saveReply(savedReply)); + databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager() + .saveReply(savedReply)); switchPage(Page.INPUT, false); closeAll(); @@ -239,8 +237,11 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP callback.onPosted(); if (bound && !loadable.isThreadMode()) { - callback.showThread(databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.site, loadable.board, replyResponse.postNo))); + callback.showThread(databaseManager.getDatabaseLoadableManager().get( + Loadable.forThread(loadable.site, loadable.board, replyResponse.postNo))); } + } else if (replyResponse.requireAuthentication) { + switchPage(Page.AUTHENTICATION, true); } else { if (replyResponse.errorMessage == null) { replyResponse.errorMessage = getString(R.string.reply_error); @@ -258,14 +259,14 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP } @Override - public void captchaLoaded(CaptchaLayoutInterface captchaLayout) { + public void onAuthenticationLoaded(AuthenticationLayoutInterface authenticationLayout) { } @Override - public void captchaEntered(CaptchaLayoutInterface captchaLayout, String challenge, String response) { + public void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response) { draft.captchaChallenge = challenge; draft.captchaResponse = response; - captchaLayout.reset(); + authenticationLayout.reset(); makeSubmitCall(); } @@ -376,15 +377,15 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP callback.setPage(Page.INPUT, animate); break; case AUTHENTICATION: - callback.setPage(Page.AUTHENTICATION, true); + if (!authenticationInited) { + authenticationInited = true; - if (!captchaInited) { - captchaInited = true; - String baseUrl = loadable.isThreadMode() ? - ChanUrls.getThreadUrlDesktop(loadable.boardCode, loadable.no) : - ChanUrls.getBoardUrlDesktop(loadable.boardCode); - callback.initCaptcha(baseUrl, ChanUrls.getCaptchaSiteKey(), this); + Authentication authentication = loadable.site.postAuthenticate(); + callback.initializeAuthentication(loadable.site, authentication, this); } + + callback.setPage(Page.AUTHENTICATION, true); + break; } } @@ -443,11 +444,10 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP void setPage(Page page, boolean animate); - void setCaptchaVersion(boolean newCaptcha); - - void initCaptcha(String baseUrl, String siteKey, CaptchaCallback callback); + void initializeAuthentication(Site site, Authentication authentication, + AuthenticationLayoutCallback callback); - void resetCaptcha(); + void resetAuthentication(); void openMessage(boolean open, boolean animate, String message, boolean autoHide); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java b/Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java new file mode 100644 index 00000000..c03a20b0 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java @@ -0,0 +1,68 @@ +/* + * 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.core.site; + +public class Authentication { + public enum Type { + NONE, + CAPTCHA1, + CAPTCHA2, + GENERIC_WEBVIEW + } + + public static Authentication fromNone() { + return new Authentication(Type.NONE); + } + + public static Authentication fromCaptcha1(String siteKey, String baseUrl) { + Authentication a = new Authentication(Type.CAPTCHA1); + a.siteKey = siteKey; + a.baseUrl = baseUrl; + return a; + } + + public static Authentication fromCaptcha2(String siteKey, String baseUrl) { + Authentication a = new Authentication(Type.CAPTCHA2); + a.siteKey = siteKey; + a.baseUrl = baseUrl; + return a; + } + + public static Authentication fromUrl(String url, String retryText, String successText) { + Authentication a = new Authentication(Type.GENERIC_WEBVIEW); + a.url = url; + a.retryText = retryText; + a.successText = successText; + return a; + } + + public final Type type; + + // captcha1 & captcha2 + public String siteKey; + public String baseUrl; + + // generic webview + public String url; + public String retryText; + public String successText; + + private Authentication(Type type) { + this.type = type; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java index dd2370c2..88f755cb 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java @@ -74,11 +74,13 @@ public interface Site { /** * This board supports posting with images. */ + // TODO(multisite) use this POSTING_IMAGE, /** * This board supports posting with a checkbox to mark the posted image as a spoiler. */ + // TODO(multisite) use this POSTING_SPOILER, } @@ -106,8 +108,9 @@ public interface Site { * Initialize the site with the given id, config, and userSettings. *

Note: do not use any managers at this point, because they rely on the sites being initialized. * Instead, use {@link #postInitialize()} - * @param id the site id - * @param config the site config + * + * @param id the site id + * @param config the site config * @param userSettings the site user settings */ void initialize(int id, SiteConfig config, SiteUserSettings userSettings); @@ -134,8 +137,6 @@ public interface Site { SiteRequestModifier requestModifier(); - SiteAuthentication authentication(); - BoardsType boardsType(); String desktopUrl(Loadable loadable, @Nullable Post post); @@ -177,6 +178,8 @@ public interface Site { void onPostError(HttpCall httpCall); } + Authentication postAuthenticate(); + void delete(DeleteRequest deleteRequest, DeleteListener deleteListener); interface DeleteListener { diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java deleted file mode 100644 index 4e1a1162..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.floens.chan.core.site; - - -public interface SiteAuthentication { - enum AuthenticationRequestType { - POSTING - } - - boolean requireAuthentication(AuthenticationRequestType type); -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java index 2fceb22b..d6192e15 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java @@ -58,13 +58,12 @@ public abstract class HttpCall implements Callback { public void onResponse(Call call, Response response) { ResponseBody body = response.body(); try { - if (response.isSuccessful() && body != null) { + if (body != null) { String responseString = body.string(); process(response, responseString); successful = true; } else { - String responseString = body == null ? "no body" : body.string(); - onFailure(call, new IOException("HTTP " + response.code() + "\n\n" + responseString)); + throw new IOException("HTTP " + response.code()); } } catch (Exception e) { exception = e; diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java b/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java index 77559db9..d82c0a0c 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java @@ -39,4 +39,5 @@ public class ReplyResponse { public int postNo; public String password; public boolean probablyBanned; + public boolean requireAuthentication; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java index e83c1b09..4b810731 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java @@ -25,11 +25,12 @@ import android.webkit.WebView; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Loadable; +import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.StringSetting; +import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Resolvable; import org.floens.chan.core.site.Site; -import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.core.site.SiteBase; import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteIcon; @@ -76,6 +77,8 @@ public class Chan4 extends SiteBase { private static final String TAG = "Chan4"; + private static final String CAPTCHA_KEY = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; + private static final Random random = new Random(); private final SiteEndpoints endpoints = new SiteEndpoints() { @@ -243,17 +246,6 @@ public class Chan4 extends SiteBase { } }; - private SiteAuthentication authentication = new SiteAuthentication() { - @Override - public boolean requireAuthentication(AuthenticationRequestType type) { - if (type == AuthenticationRequestType.POSTING) { - return !isLoggedIn(); - } - - return false; - } - }; - // Legacy settings that were global before private final StringSetting passUser; private final StringSetting passPass; @@ -361,11 +353,6 @@ public class Chan4 extends SiteBase { return siteRequestModifier; } - @Override - public SiteAuthentication authentication() { - return authentication; - } - @Override public ChanReader chanReader() { return new FutabaChanReader(); @@ -386,6 +373,19 @@ public class Chan4 extends SiteBase { }); } + @Override + public Authentication postAuthenticate() { + if (isLoggedIn()) { + return Authentication.fromNone(); + } else { + if (ChanSettings.postNewCaptcha.get()) { + return Authentication.fromCaptcha2(CAPTCHA_KEY, "https://boards.4chan.org"); + } else { + return Authentication.fromCaptcha1(CAPTCHA_KEY, "https://boards.4chan.org"); + } + } + } + @Override public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) { httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(this, deleteRequest), new HttpCall.HttpCallback() { diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8.java index 57440a64..b7bfc9f0 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8.java @@ -24,9 +24,9 @@ import android.webkit.WebView; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Loadable; +import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.Resolvable; import org.floens.chan.core.site.Site; -import org.floens.chan.core.site.SiteAuthentication; import org.floens.chan.core.site.SiteBase; import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteIcon; @@ -174,13 +174,6 @@ public class Chan8 extends SiteBase { } }; - private SiteAuthentication authentication = new SiteAuthentication() { - @Override - public boolean requireAuthentication(AuthenticationRequestType type) { - return false; - } - }; - @Override public String name() { return "8chan"; @@ -216,11 +209,6 @@ public class Chan8 extends SiteBase { return siteRequestModifier; } - @Override - public SiteAuthentication authentication() { - return authentication; - } - @Override public BoardsType boardsType() { return BoardsType.INFINITE; @@ -245,19 +233,27 @@ public class Chan8 extends SiteBase { public void post(Reply reply, final PostListener postListener) { // TODO HttpCallManager httpCallManager = getGraph().get(HttpCallManager.class); - httpCallManager.makeHttpCall(new Chan8ReplyHttpCall(this, reply), new HttpCall.HttpCallback() { - @Override - public void onHttpSuccess(CommonReplyHttpCall httpPost) { - postListener.onPostComplete(httpPost, httpPost.replyResponse); - } - - @Override - public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) { - Logger.e(TAG, "post error", e); + httpCallManager.makeHttpCall(new Chan8ReplyHttpCall(this, reply), + new HttpCall.HttpCallback() { + @Override + public void onHttpSuccess(CommonReplyHttpCall httpPost) { + postListener.onPostComplete(httpPost, httpPost.replyResponse); + } + + @Override + public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) { + Logger.e(TAG, "post error", e); + + postListener.onPostError(httpPost); + } + }); + } - postListener.onPostError(httpPost); - } - }); + @Override + public Authentication postAuthenticate() { + return Authentication.fromUrl("https://8ch.net/dnsbls_bypass.php", + "You failed the CAPTCHA", + "You may now go back and make your post"); } @Override diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java index 092da71c..43917399 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java @@ -20,14 +20,27 @@ package org.floens.chan.core.site.sites.chan8; import android.text.TextUtils; import org.floens.chan.core.site.Site; -import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.common.CommonReplyHttpCall; +import org.floens.chan.core.site.http.Reply; +import org.floens.chan.utils.Logger; +import org.jsoup.Jsoup; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; +import okhttp3.Response; public class Chan8ReplyHttpCall extends CommonReplyHttpCall { + private static final Pattern REQUIRE_AUTHENTICATION = Pattern.compile(".*\"captcha\": ?true.*"); + private static final Pattern ERROR_MESSAGE = + Pattern.compile(".*

Error

.*]*>(.*?)<\\/h2>.*"); + public Chan8ReplyHttpCall(Site site, Reply reply) { super(site, reply); } @@ -67,4 +80,41 @@ public class Chan8ReplyHttpCall extends CommonReplyHttpCall { formBuilder.addFormDataPart("spoiler", "on"); } } + + @Override + public void process(Response response, String result) throws IOException { + Logger.test(result); + + Matcher authenticationMatcher = REQUIRE_AUTHENTICATION.matcher(result); + Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result); + if (authenticationMatcher.find()) { + replyResponse.requireAuthentication = true; + replyResponse.errorMessage = result; + } else if (errorMessageMatcher.find()) { + replyResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text(); + } else { + // TODO: 8ch redirects us, but the result is a 404. + // stop redirecting. + HttpUrl url = response.request().url(); + List segments = url.pathSegments(); + + String board = null; + int threadId = 0, postId = 0; + try { + if (segments.size() == 3) { + board = segments.get(0); + threadId = Integer.parseInt( + segments.get(2).replace(".html", "")); + postId = Integer.parseInt(url.encodedFragment()); + } + } catch (NumberFormatException ignored) { + } + + if (board != null && threadId != 0 && postId != 0) { + replyResponse.threadNo = threadId; + replyResponse.postNo = postId; + replyResponse.posted = true; + } + } + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaCallback.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java similarity index 73% rename from Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaCallback.java rename to Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java index 21bad0ed..cb056c95 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaCallback.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java @@ -17,8 +17,9 @@ */ package org.floens.chan.ui.captcha; -public interface CaptchaCallback { - void captchaLoaded(CaptchaLayoutInterface captchaLayout); +public interface AuthenticationLayoutCallback { + void onAuthenticationLoaded(AuthenticationLayoutInterface authenticationLayout); - void captchaEntered(CaptchaLayoutInterface captchaLayout, String challenge, String response); + void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, + String challenge, String response); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayoutInterface.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java similarity index 83% rename from Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayoutInterface.java rename to Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java index b25ca1ea..a3f2c10a 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayoutInterface.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java @@ -17,8 +17,10 @@ */ package org.floens.chan.ui.captcha; -public interface CaptchaLayoutInterface { - void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, CaptchaCallback callback); +import org.floens.chan.core.site.Site; + +public interface AuthenticationLayoutInterface { + void initialize(Site site, AuthenticationLayoutCallback callback); void reset(); 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 ab7e28f6..abb9714f 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 @@ -31,13 +31,17 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import org.floens.chan.core.site.Authentication; +import org.floens.chan.core.site.Site; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.IOUtils; -public class CaptchaLayout extends WebView implements CaptchaLayoutInterface { +import static org.floens.chan.ui.theme.ThemeHelper.theme; + +public class CaptchaLayout extends WebView implements AuthenticationLayoutInterface { private static final String TAG = "CaptchaLayout"; - private CaptchaCallback callback; + private AuthenticationLayoutCallback callback; private boolean loaded = false; private String baseUrl; private String siteKey; @@ -56,11 +60,15 @@ public class CaptchaLayout extends WebView implements CaptchaLayoutInterface { } @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) - public void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, CaptchaCallback callback) { + @Override + public void initialize(Site site, AuthenticationLayoutCallback callback) { this.callback = callback; - this.baseUrl = baseUrl; - this.siteKey = siteKey; - this.lightTheme = lightTheme; + this.lightTheme = theme().isLightTheme; + + Authentication authentication = site.postAuthenticate(); + + this.siteKey = authentication.siteKey; + this.baseUrl = authentication.baseUrl; AndroidUtils.hideKeyboard(this); @@ -111,14 +119,14 @@ public class CaptchaLayout extends WebView implements CaptchaLayoutInterface { } private void onCaptchaLoaded() { - callback.captchaLoaded(this); + callback.onAuthenticationLoaded(this); } private void onCaptchaEntered(String challenge, String response) { if (TextUtils.isEmpty(response)) { reset(); } else { - callback.captchaEntered(this, challenge, response); + callback.onAuthenticationComplete(this, challenge, response); } } 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 new file mode 100644 index 00000000..86021c51 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java @@ -0,0 +1,158 @@ +/* + * 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; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.util.AttributeSet; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import org.floens.chan.core.site.Authentication; +import org.floens.chan.core.site.Site; +import org.floens.chan.utils.AndroidUtils; + +public class GenericWebViewAuthenticationLayout extends WebView implements AuthenticationLayoutInterface { + public static final int CHECK_INTERVAL = 800; + + private final Handler handler = new Handler(); + private boolean attachedToWindow = false; + + private Site site; + private AuthenticationLayoutCallback callback; + private Authentication authentication; + private boolean resettingFromFoundText = false; + + public GenericWebViewAuthenticationLayout(Context context) { + this(context, null); + } + + public GenericWebViewAuthenticationLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public GenericWebViewAuthenticationLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setFocusableInTouchMode(true); + } + + @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) + @Override + public void initialize(Site site, AuthenticationLayoutCallback callback) { + this.site = site; + this.callback = callback; + + authentication = site.postAuthenticate(); + + // Older versions just have to manually go back or something. + if (Build.VERSION.SDK_INT >= 17) { + WebSettings settings = getSettings(); + settings.setJavaScriptEnabled(true); + addJavascriptInterface(new WebInterface(this), "WebInterface"); + } + } + + @Override + public void reset() { + loadUrl(authentication.url); + } + + @Override + public void hardReset() { + } + + private void checkText() { + loadUrl("javascript:WebInterface.onAllText(document.documentElement.textContent)"); + } + + private void onAllText(String text) { + boolean retry = text.contains(authentication.retryText); + boolean success = text.contains(authentication.successText); + + if (retry) { + if (!resettingFromFoundText) { + resettingFromFoundText = true; + postDelayed(() -> { + resettingFromFoundText = false; + reset(); + }, 1000); + } + } else if (success) { + callback.onAuthenticationComplete(this, "", ""); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + attachedToWindow = true; + handler.postDelayed(checkTextRunnable, CHECK_INTERVAL); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + attachedToWindow = false; + handler.removeCallbacks(checkTextRunnable); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + handler.removeCallbacks(checkTextRunnable); + + if (hasWindowFocus) { + handler.postDelayed(checkTextRunnable, CHECK_INTERVAL); + } + } + + private final Runnable checkTextRunnable = new Runnable() { + @Override + public void run() { + checkText(); + reschedule(); + } + + private void reschedule() { + handler.removeCallbacks(checkTextRunnable); + if (attachedToWindow && hasWindowFocus()) { + handler.postDelayed(checkTextRunnable, CHECK_INTERVAL); + } + } + }; + + public static class WebInterface { + private final GenericWebViewAuthenticationLayout layout; + + public WebInterface(GenericWebViewAuthenticationLayout layout) { + this.layout = layout; + } + + @JavascriptInterface + public void onAllText(String text) { + AndroidUtils.runOnUiThread(() -> layout.onAllText(text)); + } + } +} 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 1a08243e..393125c5 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 @@ -34,6 +34,8 @@ import android.widget.LinearLayout; import android.widget.TextView; import org.floens.chan.R; +import org.floens.chan.core.site.Authentication; +import org.floens.chan.core.site.Site; import org.floens.chan.ui.view.FixedRatioThumbnailView; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.IOUtils; @@ -41,7 +43,7 @@ import org.floens.chan.utils.IOUtils; import static org.floens.chan.ui.theme.ThemeHelper.theme; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; -public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutInterface, View.OnClickListener { +public class LegacyCaptchaLayout extends LinearLayout implements AuthenticationLayoutInterface, View.OnClickListener { private FixedRatioThumbnailView image; private EditText input; private ImageView submit; @@ -50,7 +52,7 @@ public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutIn private String baseUrl; private String siteKey; - private CaptchaCallback callback; + private AuthenticationLayoutCallback callback; private String challenge; @@ -116,10 +118,13 @@ public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutIn } @Override - public void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, CaptchaCallback callback) { - this.baseUrl = baseUrl; - this.siteKey = siteKey; + public void initialize(Site site, AuthenticationLayoutCallback callback) { this.callback = callback; + + Authentication authentication = site.postAuthenticate(); + + this.siteKey = authentication.siteKey; + this.baseUrl = authentication.baseUrl; } @Override @@ -139,7 +144,7 @@ public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutIn private void submitCaptcha() { AndroidUtils.hideKeyboard(this); - callback.captchaEntered(this, challenge, input.getText().toString()); + callback.onAuthenticationComplete(this, challenge, input.getText().toString()); } private void onCaptchaLoaded(final String imageUrl, final String challenge) { 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 4f7a1716..ac35d094 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 @@ -42,15 +42,18 @@ import org.floens.chan.R; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.presenter.ReplyPresenter; +import org.floens.chan.core.site.Authentication; +import org.floens.chan.core.site.Site; import org.floens.chan.core.site.http.Reply; import org.floens.chan.ui.activity.StartActivity; -import org.floens.chan.ui.captcha.CaptchaCallback; +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.CaptchaLayoutInterface; +import org.floens.chan.ui.captcha.GenericWebViewAuthenticationLayout; +import org.floens.chan.ui.captcha.LegacyCaptchaLayout; import org.floens.chan.ui.drawable.DropdownArrowDrawable; import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.ImagePickDelegate; -import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.SelectionListeningEditText; import org.floens.chan.utils.AndroidUtils; @@ -74,7 +77,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply private ReplyLayoutCallback callback; private boolean newCaptcha; - private CaptchaLayoutInterface authenticationLayout; + private AuthenticationLayoutInterface authenticationLayout; private boolean openingName; private boolean blockSelectionChange = false; @@ -234,7 +237,9 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply }/* else if (v == preview) { // TODO }*/ else if (v == captchaHardReset) { - authenticationLayout.hardReset(); + if (authenticationLayout != null) { + authenticationLayout.hardReset(); + } } else if (v == commentQuoteButton) { presenter.commentQuoteClicked(); } else if (v == commentSpoilerButton) { @@ -262,39 +267,53 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply break; case AUTHENTICATION: setWrap(false); - if (authenticationLayout == null) { - if (newCaptcha) { - authenticationLayout = new CaptchaLayout(getContext()); - } else { - authenticationLayout = (CaptchaLayoutInterface) LayoutInflater.from(getContext()) - .inflate(R.layout.layout_captcha_legacy, captchaContainer, false); - } - captchaContainer.addView((View) authenticationLayout, 0); - } - - if (newCaptcha) { - AndroidUtils.hideKeyboard(this); - } setView(captchaContainer); + captchaContainer.requestFocus(View.FOCUS_DOWN); + break; } } @Override - public void setCaptchaVersion(boolean newCaptcha) { - this.newCaptcha = newCaptcha; - } + public void initializeAuthentication(Site site, Authentication authentication, + AuthenticationLayoutCallback callback) { + if (authenticationLayout == null) { + switch (authentication.type) { + case CAPTCHA1: { + final LayoutInflater inflater = LayoutInflater.from(getContext()); + authenticationLayout = (LegacyCaptchaLayout) inflater.inflate( + R.layout.layout_captcha_legacy, captchaContainer, false); + break; + } + case CAPTCHA2: { + authenticationLayout = new CaptchaLayout(getContext()); + break; + } + case GENERIC_WEBVIEW: { + authenticationLayout = new GenericWebViewAuthenticationLayout(getContext()); + break; + } + case NONE: + default: { + throw new IllegalArgumentException(); + } + } - @Override - public void initCaptcha(String baseUrl, String siteKey, CaptchaCallback callback) { - authenticationLayout.initCaptcha(baseUrl, siteKey, ThemeHelper.getInstance().getTheme().isLightTheme, callback); + captchaContainer.addView((View) authenticationLayout, 0); + } + + if (!(authenticationLayout instanceof LegacyCaptchaLayout)) { + AndroidUtils.hideKeyboard(this); + } + + authenticationLayout.initialize(site, callback); authenticationLayout.reset(); } @Override - public void resetCaptcha() { + public void resetAuthentication() { authenticationLayout.reset(); }