authentication work, 8ch captcha works

multisite
Floens 8 years ago
parent 2af30b3be1
commit 9137f5f32f
  1. 5
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
  2. 87
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
  3. 54
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  4. 68
      Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java
  5. 11
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  6. 10
      Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java
  7. 5
      Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java
  8. 1
      Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java
  9. 34
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  10. 46
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8.java
  11. 52
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ReplyHttpCall.java
  12. 7
      Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutCallback.java
  13. 6
      Clover/app/src/main/java/org/floens/chan/ui/captcha/AuthenticationLayoutInterface.java
  14. 24
      Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java
  15. 158
      Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java
  16. 17
      Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java
  17. 69
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java

@ -60,8 +60,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private final Context context; private final Context context;
public boolean isUpgrading = false;
public DatabaseHelper(Context context) { public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); super(context, DATABASE_NAME, null, DATABASE_VERSION);
@ -101,7 +99,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
@Override @Override
public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) { public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {
Logger.i(TAG, "Upgrading database from " + oldVersion + " to " + newVersion); Logger.i(TAG, "Upgrading database from " + oldVersion + " to " + newVersion);
isUpgrading = true;
if (oldVersion < 12) { if (oldVersion < 12) {
try { try {
@ -235,8 +232,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
getGraph().get(SiteManager.class).addSiteForLegacy(); getGraph().get(SiteManager.class).addSiteForLegacy();
} }
isUpgrading = false;
} }
public void reset() { public void reset() {

@ -20,6 +20,7 @@ package org.floens.chan.core.database;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull;
import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.misc.TransactionManager; import com.j256.ormlite.misc.TransactionManager;
@ -40,6 +41,8 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -62,6 +65,7 @@ public class DatabaseManager {
private static final long THREAD_HIDE_TRIM_COUNT = 50; private static final long THREAD_HIDE_TRIM_COUNT = 50;
private final ExecutorService backgroundExecutor; private final ExecutorService backgroundExecutor;
private Thread executorThread;
private final DatabaseHelper helper; private final DatabaseHelper helper;
private final List<ThreadHide> threadHides = new ArrayList<>(); private final List<ThreadHide> threadHides = new ArrayList<>();
@ -281,35 +285,70 @@ public class DatabaseManager {
} }
private <T> Future<T> executeTask(final Callable<T> taskCallable, final TaskResult<T> taskResult) { private <T> Future<T> executeTask(final Callable<T> taskCallable, final TaskResult<T> taskResult) {
if (helper.isUpgrading) { if (Thread.currentThread() == executorThread) {
DatabaseCallable<T> databaseCallable = new DatabaseCallable<>(taskCallable, taskResult);
T result = databaseCallable.call();
return new Future<T>() {
@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<T> implements Callable<T> {
private final Callable<T> taskCallable;
private final TaskResult<T> taskResult;
public DatabaseCallable(Callable<T> taskCallable, TaskResult<T> taskResult) {
this.taskCallable = taskCallable;
this.taskResult = taskResult;
}
@Override
public T call() {
executorThread = Thread.currentThread();
try { 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) { } catch (Exception e) {
Logger.e(TAG, "executeTask", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return null;
} }
return backgroundExecutor.submit(new Callable<T>() {
@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<T> { public interface TaskResult<T> {

@ -20,7 +20,6 @@ package org.floens.chan.core.presenter;
import android.text.TextUtils; import android.text.TextUtils;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.manager.WatchManager; 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.Loadable;
import org.floens.chan.core.model.orm.SavedReply; import org.floens.chan.core.model.orm.SavedReply;
import org.floens.chan.core.settings.ChanSettings; 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.Site;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse; import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.ui.captcha.CaptchaCallback; import org.floens.chan.ui.captcha.AuthenticationLayoutCallback;
import org.floens.chan.ui.captcha.CaptchaLayoutInterface; import org.floens.chan.ui.captcha.AuthenticationLayoutInterface;
import org.floens.chan.ui.helper.ImagePickDelegate; import org.floens.chan.ui.helper.ImagePickDelegate;
import java.io.File; 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.getRes;
import static org.floens.chan.utils.AndroidUtils.getString; 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 { public enum Page {
INPUT, INPUT,
AUTHENTICATION, AUTHENTICATION,
@ -75,7 +74,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
private boolean moreOpen; private boolean moreOpen;
private boolean previewOpen; private boolean previewOpen;
private boolean pickingFile; private boolean pickingFile;
private boolean captchaInited; private boolean authenticationInited;
private int selectedQuote = -1; private int selectedQuote = -1;
@Inject @Inject
@ -98,8 +97,6 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
bound = true; bound = true;
this.loadable = loadable; this.loadable = loadable;
callback.setCaptchaVersion(ChanSettings.postNewCaptcha.get());
this.board = loadable.board; this.board = loadable.board;
draft = replyManager.getReply(loadable); draft = replyManager.getReply(loadable);
@ -117,8 +114,8 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
showPreview(draft.fileName, draft.file); showPreview(draft.fileName, draft.file);
} }
if (captchaInited) { if (authenticationInited) {
callback.resetCaptcha(); callback.resetAuthentication();
} }
switchPage(Page.INPUT, false); switchPage(Page.INPUT, false);
@ -207,7 +204,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
draft.spoilerImage = draft.spoilerImage && board.spoilers; draft.spoilerImage = draft.spoilerImage && board.spoilers;
draft.captchaResponse = null; draft.captchaResponse = null;
if (loadable.getSite().authentication().requireAuthentication(SiteAuthentication.AuthenticationRequestType.POSTING)) { if (false) {
switchPage(Page.AUTHENTICATION, true); switchPage(Page.AUTHENTICATION, true);
} else { } else {
makeSubmitCall(); makeSubmitCall();
@ -226,7 +223,8 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
SavedReply savedReply = SavedReply.fromSiteBoardNoPassword( SavedReply savedReply = SavedReply.fromSiteBoardNoPassword(
loadable.site, loadable.board, replyResponse.postNo, replyResponse.password); loadable.site, loadable.board, replyResponse.postNo, replyResponse.password);
databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager().saveReply(savedReply)); databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager()
.saveReply(savedReply));
switchPage(Page.INPUT, false); switchPage(Page.INPUT, false);
closeAll(); closeAll();
@ -239,8 +237,11 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
callback.onPosted(); callback.onPosted();
if (bound && !loadable.isThreadMode()) { 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 { } else {
if (replyResponse.errorMessage == null) { if (replyResponse.errorMessage == null) {
replyResponse.errorMessage = getString(R.string.reply_error); replyResponse.errorMessage = getString(R.string.reply_error);
@ -258,14 +259,14 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
} }
@Override @Override
public void captchaLoaded(CaptchaLayoutInterface captchaLayout) { public void onAuthenticationLoaded(AuthenticationLayoutInterface authenticationLayout) {
} }
@Override @Override
public void captchaEntered(CaptchaLayoutInterface captchaLayout, String challenge, String response) { public void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout, String challenge, String response) {
draft.captchaChallenge = challenge; draft.captchaChallenge = challenge;
draft.captchaResponse = response; draft.captchaResponse = response;
captchaLayout.reset(); authenticationLayout.reset();
makeSubmitCall(); makeSubmitCall();
} }
@ -376,15 +377,15 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
callback.setPage(Page.INPUT, animate); callback.setPage(Page.INPUT, animate);
break; break;
case AUTHENTICATION: case AUTHENTICATION:
callback.setPage(Page.AUTHENTICATION, true); if (!authenticationInited) {
authenticationInited = true;
if (!captchaInited) { Authentication authentication = loadable.site.postAuthenticate();
captchaInited = true; callback.initializeAuthentication(loadable.site, authentication, this);
String baseUrl = loadable.isThreadMode() ?
ChanUrls.getThreadUrlDesktop(loadable.boardCode, loadable.no) :
ChanUrls.getBoardUrlDesktop(loadable.boardCode);
callback.initCaptcha(baseUrl, ChanUrls.getCaptchaSiteKey(), this);
} }
callback.setPage(Page.AUTHENTICATION, true);
break; break;
} }
} }
@ -443,11 +444,10 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
void setPage(Page page, boolean animate); void setPage(Page page, boolean animate);
void setCaptchaVersion(boolean newCaptcha); void initializeAuthentication(Site site, Authentication authentication,
AuthenticationLayoutCallback callback);
void initCaptcha(String baseUrl, String siteKey, CaptchaCallback callback);
void resetCaptcha(); void resetAuthentication();
void openMessage(boolean open, boolean animate, String message, boolean autoHide); void openMessage(boolean open, boolean animate, String message, boolean autoHide);

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

@ -74,11 +74,13 @@ public interface Site {
/** /**
* This board supports posting with images. * This board supports posting with images.
*/ */
// TODO(multisite) use this
POSTING_IMAGE, POSTING_IMAGE,
/** /**
* This board supports posting with a checkbox to mark the posted image as a spoiler. * This board supports posting with a checkbox to mark the posted image as a spoiler.
*/ */
// TODO(multisite) use this
POSTING_SPOILER, POSTING_SPOILER,
} }
@ -106,8 +108,9 @@ public interface Site {
* Initialize the site with the given id, config, and userSettings. * Initialize the site with the given id, config, and userSettings.
* <p><b>Note: do not use any managers at this point, because they rely on the sites being initialized. * <p><b>Note: do not use any managers at this point, because they rely on the sites being initialized.
* Instead, use {@link #postInitialize()}</b> * Instead, use {@link #postInitialize()}</b>
* @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 * @param userSettings the site user settings
*/ */
void initialize(int id, SiteConfig config, SiteUserSettings userSettings); void initialize(int id, SiteConfig config, SiteUserSettings userSettings);
@ -134,8 +137,6 @@ public interface Site {
SiteRequestModifier requestModifier(); SiteRequestModifier requestModifier();
SiteAuthentication authentication();
BoardsType boardsType(); BoardsType boardsType();
String desktopUrl(Loadable loadable, @Nullable Post post); String desktopUrl(Loadable loadable, @Nullable Post post);
@ -177,6 +178,8 @@ public interface Site {
void onPostError(HttpCall httpCall); void onPostError(HttpCall httpCall);
} }
Authentication postAuthenticate();
void delete(DeleteRequest deleteRequest, DeleteListener deleteListener); void delete(DeleteRequest deleteRequest, DeleteListener deleteListener);
interface DeleteListener { interface DeleteListener {

@ -1,10 +0,0 @@
package org.floens.chan.core.site;
public interface SiteAuthentication {
enum AuthenticationRequestType {
POSTING
}
boolean requireAuthentication(AuthenticationRequestType type);
}

@ -58,13 +58,12 @@ public abstract class HttpCall implements Callback {
public void onResponse(Call call, Response response) { public void onResponse(Call call, Response response) {
ResponseBody body = response.body(); ResponseBody body = response.body();
try { try {
if (response.isSuccessful() && body != null) { if (body != null) {
String responseString = body.string(); String responseString = body.string();
process(response, responseString); process(response, responseString);
successful = true; successful = true;
} else { } else {
String responseString = body == null ? "no body" : body.string(); throw new IOException("HTTP " + response.code());
onFailure(call, new IOException("HTTP " + response.code() + "\n\n" + responseString));
} }
} catch (Exception e) { } catch (Exception e) {
exception = e; exception = e;

@ -39,4 +39,5 @@ public class ReplyResponse {
public int postNo; public int postNo;
public String password; public String password;
public boolean probablyBanned; public boolean probablyBanned;
public boolean requireAuthentication;
} }

@ -25,11 +25,12 @@ import android.webkit.WebView;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; 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.settings.StringSetting;
import org.floens.chan.core.site.Authentication;
import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Resolvable; import org.floens.chan.core.site.Resolvable;
import org.floens.chan.core.site.Site; 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.SiteBase;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon; 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 TAG = "Chan4";
private static final String CAPTCHA_KEY = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc";
private static final Random random = new Random(); private static final Random random = new Random();
private final SiteEndpoints endpoints = new SiteEndpoints() { 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 // Legacy settings that were global before
private final StringSetting passUser; private final StringSetting passUser;
private final StringSetting passPass; private final StringSetting passPass;
@ -361,11 +353,6 @@ public class Chan4 extends SiteBase {
return siteRequestModifier; return siteRequestModifier;
} }
@Override
public SiteAuthentication authentication() {
return authentication;
}
@Override @Override
public ChanReader chanReader() { public ChanReader chanReader() {
return new FutabaChanReader(); 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 @Override
public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) { public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) {
httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(this, deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() { httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(this, deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() {

@ -24,9 +24,9 @@ import android.webkit.WebView;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; 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.Resolvable;
import org.floens.chan.core.site.Site; 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.SiteBase;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon; 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 @Override
public String name() { public String name() {
return "8chan"; return "8chan";
@ -216,11 +209,6 @@ public class Chan8 extends SiteBase {
return siteRequestModifier; return siteRequestModifier;
} }
@Override
public SiteAuthentication authentication() {
return authentication;
}
@Override @Override
public BoardsType boardsType() { public BoardsType boardsType() {
return BoardsType.INFINITE; return BoardsType.INFINITE;
@ -245,19 +233,27 @@ public class Chan8 extends SiteBase {
public void post(Reply reply, final PostListener postListener) { public void post(Reply reply, final PostListener postListener) {
// TODO // TODO
HttpCallManager httpCallManager = getGraph().get(HttpCallManager.class); HttpCallManager httpCallManager = getGraph().get(HttpCallManager.class);
httpCallManager.makeHttpCall(new Chan8ReplyHttpCall(this, reply), new HttpCall.HttpCallback<CommonReplyHttpCall>() { httpCallManager.makeHttpCall(new Chan8ReplyHttpCall(this, reply),
@Override new HttpCall.HttpCallback<CommonReplyHttpCall>() {
public void onHttpSuccess(CommonReplyHttpCall httpPost) { @Override
postListener.onPostComplete(httpPost, httpPost.replyResponse); public void onHttpSuccess(CommonReplyHttpCall httpPost) {
} postListener.onPostComplete(httpPost, httpPost.replyResponse);
}
@Override
public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) { @Override
Logger.e(TAG, "post error", e); 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 @Override

@ -20,14 +20,27 @@ package org.floens.chan.core.site.sites.chan8;
import android.text.TextUtils; import android.text.TextUtils;
import org.floens.chan.core.site.Site; 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.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.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response;
public class Chan8ReplyHttpCall extends CommonReplyHttpCall { public class Chan8ReplyHttpCall extends CommonReplyHttpCall {
private static final Pattern REQUIRE_AUTHENTICATION = Pattern.compile(".*\"captcha\": ?true.*");
private static final Pattern ERROR_MESSAGE =
Pattern.compile(".*<h1>Error</h1>.*<h2[^>]*>(.*?)<\\/h2>.*");
public Chan8ReplyHttpCall(Site site, Reply reply) { public Chan8ReplyHttpCall(Site site, Reply reply) {
super(site, reply); super(site, reply);
} }
@ -67,4 +80,41 @@ public class Chan8ReplyHttpCall extends CommonReplyHttpCall {
formBuilder.addFormDataPart("spoiler", "on"); 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<String> 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;
}
}
}
} }

@ -17,8 +17,9 @@
*/ */
package org.floens.chan.ui.captcha; package org.floens.chan.ui.captcha;
public interface CaptchaCallback { public interface AuthenticationLayoutCallback {
void captchaLoaded(CaptchaLayoutInterface captchaLayout); void onAuthenticationLoaded(AuthenticationLayoutInterface authenticationLayout);
void captchaEntered(CaptchaLayoutInterface captchaLayout, String challenge, String response); void onAuthenticationComplete(AuthenticationLayoutInterface authenticationLayout,
String challenge, String response);
} }

@ -17,8 +17,10 @@
*/ */
package org.floens.chan.ui.captcha; package org.floens.chan.ui.captcha;
public interface CaptchaLayoutInterface { import org.floens.chan.core.site.Site;
void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, CaptchaCallback callback);
public interface AuthenticationLayoutInterface {
void initialize(Site site, AuthenticationLayoutCallback callback);
void reset(); void reset();

@ -31,13 +31,17 @@ import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; 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.AndroidUtils;
import org.floens.chan.utils.IOUtils; 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 static final String TAG = "CaptchaLayout";
private CaptchaCallback callback; private AuthenticationLayoutCallback callback;
private boolean loaded = false; private boolean loaded = false;
private String baseUrl; private String baseUrl;
private String siteKey; private String siteKey;
@ -56,11 +60,15 @@ public class CaptchaLayout extends WebView implements CaptchaLayoutInterface {
} }
@SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) @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.callback = callback;
this.baseUrl = baseUrl; this.lightTheme = theme().isLightTheme;
this.siteKey = siteKey;
this.lightTheme = lightTheme; Authentication authentication = site.postAuthenticate();
this.siteKey = authentication.siteKey;
this.baseUrl = authentication.baseUrl;
AndroidUtils.hideKeyboard(this); AndroidUtils.hideKeyboard(this);
@ -111,14 +119,14 @@ public class CaptchaLayout extends WebView implements CaptchaLayoutInterface {
} }
private void onCaptchaLoaded() { private void onCaptchaLoaded() {
callback.captchaLoaded(this); callback.onAuthenticationLoaded(this);
} }
private void onCaptchaEntered(String challenge, String response) { private void onCaptchaEntered(String challenge, String response) {
if (TextUtils.isEmpty(response)) { if (TextUtils.isEmpty(response)) {
reset(); reset();
} else { } else {
callback.captchaEntered(this, challenge, response); callback.onAuthenticationComplete(this, challenge, response);
} }
} }

@ -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 <http://www.gnu.org/licenses/>.
*/
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));
}
}
}

@ -34,6 +34,8 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; 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.ui.view.FixedRatioThumbnailView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; 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.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; 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 FixedRatioThumbnailView image;
private EditText input; private EditText input;
private ImageView submit; private ImageView submit;
@ -50,7 +52,7 @@ public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutIn
private String baseUrl; private String baseUrl;
private String siteKey; private String siteKey;
private CaptchaCallback callback; private AuthenticationLayoutCallback callback;
private String challenge; private String challenge;
@ -116,10 +118,13 @@ public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutIn
} }
@Override @Override
public void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, CaptchaCallback callback) { public void initialize(Site site, AuthenticationLayoutCallback callback) {
this.baseUrl = baseUrl;
this.siteKey = siteKey;
this.callback = callback; this.callback = callback;
Authentication authentication = site.postAuthenticate();
this.siteKey = authentication.siteKey;
this.baseUrl = authentication.baseUrl;
} }
@Override @Override
@ -139,7 +144,7 @@ public class LegacyCaptchaLayout extends LinearLayout implements CaptchaLayoutIn
private void submitCaptcha() { private void submitCaptcha() {
AndroidUtils.hideKeyboard(this); 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) { private void onCaptchaLoaded(final String imageUrl, final String challenge) {

@ -42,15 +42,18 @@ import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.presenter.ReplyPresenter; 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.core.site.http.Reply;
import org.floens.chan.ui.activity.StartActivity; 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.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.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.helper.ImagePickDelegate; 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.LoadView;
import org.floens.chan.ui.view.SelectionListeningEditText; import org.floens.chan.ui.view.SelectionListeningEditText;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -74,7 +77,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply
private ReplyLayoutCallback callback; private ReplyLayoutCallback callback;
private boolean newCaptcha; private boolean newCaptcha;
private CaptchaLayoutInterface authenticationLayout; private AuthenticationLayoutInterface authenticationLayout;
private boolean openingName; private boolean openingName;
private boolean blockSelectionChange = false; private boolean blockSelectionChange = false;
@ -234,7 +237,9 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply
}/* else if (v == preview) { }/* else if (v == preview) {
// TODO // TODO
}*/ else if (v == captchaHardReset) { }*/ else if (v == captchaHardReset) {
authenticationLayout.hardReset(); if (authenticationLayout != null) {
authenticationLayout.hardReset();
}
} else if (v == commentQuoteButton) { } else if (v == commentQuoteButton) {
presenter.commentQuoteClicked(); presenter.commentQuoteClicked();
} else if (v == commentSpoilerButton) { } else if (v == commentSpoilerButton) {
@ -262,39 +267,53 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply
break; break;
case AUTHENTICATION: case AUTHENTICATION:
setWrap(false); 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); setView(captchaContainer);
captchaContainer.requestFocus(View.FOCUS_DOWN);
break; break;
} }
} }
@Override @Override
public void setCaptchaVersion(boolean newCaptcha) { public void initializeAuthentication(Site site, Authentication authentication,
this.newCaptcha = newCaptcha; 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 captchaContainer.addView((View) authenticationLayout, 0);
public void initCaptcha(String baseUrl, String siteKey, CaptchaCallback callback) { }
authenticationLayout.initCaptcha(baseUrl, siteKey, ThemeHelper.getInstance().getTheme().isLightTheme, callback);
if (!(authenticationLayout instanceof LegacyCaptchaLayout)) {
AndroidUtils.hideKeyboard(this);
}
authenticationLayout.initialize(site, callback);
authenticationLayout.reset(); authenticationLayout.reset();
} }
@Override @Override
public void resetCaptcha() { public void resetAuthentication() {
authenticationLayout.reset(); authenticationLayout.reset();
} }

Loading…
Cancel
Save