/*
* 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringEscapeUtils;
import android.content.SharedPreferences;
import android.content.res.Resources;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.HttpHeaders;
import cz.msebera.android.httpclient.NameValuePair;
import cz.msebera.android.httpclient.client.entity.UrlEncodedFormEntity;
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.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.UrlPageModel;
import nya.miku.wishmaster.api.util.RegexUtils;
import nya.miku.wishmaster.api.util.WakabaReader;
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;
public abstract class AbstractKusabaModule extends AbstractWakabaModule {
private static final Pattern ERROR_POSTING = Pattern.compile("<h2(?:[^>]*)>(.*?)</h2>", Pattern.DOTALL);
public AbstractKusabaModule(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_WAKABAMARK;
return board;
}
@Override
protected final WakabaReader getWakabaReader(InputStream stream, UrlPageModel urlModel) {
return getKusabaReader(stream, urlModel);
}
protected int getKusabaFlags() {
return ~0;
}
protected WakabaReader getKusabaReader(InputStream stream, UrlPageModel urlModel) {
return new KusabaReader(stream, getDateFormat(), canCloudflare(), getKusabaFlags());
}
@Override
public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String url = getBoardScriptUrl(model);
ExtendedMultipartBuilder postEntityBuilder = ExtendedMultipartBuilder.create().setDelegates(listener, task);
setSendPostEntity(model, postEntityBuilder);
HttpRequestModel request = HttpRequestModel.builder().setPOST(postEntityBuilder.build()).setNoRedirect(true).build();
HttpResponseModel response = null;
try {
response = HttpStreamer.getInstance().getFromUrl(url, request, httpClient, null, task);
if (response.statusCode == 302) {
for (Header header : response.headers) {
if (header != null && HttpHeaders.LOCATION.equalsIgnoreCase(header.getName())) {
return fixRelativeUrl(header.getValue());
}
}
} else if (response.statusCode == 200) {
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
IOUtils.copyStream(response.stream, output);
String htmlResponse = output.toString("UTF-8");
Matcher errorMatcher = ERROR_POSTING.matcher(htmlResponse);
if (errorMatcher.find()) throw new Exception(errorMatcher.group(1).trim());
} else throw new Exception(response.statusCode + " - " + response.statusReason);
} finally {
if (response != null) response.release();
}
return null;
}
@Override
public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String url = getBoardScriptUrl(model);
List<? extends NameValuePair> pairs = getDeleteFormAllValues(model);
HttpRequestModel request = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).setNoRedirect(true).build();
String result = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, listener, task, false);
checkDeletePostResult(model, result);
return null;
}
@Override
public String reportPost(DeletePostModel model, ProgressListener listener, CancellableTask task) throws Exception {
String url = getBoardScriptUrl(model);
List<? extends NameValuePair> pairs = getReportFormAllValues(model);
HttpRequestModel request = HttpRequestModel.builder().setPOST(new UrlEncodedFormEntity(pairs, "UTF-8")).setNoRedirect(true).build();
String result = HttpStreamer.getInstance().getStringFromUrl(url, request, httpClient, listener, task, false);
checkReportPostResult(model, result);
return null;
}
protected String getBoardScriptUrl(Object tag) {
return getUsingUrl() + "board.php";
}
protected void setSendPostEntity(SendPostModel model, ExtendedMultipartBuilder postEntityBuilder) throws Exception {
setSendPostEntityMain(model, postEntityBuilder);
setSendPostEntityAttachments(model, postEntityBuilder);
setSendPostEntityPassword(model, postEntityBuilder);
}
protected void setSendPostEntityMain(SendPostModel model, ExtendedMultipartBuilder postEntityBuilder) throws Exception {
postEntityBuilder.
addString("board", model.boardName).
addString("replythread", model.threadNumber == null ? "0" : model.threadNumber).
addString("name", model.name).
addString("em", model.sage ? "sage" : model.email).
addString("subject", model.subject).
addString("message", model.comment);
}
protected void setSendPostEntityAttachments(SendPostModel model, ExtendedMultipartBuilder postEntityBuilder) throws Exception {
if (model.attachments != null && model.attachments.length > 0) {
postEntityBuilder.addFile("imagefile", model.attachments[0], model.randomHash);
if (model.custommark) postEntityBuilder.addString("spoiler", "on");
} else if (model.threadNumber == null) postEntityBuilder.addString("nofile", "on");
}
protected void setSendPostEntityPassword(SendPostModel model, ExtendedMultipartBuilder postEntityBuilder) throws Exception {
postEntityBuilder.addString("postpassword", model.password);
}
protected List<? extends NameValuePair> getDeleteFormAllValues(DeletePostModel model) throws Exception {
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
pairs.add(new BasicNameValuePair("board", model.boardName));
pairs.add(new BasicNameValuePair("post[]", model.postNumber));
if (model.onlyFiles) pairs.add(new BasicNameValuePair("fileonly", "on"));
pairs.add(new BasicNameValuePair("postpassword", model.password));
pairs.add(new BasicNameValuePair("deletepost", getDeleteFormValue(model)));
return pairs;
}
protected List<? extends NameValuePair> getReportFormAllValues(DeletePostModel model) throws Exception {
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
pairs.add(new BasicNameValuePair("board", model.boardName));
pairs.add(new BasicNameValuePair("post[]", model.postNumber));
pairs.add(new BasicNameValuePair("reportreason", model.reportReason));
pairs.add(new BasicNameValuePair("reportpost", getReportFormValue(model)));
return pairs;
}
protected void checkDeletePostResult(DeletePostModel model, String result) throws Exception {
if (result.contains("Incorrect password")) throw new Exception("Incorrect password");
if (result.contains("Неверный пароль")) throw new Exception("Неверный пароль");
if (result.contains("Неправильный пароль")) throw new Exception("Неправильный пароль");
if (result.contains("Ошибка при попытке удалить сообщение")) throw new Exception("Ошибка при попытке удалить сообщение");
}
protected void checkReportPostResult(DeletePostModel model, String result) throws Exception {
if (result.contains("Post successfully reported")) return;
throw new Exception(result);
}
protected String getDeleteFormValue(DeletePostModel model) {
return "Delete";
}
protected String getReportFormValue(DeletePostModel model) {
return "Report";
}
protected static class KusabaReader extends WakabaReader {
public static final int FLAG_HANDLE_ADMIN_TAG = 1 << 0;
public static final int FLAG_HANDLE_MOD_TAG = 1 << 1;
public static final int FLAG_HANDLE_LOCKED_STICKY = 1 << 2;
public static final int FLAG_OMITTED_STRING_REMOVE_HREF = 1 << 3;
public static final int FLAG_HANDLE_EMBEDDED_POST_POSTPROCESS = 1 << 4;
private static final Pattern PATTERN_EMBEDDED =
Pattern.compile("<object type=\"application/x-shockwave-flash\"(?:[^>]*)data=\"([^\"]*)\"(?:[^>]*)>", Pattern.DOTALL);
private static final Pattern A_HREF = Pattern.compile("<a href[^>]*>");
private static final char[] FILTER_ADMIN = "<span class=\"admin\">".toCharArray();
private static final char[] FILTER_MOD = "<span class=\"mod\">".toCharArray();
private static final char[] FILTER_SPAN_CLOSE = "</span>".toCharArray();
private int curAdminPos = 0;
private int curModPos = 0;
private String adminTrip = null;
private final int flags;
public KusabaReader(InputStream in, DateFormat dateFormat, boolean canCloudflare, int flags) {
super(in, dateFormat, canCloudflare);
this.flags = flags;
}
public KusabaReader(Reader reader, DateFormat dateFormat, boolean canCloudflare, int flags) {
super(reader, dateFormat, canCloudflare);
this.flags = flags;
}
private boolean hasFlag(int flag) {
return (flags & flag) != 0;
}
private void addAdminTrip(String trip) {
if (adminTrip == null) adminTrip = trip; else adminTrip += trip;
}
/** переопределяющий метод должен вызвать метод суперкласса */
@Override
protected void postprocessPost(PostModel post) {
if (adminTrip != null) {
post.trip += adminTrip;
adminTrip = null;
}
if (hasFlag(FLAG_HANDLE_EMBEDDED_POST_POSTPROCESS)) {
Matcher matcher = PATTERN_EMBEDDED.matcher(post.comment);
while (matcher.find()) {
String url = matcher.group(1).replace("youtube.com/v/", "youtube.com/watch?v=");
String id = null;
if (url.contains("youtube") && url.contains("v=")) {
id = url.substring(url.indexOf("v=") + 2);
if (id.contains("&")) id = id.substring(0, id.indexOf("&"));
}
AttachmentModel attachment = new AttachmentModel();
attachment.type = AttachmentModel.TYPE_OTHER_NOTFILE;
attachment.size = -1;
attachment.path = url;
attachment.thumbnail = id != null ? ("http://img.youtube.com/vi/" + id + "/default.jpg") : null;
int oldCount = post.attachments != null ? post.attachments.length : 0;
AttachmentModel[] attachments = new AttachmentModel[oldCount + 1];
for (int i=0; i<oldCount; ++i) attachments[i] = post.attachments[i];
attachments[oldCount] = attachment;
post.attachments = attachments;
}
}
}
/** переопределяющий метод должен вызвать метод суперкласса */
@Override
protected void customFilters(int ch) throws IOException {
if (ch == FILTER_ADMIN[curAdminPos]) {
++curAdminPos;
if (curAdminPos == FILTER_ADMIN.length) {
if (hasFlag(FLAG_HANDLE_ADMIN_TAG)) {
addAdminTrip(StringEscapeUtils.unescapeHtml4(readUntilSequence(FILTER_SPAN_CLOSE)).trim());
}
curAdminPos = 0;
}
} else {
if (curAdminPos != 0) curAdminPos = ch == FILTER_ADMIN[0] ? 1 : 0;
}
if (ch == FILTER_MOD[curModPos]) {
++curModPos;
if (curModPos == FILTER_MOD.length) {
if (hasFlag(FLAG_HANDLE_MOD_TAG)) {
addAdminTrip(StringEscapeUtils.unescapeHtml4(readUntilSequence(FILTER_SPAN_CLOSE)).trim());
}
curModPos = 0;
}
} else {
if (curModPos != 0) curModPos = ch == FILTER_MOD[0] ? 1 : 0;
}
}
@Override
protected void parseOmittedString(String omitted) {
if (hasFlag(FLAG_OMITTED_STRING_REMOVE_HREF)) omitted = RegexUtils.replaceAll(omitted, A_HREF, "");
super.parseOmittedString(omitted);
}
@Override
protected void parseThumbnail(String imgTag) {
if (hasFlag(FLAG_HANDLE_LOCKED_STICKY)) {
if (imgTag.contains("/css/locked.gif")) currentThread.isClosed = true;
if (imgTag.contains("/css/sticky.gif")) currentThread.isSticky = true;
}
super.parseThumbnail(imgTag);
}
}
}