first commit

This commit is contained in:
Radek Davidek 2026-03-20 10:48:09 +01:00
commit 911853a183
160 changed files with 9105 additions and 0 deletions

8
.gitignore vendored Executable file
View File

@ -0,0 +1,8 @@
servers
target
bin
.settings
.metadata
.classpath
.project

55
pom.xml Normal file
View File

@ -0,0 +1,55 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.kamma.fabka</groupId>
<artifactId>app-httpserver</artifactId>
<version>1.0-SNAPSHOT</version>
<name>FabkovaChata HttpServer</name>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cz.kamma.fabka.httpserver.HttpServerApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,54 @@
package cz.kamma.fabka.httpserver;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class AppConfig {
private static final Properties properties = new Properties();
static {
try (InputStream in = AppConfig.class.getClassLoader().getResourceAsStream("app.properties")) {
if (in != null) {
properties.load(in);
}
} catch (IOException e) {
// Properties file not found, will use defaults
}
}
public static String get(String key, String defaultValue) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) {
value = System.getenv(toEnvFormat(key));
}
return (value == null || value.isBlank()) ? defaultValue : value;
}
public static int getInt(String key, int defaultValue) {
String value = get(key, String.valueOf(defaultValue));
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
public static long getLong(String key, long defaultValue) {
String value = get(key, String.valueOf(defaultValue));
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
public static boolean getBoolean(String key, boolean defaultValue) {
String value = get(key, String.valueOf(defaultValue));
return Boolean.parseBoolean(value);
}
private static String toEnvFormat(String key) {
return key.toUpperCase().replace(".", "_").replace("-", "_");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
package cz.kamma.fabka.httpserver.auth;
public interface AuthService {
AuthenticatedUser authenticate(String username, String password);
}

View File

@ -0,0 +1,19 @@
package cz.kamma.fabka.httpserver.auth;
public class AuthenticatedUser {
private final long userId;
private final String username;
public AuthenticatedUser(long userId, String username) {
this.userId = userId;
this.username = username;
}
public long getUserId() {
return userId;
}
public String getUsername() {
return username;
}
}

View File

@ -0,0 +1,52 @@
package cz.kamma.fabka.httpserver.auth;
import cz.kamma.fabka.httpserver.crypto.Md5;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DatabaseAuthService implements AuthService {
private static final String AUTH_SQL = "SELECT id, username FROM user_accounts WHERE userhash=? AND passwd=? LIMIT 1";
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public DatabaseAuthService(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
@Override
public AuthenticatedUser authenticate(String username, String password) {
if (isBlank(username) || isBlank(password)) {
return null;
}
String userHash = Md5.hash(username);
String passHash = Md5.hash(password);
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(AUTH_SQL)) {
ps.setString(1, userHash);
ps.setString(2, passHash);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
return new AuthenticatedUser(rs.getLong("id"), rs.getString("username"));
}
} catch (SQLException ex) {
ex.printStackTrace();
return null;
}
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}

View File

@ -0,0 +1,21 @@
package cz.kamma.fabka.httpserver.auth;
import java.util.Objects;
public class EnvAuthService implements AuthService {
private final String expectedUser;
private final String expectedPassword;
public EnvAuthService(String expectedUser, String expectedPassword) {
this.expectedUser = expectedUser;
this.expectedPassword = expectedPassword;
}
@Override
public AuthenticatedUser authenticate(String username, String password) {
if (Objects.equals(expectedUser, username) && Objects.equals(expectedPassword, password)) {
return new AuthenticatedUser(0L, username);
}
return null;
}
}

View File

@ -0,0 +1,31 @@
package cz.kamma.fabka.httpserver.crypto;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class Md5 {
private Md5() {
}
public static String hash(String value) {
if (value == null) {
return "";
}
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(digest.length * 2);
for (byte b : digest) {
String part = Integer.toHexString(b & 0xff);
if (part.length() == 1) {
hex.append('0');
}
hex.append(part);
}
return hex.toString();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("MD5 algorithm is unavailable", ex);
}
}
}

View File

@ -0,0 +1,77 @@
package cz.kamma.fabka.httpserver.http;
import java.io.IOException;
import java.io.InputStream;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
public class ClasspathStaticFileHandler implements HttpHandler {
private final String basePath;
private final String prefix;
public ClasspathStaticFileHandler(String prefix, String basePath) {
this.prefix = prefix;
this.basePath = basePath;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"GET".equals(exchange.getRequestMethod())) {
Responses.text(exchange, 405, "Method not allowed");
return;
}
String path = exchange.getRequestURI().getPath();
if (path.startsWith(prefix)) {
String rel = path.substring(prefix.length());
if (rel.startsWith("/")) {
rel = rel.substring(1);
}
String resourcePath = basePath + "/" + rel;
serveResource(exchange, resourcePath);
} else {
Responses.text(exchange, 404, "Not found");
}
}
private void serveResource(HttpExchange exchange, String resourcePath) throws IOException {
try (InputStream in = ClasspathStaticFileHandler.class.getClassLoader().getResourceAsStream(resourcePath)) {
if (in == null) {
Responses.text(exchange, 404, "Not found");
return;
}
byte[] data = in.readAllBytes();
String contentType = guessContentType(resourcePath);
Responses.send(exchange, 200, contentType, data);
}
}
private static String guessContentType(String resourcePath) {
String file = resourcePath.toLowerCase();
if (file.endsWith(".css")) {
return "text/css; charset=UTF-8";
}
if (file.endsWith(".js")) {
return "application/javascript; charset=UTF-8";
}
if (file.endsWith(".png")) {
return "image/png";
}
if (file.endsWith(".jpg") || file.endsWith(".jpeg")) {
return "image/jpeg";
}
if (file.endsWith(".gif")) {
return "image/gif";
}
if (file.endsWith(".wav")) {
return "audio/wav";
}
if (file.endsWith(".ico")) {
return "image/x-icon";
}
return "application/octet-stream";
}
}

View File

@ -0,0 +1,184 @@
package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class MultipartFormData {
private static final Pattern NAME_PATTERN = Pattern.compile("name=\"([^\"]*)\"");
private static final Pattern FILENAME_PATTERN = Pattern.compile("filename=\"([^\"]*)\"");
private final Map<String, String> fields;
private final Map<String, List<FileItem>> files;
private MultipartFormData(Map<String, String> fields, Map<String, List<FileItem>> files) {
this.fields = fields;
this.files = files;
}
public static MultipartFormData parse(HttpExchange exchange, int maxBytes, Charset textCharset) throws IOException {
String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
if (contentType == null || !contentType.toLowerCase(Locale.ROOT).startsWith("multipart/form-data")) {
return new MultipartFormData(Map.of(), Map.of());
}
String boundary = extractBoundary(contentType);
if (boundary == null || boundary.isBlank()) {
return new MultipartFormData(Map.of(), Map.of());
}
byte[] body = exchange.getRequestBody().readNBytes(maxBytes + 1);
if (body.length > maxBytes) {
throw new IOException("Multipart payload too large");
}
String raw = new String(body, StandardCharsets.ISO_8859_1);
String delimiter = "--" + boundary;
String[] parts = raw.split(Pattern.quote(delimiter));
Map<String, String> fields = new HashMap<>();
Map<String, List<FileItem>> files = new HashMap<>();
for (String part : parts) {
if (part == null || part.isBlank() || "--".equals(part.trim())) {
continue;
}
String normalized = part;
if (normalized.startsWith("\r\n")) {
normalized = normalized.substring(2);
}
if (normalized.endsWith("\r\n")) {
normalized = normalized.substring(0, normalized.length() - 2);
}
int headerEnd = normalized.indexOf("\r\n\r\n");
if (headerEnd < 0) {
continue;
}
String headerBlock = normalized.substring(0, headerEnd);
String payload = normalized.substring(headerEnd + 4);
String disposition = headerValue(headerBlock, "content-disposition");
if (disposition == null || disposition.isBlank()) {
continue;
}
String fieldName = dispositionValue(disposition, NAME_PATTERN);
if (fieldName == null || fieldName.isBlank()) {
continue;
}
String fileNameRaw = dispositionValue(disposition, FILENAME_PATTERN);
byte[] payloadBytes = payload.getBytes(StandardCharsets.ISO_8859_1);
if (fileNameRaw == null) {
String value = new String(payloadBytes, textCharset);
fields.put(fieldName, value);
continue;
}
String fileName = sanitizeFileName(new String(fileNameRaw.getBytes(StandardCharsets.ISO_8859_1), textCharset));
if (fileName.isBlank() || payloadBytes.length == 0) {
continue;
}
String fileContentType = headerValue(headerBlock, "content-type");
if (fileContentType == null || fileContentType.isBlank()) {
fileContentType = "application/octet-stream";
}
files.computeIfAbsent(fieldName, k -> new ArrayList<>())
.add(new FileItem(fieldName, fileName, fileContentType, payloadBytes));
}
return new MultipartFormData(fields, files);
}
public String field(String name) {
return fields.get(name);
}
public List<FileItem> files(String fieldName) {
return files.getOrDefault(fieldName, List.of());
}
private static String extractBoundary(String contentType) {
String[] items = contentType.split(";");
for (String item : items) {
String trimmed = item.trim();
if (!trimmed.toLowerCase(Locale.ROOT).startsWith("boundary=")) {
continue;
}
String boundary = trimmed.substring("boundary=".length());
if (boundary.startsWith("\"") && boundary.endsWith("\"") && boundary.length() >= 2) {
boundary = boundary.substring(1, boundary.length() - 1);
}
return boundary;
}
return null;
}
private static String headerValue(String headers, String headerName) {
for (String line : headers.split("\r\n")) {
int idx = line.indexOf(':');
if (idx <= 0) {
continue;
}
String key = line.substring(0, idx).trim().toLowerCase(Locale.ROOT);
if (!headerName.equalsIgnoreCase(key)) {
continue;
}
return line.substring(idx + 1).trim();
}
return null;
}
private static String dispositionValue(String disposition, Pattern pattern) {
Matcher matcher = pattern.matcher(disposition);
if (!matcher.find()) {
return null;
}
return matcher.group(1);
}
private static String sanitizeFileName(String fileName) {
String name = fileName == null ? "" : fileName.trim();
int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
if (slash >= 0 && slash + 1 < name.length()) {
name = name.substring(slash + 1);
}
return name;
}
public static final class FileItem {
private final String fieldName;
private final String fileName;
private final String contentType;
private final byte[] data;
public FileItem(String fieldName, String fileName, String contentType, byte[] data) {
this.fieldName = fieldName;
this.fileName = fileName;
this.contentType = contentType;
this.data = data;
}
public String getFieldName() {
return fieldName;
}
public String getFileName() {
return fileName;
}
public String getContentType() {
return contentType;
}
public byte[] getData() {
return data;
}
}
}

View File

@ -0,0 +1,157 @@
package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import cz.kamma.fabka.httpserver.session.SessionData;
import cz.kamma.fabka.httpserver.session.SessionManager;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class RequestContext {
private static final int MAX_FORM_BYTES = 1024 * 1024;
private final HttpExchange exchange;
private final SessionManager sessionManager;
private Map<String, String> queryParams;
private Map<String, String> formParams;
private Map<String, String> cookies;
private SessionData session;
public RequestContext(HttpExchange exchange, SessionManager sessionManager) {
this.exchange = exchange;
this.sessionManager = sessionManager;
}
public HttpExchange exchange() {
return exchange;
}
public String method() {
return exchange.getRequestMethod();
}
public String path() {
return exchange.getRequestURI().getPath();
}
public String queryParam(String key) {
return queryParams().get(key);
}
public String formParam(String key) throws IOException {
return formParams().get(key);
}
public Map<String, String> queryParams() {
if (queryParams == null) {
queryParams = parseEncodedParams(exchange.getRequestURI().getRawQuery());
}
return queryParams;
}
public Map<String, String> formParams() throws IOException {
if (formParams != null) {
return formParams;
}
String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
if (contentType == null || !contentType.toLowerCase().startsWith("application/x-www-form-urlencoded")) {
formParams = Collections.emptyMap();
return formParams;
}
byte[] body = exchange.getRequestBody().readNBytes(MAX_FORM_BYTES + 1);
if (body.length > MAX_FORM_BYTES) {
throw new IOException("Form payload too large");
}
formParams = parseEncodedParams(new String(body, StandardCharsets.UTF_8));
return formParams;
}
public Map<String, String> cookies() {
if (cookies == null) {
cookies = parseCookies(exchange.getRequestHeaders());
}
return cookies;
}
public SessionData getSession() {
if (session != null) {
return session;
}
String sessionId = cookies().get(SessionManager.COOKIE_NAME);
session = sessionManager.get(sessionId);
return session;
}
public SessionData getOrCreateSession() {
SessionData existing = getSession();
if (existing != null) {
return existing;
}
session = sessionManager.create();
addCookie(SessionManager.COOKIE_NAME, session.id(), true);
return session;
}
public void invalidateSession() {
String sessionId = cookies().get(SessionManager.COOKIE_NAME);
sessionManager.invalidate(sessionId);
expireCookie(SessionManager.COOKIE_NAME);
session = null;
}
public void addCookie(String name, String value, boolean httpOnly) {
String cookie = name + "=" + value + "; Path=/; SameSite=Lax" + (httpOnly ? "; HttpOnly" : "");
exchange.getResponseHeaders().add("Set-Cookie", cookie);
}
public void expireCookie(String name) {
exchange.getResponseHeaders().add("Set-Cookie", name + "=; Max-Age=0; Path=/; SameSite=Lax; HttpOnly");
}
private static Map<String, String> parseCookies(Headers headers) {
String rawCookie = headers.getFirst("Cookie");
if (rawCookie == null || rawCookie.isBlank()) {
return Collections.emptyMap();
}
Map<String, String> parsed = new HashMap<>();
String[] chunks = rawCookie.split(";");
for (String chunk : chunks) {
int idx = chunk.indexOf('=');
if (idx <= 0) {
continue;
}
String key = chunk.substring(0, idx).trim();
String value = chunk.substring(idx + 1).trim();
parsed.put(key, value);
}
return parsed;
}
private static Map<String, String> parseEncodedParams(String encoded) {
if (encoded == null || encoded.isBlank()) {
return Collections.emptyMap();
}
Map<String, String> parsed = new LinkedHashMap<>();
for (String pair : encoded.split("&")) {
if (pair.isBlank()) {
continue;
}
int idx = pair.indexOf('=');
String rawKey = idx >= 0 ? pair.substring(0, idx) : pair;
String rawValue = idx >= 0 ? pair.substring(idx + 1) : "";
String key = URLDecoder.decode(rawKey, StandardCharsets.UTF_8);
String value = URLDecoder.decode(rawValue, StandardCharsets.UTF_8);
parsed.put(key, value);
}
return parsed;
}
}

View File

@ -0,0 +1,32 @@
package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public final class Responses {
private Responses() {
}
public static void html(HttpExchange exchange, int status, String body) throws IOException {
send(exchange, status, "text/html; charset=UTF-8", body.getBytes(StandardCharsets.UTF_8));
}
public static void text(HttpExchange exchange, int status, String body) throws IOException {
send(exchange, status, "text/plain; charset=UTF-8", body.getBytes(StandardCharsets.UTF_8));
}
public static void redirect(HttpExchange exchange, String location) throws IOException {
exchange.getResponseHeaders().set("Location", location);
exchange.sendResponseHeaders(302, -1);
exchange.close();
}
public static void send(HttpExchange exchange, int status, String contentType, byte[] body) throws IOException {
exchange.getResponseHeaders().set("Content-Type", contentType);
exchange.sendResponseHeaders(status, body.length);
exchange.getResponseBody().write(body);
exchange.close();
}
}

View File

@ -0,0 +1,6 @@
package cz.kamma.fabka.httpserver.http;
@FunctionalInterface
public interface RouteHandler {
void handle(RequestContext ctx) throws Exception;
}

View File

@ -0,0 +1,49 @@
package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import cz.kamma.fabka.httpserver.session.SessionManager;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Router implements HttpHandler {
private final Map<String, RouteHandler> routes = new ConcurrentHashMap<>();
private final SessionManager sessionManager;
public Router(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void get(String path, RouteHandler handler) {
routes.put(key("GET", path), handler);
}
public void post(String path, RouteHandler handler) {
routes.put(key("POST", path), handler);
}
@Override
public void handle(HttpExchange exchange) throws IOException {
RequestContext ctx = new RequestContext(exchange, sessionManager);
RouteHandler handler = routes.get(key(exchange.getRequestMethod(), exchange.getRequestURI().getPath()));
if (handler == null) {
Responses.text(exchange, 404, "Not found");
return;
}
try {
handler.handle(ctx);
} catch (IllegalArgumentException ex) {
Responses.text(exchange, 400, ex.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
Responses.text(exchange, 500, "Internal server error");
}
}
private static String key(String method, String path) {
return method + " " + path;
}
}

View File

@ -0,0 +1,82 @@
package cz.kamma.fabka.httpserver.http;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class StaticFileHttpHandler implements HttpHandler {
private final Path basePath;
private final String prefix;
private final boolean directoryMode;
public StaticFileHttpHandler(Path basePath, String prefix, boolean directoryMode) {
this.basePath = basePath.toAbsolutePath().normalize();
this.prefix = prefix;
this.directoryMode = directoryMode;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"GET".equals(exchange.getRequestMethod())) {
Responses.text(exchange, 405, "Method not allowed");
return;
}
Path target;
if (directoryMode) {
String path = exchange.getRequestURI().getPath();
String rel = path.substring(prefix.length());
if (rel.startsWith("/")) {
rel = rel.substring(1);
}
target = basePath.resolve(rel).normalize();
if (!target.startsWith(basePath)) {
Responses.text(exchange, 403, "Forbidden");
return;
}
} else {
target = basePath;
}
if (!Files.exists(target) || Files.isDirectory(target)) {
Responses.text(exchange, 404, "Not found");
return;
}
String contentType = Files.probeContentType(target);
if (contentType == null) {
contentType = guessContentType(target);
}
Responses.send(exchange, 200, contentType, Files.readAllBytes(target));
}
private static String guessContentType(Path path) {
String file = path.getFileName().toString().toLowerCase();
if (file.endsWith(".css")) {
return "text/css; charset=UTF-8";
}
if (file.endsWith(".js")) {
return "application/javascript; charset=UTF-8";
}
if (file.endsWith(".png")) {
return "image/png";
}
if (file.endsWith(".jpg") || file.endsWith(".jpeg")) {
return "image/jpeg";
}
if (file.endsWith(".gif")) {
return "image/gif";
}
if (file.endsWith(".wav")) {
return "audio/wav";
}
if (file.endsWith(".ico")) {
return "image/x-icon";
}
return "application/octet-stream";
}
}

View File

@ -0,0 +1,25 @@
package cz.kamma.fabka.httpserver.repository;
public class AttachmentData {
private final String name;
private final String contentType;
private final byte[] data;
public AttachmentData(String name, String contentType, byte[] data) {
this.name = name;
this.contentType = contentType;
this.data = data;
}
public String getName() {
return name;
}
public String getContentType() {
return contentType;
}
public byte[] getData() {
return data;
}
}

View File

@ -0,0 +1,64 @@
package cz.kamma.fabka.httpserver.repository;
public class ChatLine {
private final long id;
private final long createdBy;
private final String fromName;
private final String time;
private final String text;
private final int newMessage;
private final String thumbUpUsers;
private final String thumbDownUsers;
public ChatLine(
long id,
long createdBy,
String fromName,
String time,
String text,
int newMessage,
String thumbUpUsers,
String thumbDownUsers
) {
this.id = id;
this.createdBy = createdBy;
this.fromName = fromName;
this.time = time;
this.text = text;
this.newMessage = newMessage;
this.thumbUpUsers = thumbUpUsers;
this.thumbDownUsers = thumbDownUsers;
}
public long getId() {
return id;
}
public long getCreatedBy() {
return createdBy;
}
public String getFromName() {
return fromName;
}
public String getTime() {
return time;
}
public String getText() {
return text;
}
public int getNewMessage() {
return newMessage;
}
public String getThumbUpUsers() {
return thumbUpUsers;
}
public String getThumbDownUsers() {
return thumbDownUsers;
}
}

View File

@ -0,0 +1,241 @@
package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class ChatRepository {
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague");
private static final String CHAT_VOTES_USERS_SQL =
"SELECT " +
"COALESCE((SELECT GROUP_CONCAT(ua.username SEPARATOR ',') " +
" FROM chat_voting cv JOIN user_accounts ua ON ua.id=cv.voteby " +
" WHERE cv.chatid=? AND cv.votevalue=1), '') AS thumbup, " +
"COALESCE((SELECT GROUP_CONCAT(ua.username SEPARATOR ',') " +
" FROM chat_voting cv JOIN user_accounts ua ON ua.id=cv.voteby " +
" WHERE cv.chatid=? AND cv.votevalue=-1), '') AS thumbdown";
private static final String CHAT_LINES_SQL =
"SELECT ch.id, ch.text, ch.created, ch.createdby, ua.username, " +
"CASE WHEN ch.id > COALESCE((SELECT confirmedid FROM chat_history WHERE userid=?), 0) THEN 1 ELSE 0 END AS newmess, " +
"COALESCE((SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') " +
" FROM chat_voting cv JOIN user_accounts ua2 ON ua2.id=cv.voteby " +
" WHERE cv.chatid=ch.id AND cv.votevalue=1), '') AS thumbup, " +
"COALESCE((SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') " +
" FROM chat_voting cv JOIN user_accounts ua2 ON ua2.id=cv.voteby " +
" WHERE cv.chatid=ch.id AND cv.votevalue=-1), '') AS thumbdown " +
"FROM chat ch JOIN user_accounts ua ON ua.id=ch.createdby " +
"ORDER BY ch.id DESC LIMIT ?";
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public ChatRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public boolean alreadyChatVoted(long userId, long chatId) {
if (userId <= 0 || chatId <= 0) {
return false;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement("select 1 from chat_voting where voteby=? and chatid=? limit 1")) {
ps.setLong(1, userId);
ps.setLong(2, chatId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
} catch (Exception ex) {
return false;
}
}
public void addChatVote(long userId, long chatId, int voteValue) {
if (userId <= 0 || chatId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement("insert into chat_voting (voteby, chatid, votevalue) values (?,?,?)")) {
ps.setLong(1, userId);
ps.setLong(2, chatId);
ps.setInt(3, voteValue);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public ChatVoteStats getChatVoteStats(long chatId) {
if (chatId <= 0) {
return new ChatVoteStats("", "");
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(CHAT_VOTES_USERS_SQL)) {
ps.setLong(1, chatId);
ps.setLong(2, chatId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return new ChatVoteStats("", "");
}
return new ChatVoteStats(
valueOrDefault(rs.getString("thumbup"), ""),
valueOrDefault(rs.getString("thumbdown"), "")
);
}
} catch (Exception ex) {
ex.printStackTrace();
return new ChatVoteStats("", "");
}
}
public void addChatMessage(long userId, String message) {
if (userId <= 0 || message == null || message.isBlank()) {
return;
}
String sanitized = message.replace("<", "&lt;");
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement("insert into chat (createdby, text) values (?,?)")) {
ps.setLong(1, userId);
ps.setString(2, sanitized);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public List<ChatLine> listRecentChatLines(long currentUserId, int limit) {
List<ChatLine> lines = new ArrayList<>();
if (currentUserId <= 0) {
return lines;
}
int safeLimit = limit <= 0 ? 40 : limit;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(CHAT_LINES_SQL)) {
ps.setLong(1, currentUserId);
ps.setInt(2, safeLimit);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long createdBy = rs.getLong("createdby");
int newMsg = createdBy == currentUserId ? 2 : rs.getInt("newmess");
lines.add(new ChatLine(
rs.getLong("id"),
createdBy,
valueOrDefault(rs.getString("username"), ""),
formatTime(rs.getTimestamp("created")),
valueOrDefault(rs.getString("text"), ""),
newMsg,
valueOrDefault(rs.getString("thumbup"), ""),
valueOrDefault(rs.getString("thumbdown"), "")
));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return lines;
}
public void confirmChatRead(long userId, long chatId) {
if (userId <= 0 || chatId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"insert into chat_history (userid, confirmedid) values (?,?) on duplicate key update confirmedid=?"
)) {
ps.setLong(1, userId);
ps.setLong(2, chatId);
ps.setLong(3, chatId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void confirmChatDownloaded(long userId, long chatId) {
if (userId <= 0 || chatId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"insert into chat_history (userid, downloadedid) values (?,?) on duplicate key update downloadedid=?"
)) {
ps.setLong(1, userId);
ps.setLong(2, chatId);
ps.setLong(3, chatId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public boolean hasUserNewChatMessage(long userId) {
return hasUserNewChatMessageInternal(userId, false);
}
public boolean hasUserNewChatMessageOther(long userId) {
return hasUserNewChatMessageInternal(userId, true);
}
private boolean hasUserNewChatMessageInternal(long userId, boolean onlyOtherUsers) {
if (userId <= 0) {
return false;
}
String sql = onlyOtherUsers
? "SELECT COALESCE((SELECT downloadedid FROM chat_history WHERE userid=?), 0) " +
" < COALESCE((SELECT MAX(id) FROM chat WHERE createdby<>?), 0) AS hasNew"
: "SELECT COALESCE((SELECT downloadedid FROM chat_history WHERE userid=?), 0) " +
" < COALESCE((SELECT MAX(id) FROM chat), 0) AS hasNew";
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
if (onlyOtherUsers) {
ps.setLong(2, userId);
}
try (ResultSet rs = ps.executeQuery()) {
return rs.next() && rs.getInt("hasNew") == 1;
}
} catch (Exception ex) {
return false;
}
}
public int getUnreadMessagesByUser(long userId) {
if (userId <= 0) {
return 0;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"select count(id) as cnt from messages where deleted=0 and readed=0 and to_user=?"
)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getInt("cnt") : 0;
}
} catch (Exception ex) {
return 0;
}
}
private static String formatTime(Timestamp ts) {
if (ts == null) {
return "";
}
return ts.toInstant().atZone(APP_ZONE).format(TIME_FORMAT);
}
private static String valueOrDefault(String value, String defaultValue) {
return value == null ? defaultValue : value;
}
}

View File

@ -0,0 +1,19 @@
package cz.kamma.fabka.httpserver.repository;
public class ChatVoteStats {
private final String thumbUpUsers;
private final String thumbDownUsers;
public ChatVoteStats(String thumbUpUsers, String thumbDownUsers) {
this.thumbUpUsers = thumbUpUsers;
this.thumbDownUsers = thumbDownUsers;
}
public String getThumbUpUsers() {
return thumbUpUsers;
}
public String getThumbDownUsers() {
return thumbDownUsers;
}
}

View File

@ -0,0 +1,31 @@
package cz.kamma.fabka.httpserver.repository;
public class ForumAttachment {
private final long id;
private final String name;
private final boolean picture;
private final int width;
public ForumAttachment(long id, String name, boolean picture, int width) {
this.id = id;
this.name = name;
this.picture = picture;
this.width = width;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public boolean isPicture() {
return picture;
}
public int getWidth() {
return width;
}
}

View File

@ -0,0 +1,31 @@
package cz.kamma.fabka.httpserver.repository;
public class ForumDetail {
private final long id;
private final String name;
private final String description;
private final String countdown;
public ForumDetail(long id, String name, String description, String countdown) {
this.id = id;
this.name = name;
this.description = description;
this.countdown = countdown;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getCountdown() {
return countdown;
}
}

View File

@ -0,0 +1,94 @@
package cz.kamma.fabka.httpserver.repository;
import java.util.List;
public class ForumDisplayView {
private final List<ForumMessage> messages;
private final int totalRows;
private final int startRow;
private final int endRow;
private final int totalPages;
private final int currentPage;
private final String searchText;
private final String showType;
private final String showImg;
private final String sortBy;
private final String sortType;
private final String perPage;
public ForumDisplayView(
List<ForumMessage> messages,
int totalRows,
int startRow,
int endRow,
int totalPages,
int currentPage,
String searchText,
String showType,
String showImg,
String sortBy,
String sortType,
String perPage
) {
this.messages = messages;
this.totalRows = totalRows;
this.startRow = startRow;
this.endRow = endRow;
this.totalPages = totalPages;
this.currentPage = currentPage;
this.searchText = searchText;
this.showType = showType;
this.showImg = showImg;
this.sortBy = sortBy;
this.sortType = sortType;
this.perPage = perPage;
}
public List<ForumMessage> getMessages() {
return messages;
}
public int getTotalRows() {
return totalRows;
}
public int getStartRow() {
return startRow;
}
public int getEndRow() {
return endRow;
}
public int getTotalPages() {
return totalPages;
}
public int getCurrentPage() {
return currentPage;
}
public String getSearchText() {
return searchText;
}
public String getShowType() {
return showType;
}
public String getShowImg() {
return showImg;
}
public String getSortBy() {
return sortBy;
}
public String getSortType() {
return sortType;
}
public String getPerPage() {
return perPage;
}
}

View File

@ -0,0 +1,136 @@
package cz.kamma.fabka.httpserver.repository;
import java.util.List;
public class ForumMessage {
private final long id;
private final long authorId;
private final String author;
private final String authorJoinDate;
private final String authorCity;
private final long authorPosts;
private final long createdEpochMillis;
private final int voteValue;
private final int voteYes;
private final int voteNo;
private final String voteYesUsers;
private final String voteNoUsers;
private final long quoteItemId;
private final QuotedTextItem quotedItem;
private final String createdAt;
private final String text;
private final List<ForumAttachment> attachments;
private final boolean sticky;
public ForumMessage(
long id,
long authorId,
String author,
String authorJoinDate,
String authorCity,
long authorPosts,
long createdEpochMillis,
int voteValue,
int voteYes,
int voteNo,
String voteYesUsers,
String voteNoUsers,
long quoteItemId,
QuotedTextItem quotedItem,
String createdAt,
String text,
List<ForumAttachment> attachments,
boolean sticky
) {
this.id = id;
this.authorId = authorId;
this.author = author;
this.authorJoinDate = authorJoinDate;
this.authorCity = authorCity;
this.authorPosts = authorPosts;
this.createdEpochMillis = createdEpochMillis;
this.voteValue = voteValue;
this.voteYes = voteYes;
this.voteNo = voteNo;
this.voteYesUsers = voteYesUsers;
this.voteNoUsers = voteNoUsers;
this.quoteItemId = quoteItemId;
this.quotedItem = quotedItem;
this.createdAt = createdAt;
this.text = text;
this.attachments = attachments;
this.sticky = sticky;
}
public long getId() {
return id;
}
public String getAuthor() {
return author;
}
public long getAuthorId() {
return authorId;
}
public String getAuthorJoinDate() {
return authorJoinDate;
}
public String getAuthorCity() {
return authorCity;
}
public long getAuthorPosts() {
return authorPosts;
}
public long getCreatedEpochMillis() {
return createdEpochMillis;
}
public int getVoteValue() {
return voteValue;
}
public int getVoteYes() {
return voteYes;
}
public int getVoteNo() {
return voteNo;
}
public String getVoteYesUsers() {
return voteYesUsers;
}
public String getVoteNoUsers() {
return voteNoUsers;
}
public long getQuoteItemId() {
return quoteItemId;
}
public QuotedTextItem getQuotedItem() {
return quotedItem;
}
public String getCreatedAt() {
return createdAt;
}
public String getText() {
return text;
}
public List<ForumAttachment> getAttachments() {
return attachments;
}
public boolean isSticky() {
return sticky;
}
}

View File

@ -0,0 +1,564 @@
package cz.kamma.fabka.httpserver.repository;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ForumRepository {
private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague");
private static final String FORUM_DETAIL_SQL =
"SELECT id, name, description, countdown FROM forum WHERE id=? AND active=1 LIMIT 1";
private static final String QUOTED_ITEM_SQL =
"SELECT fi.text, ua.username FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby WHERE fi.id=? LIMIT 1";
private static final String ATTACHMENTS_SQL =
"SELECT id, name, ispicture, width, size FROM attachments WHERE forumitemsid=? ORDER BY id";
private static final String ATTACHMENT_DATA_SQL =
"SELECT name, mimetype, data FROM attachments WHERE id=? LIMIT 1";
private static final String FORUM_MESSAGES_SQL =
"SELECT fi.id, fi.text, fi.created, fi.quoteitem, fi.sticky, ua.id AS author_id, ua.username, ua.created AS author_created, ua.city, " +
" (SELECT COUNT(*) FROM forum_items fi2 WHERE fi2.createdby=ua.id AND fi2.deleted=0) AS author_posts, " +
" COALESCE((SELECT SUM(votevalue) FROM voting v WHERE v.forumitemid=fi.id), 0) AS vvalue, " +
" (SELECT COUNT(*) FROM voting v WHERE v.forumitemid=fi.id AND v.votevalue=1) AS vote_yes, " +
" (SELECT COUNT(*) FROM voting v WHERE v.forumitemid=fi.id AND v.votevalue=-1) AS vote_no, " +
" (SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') FROM voting v JOIN user_accounts ua2 ON ua2.id=v.voteby WHERE v.forumitemid=fi.id AND v.votevalue=1) AS vote_yes_users, " +
" (SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') FROM voting v JOIN user_accounts ua2 ON ua2.id=v.voteby WHERE v.forumitemid=fi.id AND v.votevalue=-1) AS vote_no_users " +
"FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby " +
"WHERE fi.forumid=? AND fi.deleted=0 ORDER BY fi.created DESC";
private static final String FORUM_MESSAGES_SQL_NO_STICKY =
"SELECT fi.id, fi.text, fi.created, fi.quoteitem, 0 AS sticky, ua.id AS author_id, ua.username, ua.created AS author_created, ua.city, " +
" (SELECT COUNT(*) FROM forum_items fi2 WHERE fi2.createdby=ua.id AND fi2.deleted=0) AS author_posts, " +
" COALESCE((SELECT SUM(votevalue) FROM voting v WHERE v.forumitemid=fi.id), 0) AS vvalue, " +
" (SELECT COUNT(*) FROM voting v WHERE v.forumitemid=fi.id AND v.votevalue=1) AS vote_yes, " +
" (SELECT COUNT(*) FROM voting v WHERE v.forumitemid=fi.id AND v.votevalue=-1) AS vote_no, " +
" (SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') FROM voting v JOIN user_accounts ua2 ON ua2.id=v.voteby WHERE v.forumitemid=fi.id AND v.votevalue=1) AS vote_yes_users, " +
" (SELECT GROUP_CONCAT(ua2.username SEPARATOR ',') FROM voting v JOIN user_accounts ua2 ON ua2.id=v.voteby WHERE v.forumitemid=fi.id AND v.votevalue=-1) AS vote_no_users " +
"FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby " +
"WHERE fi.forumid=? AND fi.deleted=0 ORDER BY fi.created DESC";
private static final String ALREADY_VOTED_SQL = "SELECT 1 FROM voting WHERE voteby=? AND forumitemid=? LIMIT 1";
private static final String ADD_VOTE_SQL = "INSERT INTO voting (voteby, forumitemid, votevalue) VALUES (?,?,?)";
private static final String GET_USER_VOTE_SQL = "SELECT votevalue FROM voting WHERE voteby=? AND forumitemid=? LIMIT 1";
private static final String UPDATE_VOTE_SQL = "UPDATE voting SET votevalue=? WHERE voteby=? AND forumitemid=?";
private static final String DELETE_VOTE_SQL = "DELETE FROM voting WHERE voteby=? AND forumitemid=?";
private static final String TOGGLE_STICKY_SQL = "update forum_items set sticky=IF(sticky=1,0,1) where id=?";
private static final String DELETE_POST_SQL = "update forum_items set deleted=1 where id=? and createdby=?";
private static final String VOTE_STATS_SQL =
"SELECT " +
" (SELECT COUNT(*) FROM voting WHERE forumitemid=? AND votevalue=1) AS yes_count, " +
" (SELECT COUNT(*) FROM voting WHERE forumitemid=? AND votevalue=-1) AS no_count, " +
" (SELECT GROUP_CONCAT(ua.username SEPARATOR ',') FROM voting v JOIN user_accounts ua ON ua.id=v.voteby WHERE v.forumitemid=? AND v.votevalue=1) AS yes_users, " +
" (SELECT GROUP_CONCAT(ua.username SEPARATOR ',') FROM voting v JOIN user_accounts ua ON ua.id=v.voteby WHERE v.forumitemid=? AND v.votevalue=-1) AS no_users";
private static final String INSERT_HISTORY_SQL =
"insert into history (userid, forumid) values (?,?)";
private static final String NEW_MESSAGES_COUNT_SQL =
"SELECT f.id AS forum_id, COUNT(fi.id) AS message_count " +
"FROM forum f " +
"LEFT JOIN (" +
" SELECT forumid, MAX(created) AS last_seen " +
" FROM history WHERE userid=? GROUP BY forumid" +
") h ON h.forumid=f.id " +
"LEFT JOIN forum_items fi " +
" ON fi.forumid=f.id AND fi.deleted=0 AND (h.last_seen IS NULL OR fi.created>h.last_seen) " +
"WHERE f.active=1 " +
"GROUP BY f.id " +
"HAVING COUNT(fi.id) > 0";
private static final String FORUM_SQL =
"SELECT f.id, f.name, f.description, f.created, f.password, " +
" (SELECT COUNT(*) FROM forum_items fi WHERE fi.forumid=f.id AND fi.deleted=0) AS posts_count, " +
" (SELECT ua.username FROM forum_items fi JOIN user_accounts ua ON ua.id=fi.createdby " +
" WHERE fi.forumid=f.id AND fi.deleted=0 ORDER BY fi.created DESC LIMIT 1) AS last_post_user, " +
" (SELECT fi.created FROM forum_items fi WHERE fi.forumid=f.id AND fi.deleted=0 ORDER BY fi.created DESC LIMIT 1) AS last_post_at " +
"FROM forum f WHERE f.active=1 ORDER BY COALESCE(last_post_at, f.created) DESC";
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public ForumRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public List<ForumSummary> listActiveForums() {
List<ForumSummary> forums = new ArrayList<>();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(FORUM_SQL);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
forums.add(new ForumSummary(
rs.getLong("id"),
rs.getString("name"),
rs.getString("description"),
formatTs(rs.getTimestamp("created")),
valueOrDefault(rs.getString("last_post_user"), "N/A"),
formatTs(rs.getTimestamp("last_post_at")),
rs.getLong("posts_count"),
rs.getString("password") != null && !rs.getString("password").isBlank()
));
}
} catch (Exception ex) {
ex.printStackTrace();
}
return forums;
}
public void putHistoryRecord(long userId, long forumId) {
if (userId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(INSERT_HISTORY_SQL)) {
ps.setLong(1, userId);
ps.setLong(2, forumId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public Map<Long, Integer> getNewMessagesCountByUserId(long userId) {
Map<Long, Integer> result = new LinkedHashMap<>();
if (userId <= 0) {
return result;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(NEW_MESSAGES_COUNT_SQL)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long forumId = rs.getLong("forum_id");
int count = rs.getInt("message_count");
if (count > 0) {
result.put(forumId, count);
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return result;
}
public ForumDetail findForumById(long forumId) {
if (forumId <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(FORUM_DETAIL_SQL)) {
ps.setLong(1, forumId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
return new ForumDetail(
rs.getLong("id"),
rs.getString("name"),
rs.getString("description"),
rs.getString("countdown")
);
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public List<ForumMessage> listMessagesByForumId(long forumId) {
List<ForumMessage> messages = new ArrayList<>();
if (forumId <= 0) {
return messages;
}
if (!loadMessages(messages, forumId, FORUM_MESSAGES_SQL)) {
loadMessages(messages, forumId, FORUM_MESSAGES_SQL_NO_STICKY);
}
return messages;
}
private boolean loadMessages(List<ForumMessage> out, long forumId, String sql) {
out.clear();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, forumId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Timestamp createdTs = rs.getTimestamp("created");
long quoteItemId = rs.getLong("quoteitem");
QuotedTextItem quotedItem = quoteItemId > 0 ? findQuotedItem(quoteItemId) : null;
out.add(new ForumMessage(
rs.getLong("id"),
rs.getLong("author_id"),
valueOrDefault(rs.getString("username"), "N/A"),
formatTs(rs.getTimestamp("author_created")),
valueOrDefault(rs.getString("city"), ""),
rs.getLong("author_posts"),
createdTs == null ? 0L : createdTs.getTime(),
rs.getInt("vvalue"),
rs.getInt("vote_yes"),
rs.getInt("vote_no"),
valueOrDefault(rs.getString("vote_yes_users"), ""),
valueOrDefault(rs.getString("vote_no_users"), ""),
quoteItemId,
quotedItem,
formatTs(createdTs),
valueOrDefault(rs.getString("text"), ""),
listAttachmentsByForumItemId(rs.getLong("id")),
rs.getInt("sticky") == 1
));
}
}
return true;
} catch (Exception ex) {
return false;
}
}
public boolean alreadyVoted(long userId, long forumItemId) {
if (userId <= 0 || forumItemId <= 0) {
return false;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(ALREADY_VOTED_SQL)) {
ps.setLong(1, userId);
ps.setLong(2, forumItemId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
public Integer getUserVote(long userId, long forumItemId) {
if (userId <= 0 || forumItemId <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(GET_USER_VOTE_SQL)) {
ps.setLong(1, userId);
ps.setLong(2, forumItemId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getInt("votevalue");
}
return null;
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public void addVote(long userId, long forumItemId, int voteValue) {
if (userId <= 0 || forumItemId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(ADD_VOTE_SQL)) {
ps.setLong(1, userId);
ps.setLong(2, forumItemId);
ps.setInt(3, voteValue);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void updateVote(long userId, long forumItemId, int voteValue) {
if (userId <= 0 || forumItemId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(UPDATE_VOTE_SQL)) {
ps.setInt(1, voteValue);
ps.setLong(2, userId);
ps.setLong(3, forumItemId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void deleteVote(long userId, long forumItemId) {
if (userId <= 0 || forumItemId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(DELETE_VOTE_SQL)) {
ps.setLong(1, userId);
ps.setLong(2, forumItemId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public VoteStats getVoteStats(long forumItemId) {
if (forumItemId <= 0) {
return new VoteStats(0, 0, "", "");
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(VOTE_STATS_SQL)) {
ps.setLong(1, forumItemId);
ps.setLong(2, forumItemId);
ps.setLong(3, forumItemId);
ps.setLong(4, forumItemId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return new VoteStats(0, 0, "", "");
}
return new VoteStats(
rs.getInt("yes_count"),
rs.getInt("no_count"),
valueOrDefault(rs.getString("yes_users"), ""),
valueOrDefault(rs.getString("no_users"), "")
);
}
} catch (Exception ex) {
ex.printStackTrace();
return new VoteStats(0, 0, "", "");
}
}
public void toggleSticky(long forumItemId) {
if (forumItemId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(TOGGLE_STICKY_SQL)) {
ps.setLong(1, forumItemId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void deletePost(long forumItemId, long userId) {
if (forumItemId <= 0 || userId <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(DELETE_POST_SQL)) {
ps.setLong(1, forumItemId);
ps.setLong(2, userId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public QuotedTextItem findQuotedItem(long messageId) {
if (messageId <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(QUOTED_ITEM_SQL)) {
ps.setLong(1, messageId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
return new QuotedTextItem(
valueOrDefault(rs.getString("username"), "N/A"),
valueOrDefault(rs.getString("text"), "")
);
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public long addReply(long forumId, long userId, String message, Long quoteItem) {
if (forumId <= 0 || userId <= 0) {
return -1L;
}
String safeMessage = message == null ? "" : message;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword)) {
conn.setAutoCommit(false);
try (PreparedStatement insert = conn.prepareStatement(
"insert into forum_items (forumid, createdby, text, quoteitem) VALUES (?,?,?,?)",
Statement.RETURN_GENERATED_KEYS
)) {
insert.setLong(1, forumId);
insert.setLong(2, userId);
insert.setString(3, safeMessage);
if (quoteItem != null && quoteItem > 0) {
insert.setLong(4, quoteItem);
} else {
insert.setNull(4, Types.BIGINT);
}
insert.executeUpdate();
long forumItemId = -1L;
try (ResultSet keys = insert.getGeneratedKeys()) {
if (keys.next()) {
forumItemId = keys.getLong(1);
}
}
if (forumItemId <= 0) {
conn.rollback();
return -1L;
}
try (PreparedStatement upd = conn.prepareStatement("update forum set last_post=? where id=?")) {
upd.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
upd.setLong(2, forumId);
upd.executeUpdate();
}
conn.commit();
return forumItemId;
}
} catch (Exception ex) {
ex.printStackTrace();
return -1L;
}
}
public boolean addAttachment(long forumItemId, String fileName, byte[] data, String contentType) {
if (forumItemId <= 0 || fileName == null || fileName.isBlank() || data == null || data.length == 0) {
return false;
}
String safeContentType = (contentType == null || contentType.isBlank()) ? "application/octet-stream" : contentType;
int width = 0;
int height = 0;
boolean isPicture = false;
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(data));
if (image != null) {
isPicture = true;
width = image.getWidth();
height = image.getHeight();
}
} catch (Exception ignored) {
// If image decode fails, persist as generic attachment.
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"insert into attachments (forumitemsid, name, data, ispicture, width, height, size, mimetype) values (?,?,?,?,?,?,?,?)"
)) {
ps.setLong(1, forumItemId);
ps.setString(2, fileName);
ps.setBytes(3, data);
ps.setInt(4, isPicture ? 1 : 0);
ps.setInt(5, width);
ps.setInt(6, height);
ps.setInt(7, data.length);
ps.setString(8, safeContentType);
ps.executeUpdate();
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
public boolean createThread(long userId, String forumName, String description, String password) {
if (userId <= 0) {
return false;
}
String safeName = forumName == null ? "" : forumName.trim();
String safeDesc = description == null ? "" : description.trim();
if (safeName.isBlank() || safeDesc.isBlank()) {
return false;
}
String safePassword = (password == null || password.isBlank()) ? null : password.trim();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"insert into forum (name, description, password, createdby, active) values (?,?,?,?,1)"
)) {
ps.setString(1, safeName);
ps.setString(2, safeDesc);
if (safePassword == null) {
ps.setNull(3, Types.VARCHAR);
} else {
ps.setString(3, safePassword);
}
ps.setLong(4, userId);
ps.executeUpdate();
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
public List<ForumAttachment> listAttachmentsByForumItemId(long forumItemId) {
List<ForumAttachment> out = new ArrayList<>();
if (forumItemId <= 0) {
return out;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(ATTACHMENTS_SQL)) {
ps.setLong(1, forumItemId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
out.add(new ForumAttachment(
rs.getLong("id"),
valueOrDefault(rs.getString("name"), "attachment"),
rs.getInt("ispicture") == 1,
rs.getInt("width")
));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return out;
}
public AttachmentData findAttachmentData(long attachmentId) {
if (attachmentId <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(ATTACHMENT_DATA_SQL)) {
ps.setLong(1, attachmentId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
byte[] data = rs.getBytes("data");
if (data == null || data.length == 0) {
return null;
}
String contentType = rs.getString("mimetype");
if (contentType == null || contentType.isBlank()) {
contentType = "application/octet-stream";
}
return new AttachmentData(
valueOrDefault(rs.getString("name"), "attachment.bin"),
contentType,
data
);
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
private static String valueOrDefault(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value;
}
private static String formatTs(Timestamp ts) {
if (ts == null) {
return "N/A";
}
return DATETIME_FORMAT.format(ts.toInstant().atZone(APP_ZONE).toLocalDateTime());
}
}

View File

@ -0,0 +1,55 @@
package cz.kamma.fabka.httpserver.repository;
public class ForumSummary {
private final long id;
private final String name;
private final String description;
private final String createdAt;
private final String lastPostUser;
private final String lastPostAt;
private final long postsCount;
private final boolean locked;
public ForumSummary(long id, String name, String description, String createdAt, String lastPostUser, String lastPostAt, long postsCount, boolean locked) {
this.id = id;
this.name = name;
this.description = description;
this.createdAt = createdAt;
this.lastPostUser = lastPostUser;
this.lastPostAt = lastPostAt;
this.postsCount = postsCount;
this.locked = locked;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getCreatedAt() {
return createdAt;
}
public String getLastPostUser() {
return lastPostUser;
}
public String getLastPostAt() {
return lastPostAt;
}
public long getPostsCount() {
return postsCount;
}
public boolean isLocked() {
return locked;
}
}

View File

@ -0,0 +1,49 @@
package cz.kamma.fabka.httpserver.repository;
public class MemberProfile {
private final long id;
private final String username;
private final String firstName;
private final String lastName;
private final String city;
private final String email;
private final String createdAt;
public MemberProfile(long id, String username, String firstName, String lastName, String city, String email, String createdAt) {
this.id = id;
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.city = city;
this.email = email;
this.createdAt = createdAt;
}
public long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getCity() {
return city;
}
public String getEmail() {
return email;
}
public String getCreatedAt() {
return createdAt;
}
}

View File

@ -0,0 +1,140 @@
package cz.kamma.fabka.httpserver.repository;
import cz.kamma.fabka.httpserver.crypto.Md5;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class MemberRepository {
private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague");
private static final String SELECT_PROFILE_SQL =
"select id, username, firstname, surename, city, email, created from user_accounts where id=? limit 1";
private static final String UPDATE_INFO_SQL =
"update user_accounts set email=?, firstname=?, surename=?, city=? where id=?";
private static final String SELECT_PASSWORD_SQL =
"select passwd from user_accounts where id=? limit 1";
private static final String UPDATE_PASSWORD_SQL =
"update user_accounts set passwd=? where id=?";
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public MemberRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public MemberProfile findByUserId(long userId) {
if (userId <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(SELECT_PROFILE_SQL)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
return new MemberProfile(
rs.getLong("id"),
valueOrDefault(rs.getString("username")),
valueOrDefault(rs.getString("firstname")),
valueOrDefault(rs.getString("surename")),
valueOrDefault(rs.getString("city")),
valueOrDefault(rs.getString("email")),
formatTs(rs.getTimestamp("created"))
);
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public boolean updatePersonalInfo(long userId, String email, String firstName, String lastName, String city) {
if (userId <= 0) {
return false;
}
String safeEmail = safe(email);
String safeFirstName = safe(firstName);
String safeLastName = safe(lastName);
String safeCity = safe(city);
if (safeEmail.isBlank() || safeFirstName.isBlank() || safeLastName.isBlank() || safeCity.isBlank()) {
return false;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(UPDATE_INFO_SQL)) {
ps.setString(1, safeEmail);
ps.setString(2, safeFirstName);
ps.setString(3, safeLastName);
ps.setString(4, safeCity);
ps.setLong(5, userId);
return ps.executeUpdate() > 0;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
public boolean changePassword(long userId, String oldPassword, String newPassword) {
if (userId <= 0) {
return false;
}
String safeOld = safe(oldPassword);
String safeNew = safe(newPassword);
if (safeOld.isBlank() || safeNew.isBlank()) {
return false;
}
String currentHash;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(SELECT_PASSWORD_SQL)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return false;
}
currentHash = valueOrDefault(rs.getString("passwd"));
}
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
if (!Md5.hash(safeOld).equalsIgnoreCase(currentHash)) {
return false;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(UPDATE_PASSWORD_SQL)) {
ps.setString(1, Md5.hash(safeNew));
ps.setLong(2, userId);
return ps.executeUpdate() > 0;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
private static String safe(String value) {
return value == null ? "" : value.trim();
}
private static String valueOrDefault(String value) {
return value == null ? "" : value;
}
private static String formatTs(Timestamp ts) {
if (ts == null) {
return "N/A";
}
return DATETIME_FORMAT.format(ts.toInstant().atZone(APP_ZONE).toLocalDateTime());
}
}

View File

@ -0,0 +1,25 @@
package cz.kamma.fabka.httpserver.repository;
public class MessageRenderSettings {
private final String youtubeSnippet;
private final String webmSnippet;
private final String youtubeReplaceUrls;
public MessageRenderSettings(String youtubeSnippet, String webmSnippet, String youtubeReplaceUrls) {
this.youtubeSnippet = youtubeSnippet;
this.webmSnippet = webmSnippet;
this.youtubeReplaceUrls = youtubeReplaceUrls;
}
public String getYoutubeSnippet() {
return youtubeSnippet;
}
public String getWebmSnippet() {
return webmSnippet;
}
public String getYoutubeReplaceUrls() {
return youtubeReplaceUrls;
}
}

View File

@ -0,0 +1,166 @@
package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
public class MysqlClientRepository {
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public MysqlClientRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public List<String> listDatabases() {
List<String> result = new ArrayList<>();
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SHOW DATABASES")) {
while (rs.next()) {
result.add(rs.getString(1));
}
} catch (SQLException ignored) {
// Keep UI usable even if metadata query fails.
}
return result;
}
public List<String> listTables(String database) {
List<String> result = new ArrayList<>();
if (database == null || database.isBlank()) {
return result;
}
String sql = "SHOW TABLES FROM " + qid(database);
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
result.add(rs.getString(1));
}
} catch (SQLException ignored) {
// Keep UI usable even if metadata query fails.
}
return result;
}
public String showCreateTable(String database, String tableName) throws SQLException {
if (database == null || database.isBlank() || tableName == null || tableName.isBlank()) {
return "";
}
String useSql = "USE " + qid(database);
String createSql = "SHOW CREATE TABLE " + qid(tableName);
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
Statement stmt = conn.createStatement()) {
stmt.execute(useSql);
try (ResultSet rs = stmt.executeQuery(createSql)) {
if (rs.next()) {
return rs.getString(2);
}
return "";
}
}
}
public SqlExecution executeSql(String database, String sql) throws SQLException {
SqlExecution result = new SqlExecution();
result.setExecutedSql(sql == null ? "" : sql);
if (sql == null || sql.isBlank()) {
return result;
}
String useSql = (database == null || database.isBlank()) ? null : "USE " + qid(database);
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
Statement stmt = conn.createStatement()) {
if (useSql != null) {
stmt.execute(useSql);
}
boolean hasResultSet = stmt.execute(sql);
if (hasResultSet) {
result.setResultSet(true);
try (ResultSet rs = stmt.getResultSet()) {
ResultSetMetaData rsmd = rs.getMetaData();
int count = rsmd.getColumnCount();
List<String> columns = new ArrayList<>();
for (int i = 1; i <= count; i++) {
columns.add(rsmd.getColumnLabel(i));
}
result.setColumns(columns);
List<List<String>> rows = new ArrayList<>();
while (rs.next()) {
List<String> row = new ArrayList<>();
for (int i = 1; i <= count; i++) {
row.add(rs.getString(i));
}
rows.add(row);
}
result.setRows(rows);
}
} else {
result.setResultSet(false);
result.setUpdateCount(stmt.getUpdateCount());
}
}
return result;
}
private static String qid(String identifier) {
return "`" + identifier.replace("`", "``") + "`";
}
public static final class SqlExecution {
private String executedSql = "";
private boolean resultSet;
private int updateCount;
private List<String> columns = List.of();
private List<List<String>> rows = List.of();
public String getExecutedSql() {
return executedSql;
}
public void setExecutedSql(String executedSql) {
this.executedSql = executedSql;
}
public boolean isResultSet() {
return resultSet;
}
public void setResultSet(boolean resultSet) {
this.resultSet = resultSet;
}
public int getUpdateCount() {
return updateCount;
}
public void setUpdateCount(int updateCount) {
this.updateCount = updateCount;
}
public List<String> getColumns() {
return columns;
}
public void setColumns(List<String> columns) {
this.columns = columns == null ? List.of() : columns;
}
public List<List<String>> getRows() {
return rows;
}
public void setRows(List<List<String>> rows) {
this.rows = rows == null ? List.of() : rows;
}
}
}

View File

@ -0,0 +1,57 @@
package cz.kamma.fabka.httpserver.repository;
public class PrivateMessageItem {
private final long id;
private final long fromUserId;
private final String fromUsername;
private final String fromJoinDate;
private final long fromPosts;
private final String createdAt;
private final String message;
public PrivateMessageItem(
long id,
long fromUserId,
String fromUsername,
String fromJoinDate,
long fromPosts,
String createdAt,
String message
) {
this.id = id;
this.fromUserId = fromUserId;
this.fromUsername = fromUsername;
this.fromJoinDate = fromJoinDate;
this.fromPosts = fromPosts;
this.createdAt = createdAt;
this.message = message;
}
public long getId() {
return id;
}
public long getFromUserId() {
return fromUserId;
}
public String getFromUsername() {
return fromUsername;
}
public String getFromJoinDate() {
return fromJoinDate;
}
public long getFromPosts() {
return fromPosts;
}
public String getCreatedAt() {
return createdAt;
}
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,273 @@
package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class PrivateMessageRepository {
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final ZoneId APP_ZONE = ZoneId.of("Europe/Prague");
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public PrivateMessageRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public PrivateMessageStats stats(long userId) {
if (userId <= 0) {
return new PrivateMessageStats(0, 0);
}
int unread = scalarCount("select count(id) as cnt from messages where deleted=0 and readed=0 and to_user=?", userId);
int total = scalarCount("select count(id) as cnt from messages where deleted=0 and (to_user=? or from_user=?)", userId, userId);
return new PrivateMessageStats(unread, total);
}
public List<PrivateThreadSummary> listThreads(long userId, String orderBy) {
List<PrivateThreadSummary> out = new ArrayList<>();
if (userId <= 0) {
return out;
}
String order = sanitizeOrder(orderBy);
String sql = "select m.id, m.title, m.created, m.readed, " +
"coalesce((select max(created) from messages where reply_to=m.id), m.created) as lastitem, " +
"ua.username as sender, ua2.username as recipient, " +
"coalesce((select count(id) from messages where reply_to=m.id),0) as replies, " +
"coalesce((select count(id) from messages where deleted=0 and readed=0 and to_user=? and (id=m.id or reply_to=m.id)),0) as unread_thread " +
"from messages m " +
"join user_accounts ua on ua.id=m.from_user " +
"join user_accounts ua2 on ua2.id=m.to_user " +
"where (m.to_user=? or m.from_user=?) and m.deleted=0 and m.reply_to is null " +
"order by " + order;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
ps.setLong(2, userId);
ps.setLong(3, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
out.add(new PrivateThreadSummary(
rs.getLong("id"),
valueOrDefault(rs.getString("title"), ""),
formatTs(rs.getTimestamp("created")),
valueOrDefault(rs.getString("sender"), ""),
valueOrDefault(rs.getString("recipient"), ""),
rs.getLong("replies"),
rs.getInt("unread_thread") == 0
));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return out;
}
public void bulkAction(long userId, List<Long> ids, String action) {
if (userId <= 0 || ids == null || ids.isEmpty() || action == null || action.isBlank()) {
return;
}
StringBuilder inPart = new StringBuilder();
for (int i = 0; i < ids.size(); i++) {
if (i > 0) {
inPart.append(",");
}
inPart.append("?");
}
String sql;
if ("delete".equalsIgnoreCase(action)) {
sql = "update messages set deleted=1 where id in (" + inPart + ") and (to_user=? or from_user=?)";
} else if ("read".equalsIgnoreCase(action)) {
sql = "update messages set readed=1 where id in (" + inPart + ") and (to_user=? or from_user=?)";
} else if ("unread".equalsIgnoreCase(action)) {
sql = "update messages set readed=0 where id in (" + inPart + ") and (to_user=? or from_user=?)";
} else {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
int idx = 1;
for (Long id : ids) {
ps.setLong(idx++, id == null ? -1L : id);
}
ps.setLong(idx++, userId);
ps.setLong(idx, userId);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public String usernameById(long userId) {
if (userId <= 0) {
return "";
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement("select username from user_accounts where id=? limit 1")) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? valueOrDefault(rs.getString("username"), "") : "";
}
} catch (Exception ex) {
return "";
}
}
public PrivateThreadRoot threadRoot(long userId, long pmid) {
if (userId <= 0 || pmid <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"select id, from_user, to_user, title from messages where id=? and deleted=0 and (to_user=? or from_user=?) limit 1"
)) {
ps.setLong(1, pmid);
ps.setLong(2, userId);
ps.setLong(3, userId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
long fromUser = rs.getLong("from_user");
long toUser = rs.getLong("to_user");
long other = fromUser == userId ? toUser : fromUser;
String title = valueOrDefault(rs.getString("title"), "");
String replyTitle = title.startsWith("Re:") ? title : "Re: " + title;
return new PrivateThreadRoot(
rs.getLong("id"),
other,
title,
replyTitle,
usernameById(other)
);
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public void markThreadRead(long userId, long pmid) {
if (userId <= 0 || pmid <= 0) {
return;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"update messages set readed=1 where to_user=? and (id=? or reply_to=?)"
)) {
ps.setLong(1, userId);
ps.setLong(2, pmid);
ps.setLong(3, pmid);
ps.executeUpdate();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public List<PrivateMessageItem> threadMessages(long userId, long pmid) {
List<PrivateMessageItem> out = new ArrayList<>();
if (userId <= 0 || pmid <= 0) {
return out;
}
String sql = "select m.id, m.from_user, m.message, m.created, ua.username, ua.created as user_created, " +
"(select count(*) from forum_items fi where fi.createdby=ua.id and fi.deleted=0) as posts " +
"from messages m join user_accounts ua on ua.id=m.from_user " +
"where (m.to_user=? or m.from_user=?) and m.deleted=0 and (m.id=? or m.reply_to=?) " +
"order by m.created desc";
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
ps.setLong(2, userId);
ps.setLong(3, pmid);
ps.setLong(4, pmid);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
out.add(new PrivateMessageItem(
rs.getLong("id"),
rs.getLong("from_user"),
valueOrDefault(rs.getString("username"), ""),
formatTs(rs.getTimestamp("user_created")),
rs.getLong("posts"),
formatTs(rs.getTimestamp("created")),
valueOrDefault(rs.getString("message"), "")
));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return out;
}
public boolean send(long fromUser, long toUser, String title, String message, Long replyTo) {
if (fromUser <= 0 || toUser <= 0) {
return false;
}
if (title == null || title.isBlank() || message == null || message.isBlank()) {
return false;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(
"insert into messages (from_user, to_user, title, message, deleted, readed, reply_to) values (?,?,?,?,0,0,?)"
)) {
ps.setLong(1, fromUser);
ps.setLong(2, toUser);
ps.setString(3, title);
ps.setString(4, message);
if (replyTo != null && replyTo > 0) {
ps.setLong(5, replyTo);
} else {
ps.setNull(5, Types.BIGINT);
}
ps.executeUpdate();
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
private int scalarCount(String sql, long... params) {
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(sql)) {
for (int i = 0; i < params.length; i++) {
ps.setLong(i + 1, params[i]);
}
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getInt("cnt") : 0;
}
} catch (Exception ex) {
return 0;
}
}
private static String sanitizeOrder(String orderBy) {
if ("readed asc, lastitem asc".equalsIgnoreCase(orderBy)) {
return "readed asc, lastitem asc";
}
return "readed desc, lastitem desc";
}
private static String formatTs(Timestamp ts) {
if (ts == null) {
return "";
}
return ts.toInstant().atZone(APP_ZONE).format(DATE_TIME);
}
private static String valueOrDefault(String value, String dflt) {
return value == null ? dflt : value;
}
}

View File

@ -0,0 +1,19 @@
package cz.kamma.fabka.httpserver.repository;
public class PrivateMessageStats {
private final int unread;
private final int total;
public PrivateMessageStats(int unread, int total) {
this.unread = unread;
this.total = total;
}
public int getUnread() {
return unread;
}
public int getTotal() {
return total;
}
}

View File

@ -0,0 +1,37 @@
package cz.kamma.fabka.httpserver.repository;
public class PrivateThreadRoot {
private final long rootId;
private final long otherUserId;
private final String title;
private final String replyTitle;
private final String otherUsername;
public PrivateThreadRoot(long rootId, long otherUserId, String title, String replyTitle, String otherUsername) {
this.rootId = rootId;
this.otherUserId = otherUserId;
this.title = title;
this.replyTitle = replyTitle;
this.otherUsername = otherUsername;
}
public long getRootId() {
return rootId;
}
public long getOtherUserId() {
return otherUserId;
}
public String getTitle() {
return title;
}
public String getReplyTitle() {
return replyTitle;
}
public String getOtherUsername() {
return otherUsername;
}
}

View File

@ -0,0 +1,57 @@
package cz.kamma.fabka.httpserver.repository;
public class PrivateThreadSummary {
private final long id;
private final String title;
private final String createdAt;
private final String senderName;
private final String recipientName;
private final long replies;
private final boolean allRead;
public PrivateThreadSummary(
long id,
String title,
String createdAt,
String senderName,
String recipientName,
long replies,
boolean allRead
) {
this.id = id;
this.title = title;
this.createdAt = createdAt;
this.senderName = senderName;
this.recipientName = recipientName;
this.replies = replies;
this.allRead = allRead;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getCreatedAt() {
return createdAt;
}
public String getSenderName() {
return senderName;
}
public String getRecipientName() {
return recipientName;
}
public long getReplies() {
return replies;
}
public boolean isAllRead() {
return allRead;
}
}

View File

@ -0,0 +1,19 @@
package cz.kamma.fabka.httpserver.repository;
public class QuotedTextItem {
private final String author;
private final String text;
public QuotedTextItem(String author, String text) {
this.author = author;
this.text = text;
}
public String getAuthor() {
return author;
}
public String getText() {
return text;
}
}

View File

@ -0,0 +1,96 @@
package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;
public class SettingsRepository {
private static final String SETTING_SQL = "SELECT value FROM settings WHERE name=? LIMIT 1";
private static final String USER_SETTINGS_SQL = "SELECT name, value FROM settings WHERE userid=?";
private static final String UPDATE_USER_SETTING_SQL = "UPDATE settings SET value=? WHERE userid=? AND name=?";
private static final String INSERT_USER_SETTING_SQL = "INSERT INTO settings (userid, name, value) VALUES (?,?,?)";
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public SettingsRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public MessageRenderSettings loadMessageRenderSettings() {
String youtubeSnippet = getSetting("YOUTUBE_EMBED_SNIPPET");
String webmSnippet = getSetting("WEBM_EMBED_SNIPPET");
String youtubeReplace = getSetting("YOUTUBE_REPLACE_URL");
return new MessageRenderSettings(youtubeSnippet, webmSnippet, youtubeReplace);
}
public Map<String, String> loadUserSettings(long userId) {
Map<String, String> out = new HashMap<>();
if (userId <= 0) {
return out;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(USER_SETTINGS_SQL)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String key = rs.getString("name");
String value = rs.getString("value");
if (key != null && !key.isBlank() && value != null) {
out.put(key, value);
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return out;
}
public void upsertUserSetting(long userId, String name, String value) {
if (userId <= 0 || name == null || name.isBlank()) {
return;
}
String safeValue = value == null ? "" : value;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword)) {
int updated;
try (PreparedStatement up = conn.prepareStatement(UPDATE_USER_SETTING_SQL)) {
up.setString(1, safeValue);
up.setLong(2, userId);
up.setString(3, name);
updated = up.executeUpdate();
}
if (updated < 1) {
try (PreparedStatement ins = conn.prepareStatement(INSERT_USER_SETTING_SQL)) {
ins.setLong(1, userId);
ins.setString(2, name);
ins.setString(3, safeValue);
ins.executeUpdate();
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
private String getSetting(String key) {
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(SETTING_SQL)) {
ps.setString(1, key);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getString(1);
}
}
} catch (Exception ex) {
return null;
}
return null;
}
}

View File

@ -0,0 +1,19 @@
package cz.kamma.fabka.httpserver.repository;
public class UserIcon {
private final byte[] data;
private final String contentType;
public UserIcon(byte[] data, String contentType) {
this.data = data;
this.contentType = contentType;
}
public byte[] getData() {
return data;
}
public String getContentType() {
return contentType;
}
}

View File

@ -0,0 +1,66 @@
package cz.kamma.fabka.httpserver.repository;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class UserIconRepository {
private static final String USER_ICON_SQL = "SELECT data, mimetype FROM user_icon WHERE userid=? ORDER BY id DESC LIMIT 1";
private static final String INSERT_USER_ICON_SQL = "INSERT INTO user_icon (userid, data, mimetype) VALUES (?,?,?)";
private final String jdbcUrl;
private final String jdbcUser;
private final String jdbcPassword;
public UserIconRepository(String jdbcUrl, String jdbcUser, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUser = jdbcUser;
this.jdbcPassword = jdbcPassword;
}
public UserIcon findLatestByUserId(long userId) {
if (userId <= 0) {
return null;
}
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(USER_ICON_SQL)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
byte[] data = rs.getBytes("data");
if (data == null || data.length == 0) {
return null;
}
String mime = rs.getString("mimetype");
if (mime == null || mime.isBlank()) {
mime = "image/jpeg";
}
return new UserIcon(data, mime);
}
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public boolean saveUserIcon(long userId, byte[] data, String contentType) {
if (userId <= 0 || data == null || data.length == 0) {
return false;
}
String safeMime = (contentType == null || contentType.isBlank()) ? "application/octet-stream" : contentType;
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
PreparedStatement ps = conn.prepareStatement(INSERT_USER_ICON_SQL)) {
ps.setLong(1, userId);
ps.setBytes(2, data);
ps.setString(3, safeMime);
ps.executeUpdate();
return true;
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
}

View File

@ -0,0 +1,31 @@
package cz.kamma.fabka.httpserver.repository;
public class VoteStats {
private final int yes;
private final int no;
private final String yesUsers;
private final String noUsers;
public VoteStats(int yes, int no, String yesUsers, String noUsers) {
this.yes = yes;
this.no = no;
this.yesUsers = yesUsers;
this.noUsers = noUsers;
}
public int getYes() {
return yes;
}
public int getNo() {
return no;
}
public String getYesUsers() {
return yesUsers;
}
public String getNoUsers() {
return noUsers;
}
}

View File

@ -0,0 +1,43 @@
package cz.kamma.fabka.httpserver.session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SessionData {
private final String id;
private volatile long lastAccessMillis;
private final ConcurrentHashMap<String, Object> attributes = new ConcurrentHashMap<>();
public SessionData(String id) {
this.id = id;
this.lastAccessMillis = System.currentTimeMillis();
}
public String id() {
return id;
}
public long lastAccessMillis() {
return lastAccessMillis;
}
public void touch() {
lastAccessMillis = System.currentTimeMillis();
}
public Object getAttribute(String key) {
return attributes.get(key);
}
public void setAttribute(String key, Object value) {
if (value == null) {
attributes.remove(key);
return;
}
attributes.put(key, value);
}
public Map<String, Object> attributes() {
return attributes;
}
}

View File

@ -0,0 +1,94 @@
package cz.kamma.fabka.httpserver.session;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class SessionManager {
public static final String COOKIE_NAME = "FCHSESSION";
private final long timeoutMillis;
private final ConcurrentHashMap<String, SessionData> sessions = new ConcurrentHashMap<>();
public SessionManager(Duration timeout) {
this.timeoutMillis = timeout.toMillis();
}
public SessionData get(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return null;
}
SessionData data = sessions.get(sessionId);
if (data == null) {
return null;
}
if (isExpired(data)) {
sessions.remove(sessionId);
return null;
}
data.touch();
return data;
}
public SessionData create() {
cleanupExpired();
String id = UUID.randomUUID().toString();
SessionData data = new SessionData(id);
sessions.put(id, data);
return data;
}
public void invalidate(String sessionId) {
if (sessionId != null) {
sessions.remove(sessionId);
}
}
public int countSessionsWithAttribute(String attributeKey) {
cleanupExpired();
int count = 0;
for (SessionData session : sessions.values()) {
Object value = session.getAttribute(attributeKey);
if (value != null && !String.valueOf(value).isBlank()) {
count++;
}
}
return count;
}
public List<String> sessionAttributeValues(String attributeKey) {
cleanupExpired();
List<String> values = new ArrayList<>();
for (SessionData session : sessions.values()) {
Object value = session.getAttribute(attributeKey);
if (value == null) {
continue;
}
String asString = String.valueOf(value).trim();
if (!asString.isBlank()) {
values.add(asString);
}
}
return values;
}
public List<SessionData> activeSessions() {
cleanupExpired();
return new ArrayList<>(sessions.values());
}
private boolean isExpired(SessionData data) {
return System.currentTimeMillis() - data.lastAccessMillis() > timeoutMillis;
}
private void cleanupExpired() {
for (Map.Entry<String, SessionData> entry : sessions.entrySet()) {
if (isExpired(entry.getValue())) {
sessions.remove(entry.getKey());
}
}
}
}

View File

@ -0,0 +1,123 @@
package cz.kamma.fabka.httpserver.web;
import java.util.StringTokenizer;
public final class LegacyMessageFormatter {
private LegacyMessageFormatter() {
}
public static String convertMessageToHtml(
String message,
String youtubeSnippet,
String webmSnippet,
String youtubeReplaceUrls
) {
if (message == null || message.isEmpty()) {
return message == null ? "" : message;
}
String rendered = message;
if (youtubeSnippet != null && youtubeSnippet.length() > 10) {
rendered = processYoutubeLink(rendered, youtubeSnippet, youtubeReplaceUrls);
}
if (webmSnippet != null && webmSnippet.length() > 10) {
rendered = processWebMLink(rendered, webmSnippet);
}
StringTokenizer strtok = new StringTokenizer(rendered, "\n\t ");
while (strtok.hasMoreTokens()) {
String part = strtok.nextToken();
String lower = part.toLowerCase();
if (part.startsWith("http://") || part.startsWith("https://")) {
if (lower.endsWith(".gif") || lower.endsWith(".png") || lower.endsWith(".jpg")
|| lower.endsWith(".bmp")) {
rendered = rendered.replace(part, "<img src=\"" + part + "\" />");
} else {
rendered = rendered.replace(part, "<a href=\"" + part + "\" target=\"_blank\">" + part + "</a>");
}
}
}
return rendered.replace("\r\n", "<br/>").replace("\n", "<br/>");
}
private static String processYoutubeLink(String message, String youtubeSnippet, String replaceUrls) {
String rendered = message;
if (replaceUrls != null && !replaceUrls.isBlank()) {
String[] tmp = replaceUrls.split(",");
for (String rep : tmp) {
rendered = rendered.replace(rep.trim(), "www.youtube.com");
}
}
String[] parts = null;
String part = null;
if (rendered.contains("<object ") && rendered.contains("</object>")) {
part = rendered.substring(rendered.indexOf("<object "), rendered.indexOf("</object>"));
parts = part.split(" ");
} else if (rendered.contains("<iframe ") && rendered.contains("</iframe>")) {
part = rendered.substring(rendered.indexOf("<iframe "), rendered.indexOf("</iframe>"));
parts = part.split(" ");
} else if (rendered.contains("//www.youtube.com")) {
StringTokenizer strtok = new StringTokenizer(rendered, " \n\t");
while (strtok.hasMoreTokens()) {
part = strtok.nextToken();
if (part.contains("//www.youtube.com")) {
String videoId = extractYoutubeToken(part);
if (videoId != null) {
return rendered.replace(part, youtubeSnippet.replace("#YT_LINK#", videoId));
}
}
}
int from = rendered.indexOf("http://www.youtube.com");
if (from >= 0) {
int to = rendered.indexOf(" ", from);
if (to > from) {
part = rendered.substring(from, to);
parts = part.split(" ");
}
}
}
if (part == null || parts == null) {
return rendered;
}
for (String p : parts) {
if (p.startsWith("src=") && p.contains("youtube.com")) {
String[] split = p.split("=", 2);
if (split.length < 2) {
continue;
}
String videoId = extractYoutubeToken(split[1]);
if (videoId != null) {
return rendered.replace(part, youtubeSnippet.replace("#YT_LINK#", videoId));
}
}
}
return rendered;
}
private static String processWebMLink(String message, String webmSnippet) {
String rendered = message;
StringTokenizer strtok = new StringTokenizer(rendered, " \n\t");
while (strtok.hasMoreTokens()) {
String part = strtok.nextToken();
String lower = part.toLowerCase();
if (lower.startsWith("http") && lower.endsWith(".webm")) {
rendered = rendered.replace(part, webmSnippet.replace("#WEBM_URL#", part));
}
}
return rendered;
}
private static String extractYoutubeToken(String value) {
StringTokenizer strtok = new StringTokenizer(value, "/=&?\"");
while (strtok.hasMoreTokens()) {
String tok = strtok.nextToken().trim();
if (tok.length() == 11) {
return tok;
}
}
return null;
}
}

View File

@ -0,0 +1,919 @@
package cz.kamma.fabka.httpserver.web;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import cz.kamma.fabka.httpserver.repository.ForumAttachment;
import cz.kamma.fabka.httpserver.repository.ForumDetail;
import cz.kamma.fabka.httpserver.repository.ForumDisplayView;
import cz.kamma.fabka.httpserver.repository.ForumMessage;
import cz.kamma.fabka.httpserver.repository.ForumSummary;
import cz.kamma.fabka.httpserver.repository.MemberProfile;
import cz.kamma.fabka.httpserver.repository.MessageRenderSettings;
import cz.kamma.fabka.httpserver.repository.MysqlClientRepository;
import cz.kamma.fabka.httpserver.repository.PrivateMessageItem;
import cz.kamma.fabka.httpserver.repository.PrivateMessageStats;
import cz.kamma.fabka.httpserver.repository.PrivateThreadRoot;
import cz.kamma.fabka.httpserver.repository.PrivateThreadSummary;
import cz.kamma.fabka.httpserver.repository.QuotedTextItem;
public final class Pages {
private static final String LOGIN_TEMPLATE = readTemplate("webapp/login.html");
private static final String FORUM_TEMPLATE = readTemplate("webapp/forum.html");
private static final String FORUM_DISPLAY_TEMPLATE = readTemplate("webapp/forumdisplay.html");
private static final String CHAT_TEMPLATE = readTemplate("webapp/chat.html");
private static final String PRIVATE_TEMPLATE = readTemplate("webapp/private.html");
private static final String NEW_PM_TEMPLATE = readTemplate("webapp/newpm.html");
private static final String MESSAGE_TEMPLATE = readTemplate("webapp/message.html");
private static final String NEW_THREAD_TEMPLATE = readTemplate("webapp/newthread.html");
private static final String MEMBER_TEMPLATE = readTemplate("webapp/member.html");
private Pages() {
}
public static String loginPage(boolean showError) {
String error = showError
? "<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='70%' align='center'>\n"
+ " <tbody><tr><td class='panelsurround' align='center'>\n"
+ " <div style='margin:10px;color:red'>Invalid user or password.</div>\n"
+ " </td></tr></tbody></table><br/>\n"
: "";
return LOGIN_TEMPLATE
.replace("{{COMMON_HEADER}}", "")
.replace("{{COMMON_FOOTER}}", renderLoginFooter())
.replace("{{ERROR_BLOCK}}", error);
}
public static String forumPage(
String username,
List<ForumSummary> forums,
Map<Long, Integer> newCounts,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
StringBuilder forumRows = new StringBuilder();
if (forums == null || forums.isEmpty()) {
forumRows.append("<tr><td class='alt1' colspan='3'>No forums found.</td></tr>\n");
} else {
for (ForumSummary forum : forums) {
forumRows.append("<tr>")
.append("<td class='alt1Active' align='left'>")
.append("<div><a href='/forumdisplay?f=").append(forum.getId()).append("'><strong>")
.append(escapeHtml(forum.getName())).append("</strong></a>")
.append(forum.isLocked() ? "<img src='/images/locked.gif' title='Locked thread'/>" : "")
.append("<span style='color: red;font-size: x-small;font-weight: bold' id='newCount")
.append(forum.getId())
.append("'>")
.append(renderNewCountText(newCounts == null ? null : newCounts.get(forum.getId())))
.append("</span>")
.append("</div>")
.append("<div class='smallfont'>").append(escapeHtml(forum.getDescription())).append("</div>")
.append("<div class='smallfont'>").append(escapeHtml(forum.getCreatedAt())).append("</div>")
.append("</td>")
.append("<td class='alt2'>")
.append("<div class='smallfont' align='left'>")
.append("<div style='white-space: nowrap'>").append(escapeHtml(forum.getLastPostUser())).append("</div>")
.append("<div style='white-space: nowrap'><span class='time'>").append(escapeHtml(forum.getLastPostAt())).append("</span></div>")
.append("</div>")
.append("</td>")
.append("<td class='alt1' align='center'>")
.append(forum.getPostsCount())
.append("</td>")
.append("</tr>\n");
}
}
String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty())
? "<span class='smallfont'>-</span>"
: loggedUsers.stream()
.map(user -> "<a href='#'>" + escapeHtml(user) + " (0)</a>")
.collect(Collectors.joining(", "));
return FORUM_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "New thread", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{FORUM_ROWS}}", forumRows.toString())
.replace("{{LOGGED_USERS_COUNT}}", String.valueOf(Math.max(0, loggedUsersCount)))
.replace("{{LOGGED_USERS_LIST}}", loggedUsersHtml);
}
private static String renderNewCountText(Integer count) {
if (count == null || count <= 0) {
return "";
}
return " " + count + " new message(s)";
}
public static String forumDisplayPage(
String username,
long currentUserId,
ForumDetail forum,
ForumDisplayView view,
Long quoteItemId,
QuotedTextItem quotedTextItem,
int loggedUsersCount,
List<String> loggedUsers,
MessageRenderSettings renderSettings,
PrivateMessageStats pmStats
) {
StringBuilder messageRows = new StringBuilder();
List<ForumMessage> messages = view == null ? List.of() : view.getMessages();
boolean showFull = view == null || !"simple".equalsIgnoreCase(view.getShowType());
if (messages == null || messages.isEmpty()) {
messageRows.append("<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>")
.append("<tbody><tr><td class='alt1'>No messages.</td></tr></tbody></table>\n");
} else {
for (ForumMessage message : messages) {
messageRows.append("<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>")
.append("<tbody>")
.append("<tr><td class='thead'>")
.append("<div class='normal'>")
.append(escapeHtml(message.getCreatedAt()))
.append(" <span class='voting-buttons'>")
.append("<a title='").append(escapeHtml(message.getVoteYesUsers())).append("' ")
.append("id='yes").append(message.getId()).append("' class='voting_yes' ")
.append("href=\"javascript:AX_voting(").append(message.getId()).append(",'yes')\">")
.append(message.getVoteYes()).append("</a>")
.append("<a title='").append(escapeHtml(message.getVoteNoUsers())).append("' ")
.append("id='no").append(message.getId()).append("' class='voting_no' ")
.append("href=\"javascript:AX_voting(").append(message.getId()).append(",'no')\">")
.append(message.getVoteNo()).append("</a>")
.append("</span>")
.append("<a href='/forumdisplay?f=").append(forum == null ? 0 : forum.getId())
.append("&q=").append(message.getId()).append("'><img align='right' src='/images/quote.gif'/></a>")
.append(message.getAuthorId() == currentUserId
? "<a href='/process/deletepost.jsp?forumid=" + (forum == null ? 0 : forum.getId())
+ "&postid=" + message.getId()
+ "' onclick=\"return confirm('Are you sure you want to delete this post ?');\">"
+ "<img align='right' src='/images/delete.gif'/></a>"
: "")
.append(" <a href='/process/setsticky.jsp?forumid=")
.append(forum == null ? 0 : forum.getId())
.append("&postid=").append(message.getId()).append("'>")
.append(message.isSticky() ? "[STICKY]" : "SET [STICKY]")
.append("</a>")
.append("</div></td></tr>")
.append(showFull
? "<tr><td class='alt2' style='padding:0'>"
+ "<table cellpadding='0' cellspacing='6' border='0' width='100%'><tbody><tr>"
+ "<td valign='middle' nowrap='nowrap'><img src='/process/showimage?userIcon=yes&uid=" + message.getAuthorId() + "' width='80'/></td>"
+ "<td valign='middle' nowrap='nowrap'><div style='color: blue; font-weight:bold'><a href='#'>" + escapeHtml(message.getAuthor()) + "</a></div></td>"
+ "<td width='100%'>&nbsp;</td>"
+ "<td valign='top' nowrap='nowrap'><div class='smallfont'>"
+ "<div>Join Date: " + escapeHtml(message.getAuthorJoinDate()) + "</div>"
+ "<div>Location: " + escapeHtml(message.getAuthorCity()) + "</div>"
+ "<div>Posts: " + message.getAuthorPosts() + "</div>"
+ "</div></td>"
+ "</tr></tbody></table>"
+ "</td></tr>"
: "")
.append("<tr><td class='alt1'><div>")
.append(renderQuotedBlock(message.getQuotedItem()))
.append(LegacyMessageFormatter.convertMessageToHtml(
message.getText(),
renderSettings == null ? null : renderSettings.getYoutubeSnippet(),
renderSettings == null ? null : renderSettings.getWebmSnippet(),
renderSettings == null ? null : renderSettings.getYoutubeReplaceUrls()
))
.append(renderAttachments(message.getAttachments(), view == null ? "true" : view.getShowImg()))
.append("</div></td></tr>")
.append("</tbody></table>\n");
}
}
String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty())
? "<span class='smallfont'>-</span>"
: loggedUsers.stream()
.map(user -> "<a href='#'>" + escapeHtml(user) + " (0)</a>")
.collect(Collectors.joining(", "));
String forumName = forum == null ? "" : forum.getName();
String forumDesc = forum == null ? "" : forum.getDescription();
String forumCountdown = forum == null ? "" : valueOrDefault(forum.getCountdown(), "");
long loadPageTime = System.currentTimeMillis();
long forumId = forum == null ? 0 : forum.getId();
String searchText = view == null ? "" : valueOrDefault(view.getSearchText(), "");
String showType = view == null ? "full" : valueOrDefault(view.getShowType(), "full");
String showImg = view == null ? "true" : valueOrDefault(view.getShowImg(), "true");
String sortBy = view == null ? "fi.id" : valueOrDefault(view.getSortBy(), "fi.id");
String sortType = view == null ? "desc" : valueOrDefault(view.getSortType(), "desc");
String perPage = view == null ? "50" : valueOrDefault(view.getPerPage(), "50");
String baseLink = "/forumdisplay?f=" + forumId
+ "&stext=" + url(searchText)
+ "&showType=" + url(showType)
+ "&showImg=" + url(showImg)
+ "&sortBy=" + url(sortBy)
+ "&perpage=" + url(perPage);
int rowsFrom = view == null ? 0 : view.getStartRow();
int rowsTo = view == null ? 0 : view.getEndRow();
int rowsTotal = view == null ? 0 : view.getTotalRows();
int currentPage = view == null ? 1 : Math.max(1, view.getCurrentPage());
int totalPages = view == null ? 1 : Math.max(1, view.getTotalPages());
String quoteBlock = "";
if (quotedTextItem != null) {
quoteBlock = "<div align='left' style='max-width:640px; width:640px'>"
+ "<div class='smallfont' style='margin-bottom:2px'>Quoting:</div>"
+ "<table cellpadding='6' cellspacing='0' border='0' width='100%'><tbody><tr>"
+ "<td class='alt3' style='border:1px inset'>"
+ "<div>Originally Posted by <strong>" + escapeHtml(quotedTextItem.getAuthor()) + "</strong></div>"
+ "<div style='font-style:italic'>" + escapeHtml(quotedTextItem.getText()).replace("\n", "<br/>") + "</div>"
+ "</td></tr></tbody></table>"
+ "</div><br/>";
}
return FORUM_DISPLAY_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, forumName, true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{FORUM_NAME}}", escapeHtml(forumName))
.replace("{{FORUM_DESCRIPTION}}", escapeHtml(forumDesc))
.replace("{{COUNTDOWN_BLOCK}}", buildCountdownBlock(forumCountdown, loadPageTime))
.replace("{{MESSAGE_ROWS}}", messageRows.toString())
.replace("{{FORUM_ID}}", String.valueOf(forumId))
.replace("{{SEARCH_TEXT}}", escapeHtml(searchText))
.replace("{{SHOW_TYPE}}", escapeHtml(showType))
.replace("{{SHOW_IMG}}", escapeHtml(showImg))
.replace("{{SORT_BY}}", escapeHtml(sortBy))
.replace("{{SORT_TYPE}}", escapeHtml(sortType))
.replace("{{NEXT_SORT_TYPE}}", "desc".equalsIgnoreCase(sortType) ? "asc" : "desc")
.replace("{{PER_PAGE}}", escapeHtml(perPage))
.replace("{{BASE_LINK}}", baseLink)
.replace("{{QUOTE_ITEM}}", String.valueOf(quoteItemId == null ? 0 : quoteItemId))
.replace("{{QUOTE_BLOCK}}", quoteBlock)
.replace("{{SHOW_TYPE_OPTIONS}}", options(
new String[]{"full", "simple"},
new String[]{"Full", "Simple"},
showType
))
.replace("{{SHOW_IMG_OPTIONS}}", options(
new String[]{"true", "false", "thumbnail", "only"},
new String[]{"Yes", "No", "Thumbnail", "Images only"},
showImg
))
.replace("{{SORT_BY_OPTIONS}}", options(
new String[]{"fi.id", "ua.username", "vvalue"},
new String[]{"Time", "Author", "Popularity"},
sortBy
))
.replace("{{PER_PAGE_OPTIONS}}", options(
new String[]{"10", "25", "50", "100", "all"},
new String[]{"10", "25", "50", "100", "All"},
perPage
))
.replace("{{ROWS_FROM}}", String.valueOf(rowsFrom))
.replace("{{ROWS_TO}}", String.valueOf(rowsTo))
.replace("{{ROWS_TOTAL}}", String.valueOf(rowsTotal))
.replace("{{PAGE_TDS}}", buildPageTds(baseLink, currentPage, totalPages))
.replace("{{PAGE_LINKS}}", buildPageLinks(baseLink, currentPage, totalPages))
.replace("{{LOGGED_USERS_COUNT}}", String.valueOf(Math.max(0, loggedUsersCount)))
.replace("{{LOGGED_USERS_LIST}}", loggedUsersHtml);
}
private static String buildCountdownBlock(String countdownScript, long loadPageTime) {
if (countdownScript == null || countdownScript.isBlank()) {
return "";
}
return "<script type='text/javascript'>"
+ "var loadPageTimeJS=(new Date()).getTime();"
+ "var loadPageTime=" + loadPageTime + ";"
+ "var timeDiff=loadPageTime-loadPageTimeJS;"
+ "var __fcCountdownTimer=null;"
+ "function countdown_clock(year,month,day,hour,minute,format){"
+ "var host=document.getElementById('countdown');"
+ "if(!host){host=document.createElement('div');host.id='countdown';host.style.color='red';host.style.fontWeight='bold';host.style.textAlign='center';"
+ "var anchor=document.getElementById('countdown-anchor');if(anchor){anchor.appendChild(host);}}"
+ "countdown(year,month,day,hour,minute,format);"
+ "}"
+ "function countdown(year,month,day,hour,minute,format){"
+ "var now=(new Date()).getTime()+timeDiff;"
+ "var target=(new Date(year,month-1,day,hour,minute,0)).getTime();"
+ "var left=Math.round((target-now)/1000);"
+ "var millis=now-target;"
+ "if(left<0){left=0;}"
+ "var el=document.getElementById('countdown');"
+ "if(!el){return;}"
+ "var str='';"
+ "switch(format){"
+ "case 0:str=left+' seconds';break;"
+ "case 1:"
+ "var days=Math.floor(left/(60*60*24));"
+ "var rem=left%(60*60*24);"
+ "var hours=Math.floor(rem/(60*60));"
+ "rem%=60*60;"
+ "var minutes2=Math.floor(rem/60);"
+ "var seconds=rem%60;"
+ "str='Departure countdown: '+days+' day'+(days===1?'':'s')+' '+hours+' hour'+(hours===1?'':'s')+' '+minutes2+' minute'+(minutes2===1?'':'s')+' and '+seconds+' second'+(seconds===1?'':'s');"
+ "break;"
+ "case 2:"
+ "var abs=''+Math.abs(millis);"
+ "while(abs.length<11){abs='0'+abs;}"
+ "var minuteStr=(minute<10?'0':'')+minute;"
+ "str=(millis<0?'T-':'T+')+abs+' milliseconds'+(millis<0?' :-)':' :-(')+'<br/>Departure: '+day+'.'+month+'.'+year+' '+hour+':'+minuteStr;"
+ "break;"
+ "case 3:str='Current time: '+(new Date(now));break;"
+ "default:str=left+' seconds';break;"
+ "}"
+ "el.innerHTML=str;"
+ "if(__fcCountdownTimer){clearTimeout(__fcCountdownTimer);}"
+ "__fcCountdownTimer=setTimeout(function(){countdown(year,month,day,hour,minute,format);},1);"
+ "}"
+ "</script>"
+ "<div id='countdown-anchor'></div>"
+ "<script type='text/javascript'>"
+ countdownScript
+ "</script><br/>";
}
public static String chatPage(
String username,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty())
? "<span class='smallfont'>-</span>"
: loggedUsers.stream()
.map(user -> "<a href='#'>" + escapeHtml(user) + " (0)</a>")
.collect(Collectors.joining(", "));
return CHAT_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "Chat", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{LOGGED_USERS_COUNT}}", String.valueOf(Math.max(0, loggedUsersCount)))
.replace("{{LOGGED_USERS_LIST}}", loggedUsersHtml);
}
public static String privatePage(
String username,
List<PrivateThreadSummary> threads,
String orderBy,
String perPage,
int currentPage,
int totalPages,
int rowsFrom,
int rowsTo,
int totalRows,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
StringBuilder rows = new StringBuilder();
if (threads == null || threads.isEmpty()) {
rows.append("<tr><td class='alt1' colspan='4'>No private messages.</td></tr>");
} else {
for (PrivateThreadSummary thread : threads) {
rows.append("<tr>")
.append("<td class='alt1'><img src='")
.append(thread.isAllRead() ? "/images/pm_read.gif" : "/images/pm.gif")
.append("' alt='' border='0'/></td>")
.append("<td class='alt2'>&nbsp;</td>")
.append("<td class='alt1 alt1Active' width='100%'>")
.append("<div><span style='float:right' class='smallfont'>").append(escapeHtml(thread.getCreatedAt())).append("</span>")
.append(thread.isAllRead() ? "" : "<strong>")
.append("<a href='/message?pmid=").append(thread.getId()).append("'>").append(escapeHtml(thread.getTitle())).append("</a>")
.append(thread.isAllRead() ? "" : "</strong>")
.append("</div>")
.append("<div class='smallfont'>").append(escapeHtml(thread.getSenderName())).append(" -> ").append(escapeHtml(thread.getRecipientName())).append("</div>")
.append("<div class='smallfont'>Replies: ").append(thread.getReplies()).append("</div>")
.append("</td>")
.append("<td class='alt2' align='center' style='padding:0'><input type='checkbox' name='pmids' value='")
.append(thread.getId()).append("'/></td>")
.append("</tr>");
}
}
String safeOrder = valueOrDefault(orderBy, "readed desc, lastitem desc");
String nextOrder = safeOrder.contains("desc") ? "readed asc, lastitem asc" : "readed desc, lastitem desc";
String safePerPage = valueOrDefault(perPage, "25");
String baseLink = "/private?oBy=" + url(safeOrder) + "&perpage=" + url(safePerPage);
String dateSortLink = "/private?oBy=" + url(nextOrder) + "&perpage=" + url(safePerPage) + "&iPageNo=1";
return PRIVATE_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "Private messages", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{THREAD_ROWS}}", rows.toString())
.replace("{{THREAD_COUNT}}", String.valueOf(totalRows))
.replace("{{ORDER_BY}}", escapeHtml(safeOrder))
.replace("{{PER_PAGE}}", escapeHtml(safePerPage))
.replace("{{I_PAGE_NO}}", String.valueOf(currentPage))
.replace("{{PER_PAGE_OPTIONS}}", options(
new String[]{"10", "25", "50", "100", "all"},
new String[]{"10", "25", "50", "100", "All"},
safePerPage
))
.replace("{{ROWS_FROM}}", String.valueOf(rowsFrom))
.replace("{{ROWS_TO}}", String.valueOf(rowsTo))
.replace("{{ROWS_TOTAL}}", String.valueOf(totalRows))
.replace("{{PAGE_LINKS}}", buildPageLinks(baseLink, currentPage, totalPages))
.replace("{{DATE_SORT_LINK}}", dateSortLink);
}
public static String newPmPage(
String username,
long toUserId,
String toUsername,
String error,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
return NEW_PM_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "Private messages", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{TO_USER_ID}}", String.valueOf(toUserId))
.replace("{{TO_USERNAME}}", escapeHtml(toUsername))
.replace("{{ERROR_BLOCK}}", renderErrorBlock(error));
}
public static String newThreadPage(
String username,
String error,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
return NEW_THREAD_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "New thread", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{USERNAME}}", escapeHtml(valueOrDefault(username, "")))
.replace("{{ERROR_BLOCK}}", renderErrorBlock(error));
}
public static String memberPage(
String username,
MemberProfile profile,
boolean soundOn,
String error,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
String empty = "";
return MEMBER_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "Member details", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{ERROR_BLOCK}}", renderErrorBlock(error))
.replace("{{USERNAME}}", profile == null ? empty : escapeHtml(valueOrDefault(profile.getUsername(), empty)))
.replace("{{JOIN_DATE}}", profile == null ? "N/A" : escapeHtml(valueOrDefault(profile.getCreatedAt(), "N/A")))
.replace("{{EMAIL}}", profile == null ? empty : escapeHtml(valueOrDefault(profile.getEmail(), empty)))
.replace("{{FIRST_NAME}}", profile == null ? empty : escapeHtml(valueOrDefault(profile.getFirstName(), empty)))
.replace("{{LAST_NAME}}", profile == null ? empty : escapeHtml(valueOrDefault(profile.getLastName(), empty)))
.replace("{{CITY}}", profile == null ? empty : escapeHtml(valueOrDefault(profile.getCity(), empty)))
.replace("{{SOUND_CHECKED}}", soundOn ? "checked='checked'" : "");
}
public static String messagePage(
String username,
PrivateThreadRoot root,
List<PrivateMessageItem> messages,
String perPage,
int currentPage,
int totalPages,
int rowsFrom,
int rowsTo,
int totalRows,
String error,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
StringBuilder rows = new StringBuilder();
if (messages == null || messages.isEmpty()) {
rows.append("<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'><tbody><tr><td class='alt1'>No messages.</td></tr></tbody></table>");
} else {
for (PrivateMessageItem item : messages) {
rows.append("<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>")
.append("<tbody><tr><td class='thead'><div class='normal'><a name='post'><img class='inlineimg' src='/images/post_old.gif' alt='' border='0'/></a> ")
.append(escapeHtml(item.getCreatedAt()))
.append("</div></td></tr>")
.append("<tr><td class='alt2' style='padding:0'><table cellpadding='0' cellspacing='6' border='0' width='100%'><tbody><tr>")
.append("<td nowrap='nowrap'><div class='bigusername'><a href='#'>").append(escapeHtml(item.getFromUsername())).append("</a></div></td>")
.append("<td width='100%'>&nbsp;</td><td valign='top' nowrap='nowrap'><div class='smallfont'>")
.append("<div>Join Date: ").append(escapeHtml(item.getFromJoinDate())).append("</div>")
.append("<div>Posts: ").append(item.getFromPosts()).append("</div>")
.append("</div></td></tr></tbody></table></td></tr>")
.append("<tr><td class='alt1'><div id='post_message_'>")
.append(LegacyMessageFormatter.convertMessageToHtml(item.getMessage(), null, null, null))
.append("</div></td></tr></tbody></table>");
}
}
String empty = "";
long pmId = root == null ? 0 : root.getRootId();
long toUserId = root == null ? 0 : root.getOtherUserId();
String safePerPage = valueOrDefault(perPage, "10");
String baseLink = "/message?pmid=" + pmId + "&perpage=" + url(safePerPage);
return MESSAGE_TEMPLATE
.replace("{{COMMON_HEADER}}", renderCommonHeader(username, "Private messages", true, pmStats))
.replace("{{COMMON_FOOTER}}", renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers))
.replace("{{ERROR_BLOCK}}", renderErrorBlock(error))
.replace("{{TO_USERNAME}}", root == null ? empty : escapeHtml(root.getOtherUsername()))
.replace("{{PM_ID}}", String.valueOf(pmId))
.replace("{{RE_TITLE}}", root == null ? empty : escapeHtml(root.getReplyTitle()))
.replace("{{TO_USER_ID}}", String.valueOf(toUserId))
.replace("{{PER_PAGE}}", escapeHtml(safePerPage))
.replace("{{I_PAGE_NO}}", String.valueOf(currentPage))
.replace("{{THREAD_TITLE}}", root == null ? empty : escapeHtml(root.getTitle()))
.replace("{{MESSAGE_ROWS}}", rows.toString())
.replace("{{PER_PAGE_OPTIONS}}", options(
new String[]{"10", "25", "50", "100", "all"},
new String[]{"10", "25", "50", "100", "All"},
safePerPage
))
.replace("{{ROWS_FROM}}", String.valueOf(rowsFrom))
.replace("{{ROWS_TO}}", String.valueOf(rowsTo))
.replace("{{ROWS_TOTAL}}", String.valueOf(totalRows))
.replace("{{PAGE_LINKS}}", buildPageLinks(baseLink, currentPage, totalPages));
}
public static String mysqlClientPage(
String username,
List<String> databases,
List<String> tables,
String selectedDatabase,
String selectedTable,
String rowsToShow,
String myQuery,
String tableSql,
String command,
String info,
String error,
MysqlClientRepository.SqlExecution result,
int loggedUsersCount,
List<String> loggedUsers,
PrivateMessageStats pmStats
) {
StringBuilder dbOptions = new StringBuilder();
if (databases == null || databases.isEmpty()) {
dbOptions.append("<option value=''>-</option>");
} else {
for (String db : databases) {
dbOptions.append("<option value='").append(escapeHtml(db)).append("'");
if (db != null && db.equalsIgnoreCase(valueOrDefault(selectedDatabase, ""))) {
dbOptions.append(" selected");
}
dbOptions.append(">").append(escapeHtml(db)).append("</option>");
}
}
StringBuilder tableOptions = new StringBuilder();
if (tables == null || tables.isEmpty()) {
tableOptions.append("<option value=''>-</option>");
} else {
for (String table : tables) {
tableOptions.append("<option value='").append(escapeHtml(table)).append("'");
if (table != null && table.equalsIgnoreCase(valueOrDefault(selectedTable, ""))) {
tableOptions.append(" selected");
}
tableOptions.append(">").append(escapeHtml(table)).append("</option>");
}
}
StringBuilder resultHtml = new StringBuilder();
if (result != null) {
if (result.isResultSet()) {
resultHtml.append("<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'><tbody>");
resultHtml.append("<tr><td class='thead' colspan='").append(Math.max(1, result.getColumns().size())).append("'>Result set</td></tr>");
resultHtml.append("<tr>");
for (String column : result.getColumns()) {
resultHtml.append("<td class='alt2'><strong>").append(escapeHtml(column)).append("</strong></td>");
}
resultHtml.append("</tr>");
for (List<String> row : result.getRows()) {
resultHtml.append("<tr>");
for (String value : row) {
resultHtml.append("<td class='alt1'>").append(escapeHtml(value == null ? "NULL" : value)).append("</td>");
}
resultHtml.append("</tr>");
}
if (result.getRows().isEmpty()) {
resultHtml.append("<tr><td class='alt1' colspan='").append(Math.max(1, result.getColumns().size())).append("'>No rows.</td></tr>");
}
resultHtml.append("</tbody></table>");
} else {
resultHtml.append("<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'><tbody>")
.append("<tr><td class='alt1'>Affected rows: ").append(result.getUpdateCount()).append("</td></tr>")
.append("</tbody></table>");
}
}
String infoBlock = (info == null || info.isBlank())
? ""
: "<div style='margin:10px 0;color:green'><strong>" + escapeHtml(info) + "</strong></div>";
String errorBlock = (error == null || error.isBlank())
? ""
: "<div style='margin:10px 0;color:red'><strong>" + escapeHtml(error) + "</strong></div>";
String body = "<!DOCTYPE html><html lang='en'><head>"
+ "<meta charset='UTF-8'/>"
+ "<meta name='viewport' content='width=device-width, initial-scale=1.0'/>"
+ "<link rel='stylesheet' type='text/css' href='/css/all.css'/>"
+ "<title>kAmMa's Forum MySQL Client</title>"
+ "</head><body>"
+ "<div class='page' style='width:100%; text-align:left'><div style='padding:0 25px 0 25px'>"
+ "<br/>"
+ renderCommonHeader(username, "MySQL Client", true, pmStats)
+ "<br/>"
+ infoBlock
+ errorBlock
+ "<form action='/mysqlc' method='post' name='mysqlcform'>"
+ "<input type='hidden' name='command' value='" + escapeHtml(valueOrDefault(command, "")) + "'/>"
+ "<div style='margin-bottom:8px'>"
+ "<select name='database'>" + dbOptions + "</select> "
+ "<button type='submit' onclick=\"this.form.command.value='changeDatabase'\">Select Database</button> "
+ "<button type='submit' onclick=\"this.form.command.value='deleteDatabase';return confirm('Are you sure?');\">Delete Database</button> "
+ "<input type='text' name='databasename' value=''/> "
+ "<button type='submit' onclick=\"this.form.command.value='createDatabase';return confirm('Are you sure?');\">Create Database</button>"
+ "</div>"
+ "<div style='margin-bottom:8px'>"
+ "<select name='tablename'>" + tableOptions + "</select> "
+ "<button type='submit' onclick=\"this.form.command.value='changeTable'\">Select Table</button> "
+ "Show rows: <input type='text' name='rowsToShow' value='" + escapeHtml(valueOrDefault(rowsToShow, "50")) + "' style='width:60px'/> "
+ "<button type='submit' onclick=\"this.form.command.value='deleteTable';return confirm('Are you sure?');\">Delete Table</button> "
+ "<button type='submit' onclick=\"this.form.command.value='showCreateTable'\">Show Create Table</button>"
+ "</div>"
+ "<hr/>"
+ "<div style='margin-bottom:8px'>"
+ "<textarea name='myQuery' cols='100' rows='8'>" + escapeHtml(valueOrDefault(myQuery, "")) + "</textarea><br/>"
+ "<button type='submit' onclick=\"this.form.command.value='executeMyQuery'\">Execute Query</button>"
+ "</div>"
+ "<div style='margin-bottom:8px'>"
+ "<textarea name='tableSql' cols='100' rows='4' placeholder='CREATE TABLE ...'>"
+ escapeHtml(valueOrDefault(tableSql, ""))
+ "</textarea><br/>"
+ "<button type='submit' onclick=\"this.form.command.value='createTable';return confirm('Are you sure?');\">Create Table</button>"
+ "</div>"
+ "</form>"
+ resultHtml
+ "<br/>"
+ renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers)
+ "</div></div></body></html>";
return body;
}
private static String renderCommonHeader(String username, String section, boolean authenticated, PrivateMessageStats pmStats) {
String welcomeName = escapeHtml(valueOrDefault(username, "Guest"));
String sectionHtml = escapeHtml(valueOrDefault(section, ""));
String sectionPart = sectionHtml.isBlank()
? ""
: ("New thread".equalsIgnoreCase(sectionHtml) && authenticated
? " - <a href='/newthread'>" + sectionHtml + "</a>"
: " - " + sectionHtml);
int unread = pmStats == null ? 0 : Math.max(0, pmStats.getUnread());
int total = pmStats == null ? 0 : Math.max(0, pmStats.getTotal());
boolean mysqlAdmin = authenticated && "kamma".equalsIgnoreCase(valueOrDefault(username, ""));
String newPmBanner = unread > 0
? "<span id='newPM'><strong><a href='/private'><img src='/images/pm.gif'/> You have unread Private Message</a></strong></span>"
: "<span id='newPM'>&nbsp;</span>";
String mysqlLink = mysqlAdmin ? " | <a href='/mysqlc'>MySQL Client</a>" : "";
String rightBlock = authenticated
? "<td class='alt2' nowrap='nowrap' style='vertical-align:top; padding:6px'>"
+ "<img src='/process/showimage?userIcon=yes' width='80' alt='User image'/>"
+ "</td>"
+ "<td class='alt2' nowrap='nowrap' style='vertical-align:top; padding:6px'>"
+ "<div style='color: green; font-weight: bold; text-align: left' id='currenttime'></div><br/>"
+ "<div class='smallfont'><strong>Welcome, <a href='/member' style='padding-left:0; padding-right:0'>" + welcomeName + "</a>.</strong><br/>"
+ "You last visited: <span class='time' id='lastvisited'>N/A</span></div>"
+ "<div class='smallfont'><a href='/private'>Private Messages</a>: Unread " + unread + ", Total " + total + ".</div>"
+ "<div class='smallfont'><strong><a href='/forum'>Forum</a> | <a href='/chat'>Chat</a> | "
+ "<a href='/process/logout' onclick=\"event.preventDefault(); document.getElementById('logout-form').submit();\">Logout</a>"
+ mysqlLink + "</strong></div>"
+ "<form id='logout-form' method='post' action='/process/logout' style='display:none'></form>"
+ "</td>"
: "<td class='alt2' nowrap='nowrap' style='vertical-align:top; padding:6px'>"
+ "<div style='color: green; font-weight: bold; text-align: left' id='currenttime'></div><br/>"
+ "<div class='smallfont'><strong>Welcome, " + welcomeName + ".</strong></div>"
+ "<div class='smallfont'><strong><a href='/login'>Login</a></strong></div>"
+ "</td>";
return "<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>"
+ "<tbody><tr><td class='alt1' width='100%'>"
+ "<table cellpadding='0' cellspacing='0' border='0' width='100%'><tbody><tr valign='bottom'>"
+ "<td width='100%'><span class='navbar'><strong><a href='/forum'>kAmMa's Forum</a>"
+ sectionPart
+ "</strong></span></td><td nowrap='nowrap'>" + newPmBanner + "</td>"
+ rightBlock
+ "</tr></tbody></table>"
+ "</td></tr></tbody></table>"
+ "<script type='text/javascript'>"
+ "function pad(v){return v<10?'0'+v:''+v;}"
+ "function clock(){var now=new Date();var text=pad(now.getDate())+'.'+pad(now.getMonth()+1)+'.'+now.getFullYear()+' '+pad(now.getHours())+':'+pad(now.getMinutes())+':'+pad(now.getSeconds());"
+ "var el=document.getElementById('currenttime');if(el){el.textContent=text;}var lv=document.getElementById('lastvisited');if(lv){lv.textContent=text.substring(0,16);}}"
+ "clock();setInterval(clock,1000);"
+ "</script>";
}
private static String renderCommonFooter(int loggedUsersCount, List<String> loggedUsers) {
String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty())
? "<span class='smallfont'>-</span>"
: loggedUsers.stream()
.map(user -> "<a href='#'>" + escapeHtml(user) + " (0)</a>")
.collect(Collectors.joining(", "));
return "<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>"
+ "<tbody><tr><td class='thead' colspan='2'>Currently Logged Users: <span id='userCount'>"
+ Math.max(0, loggedUsersCount)
+ "</span></td></tr></tbody>"
+ "<tbody id='collapseobj_forumhome_activeusers'><tr>"
+ "<td class='alt2'><img src='/images/whos_online.gif' alt='Who&#39;s Online' border='0'/></td>"
+ "<td class='alt1' width='100%'><div class='smallfont'><div id='userList'>"
+ loggedUsersHtml
+ "</div></div></td></tr></tbody></table><br/>"
+ "<div align='center'><div class='smallfont' align='center'>"
+ "kAmMa's Forum System Version 2.43<br/>Copyright &copy;2006-2021"
+ "</div></div>"
+ "<script type='text/javascript'>"
+ "(function(){"
+ "if(window.__fcAsyncUpdateStarted){return;}"
+ "window.__fcAsyncUpdateStarted=true;"
+ "function tick(){"
+ "if(typeof AX_asynchUpdate==='function'){AX_asynchUpdate();}"
+ "setTimeout(tick,5000);"
+ "}"
+ "tick();"
+ "})();"
+ "</script>";
}
private static String renderErrorBlock(String error) {
if (error == null || error.isBlank()) {
return "";
}
return "<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='70%' align='center'>"
+ "<tbody><tr><td class='panelsurround' align='center'>"
+ "<div style='margin:10px;color:red'>" + error + "</div>"
+ "</td></tr></tbody></table><br/>";
}
private static String renderLoginFooter() {
return "<br/><div align='center'><div class='smallfont' align='center'>"
+ "kAmMa's Forum System Version 2.43<br/>Copyright &copy;2006-2021"
+ "</div></div>";
}
private static String renderQuotedBlock(QuotedTextItem quotedItem) {
if (quotedItem == null) {
return "";
}
return "<div style='margin:20px; margin-top:5px;'>"
+ "<div class='smallfont' style='margin-bottom:2px'>Quote:</div>"
+ "<table cellpadding='6' cellspacing='0' border='0' width='100%'><tbody><tr>"
+ "<td class='alt3' style='border:1px inset'>"
+ "<div>Originally Posted by <strong>" + escapeHtml(quotedItem.getAuthor()) + "</strong></div>"
+ "<div style='font-style:italic'>"
+ LegacyMessageFormatter.convertMessageToHtml(quotedItem.getText(), null, null, null)
+ "</div></td></tr></tbody></table></div>";
}
private static String renderAttachments(List<ForumAttachment> attachments, String showImg) {
if (attachments == null || attachments.isEmpty()) {
return "";
}
String mode = showImg == null ? "true" : showImg.toLowerCase();
StringBuilder sb = new StringBuilder();
sb.append("<fieldset class='fieldset'><legend>Attached Files</legend><table cellpadding='0' cellspacing='3' border='0'>");
boolean first = true;
for (ForumAttachment a : attachments) {
if (!first) {
sb.append("<tr><td><hr/></td></tr>");
}
first = false;
sb.append("<tr><td>");
if (a.isPicture()) {
if ("true".equals(mode) || "yes".equals(mode) || "only".equals(mode)) {
sb.append("<a href='/process/download?attachmentid=").append(a.getId()).append("'>")
.append(escapeHtml(a.getName())).append("</a><br/>")
.append("<img src='/process/showimage?attachmentid=").append(a.getId()).append("' ")
.append(a.getWidth() > 800 ? "width='800'" : "")
.append("/>");
} else if ("thumbnail".equals(mode)) {
sb.append("<a href='/process/download?attachmentid=").append(a.getId()).append("'>")
.append(escapeHtml(a.getName())).append("</a><br/>")
.append("<a href='/process/download?attachmentid=").append(a.getId()).append("'>")
.append("<img src='/process/showimage?type=thumbnail&attachmentid=").append(a.getId()).append("' width='150'/></a>");
} else {
sb.append("<a href='/process/download?attachmentid=").append(a.getId()).append("'>")
.append(escapeHtml(a.getName())).append("</a>");
}
} else {
sb.append("<a href='/process/download?attachmentid=").append(a.getId()).append("'>")
.append(escapeHtml(a.getName())).append("</a>");
}
sb.append("</td></tr>");
}
sb.append("</table></fieldset>");
return sb.toString();
}
private static String options(String[] values, String[] labels, String selected) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.length; i++) {
sb.append("<option value='").append(escapeHtml(values[i])).append("'");
if (values[i].equalsIgnoreCase(selected)) {
sb.append(" selected");
}
sb.append(">").append(escapeHtml(labels[i])).append("</option>");
}
return sb.toString();
}
private static String buildPageLinks(String baseLink, int currentPage, int totalPages) {
if (totalPages <= 1) {
return "";
}
StringBuilder sb = new StringBuilder();
int pageWindow = 5;
int currentBlock = (int) Math.ceil(currentPage / (double) pageWindow);
int startPage = ((currentBlock - 1) * pageWindow) + 1;
int endPage = Math.min(startPage + pageWindow - 1, totalPages);
if (startPage > 1) {
int previousBlockPage = startPage - 1;
sb.append(" <a href='").append(baseLink).append("&iPageNo=").append(previousBlockPage)
.append("'><strong>&laquo;</strong> Previous</a>");
}
for (int i = startPage; i <= endPage; i++) {
if (i == currentPage) {
sb.append(" <strong>").append(i).append("</strong>");
} else {
sb.append(" <a href='").append(baseLink).append("&iPageNo=").append(i).append("'>").append(i).append("</a>");
}
}
if (endPage < totalPages) {
int nextBlockPage = endPage + 1;
sb.append(" <a href='").append(baseLink).append("&iPageNo=").append(nextBlockPage)
.append("'>Next <strong>&raquo;</strong></a>");
}
return sb.toString();
}
private static String buildPageTds(String baseLink, int currentPage, int totalPages) {
if (totalPages <= 1) {
return "<td class='alt2'><span class='smallfont'><strong>1</strong></span></td>";
}
StringBuilder sb = new StringBuilder();
int pageWindow = 5;
int currentBlock = (int) Math.ceil(currentPage / (double) pageWindow);
int startPage = ((currentBlock - 1) * pageWindow) + 1;
int endPage = Math.min(startPage + pageWindow - 1, totalPages);
if (startPage > 1) {
int previousBlockPage = startPage - 1;
sb.append("<td class='alt1' nowrap='nowrap'><a class='smallfont' href='")
.append(baseLink).append("&iPageNo=").append(previousBlockPage)
.append("'><strong>&laquo;</strong> Previous</a></td>");
}
for (int i = startPage; i <= endPage; i++) {
if (i == currentPage) {
sb.append("<td class='alt2'><span class='smallfont'><strong>").append(i).append("</strong></span></td>");
} else {
sb.append("<td class='alt1'><a class='smallfont' href='")
.append(baseLink).append("&iPageNo=").append(i).append("'>")
.append(i).append("</a></td>");
}
}
if (endPage < totalPages) {
int nextBlockPage = endPage + 1;
sb.append("<td class='alt1' nowrap='nowrap'><a class='smallfont' href='")
.append(baseLink).append("&iPageNo=").append(nextBlockPage)
.append("'>Next <strong>&raquo;</strong></a></td>");
}
return sb.toString();
}
private static String url(String value) {
return URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8);
}
private static String valueOrDefault(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value;
}
private static String escapeHtml(String input) {
if (input == null) {
return "";
}
return input.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
private static String readTemplate(String path) {
try (InputStream in = Pages.class.getClassLoader().getResourceAsStream(path)) {
if (in == null) {
throw new IllegalStateException("Template not found: " + path);
}
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new IllegalStateException("Failed to read template: " + path, ex);
}
}
}

View File

@ -0,0 +1,16 @@
# Database Configuration
app.db.url=jdbc:mariadb://server01:3306/fabkovachata?useUnicode=true&characterEncoding=UTF-8
app.db.user=fabkovachata
app.db.password=Fch621420+
app.db.mysql_admin_url=jdbc:mariadb://server01:3306/?useUnicode=true&characterEncoding=UTF-8
# Server Configuration
app.server.port=8080
app.server.threads=10
# Session Configuration
app.session.timeout.minutes=30
# Multipart Configuration
app.multipart.max_bytes=52428800
app.multipart.icon_max_bytes=1048576

View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px'>
<br/>
{{COMMON_HEADER}}
<br/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<thead>
<tr align='center'>
<td class='thead' width='100%' align='left'>Chat</td>
</tr>
</thead>
<tbody>
<tr align='center'>
<td class='alt1Active' align='left'>
<form action='' name='chatForm' onsubmit='return sendMessage();'>
<div class='smallfont'>Message:</div>
<div id='vB_Editor_QR' class='vBulletin_editor'>
<div class='controlbar' style='padding-right:8px'>
<fieldset style='border:0; padding:0; margin:0'>
<input type='text' onkeypress='return makeRecordsReaded(event)' name='message' style='width:100%'/>
<input type='submit' class='button' value='Send Message'/>
</fieldset>
</div>
</div>
<br/>
<hr/>
<br/>
<div id='main_chat'><center><h3>Loading messages...</h3></center></div>
</form>
</td>
</tr>
</tbody>
</table>
<br/>
<div class='smallfont'><label><input type='checkbox' id='soundon' checked='checked'/> Sound notification</label></div>
<br/>
{{COMMON_FOOTER}}
</div>
</div>
<script type='text/javascript'>
function sendMessage() {
var maxChatId = 0;
var divs = document.getElementsByTagName('div');
for (var i = 0; i <= (divs.length - 1); i++) {
var att = divs[i].attributes.getNamedItem('id');
if (att && att.value.indexOf('chatId_') > -1) {
var currChatId = parseInt(att.value.substring(7));
if (currChatId > maxChatId) {
maxChatId = currChatId;
}
}
}
AX_sendChatMessage(encodeURIComponent(document.chatForm.message.value), maxChatId);
document.chatForm.message.value = '';
document.chatForm.message.focus();
return false;
}
function makeRecordsReaded(e) {
var keynum;
if (window.event) {
keynum = e.keyCode;
} else if (e.which) {
keynum = e.which;
}
if (keynum === 13) {
var title = document.title;
title = title.replace('(CHAT+)', '');
document.title = title.trim();
}
return true;
}
function asynchUpdateChat() {
AX_asynchUpdateChat();
setTimeout('asynchUpdateChat();', 5000);
}
function firstUpdateChat() {
AX_firstUpdateChat();
setTimeout('asynchUpdateChat();', 5000);
}
firstUpdateChat();
</script>
</body>
</html>

View File

@ -0,0 +1,310 @@
var URLsuffix = '/process/ajaxreq.jsp';
//var URLsuffix = '/FabkovaChata/process/ajaxreq.jsp';
var ajaxHttp = getAjaxLibrary();
function getAjaxLibrary() {
var activexmodes = [ "Msxml2.XMLHTTP", "Microsoft.XMLHTTP" ] // activeX
// versions
// to check
// for in IE
if (window.ActiveXObject) { // Test for support for ActiveXObject in IE
// first (as XMLHttpRequest in IE7 is broken)
for ( var i = 0; i < activexmodes.length; i++) {
try {
return new ActiveXObject(activexmodes[i])
} catch (e) {
// suppress error
}
}
} else if (window.XMLHttpRequest) // if Mozilla, Safari etc
return new XMLHttpRequest()
else
return false
}
/*******************************************************************************
* F U N C T I O N S
******************************************************************************/
function getHost() {
var url = ""+window.location;
var prot = url.split('//')[0];
var urlparts = url.split('//')[1].split('/');
return urlparts[0];
}
function getProt() {
var url = ""+window.location;
return url.split('//')[0];
}
function ajaxSendRequest(data, onReadyStateChange)
{
var URL = getProt()+'//'+getHost()+URLsuffix;
//if (ajaxHttp.overrideMimeType)
//ajaxHttp.overrideMimeType('text/xml')
ajaxHttp.open("POST", URL , true);
ajaxHttp.onreadystatechange = onReadyStateChange;
ajaxHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
ajaxHttp.send(data);
}
function ajaxResponseReady()
{
if (ajaxHttp.readyState == 4) {
/* received */
if (ajaxHttp.status == 200 || window.location.href.indexOf("http")==-1) {
/* response is ok */
return true;
} else {
return false;
}
}
return false;
}
function ajaxGetXmlResponse() {
if (ajaxHttp.responseXML && ajaxHttp.responseXML.parseError && (ajaxHttp.responseXML.parseError.errorCode !=0)) {
ajaxGetXmlError(true);
return null;
} else {
return ajaxHttp.responseXML;
}
}
function ajaxGetXmlError(withalert) {
if (ajaxHttp.responseXML.parseError.errorCode !=0 ) {
line = ajaxHttp.responseXML.parseError.line;
pos = ajaxHttp.responseXML.parseError.linepos;
error = ajaxHttp.responseXML.parseError.reason;
error = error + "Contact the support ! and send the following informations: error is line " + line + " position " + pos;
error = error + " >>" + ajaxHttp.responseXML.parseError.srcText.substring(pos);
error = error + "GLOBAL:" + ajaxHttp.responseText;
if (withalert)
alert(error);
return error;
} else {
return "";
}
}
function convertLinks(text)
{
var myArray = text.split(" ");
for (var i = 0 ; i < myArray.length ; i++) {
var part = myArray[i];
if (part.indexOf("http://")==0) {
text = text.replace(part, '<a href="' + part + '">' + part + '</a>');
}
}
return text;
}
/*******************************************************************************
* ajax call functions
*/
function AX_voting(id, yes_no)
{
var data = 'ajaxMethod=ajaxVoting_'+yes_no +'&universalId='+id;
ajaxSendRequest(data, AX_votingResponse);
}
function AX_chat_voting(id, yes_no)
{
var data = 'ajaxMethod=ajaxChatVoting_'+yes_no +'&universalId='+id;
ajaxSendRequest(data, AX_chatVotingResponse);
}
function AX_sendChatMessage(text, chatid)
{
var data = 'ajaxMethod=chat_text_' + text +'&universalId='+chatid;
ajaxSendRequest(data, AX_asynchUpdateResponse);
}
function AX_asynchUpdate()
{
var data = 'ajaxMethod=asynchUpdate';
ajaxSendRequest(data, AX_asynchUpdateResponse);
}
function AX_asynchUpdateChat()
{
var data = 'ajaxMethod=asynchUpdateChat';
ajaxSendRequest(data, AX_asynchUpdateResponse);
}
function AX_firstUpdateChat()
{
var data = 'ajaxMethod=firstUpdateChat';
ajaxSendRequest(data, AX_asynchUpdateResponse);
}
/*******************************************************************************
* response functions
*/
function AX_votingResponse()
{
if (ajaxResponseReady()) {
if (ajaxHttp.responseText.indexOf('invalid') == -1) {
var xmlDocument = ajaxGetXmlResponse();
var id = xmlDocument.getElementsByTagName("id")[0].firstChild.data;
var yes = xmlDocument.getElementsByTagName("yes")[0].firstChild.data;
var no = xmlDocument.getElementsByTagName("no")[0].firstChild.data;
document.getElementById('yes' + id).innerHTML = yes;
document.getElementById('no' + id).innerHTML = no;
}
}
}
function AX_chatVotingResponse()
{
if (ajaxResponseReady()) {
if (ajaxHttp.responseText.indexOf('invalid') == -1) {
var xmlDocument = ajaxGetXmlResponse();
var id = xmlDocument.getElementsByTagName("id")[0].firstChild.data;
var thumbup = xmlDocument.getElementsByTagName("thumbup")[0].firstChild;
var thumbdown = xmlDocument.getElementsByTagName("thumbdown")[0].firstChild;
var yesVotes = 0;
var noVotes = 0;
var yesNames = '';
var noNames = '';
if (thumbup!==null && thumbup.data!==null) {
yesNames = thumbup.data;
yesVotes = yesNames.split(',').length;
}
if (thumbdown!==null && thumbdown.data!==null) {
noNames = thumbdown.data;
noVotes = noNames.split(',').length;
}
document.getElementById('yes' + id).innerHTML = yesVotes;
document.getElementById('no' + id).innerHTML = noVotes;
document.getElementById('yes' + id).title = yesNames;
document.getElementById('no' + id).title = noNames;
}
}
}
function AX_asynchUpdateResponse()
{
if (ajaxResponseReady()) {
if (ajaxHttp.responseText.indexOf('invalid') == -1) {
var xmlDocument = ajaxGetXmlResponse();
var userNames = xmlDocument.getElementsByTagName("userName");
var userIds = xmlDocument.getElementsByTagName("userId");
var inactives = xmlDocument.getElementsByTagName("inactive");
var inChat = xmlDocument.getElementsByTagName("inChat");
var resStr = "";
for (var i=0; i <= (userNames.length-1); i++)
{
if (inChat[i].firstChild.data=='true')
resStr = resStr + "<a href='newpm.jsp?uid=" + userIds[i].firstChild.data + "'>" + userNames[i].firstChild.data + " [Chat] (" + inactives[i].firstChild.data + ")</a>, ";
else
resStr = resStr + "<a href='newpm.jsp?uid=" + userIds[i].firstChild.data + "'>" + userNames[i].firstChild.data + " (" + inactives[i].firstChild.data + ")</a>, ";
}
document.getElementById('userCount').innerHTML = ""+userNames.length;
document.getElementById('userList').innerHTML = resStr.substring(0, resStr.length-2);
var fromNames = xmlDocument.getElementsByTagName("fromName");
var chatIds = xmlDocument.getElementsByTagName("chatId");
var newMess = xmlDocument.getElementsByTagName("newMess");
var times = xmlDocument.getElementsByTagName("time");
var texts = xmlDocument.getElementsByTagName("text");
var thumbup = xmlDocument.getElementsByTagName("thumbup");
var thumbdown = xmlDocument.getElementsByTagName("thumbdown");
var resStr = "";
var chatNode = document.getElementById('main_chat');
var newdiv = '';
for (var i=0; i <= (chatIds.length-1); i++)
{
var divIdName = 'chatId_'+chatIds[i].firstChild.data;
var color = 'color:#FF0000';
if (newMess[i].firstChild.data=='1')
color = 'color:#FF0000';
else if (newMess[i].firstChild.data=='2')
color = 'color:#0000FF';
else
color = 'color:#000000';
/*
var yesVotes = 0;
var noVotes = 0;
var yesNames = ' ';
var noNames = ' ';
if (thumbup[i].firstChild!==null) {
yesNames = thumbup[i].firstChild.data;
yesVotes = yesNames.split(',').length;
}
if (thumbdown[i].firstChild!==null) {
noNames = thumbdown[i].firstChild.data;
noVotes = noNames.split(',').length;
}
var chatVotingLine = ' <span class=\"voting-buttons\"><a title=\"'+yesNames+'\" id=\"yes'+chatIds[i].firstChild.data+'\" class=\"voting_yes\" href="javascript:AX_chat_voting('+chatIds[i].firstChild.data+',\'yes\')">'+yesVotes+'</a><a title=\"'+noNames+'\" id=\"no'+chatIds[i].firstChild.data+'\" class=\"voting_no\" href=\"javascript:AX_chat_voting('+chatIds[i].firstChild.data+',\'no\')">'+noVotes+'</a></span>';
*/
var chatVotingLine = '';
newdiv += '<div id=\"'+divIdName+'\" style=\"'+color+'\">['+times[i].firstChild.data+chatVotingLine+'] - '+fromNames[i].firstChild.data+': '+convertLinks(texts[i].firstChild.data)+'</div>';
}
if (chatIds.length>0)
chatNode.innerHTML = newdiv;
var newTitle = '';
var fids = xmlDocument.getElementsByTagName("forumId");
var messCounts = xmlDocument.getElementsByTagName("messageCount");
if (messCounts.length>0 || document.title.indexOf('(1+)')>-1) {
newTitle += '(1+)';
}
var newChatMessages = xmlDocument.getElementsByTagName("newChatMessages");
if (newChatMessages.length>0 || document.title.indexOf('(CHAT+)')>-1) {
newTitle += '(CHAT+)';
}
var newPMs = xmlDocument.getElementsByTagName("newPMCount");
if (newPMs.length>0) {
document.getElementById('newPM').innerHTML = "<STRONG><a href='private.jsp'><img src='images/pm.gif'/> You have unread Private Message</STRONG>";
if (document.title.indexOf('(PM+)')<0) {
newTitle += '(PM+)';
}
}
if (newTitle.length!='') {
document.title = newTitle + " kAmMa's Forum";
}
var resStr = '';
for (var i=0; i <= (fids.length-1); i++)
{
resStr = " "+messCounts[i].firstChild.data+" new message(s)";
if (document.getElementById('newCount'+fids[i].firstChild.data)!=null) {
document.getElementById('newCount'+fids[i].firstChild.data).innerHTML = resStr;
}
}
var playNotification = xmlDocument.getElementsByTagName("playNotification");
if (playNotification.length>0 && playNotification[0].firstChild.data=='1' && document.getElementById('soundon')!=null) {
var embed=document.createElement('object');
embed.setAttribute('type','audio/wav');
embed.setAttribute('data', 'notif.wav');
embed.setAttribute('autostart', true);
document.getElementsByTagName('body')[0].appendChild(embed);
}
}
}
}
/*
* END of response functions
*/

View File

@ -0,0 +1,453 @@
body
{
background: #FFFFCC;
color: #000000;
font: 10pt verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
margin: 5px 10px 10px 10px;
padding: 0px;
}
.voting-buttons
{
position: relative;
bottom: 1px;
}
.voting-buttons a
{
float: none !important;
padding-bottom: 0px !important;
padding-top: 0px !important;
}
a.voting_yes:link, a.voting_yes:visited {
color: #3C922F;
font-weight: bold;
background: url(../images/voting_yes.png) green no-repeat;
border: 1px outset #3C922F;
padding: 2px 4px 2px 20px;
white-space: nowrap;
float: left;
line-height: 10px;
text-decoration: none;
}
a.voting_no:link, a.voting_no:visited {
color: #AE3738;
font-weight: bold;
background: url(../images/voting_no.png) red no-repeat;
border: 1px outset #AE3738;
padding: 2px 4px 2px 20px;
white-space: nowrap;
float: left;
line-height: 10px;
text-decoration: none;
}
a:link, body_alink
{
color: #0000C0;
text-decoration: none;
}
a:visited, body_avisited
{
color: #0000C0;
text-decoration: none;
}
a:hover, a:active, body_ahover
{
color: #663333;
text-decoration: none;
}
.page
{
background: #FFFFF1;
color: #000000;
}
.page a:link, .page_alink
{
color: #0000C0;
}
.page a:visited, .page_avisited
{
color: #0000C0;
}
.page a:hover, .page a:active, .page_ahover
{
color: #663333;
}
td, th, p, li
{
font: 10pt verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.tborder
{
background: #FFFFCC;
color: #000000;
border: 1px solid #0B198C;
}
.tcat
{
background: #FFFFCC url(../images/cat-line.gif) repeat-x top left;
color: #000000;
font: bold 9pt verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.tcat a:link, .tcat_alink
{
color: #0000C0;
text-decoration: none;
}
.tcat a:visited, .tcat_avisited
{
color: #0000C0;
text-decoration: none;
}
.tcat a:hover, .tcat a:active, .tcat_ahover
{
color: #663333;
text-decoration: none;
}
.thead
{
background: #663333 url(../images/cellpic-fp-big.gif) repeat-x top left;
color: #FFFFF1;
font: bold 11px tahoma, verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.thead a:link, .thead_alink
{
color: #FFFFF1;
}
.thead a:visited, .thead_avisited
{
color: #FFFFF1;
}
.thead
{
background: #663333 url(../images/cellpic-fp-big.gif) repeat-x top left;
color: #FFFFF1;
font: bold 11px tahoma, verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.thead a:hover, .thead a:active, .thead_ahover
{
color: #FFFFCC;
}
.tfoot
{
background: #663333 url(../images/cellpic-fp.gif) repeat-x top left;
color: #000000;
}
.tfoot a:link, .tfoot_alink
{
color: #FFFFF1;
}
.tfoot a:visited, .tfoot_avisited
{
color: #FFFFF1;
}
.tfoot a:hover, .tfoot a:active, .tfoot_ahover
{
color: #FFFFCC;
text-decoration: underline;
}
.alt1, .alt1Active
{
background: #FFFFCC;
color: #000000;
font-size: 9pt;
}
.alt2, .alt2Active
{
background: #FFFF99;
color: #000000;
}
.inlinemod
{
background: #FFFFCC;
color: #000000;
}
.wysiwyg
{
background: #FFFFCC;
color: #000000;
font: 10pt verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
margin: 5px 10px 10px 10px;
padding: 0px;
}
textarea, .bginput
{
background: #FFFFCC;
font: 10pt verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.bginput option, .bginput optgroup
{
font-size: 10pt;
font-family: verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.button
{
background: #FFFFF1;
color: #000000;
font: 11px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
select
{
background: #FFFFCC;
font: 11px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
option, optgroup
{
font-size: 11px;
font-family: verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.smallfont
{
font: 11px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.time
{
color: #000000;
}
.navbar
{
color: #0000C0;
font: 11px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.highlight
{
font-weight: bold;
}
.fjsel
{
background: #FFFFCC;
color: #000000;
}
.fjdpth0
{
background: #FFFFF1;
color: #000000;
}
.panel
{
background: #FFFFF1;
color: #000000;
padding: 10px;
border: 2px outset;
}
.panelsurround
{
background: #FFFFCC;
color: #000000;
}
legend
{
background: transparent;
color: #0000C0;
font: 11px tahoma, verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.vbmenu_control
{
background: #663333 url(../images/cellpic-fp.gif) repeat-x top left;
color: #FFFFF1;
font: bold 11px tahoma, verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
padding: 3px 6px 3px 6px;
white-space: nowrap;
}
.vbmenu_control a:link, .vbmenu_control_alink
{
color: #FFFFFF;
text-decoration: none;
}
.vbmenu_control a:visited, .vbmenu_control_avisited
{
color: #FFFFFF;
text-decoration: none;
}
.vbmenu_control a:hover, .vbmenu_control a:active, .vbmenu_control_ahover
{
color: #FFFFFF;
text-decoration: underline;
}
.vbmenu_popup
{
background: #FFFFFF;
color: #000000;
border: 1px solid #0B198C;
}
.vbmenu_option
{
background: #FFFFF1;
color: #000000;
font: 11px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
white-space: nowrap;
cursor: pointer;
}
.vbmenu_option a:link, .vbmenu_option_alink
{
color: #000000;
text-decoration: none;
}
.vbmenu_option a:visited, .vbmenu_option_avisited
{
color: #330000;
text-decoration: none;
}
.vbmenu_option a:hover, .vbmenu_option a:active, .vbmenu_option_ahover
{
color: #330000;
text-decoration: none;
}
.vbmenu_hilite
{
background: #FFFFCC;
color: #330000;
font: 11px verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
white-space: nowrap;
cursor: pointer;
}
.vbmenu_hilite a:link, .vbmenu_hilite_alink
{
color: #330000;
text-decoration: none;
}
.vbmenu_hilite a:visited, .vbmenu_hilite_avisited
{
color: #330000;
text-decoration: none;
}
.vbmenu_hilite a:hover, .vbmenu_hilite a:active, .vbmenu_hilite_ahover
{
color: #330000;
text-decoration: none;
}
/* ***** styling for 'big' usernames on postbit etc. ***** */
.bigusername { font-size: 10pt; text-decoration: none;}
/* ***** small padding on 'thead' elements ***** */
td.thead, th.thead, div.thead { padding: 4px; }
/* ***** basic styles for multi-page nav elements */
.pagenav a { text-decoration: none; }
.pagenav td { padding: 2px 4px 2px 4px; }
/* ***** de-emphasized text */
.shade, a.shade:link, a.shade:visited { color: #777777; text-decoration: none; }
a.shade:active, a.shade:hover { color: #FF4400; text-decoration: underline; }
.tcat .shade, .thead .shade, .tfoot .shade { color: #DDDDDD; }
/* ***** define margin and font-size for elements inside panels ***** */
.fieldset { margin-bottom: 6px; }
.fieldset, .fieldset td, .fieldset p, .fieldset li { font-size: 11px; }
/* vbPortal Extras */
.urlrow, .textrow, .blockform, .boxform, .loginform {
margin: 0px;
font-family: verdana, geneva, lucida, 'lucida grande', arial, helvetica, sans-serif;
}
.textrow, .blockform, .boxform, .loginform {
font-size: 10px;
}
.urlrow {
font-size: 11px;
}
.textrow, .urlrow {
padding: 2px 2px;
}
.blockform, .loginform {
padding: 0px;
}
.boxform {
padding: 2px;
}
.gogif {
padding: 0px 3px 0px 3px;
margin: 0px;
}
/* *****thead2 for calendar made by flar ***** */
.thead2 {url: images/gradients/cellpic-fp-big.gif repeat-x top left; }
/* *****alt3 alternating color 3 made by flar ***** */
.alt3 {background-color:#FFFFF1;}
.nodisplay {
display: none;
}
#autosearch{
float:left;
width:205px;
margin:5px 0 0 0;
}
#menucontainer{
float:left;
position:relative;
width:250px;
height:104px;
}
#results {
}
#results ul {
z-index:10;
position: absolute;
top: 94px;
left: 0px;
border: 1px solid #bfbfbf;
list-style: none;
width: 208px;
display:block;
margin:0;
padding:0;
}
#results ul li {
position:relative;
margin:0;
padding:0;
width:198px;
}
#results ul li a{
display: block;
color: #444;
background: #fff;
text-decoration: none;
padding: 1px 4px 2px 6px;
width:198px;
}
* html #results ul li a {
margin:0;
padding:0;
display:block;
}
#results ul li a strong {
color: #000;
}
#results ul li a:hover, #results ul li a.hover {
background: #0056f4;
color: #fff;
}
#results ul li a:hover strong, #results ul li a.hover strong {
color: #fff;
}
input#s{
margin-top:4px;
width:205px;
font: 12px/12px Verdana, sans-serif;
color:#666666;
padding:3px 5px;
}
.xdaclear{
clear:both;
overflow:hidden;
}
ul.menu {list-style:none; margin:0; padding:0;}
ul.menu * {margin:0; padding:0}
ul.menu a {display:block; color:#000; text-decoration:none}
ul.menu li {position:relative; float:left; margin-right:2px;font-size:11px;}
ul.menu ul {position:absolute; top:26px; left:0; display:none; opacity:0; list-style:none;width:230px;}
ul.menu ul li {position:relative; border:1px solid #aaa; border-top:none; width:230px; margin:0}
ul.menu ul li a {display:block; padding:3px 5px 3px 12px; background-color:#ffffff}
ul.menu ul li a:hover {background-color:#c5c5c5}
ul.menu ul ul {left:-230px; top:-1px}
ul.menu .menulink {border:1px solid #aaa; padding:5px 5px 5px 5px; font-weight:bold; background:url(/images/header.gif); width:230px}
ul.menu .menulink:hover, ul.menu .menuhover {background:url(/images/header_over.gif)}
ul.menu .sub {background:#fff url(/images/arrow.gif) 4px no-repeat}
ul.menu .topline {border-top:1px solid #aaa}

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px'>
<br/>
{{COMMON_HEADER}}
<br/>
<div align='right'>
<table class='tborder' cellpadding='3' cellspacing='1' border='0'>
<tbody><tr>
<td class='vbmenu_control' style='font-weight:normal'>
<form action='#' method='post'>
Search in threads:
<input type='text' class='bginput' style='font-size:11px' name='stext' tabindex='1'/>
</form>
</td>
</tr></tbody>
</table>
</div>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr align='center'>
<td class='thead' width='100%' align='left'>Thread</td>
<td class='thead' width='175'>Last Post</td>
<td class='thead'>Posts</td>
</tr>
{{FORUM_ROWS}}
</tbody>
</table>
<br/>
{{COMMON_FOOTER}}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum - {{FORUM_NAME}}</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px'>
<br/>
{{COMMON_HEADER}}
<br/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr><td class='thead'>{{FORUM_NAME}}</td></tr>
<tr><td class='alt2'>{{FORUM_DESCRIPTION}}</td></tr>
</tbody>
</table>
<br/>
<script type='text/javascript'>
function toggleAttachmentForm() {
var el = document.getElementById('hideshow');
if (!el) {
return;
}
if (el.style.display === 'none' || el.style.display === '') {
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
</script>
<form action='/process/replythread' method='post' enctype='multipart/form-data' name='newpost'>
<input type='hidden' name='forumid' value='{{FORUM_ID}}'/>
<input type='hidden' name='quoteItem' value='{{QUOTE_ITEM}}'/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<thead>
<tr><td class='tcat' colspan='2'>Reply</td></tr>
</thead>
<tbody>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
{{QUOTE_BLOCK}}
<div align='left' style='max-width:640px; width:640px'>
<div class='smallfont'>Message:</div>
<textarea name='message' rows='10' cols='60' style='height:100px; width:100%' dir='ltr'></textarea>
</div>
<div id='hideshow' style='display:none'>
<fieldset class='fieldset'>
<legend>Upload File from your Computer</legend>
<table cellpadding='0' cellspacing='3' width='100%' border='0'>
<tr valign='bottom'>
<td><input type='file' class='bginput' name='attachment[]' size='30' multiple/></td>
</tr>
</table>
</fieldset>
</div>
</div>
<div style='margin-top:6px'>
<input type='submit' class='button' value='Post Reply / Refresh'/>
<input type='button' class='button' value='Add attachment' onclick='toggleAttachmentForm();'/>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
{{COUNTDOWN_BLOCK}}
<div align='right'>
<table class='tborder' cellpadding='3' cellspacing='1' border='0'>
<tbody><tr>
<td class='vbmenu_control' style='font-weight:normal'>
<form method='get' action='/forumdisplay' style='display:inline'>
<input type='hidden' name='f' value='{{FORUM_ID}}'/>
<input type='hidden' name='showType' value='{{SHOW_TYPE}}'/>
<input type='hidden' name='showImg' value='{{SHOW_IMG}}'/>
<input type='hidden' name='sortBy' value='{{SORT_BY}}'/>
<input type='hidden' name='sortType' value='{{SORT_TYPE}}'/>
<input type='hidden' name='perpage' value='{{PER_PAGE}}'/>
<input type='hidden' name='iPageNo' value='1'/>
Search in thread:
<input type='text' class='bginput' style='font-size:11px' name='stext' value='{{SEARCH_TEXT}}'/>
</form>
</td>
<td class='vbmenu_control' style='font-weight:normal'>
Show type:
<select class='bginput' onchange="location.href='{{BASE_LINK}}&showType='+this.value">{{SHOW_TYPE_OPTIONS}}</select>
</td>
<td class='vbmenu_control' style='font-weight:normal'>
Show images:
<select class='bginput' onchange="location.href='{{BASE_LINK}}&showImg='+this.value">{{SHOW_IMG_OPTIONS}}</select>
</td>
<td class='vbmenu_control' style='font-weight:normal'>
Sort messages by:
<select class='bginput' onchange="location.href='{{BASE_LINK}}&sortBy='+this.value">{{SORT_BY_OPTIONS}}</select>
<a href='{{BASE_LINK}}&sortType={{NEXT_SORT_TYPE}}'><img title='{{SORT_TYPE}}' src='/images/sort{{SORT_TYPE}}.gif'/></a>
</td>
<td class='vbmenu_control' style='font-weight:normal'>
Records per page:
<select class='bginput' onchange="location.href='{{BASE_LINK}}&perpage='+this.value">{{PER_PAGE_OPTIONS}}</select>
</td>
<td class='vbmenu_control' style='font-weight:normal'>Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}}</td>
{{PAGE_TDS}}
</tr></tbody>
</table>
</div>
<br/>
{{MESSAGE_ROWS}}
<br/>
{{COMMON_FOOTER}}
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<title>kAmMa's Forum</title>
</head>
<body>
<div align='center'>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px' align='left'>
<br/>
{{COMMON_HEADER}}
<br/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody><tr align='center'><td class='alt2' nowrap='nowrap' style='padding:0'>
<form action='/process/login' method='post'>
<table cellpadding='0' cellspacing='3' border='0'>
<tbody>
<tr>
<td class='smallfont' style='white-space:nowrap'><label for='navbar_username'>User Name</label></td>
<td><input type='text' class='bginput' style='font-size:11px' name='uname' id='navbar_username' size='10' required/></td>
<td>&nbsp;</td>
</tr>
<tr>
<td class='smallfont'><label for='navbar_password'>Password</label></td>
<td><input type='password' class='bginput' style='font-size:11px' name='passwd' id='navbar_password' size='10' required/></td>
<td align='left'><input type='submit' class='button' value='Log in'/></td>
</tr>
</tbody>
</table>
</form>
</td></tr></tbody>
</table>
<br/>
{{ERROR_BLOCK}}
{{COMMON_FOOTER}}
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum - Member</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px' align='left'>
<br/>
{{COMMON_HEADER}}
<br/>
{{ERROR_BLOCK}}
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr><td class='tcat'>Member details</td></tr>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
<div style='width:640px' align='left'>
<fieldset class='fieldset'>
<legend>Account</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr><td>Username: <strong>{{USERNAME}}</strong></td></tr>
<tr><td>Join Date: <strong>{{JOIN_DATE}}</strong></td></tr>
</tbody>
</table>
</fieldset>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<br/>
<form action='/process/saveicon' method='post' enctype='multipart/form-data'>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr><td class='tcat'>Change user icon</td></tr>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
<div style='width:640px' align='left'>
<fieldset class='fieldset'>
<legend>User Icon</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr>
<td>
Current user icon<br/>
<img src='/process/showimage?userIcon=yes' width='80' onerror="this.style.display='none'; document.getElementById('noicon').style.display='inline';"/>
<span id='noicon' style='display:none'>Not defined</span>
</td>
</tr>
<tr>
<td>
Browse your local disk for user icon<br/>
<input type='file' class='bginput' name='iconfile' size='30'/>
</td>
</tr>
</tbody>
</table>
</fieldset>
<div style='margin-top:6px' align='center'>
<input type='submit' class='button' value='Save icon'/>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
<form action='/process/savemember' method='post' onsubmit='return verifyPasswords();'>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr><td class='tcat'>Change password</td></tr>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
<div style='width:640px' align='left'>
<fieldset class='fieldset'>
<legend>Old Password</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr><td>Please enter your current password.</td></tr>
<tr><td><input type='password' class='bginput' id='oldpassword' name='oldpassword' size='25' maxlength='50' value=''/></td></tr>
</tbody>
</table>
</fieldset>
<fieldset class='fieldset'>
<legend>New Password</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr><td colspan='2'>Please enter a new password for your account.</td></tr>
<tr>
<td>New Password:<br/><input type='password' class='bginput' id='password' name='password' size='25' maxlength='50' value=''/></td>
<td>Confirm New Password:<br/><input type='password' class='bginput' id='passwordconfirm' name='passwordconfirm' size='25' maxlength='50' value=''/></td>
</tr>
</tbody>
</table>
</fieldset>
<div style='margin-top:6px' align='center'>
<input type='submit' class='button' name='changePwd' value='Save password'/>
<input type='reset' class='button' value='Reset Fields'/>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
<form action='/process/savemember' method='post' onsubmit='return verifyEmails();'>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr><td class='tcat'>Change personal info</td></tr>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
<div style='width:640px' align='left'>
<fieldset class='fieldset'>
<legend>Email Address</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr><td colspan='2'>Please enter a valid email address.</td></tr>
<tr>
<td>Email Address:<br/><input type='text' class='bginput' id='email' name='email' size='25' maxlength='50' value='{{EMAIL}}'/></td>
<td>Confirm Email Address:<br/><input type='text' class='bginput' id='emailconfirm' name='emailconfirm' size='25' maxlength='50' value='{{EMAIL}}'/></td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset class='fieldset'>
<legend>Other informations</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr><td>First Name:<br/><input type='text' class='bginput' name='firstname' size='25' maxlength='50' value='{{FIRST_NAME}}'/></td><td>&nbsp;</td></tr>
<tr><td>Last Name:<br/><input type='text' class='bginput' name='lastname' size='25' maxlength='50' value='{{LAST_NAME}}'/></td><td>&nbsp;</td></tr>
<tr><td>City:<br/><input type='text' class='bginput' name='city' size='25' maxlength='50' value='{{CITY}}'/></td><td>&nbsp;</td></tr>
</tbody>
</table>
</fieldset>
<fieldset class='fieldset'>
<legend>Sounds</legend>
<table cellpadding='0' cellspacing='3' border='0' width='400'>
<tbody>
<tr><td>Sound notifications: <input type='checkbox' class='bginput' name='soundonoff' value='soundon' {{SOUND_CHECKED}}/></td><td>&nbsp;</td></tr>
</tbody>
</table>
</fieldset>
<div style='margin-top:6px' align='center'>
<input type='submit' class='button' name='changeInfo' value='Save personal info'/>
<input type='reset' class='button' value='Reset Fields'/>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
{{COMMON_FOOTER}}
</div>
</div>
<script type='text/javascript'>
function verifyPasswords() {
var oldPass = document.getElementById('oldpassword').value;
var pass1 = document.getElementById('password').value;
var pass2 = document.getElementById('passwordconfirm').value;
if (!oldPass || !pass1 || !pass2) {
alert('If you want to change password, please fill out all password fields.');
return false;
}
if (pass1 !== pass2) {
alert('The entered passwords do not match.');
return false;
}
return true;
}
function verifyEmails() {
var e1 = document.getElementById('email').value;
var e2 = document.getElementById('emailconfirm').value;
if (!e1 || !e2) {
alert('Please fill out both email fields.');
return false;
}
if (e1 !== e2) {
alert('The entered emails do not match.');
return false;
}
return true;
}
</script>
</body>
</html>

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px' align='left'>
<br/>
{{COMMON_HEADER}}
<br/>
{{ERROR_BLOCK}}
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<thead>
<tr><td class='tcat'>Reply to <span style='color:blue;font-weight:bold'>{{TO_USERNAME}}</span></td></tr>
</thead>
<tbody>
<tr>
<td class='panelsurround' align='center'>
<form action='/process/replymessage' method='post'>
<div class='panel'>
<div align='center'>
<div style='max-width:640px; width:auto'>
<div class='smallfont messagetext' style='text-align:left'>Message:</div>
<div id='vB_Editor_QR' class='vBulletin_editor'>
<div class='controlbar' style='padding-right:8px'>
<fieldset style='border:0; padding:0; margin:0'>
<textarea name='message' rows='10' cols='60' style='height:120px; width:100%'></textarea>
</fieldset>
</div>
</div>
<div class='submit_button'>
<input type='hidden' name='pmid' value='{{PM_ID}}'/>
<input type='hidden' name='title' value='{{RE_TITLE}}'/>
<input type='hidden' name='to_user' value='{{TO_USER_ID}}'/>
<input type='hidden' name='perpage' value='{{PER_PAGE}}'/>
<input type='hidden' name='iPageNo' value='{{I_PAGE_NO}}'/>
</div>
</div>
</div>
</div>
<div style='margin-top:6px'><input type='submit' value='Submit Message' class='button'/></div>
</form>
</td>
</tr>
</tbody>
</table>
<br/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center' style='border-bottom-width:0'>
<tbody><tr><td class='tcat'><div class='normal' style='float:right'>&nbsp;</div>Private Messages<span class='normal'>: {{THREAD_TITLE}}</span></td></tr></tbody>
</table>
<div align='right'>
<table class='tborder' cellpadding='3' cellspacing='1' border='0'>
<tbody><tr>
<td class='vbmenu_control' style='font-weight:normal'>
<form method='get' action='/message' style='display:inline'>
<input type='hidden' name='pmid' value='{{PM_ID}}'/>
<input type='hidden' name='iPageNo' value='1'/>
Records per page:
<select class='bginput' name='perpage' onchange='this.form.submit()'>{{PER_PAGE_OPTIONS}}</select>
</form>
<span class='smallfont'> Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}} {{PAGE_LINKS}}</span>
</td>
</tr></tbody>
</table>
</div>
<br/>
{{MESSAGE_ROWS}}
<br/>
{{COMMON_FOOTER}}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px' align='left'>
<br/>
{{COMMON_HEADER}}
<br/>
{{ERROR_BLOCK}}
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center' style='border-bottom-width:0'>
<tbody>
<tr>
<td class='tcat'>
<div class='normal' style='float:right'>&nbsp;</div>
Private Message to <span class='normal'>: {{TO_USERNAME}}</span>
</td>
</tr>
</tbody>
</table>
<form action='/process/replymessage' method='post'>
<input type='hidden' name='to_user' value='{{TO_USER_ID}}'/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
<div style='width:640px' align='left'>
<table cellpadding='0' cellspacing='0' border='0' class='fieldset'>
<tbody>
<tr><td class='smallfont' colspan='3'>Title:</td></tr>
<tr>
<td><input type='text' class='bginput' name='title' value='' size='40' maxlength='85'/></td>
<td>&nbsp;&nbsp;</td>
</tr>
</tbody>
</table>
<div class='smallfont'>Message text:</div>
<div style='text-align:left; padding-right:8px'>
<textarea name='message' rows='10' cols='60' style='height:250px; width:100%'></textarea>
</div>
</div>
</div>
<div style='margin-top:6px'><input type='submit' class='button' value='Submit Message'/></div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
{{COMMON_FOOTER}}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum - New Thread</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px' align='left'>
<br/>
{{COMMON_HEADER}}
<br/>
{{ERROR_BLOCK}}
<form action='/process/newthread' method='post'>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<tbody>
<tr>
<td class='tcat'>Post New Thread</td>
</tr>
<tr>
<td class='panelsurround' align='center'>
<div class='panel'>
<div style='width:640px' align='left'>
<div class='smallfont' style='float:right'>Logged in as <a href='/member'>{{USERNAME}}</a></div>
<table cellpadding='0' cellspacing='0' border='0' class='fieldset'>
<tbody>
<tr><td class='smallfont' colspan='3'>Title:</td></tr>
<tr>
<td><input type='text' class='bginput' name='forumname' value='' size='40' maxlength='85'/></td>
<td>&nbsp;&nbsp;</td>
</tr>
</tbody>
</table>
<div class='smallfont'>Description:</div>
<div style='text-align:left; padding-right:8px'>
<textarea name='description' rows='10' cols='60' style='height:250px; width:100%'></textarea>
</div>
<fieldset class='fieldset' style='margin-top:6px'>
<legend>Thread password</legend>
<div style='padding:3px'><span><input type='text' class='bginput' name='password' value='' size='40'/></span></div>
</fieldset>
</div>
</div>
<div style='margin-top:6px'>
<input type='submit' class='button' name='sbutton' value='Submit New Thread'/>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
{{COMMON_FOOTER}}
</div>
</div>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'/>
<meta http-equiv='Expires' content='0'/>
<link rel='stylesheet' type='text/css' href='/css/all.css'/>
<script type='text/javascript' src='/client.js?ver=2.43'></script>
<title>kAmMa's Forum</title>
</head>
<body>
<div class='page' style='width:100%; text-align:left'>
<div style='padding:0 25px 0 25px' align='left'>
<br/>
{{COMMON_HEADER}}
<br/>
<div align='right'>
<table class='tborder' cellpadding='3' cellspacing='1' border='0'>
<tbody><tr>
<td class='vbmenu_control' style='font-weight:normal'>
<form method='get' action='/private' style='display:inline'>
<input type='hidden' name='oBy' value='{{ORDER_BY}}'/>
<input type='hidden' name='iPageNo' value='1'/>
Records per page:
<select class='bginput' name='perpage' onchange='this.form.submit()'>{{PER_PAGE_OPTIONS}}</select>
</form>
<span class='smallfont'> Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}} {{PAGE_LINKS}}</span>
</td>
</tr></tbody>
</table>
</div>
<br/>
<form method='post' action='/private' name='pmform' onsubmit='return preparePmIds();'>
<input type='hidden' name='oBy' value='{{ORDER_BY}}'/>
<input type='hidden' name='perpage' value='{{PER_PAGE}}'/>
<input type='hidden' name='iPageNo' value='{{I_PAGE_NO}}'/>
<input type='hidden' name='pmidsCsv' value='' id='pmidsCsv'/>
<table class='tborder' cellpadding='6' cellspacing='1' border='0' width='100%' align='center'>
<thead>
<tr>
<td class='tcat' colspan='4' style='padding:6px; padding-right:0'>
<span class='smallfont' style='float:right'>
<label>
Messages: <strong>{{THREAD_COUNT}}</strong>
<input type='checkbox' title='Check / Uncheck All' onclick='checkUncheckAll(this);'/>
</label>
</span>
<div class='smallfont'>Private Messages</div>
</td>
</tr>
</thead>
<tbody>
<tr class='block_title'>
<td class='thead' colspan='2'></td>
<td class='thead'>
<div style='float:right'><a href='{{DATE_SORT_LINK}}'>Date</a></div>
<div>Title / Sender -> Recipient</div>
</td>
<td class='thead'></td>
</tr>
</tbody>
<tbody id='collapseobj_pmf0_old_messages'>
{{THREAD_ROWS}}
</tbody>
<tbody>
<tr>
<td class='tfoot' align='right' colspan='4'>
<div class='smallfont' style='color:white'>
Selected Messages:
<select name='dowhat'>
<option value='delete'>Delete</option>
<option value='read'>Mark as read</option>
<option value='unread'>Mark as unread</option>
</select>
<input type='submit' class='button' value='Go'/>
</div>
</td>
</tr>
</tbody>
</table>
</form>
<br/>
{{COMMON_FOOTER}}
</div>
</div>
<script type='text/javascript'>
function checkUncheckAll(field) {
var elems = document.getElementsByName('pmids');
for (var i = 0; i < elems.length; i++) {
elems[i].checked = !!field.checked;
}
}
function preparePmIds() {
var elems = document.getElementsByName('pmids');
var ids = [];
for (var i = 0; i < elems.length; i++) {
if (elems[i].checked) {
ids.push(elems[i].value);
}
}
document.getElementById('pmidsCsv').value = ids.join(',');
return true;
}
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
# Database Configuration
app.db.url=jdbc:mariadb://server01:3306/fabkovachata?useUnicode=true&characterEncoding=UTF-8
app.db.user=fabkovachata
app.db.password=Fch621420+
app.db.mysql_admin_url=jdbc:mariadb://server01:3306/?useUnicode=true&characterEncoding=UTF-8
# Server Configuration
app.server.port=8080
app.server.threads=10
# Session Configuration
app.session.timeout.minutes=30
# Multipart Configuration
app.multipart.max_bytes=52428800
app.multipart.icon_max_bytes=1048576

Some files were not shown because too many files have changed in this diff Show More