nya.miku.wishmaster.api.AbstractVichanModule.java Source code

Java tutorial

Introduction

Here is the source code for nya.miku.wishmaster.api.AbstractVichanModule.java

Source

/*
 * Overchan Android (Meta Imageboard Client)
 * Copyright (C) 2014-2016  miku-nyan <https://github.com/miku-nyan>
 *     
 * 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 nya.miku.wishmaster.api;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.tuple.Pair;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.HttpHeaders;
import cz.msebera.android.httpclient.NameValuePair;
import cz.msebera.android.httpclient.client.HttpClient;
import cz.msebera.android.httpclient.client.entity.UrlEncodedFormEntity;
import cz.msebera.android.httpclient.entity.mime.content.ByteArrayBody;
import cz.msebera.android.httpclient.message.BasicHeader;
import cz.msebera.android.httpclient.message.BasicNameValuePair;

import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.api.interfaces.ProgressListener;
import nya.miku.wishmaster.api.models.AttachmentModel;
import nya.miku.wishmaster.api.models.BadgeIconModel;
import nya.miku.wishmaster.api.models.BoardModel;
import nya.miku.wishmaster.api.models.DeletePostModel;
import nya.miku.wishmaster.api.models.PostModel;
import nya.miku.wishmaster.api.models.SendPostModel;
import nya.miku.wishmaster.api.models.ThreadModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.api.util.CryptoUtils;
import nya.miku.wishmaster.api.util.RegexUtils;
import nya.miku.wishmaster.api.util.UrlPathUtils;
import nya.miku.wishmaster.api.util.WakabaUtils;
import nya.miku.wishmaster.common.IOUtils;
import nya.miku.wishmaster.http.ExtendedMultipartBuilder;
import nya.miku.wishmaster.http.streamer.HttpRequestModel;
import nya.miku.wishmaster.http.streamer.HttpResponseModel;
import nya.miku.wishmaster.http.streamer.HttpStreamer;
import nya.miku.wishmaster.http.streamer.HttpWrongStatusCodeException;
import nya.miku.wishmaster.lib.org_json.JSONArray;
import nya.miku.wishmaster.lib.org_json.JSONObject;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.support.annotation.NonNull;

public abstract class AbstractVichanModule extends AbstractWakabaModule {
    private static final Pattern ATTACHMENT_EMBEDDED_LINK = Pattern.compile("<a[^>]*href=\"([^\">]*)\"[^>]*>");
    private static final Pattern ATTACHMENT_EMBEDDED_THUMB = Pattern.compile("<img[^>]*src=\"([^\">]*)\"[^>]*>");

    private static final Pattern ERROR_PATTERN = Pattern.compile("<h2 [^>]*>(.*?)</h2>");

    public AbstractVichanModule(SharedPreferences preferences, Resources resources) {
        super(preferences, resources);
    }

    @Override
    public BoardModel getBoard(String shortName, ProgressListener listener, CancellableTask task) throws Exception {
        BoardModel board = super.getBoard(shortName, listener, task);
        board.timeZoneId = "UTC";
        board.defaultUserName = "Anonymous";
        board.readonlyBoard = false;
        board.requiredFileForNewThread = true;
        board.allowDeletePosts = true;
        board.allowDeleteFiles = true;
        board.allowReport = BoardModel.REPORT_WITH_COMMENT;
        board.allowNames = true;
        board.allowSubjects = true;
        board.allowSage = true;
        board.allowEmails = true;
        board.ignoreEmailIfSage = true;
        board.allowCustomMark = false;
        board.allowRandomHash = true;
        board.allowIcons = false;
        board.attachmentsMaxCount = 1;
        board.attachmentsFormatFilters = null;
        board.markType = BoardModel.MARK_BBCODE;
        board.firstPage = 1;
        board.catalogAllowed = true;
        return board;
    }

    @Override
    public ThreadModel[] getThreadsList(String boardName, int page, ProgressListener listener, CancellableTask task,
            ThreadModel[] oldList) throws Exception {
        String url = getUsingUrl() + boardName + "/" + (page - 1) + ".json";
        JSONObject response = downloadJSONObject(url, oldList != null, listener, task);
        if (response == null)
            return oldList;
        JSONArray threads = response.getJSONArray("threads");
        ThreadModel[] result = new ThreadModel[threads.length()];
        for (int i = 0, len = threads.length(); i < len; ++i) {
            JSONArray posts = threads.getJSONObject(i).getJSONArray("posts");
            JSONObject op = posts.getJSONObject(0);
            ThreadModel curThread = mapThreadModel(op, boardName);
            curThread.posts = new PostModel[posts.length()];
            for (int j = 0, plen = posts.length(); j < plen; ++j) {
                curThread.posts[j] = mapPostModel(posts.getJSONObject(j), boardName);
            }
            result[i] = curThread;
        }
        return result;
    }

    @Override
    public PostModel[] getPostsList(String boardName, String threadNumber, ProgressListener listener,
            CancellableTask task, PostModel[] oldList) throws Exception {
        String url = getUsingUrl() + boardName + "/res/" + threadNumber + ".json";
        JSONObject response = downloadJSONObject(url, oldList != null, listener, task);
        if (response == null)
            return oldList;
        JSONArray posts = response.getJSONArray("posts");
        PostModel[] result = new PostModel[posts.length()];
        for (int i = 0, len = posts.length(); i < len; ++i) {
            result[i] = mapPostModel(posts.getJSONObject(i), boardName);
        }
        if (oldList != null) {
            result = ChanModels.mergePostsLists(Arrays.asList(oldList), Arrays.asList(result));
        }
        return result;
    }

    @Override
    public ThreadModel[] getCatalog(String boardName, int catalogType, ProgressListener listener,
            CancellableTask task, ThreadModel[] oldList) throws Exception {
        String url = getUsingUrl() + boardName + "/catalog.json";
        JSONArray response = downloadJSONArray(url, oldList != null, listener, task);
        if (response == null)
            return oldList;
        List<ThreadModel> threads = new ArrayList<>();
        for (int i = 0, len = response.length(); i < len; ++i) {
            JSONArray curArray = response.getJSONObject(i).getJSONArray("threads");
            for (int j = 0, clen = curArray.length(); j < clen; ++j) {
                JSONObject curThreadJson = curArray.getJSONObject(j);
                ThreadModel curThread = new ThreadModel();
                curThread.threadNumber = Long.toString(curThreadJson.getLong("no"));
                curThread.postsCount = curThreadJson.optInt("replies", -2) + 1;
                curThread.attachmentsCount = curThreadJson.optInt("images", -2) + 1;
                curThread.isSticky = curThreadJson.optInt("sticky") == 1;
                curThread.isClosed = curThreadJson.optInt("closed") == 1;
                curThread.posts = new PostModel[] { mapPostModel(curThreadJson, boardName) };
                threads.add(curThread);
            }
        }
        return threads.toArray(new ThreadModel[threads.size()]);
    }

    protected ThreadModel mapThreadModel(JSONObject opPost, String boardName) {
        ThreadModel curThread = new ThreadModel();
        curThread.threadNumber = Long.toString(opPost.getLong("no"));
        curThread.postsCount = opPost.optInt("replies", -2) + 1;
        curThread.attachmentsCount = opPost.optInt("images", -2) + 1;
        if (curThread.attachmentsCount >= 0)
            curThread.attachmentsCount += opPost.optInt("omitted_images", 0);
        curThread.isSticky = opPost.optInt("sticky") == 1;
        curThread.isClosed = opPost.optInt("closed") == 1;
        return curThread;
    }

    protected PostModel mapPostModel(JSONObject object, String boardName) {
        PostModel model = new PostModel();
        model.number = Long.toString(object.getLong("no"));
        model.name = StringEscapeUtils
                .unescapeHtml4(RegexUtils.removeHtmlSpanTags(object.optString("name", "Anonymous")));
        model.subject = StringEscapeUtils.unescapeHtml4(object.optString("sub", ""));
        model.comment = object.optString("com", "");
        model.email = object.optString("email", "");
        model.trip = object.optString("trip", "");
        String capcode = object.optString("capcode", "none");
        if (!capcode.equals("none"))
            model.trip += "##" + capcode;
        String countryIcon = object.optString("country", "");
        if (!countryIcon.equals("")) {
            BadgeIconModel icon = new BadgeIconModel();
            icon.source = "/static/flags/" + countryIcon.toLowerCase(Locale.US) + ".png";
            icon.description = object.optString("country_name");
            model.icons = new BadgeIconModel[] { icon };
        }
        model.op = false;
        String id = object.optString("id", "");
        model.sage = id.equalsIgnoreCase("Heaven") || model.email.toLowerCase(Locale.US).contains("sage");
        if (!id.equals(""))
            model.name += (" ID:" + id);
        if (!id.equals("") && !id.equalsIgnoreCase("Heaven"))
            model.color = CryptoUtils.hashIdColor(id);
        model.timestamp = object.getLong("time") * 1000;
        model.parentThread = object.optString("resto", "0");
        if (model.parentThread.equals("0"))
            model.parentThread = model.number;

        List<AttachmentModel> attachments = null;
        boolean isSpoiler = object.optInt("spoiler") == 1;
        AttachmentModel rootAttachment = mapAttachment(object, boardName, isSpoiler);
        if (rootAttachment != null) {
            attachments = new ArrayList<>();
            attachments.add(rootAttachment);
            JSONArray extraFiles = object.optJSONArray("extra_files");
            if (extraFiles != null && extraFiles.length() != 0) {
                for (int i = 0, len = extraFiles.length(); i < len; ++i) {
                    AttachmentModel attachment = mapAttachment(extraFiles.getJSONObject(i), boardName, isSpoiler);
                    if (attachment != null)
                        attachments.add(attachment);
                }
            }
        }
        String embed = object.optString("embed", "");
        if (!embed.equals("")) {
            AttachmentModel embedAttachment = new AttachmentModel();
            embedAttachment.type = AttachmentModel.TYPE_OTHER_NOTFILE;
            Matcher linkMatcher = ATTACHMENT_EMBEDDED_LINK.matcher(embed);
            if (linkMatcher.find()) {
                embedAttachment.path = linkMatcher.group(1);
                if (embedAttachment.path.startsWith("//"))
                    embedAttachment.path = (useHttps() ? "https:" : "http:") + embedAttachment.path;
                Matcher thumbMatcher = ATTACHMENT_EMBEDDED_THUMB.matcher(embed);
                if (thumbMatcher.find()) {
                    embedAttachment.thumbnail = thumbMatcher.group(1);
                    if (embedAttachment.thumbnail.startsWith("//"))
                        embedAttachment.thumbnail = (useHttps() ? "https:" : "http:") + embedAttachment.thumbnail;
                }
                embedAttachment.isSpoiler = isSpoiler;
                embedAttachment.size = -1;
                if (attachments != null)
                    attachments.add(embedAttachment);
                else
                    attachments = Collections.singletonList(embedAttachment);
            }
        }
        if (attachments != null)
            model.attachments = attachments.toArray(new AttachmentModel[attachments.size()]);
        return model;
    }

    protected AttachmentModel mapAttachment(JSONObject object, String boardName, boolean isSpoiler) {
        String ext = object.optString("ext", "");
        if (!ext.equals("")) {
            AttachmentModel attachment = new AttachmentModel();
            switch (ext) {
            case ".jpeg":
            case ".jpg":
            case ".png":
                attachment.type = AttachmentModel.TYPE_IMAGE_STATIC;
                break;
            case ".gif":
                attachment.type = AttachmentModel.TYPE_IMAGE_GIF;
                break;
            case ".svg":
            case ".svgz":
                attachment.type = AttachmentModel.TYPE_IMAGE_SVG;
                break;
            case ".mp3":
            case ".ogg":
                attachment.type = AttachmentModel.TYPE_AUDIO;
                break;
            case ".webm":
            case ".mp4":
                attachment.type = AttachmentModel.TYPE_VIDEO;
                break;
            default:
                attachment.type = AttachmentModel.TYPE_OTHER_FILE;
            }
            attachment.size = object.optInt("fsize", -1);
            if (attachment.size > 0)
                attachment.size = Math.round(attachment.size / 1024f);
            attachment.width = object.optInt("w", -1);
            attachment.height = object.optInt("h", -1);
            attachment.originalName = object.optString("filename", "") + ext;
            attachment.isSpoiler = isSpoiler;
            String tim = object.optString("tim", "");
            if (tim.length() > 0) {
                attachment.thumbnail = isSpoiler || attachment.type == AttachmentModel.TYPE_AUDIO ? null
                        : getAttachmentThumbnailPath(boardName, ext, tim);
                attachment.path = getAttachmentPath(boardName, ext, tim);
                return attachment;
            }
        }
        return null;
    }

    @NonNull
    protected String getAttachmentPath(String boardName, String ext, String tim) {
        return "/" + boardName + "/src/" + tim + ext;
    }

    @NonNull
    protected String getAttachmentThumbnailPath(String boardName, String ext, String tim) {
        return "/" + boardName + "/thumb/" + tim + ".jpg";
    }

    @Override
    public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception {
        UrlPageModel urlModel = new UrlPageModel();
        urlModel.chanName = getChanName();
        urlModel.boardName = model.boardName;
        if (model.threadNumber == null) {
            urlModel.type = UrlPageModel.TYPE_BOARDPAGE;
            urlModel.boardPage = UrlPageModel.DEFAULT_FIRST_PAGE;
        } else {
            urlModel.type = UrlPageModel.TYPE_THREADPAGE;
            urlModel.threadNumber = model.threadNumber;
        }
        String referer = buildUrl(urlModel);
        List<Pair<String, String>> fields = VichanAntiBot.getFormValues(referer, task, httpClient);

        if (task != null && task.isCancelled())
            throw new Exception("interrupted");

        ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create()
                .setCharset(Charset.forName("UTF-8")).setDelegates(listener, task);
        for (Pair<String, String> pair : fields) {
            if (pair.getKey().equals("spoiler") && !model.custommark)
                continue;
            String val;
            switch (pair.getKey()) {
            case "name":
                val = model.name;
                break;
            case "email":
                val = getSendPostEmail(model);
                break;
            case "subject":
                val = model.subject;
                break;
            case "body":
                val = model.comment;
                break;
            case "password":
                val = model.password;
                break;
            case "spoiler":
                val = "on";
                break;
            default:
                val = pair.getValue();
            }
            if (pair.getKey().equals("file")) {
                if (model.attachments != null && model.attachments.length > 0) {
                    postEntityBuilder.addFile(pair.getKey(), model.attachments[0], model.randomHash);
                } else {
                    postEntityBuilder.addPart(pair.getKey(), new ByteArrayBody(new byte[0], ""));
                }
            } else {
                postEntityBuilder.addString(pair.getKey(), val);
            }
        }

        String url = getUsingUrl() + "post.php";
        Header[] customHeaders = new Header[] { new BasicHeader(HttpHeaders.REFERER, referer) };
        HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build())
                .setCustomHeaders(customHeaders).setNoRedirect(true).build();
        HttpResponseModel response = null;
        try {
            response = HttpStreamer.getInstance().getFromUrl(url, request, httpClient, listener, task);
            if (response.statusCode == 200 || response.statusCode == 400) {
                ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
                IOUtils.copyStream(response.stream, output);
                String htmlResponse = output.toString("UTF-8");
                Matcher errorMatcher = ERROR_PATTERN.matcher(htmlResponse);
                if (errorMatcher.find())
                    throw new Exception(errorMatcher.group(1));
            } else if (response.statusCode == 303) {
                for (Header header : response.headers) {
                    if (header != null && HttpHeaders.LOCATION.equalsIgnoreCase(header.getName())) {
                        return fixRelativeUrl(header.getValue());
                    }
                }
            }
            throw new Exception(response.statusCode + " - " + response.statusReason);
        } finally {
            if (response != null)
                response.release();
        }
    }

    @Override
    public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task)
            throws Exception {
        String url = getUsingUrl() + "post.php";
        List<NameValuePair> pairs = new ArrayList<NameValuePair>();
        pairs.add(new BasicNameValuePair("board", model.boardName));
        pairs.add(new BasicNameValuePair("delete_" + model.postNumber, "on"));
        if (model.onlyFiles)
            pairs.add(new BasicNameValuePair("file", "on"));
        pairs.add(new BasicNameValuePair("password", model.password));
        pairs.add(new BasicNameValuePair("delete", getDeleteFormValue(model)));
        pairs.add(new BasicNameValuePair("reason", ""));

        UrlPageModel refererPage = new UrlPageModel();
        refererPage.type = UrlPageModel.TYPE_THREADPAGE;
        refererPage.chanName = getChanName();
        refererPage.boardName = model.boardName;
        refererPage.threadNumber = model.threadNumber;
        Header[] customHeaders = new Header[] { new BasicHeader(HttpHeaders.REFERER, buildUrl(refererPage)) };
        HttpRequestModel rqModel = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8"))
                .setCustomHeaders(customHeaders).setNoRedirect(true).build();
        HttpResponseModel response = null;
        try {
            response = HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, listener, task);
            if (response.statusCode == 200 || response.statusCode == 400 || response.statusCode == 303) {
                ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
                IOUtils.copyStream(response.stream, output);
                String htmlResponse = output.toString("UTF-8");
                Matcher errorMatcher = ERROR_PATTERN.matcher(htmlResponse);
                if (errorMatcher.find())
                    throw new Exception(errorMatcher.group(1));
                return null;
            }
            throw new Exception(response.statusCode + " - " + response.statusReason);
        } finally {
            if (response != null)
                response.release();
        }
    }

    @Override
    public String reportPost(DeletePostModel model, ProgressListener listener, CancellableTask task)
            throws Exception {
        String url = getUsingUrl() + "post.php";
        List<NameValuePair> pairs = new ArrayList<NameValuePair>();
        pairs.add(new BasicNameValuePair("board", model.boardName));
        pairs.add(new BasicNameValuePair("delete_" + model.postNumber, "on"));
        pairs.add(new BasicNameValuePair("password", ""));
        pairs.add(new BasicNameValuePair("reason", model.reportReason));
        pairs.add(new BasicNameValuePair("report", getReportFormValue(model)));

        UrlPageModel refererPage = new UrlPageModel();
        refererPage.type = UrlPageModel.TYPE_THREADPAGE;
        refererPage.chanName = getChanName();
        refererPage.boardName = model.boardName;
        refererPage.threadNumber = model.threadNumber;
        Header[] customHeaders = new Header[] { new BasicHeader(HttpHeaders.REFERER, buildUrl(refererPage)) };
        HttpRequestModel rqModel = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8"))
                .setCustomHeaders(customHeaders).setNoRedirect(true).build();
        HttpResponseModel response = null;
        try {
            response = HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, listener, task);
            if (response.statusCode == 200 || response.statusCode == 400 || response.statusCode == 303) {
                ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
                IOUtils.copyStream(response.stream, output);
                String htmlResponse = output.toString("UTF-8");
                Matcher errorMatcher = ERROR_PATTERN.matcher(htmlResponse);
                if (errorMatcher.find())
                    throw new Exception(errorMatcher.group(1));
                return null;
            }
            throw new Exception(response.statusCode + " - " + response.statusReason);
        } finally {
            if (response != null)
                response.release();
        }
    }

    protected String getSendPostEmail(SendPostModel model) {
        return model.sage ? "sage" : model.email;
    }

    protected String getDeleteFormValue(DeletePostModel model) {
        return "Delete";
    }

    protected String getReportFormValue(DeletePostModel model) {
        return "Report";
    }

    @Override
    public String buildUrl(UrlPageModel model) throws IllegalArgumentException {
        if (!model.chanName.equals(getChanName()))
            throw new IllegalArgumentException("wrong chan");
        if (model.type == UrlPageModel.TYPE_CATALOGPAGE)
            return getUsingUrl() + model.boardName + "/catalog.html";
        if (model.type == UrlPageModel.TYPE_BOARDPAGE && model.boardPage == 1)
            return (getUsingUrl() + model.boardName + "/");
        return WakabaUtils.buildUrl(model, getUsingUrl());
    }

    @Override
    public UrlPageModel parseUrl(String url) throws IllegalArgumentException {
        String urlPath = UrlPathUtils.getUrlPath(url, getAllDomains());
        if (urlPath == null)
            throw new IllegalArgumentException("wrong domain");
        if (url.contains("/catalog.html")) {
            try {
                int index = url.indexOf("/catalog.html");
                String left = url.substring(0, index);
                UrlPageModel model = new UrlPageModel();
                model.chanName = getChanName();
                model.type = UrlPageModel.TYPE_CATALOGPAGE;
                model.boardName = left.substring(left.lastIndexOf('/') + 1);
                model.catalogType = 0;
                return model;
            } catch (Exception e) {
            }
        }
        UrlPageModel model = WakabaUtils.parseUrlPath(urlPath, getChanName());
        if (model.type == UrlPageModel.TYPE_BOARDPAGE && model.boardPage == 0)
            model.boardPage = 1;
        return model;
    }

    @Override
    public void downloadFile(String url, OutputStream out, ProgressListener listener, CancellableTask task)
            throws Exception {
        try {
            super.downloadFile(url, out, listener, task);
        } catch (HttpWrongStatusCodeException e) {
            if (url.contains("/thumb/") && url.endsWith(".jpg") && e.getStatusCode() == 404) {
                super.downloadFile(url.substring(0, url.length() - 3) + "png", out, listener, task);
            } else {
                throw e;
            }
        }
    }

    @Override
    public String fixRelativeUrl(String url) {
        if (url.startsWith("?/"))
            url = url.substring(1);
        return super.fixRelativeUrl(url);
    }

    protected static class VichanAntiBot {
        public static List<Pair<String, String>> getFormValues(String url, CancellableTask task,
                HttpClient httpClient) throws Exception {
            return getFormValues(url, HttpRequestModel.DEFAULT_GET, task, httpClient, "<form name=\"post\"",
                    "</form>");
        }

        public static List<Pair<String, String>> getFormValues(String url, HttpRequestModel requestModel,
                CancellableTask task, HttpClient client, String startForm, String endForm) throws Exception {
            VichanAntiBot reader = null;
            HttpRequestModel request = requestModel;
            HttpResponseModel response = null;
            try {
                response = HttpStreamer.getInstance().getFromUrl(url, request, client, null, task);
                reader = new VichanAntiBot(response.stream, startForm, endForm);
                return reader.readForm();
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (Exception e) {
                    }
                }
                if (response != null)
                    response.release();
            }
        }

        private StringBuilder readBuffer = new StringBuilder();
        private List<Pair<String, String>> result = null;

        private String currentName = null;
        private String currentValue = null;
        private boolean currentTextarea = false;
        private boolean currentReading = false;

        private final char[] start;
        private final char[][] filters;
        private final Reader in;

        private static final int FILTER_INPUT_OPEN = 0;
        private static final int FILTER_TEXTAREA_OPEN = 1;
        private static final int FILTER_NAME_OPEN = 2;
        private static final int FILTER_VALUE_OPEN = 3;
        private static final int FILTER_TAG_CLOSE = 4;
        private static final int FILTER_TAG_BEFORE_CLOSE = 5;

        private VichanAntiBot(InputStream in, String start, String end) {
            this.start = start.toCharArray();
            this.filters = new char[][] { "<input".toCharArray(), "<textarea".toCharArray(),
                    "name=\"".toCharArray(), "value=\"".toCharArray(), ">".toCharArray(), "/".toCharArray(),
                    end.toCharArray() };
            this.in = new BufferedReader(new InputStreamReader(in));
        }

        private List<Pair<String, String>> readForm() throws IOException {
            result = new ArrayList<>();
            skipUntilSequence(start);
            int filtersCount = filters.length;
            int[] pos = new int[filtersCount];
            int[] len = new int[filtersCount];
            for (int i = 0; i < filtersCount; ++i)
                len[i] = filters[i].length;

            int curChar;
            while ((curChar = in.read()) != -1) {
                for (int i = 0; i < filtersCount; ++i) {
                    if (curChar == filters[i][pos[i]]) {
                        ++pos[i];
                        if (pos[i] == len[i]) {
                            if (i == filtersCount - 1) {
                                return result;
                            }
                            handleFilter(i);
                            pos[i] = 0;
                        }
                    } else {
                        if (pos[i] != 0)
                            pos[i] = curChar == filters[i][0] ? 1 : 0;
                    }
                }
            }
            return result;
        }

        private void handleFilter(int i) throws IOException {
            switch (i) {
            case FILTER_INPUT_OPEN:
                currentReading = true;
                currentTextarea = false;
                break;
            case FILTER_TEXTAREA_OPEN:
                currentReading = true;
                currentTextarea = true;
                break;
            case FILTER_NAME_OPEN:
                currentName = StringEscapeUtils.unescapeHtml4(readUntilSequence("\"".toCharArray()));
                break;
            case FILTER_VALUE_OPEN:
                currentValue = StringEscapeUtils.unescapeHtml4(readUntilSequence("\"".toCharArray()));
                break;
            case FILTER_TAG_CLOSE:
                if (currentTextarea) {
                    currentValue = StringEscapeUtils.unescapeHtml4(readUntilSequence("<".toCharArray()));
                }
                if (currentReading && currentName != null)
                    result.add(Pair.of(currentName, currentValue != null ? currentValue : ""));
                currentName = null;
                currentValue = null;
                currentReading = false;
                currentTextarea = false;
                break;
            case FILTER_TAG_BEFORE_CLOSE: // <textarea ..... />
                currentTextarea = false;
                break;
            }
        }

        private void skipUntilSequence(char[] sequence) throws IOException {
            int len = sequence.length;
            if (len == 0)
                return;
            int pos = 0;
            int curChar;
            while ((curChar = in.read()) != -1) {
                if (curChar == sequence[pos]) {
                    ++pos;
                    if (pos == len)
                        break;
                } else {
                    if (pos != 0)
                        pos = curChar == sequence[0] ? 1 : 0;
                }
            }
        }

        private String readUntilSequence(char[] sequence) throws IOException {
            int len = sequence.length;
            if (len == 0)
                return "";
            readBuffer.setLength(0);
            int pos = 0;
            int curChar;
            while ((curChar = in.read()) != -1) {
                readBuffer.append((char) curChar);
                if (curChar == sequence[pos]) {
                    ++pos;
                    if (pos == len)
                        break;
                } else {
                    if (pos != 0)
                        pos = curChar == sequence[0] ? 1 : 0;
                }
            }
            int buflen = readBuffer.length();
            if (buflen >= len) {
                readBuffer.setLength(buflen - len);
                return readBuffer.toString();
            } else {
                return "";
            }
        }

        public void close() throws IOException {
            in.close();
        }
    }
}