first commit
8
.gitignore
vendored
Executable file
@ -0,0 +1,8 @@
|
||||
servers
|
||||
target
|
||||
bin
|
||||
.settings
|
||||
.metadata
|
||||
.classpath
|
||||
.project
|
||||
|
||||
55
pom.xml
Normal 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>
|
||||
54
src/main/java/cz/kamma/fabka/httpserver/AppConfig.java
Normal 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("-", "_");
|
||||
}
|
||||
}
|
||||
1448
src/main/java/cz/kamma/fabka/httpserver/HttpServerApplication.java
Normal file
@ -0,0 +1,5 @@
|
||||
package cz.kamma.fabka.httpserver.auth;
|
||||
|
||||
public interface AuthService {
|
||||
AuthenticatedUser authenticate(String username, String password);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
31
src/main/java/cz/kamma/fabka/httpserver/crypto/Md5.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
157
src/main/java/cz/kamma/fabka/httpserver/http/RequestContext.java
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/main/java/cz/kamma/fabka/httpserver/http/Responses.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package cz.kamma.fabka.httpserver.http;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RouteHandler {
|
||||
void handle(RequestContext ctx) throws Exception;
|
||||
}
|
||||
49
src/main/java/cz/kamma/fabka/httpserver/http/Router.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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("<", "<");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
919
src/main/java/cz/kamma/fabka/httpserver/web/Pages.java
Normal 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%'> </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'> </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%'> </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'> </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'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 ©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 ©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>«</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>»</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>«</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>»</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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/main/resources/app.properties
Normal 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
|
||||
103
src/main/resources/webapp/chat.html
Normal 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>
|
||||
310
src/main/resources/webapp/client.js
Normal 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
|
||||
*/
|
||||
453
src/main/resources/webapp/css/all.css
Normal 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}
|
||||
BIN
src/main/resources/webapp/favicon.ico
Normal file
|
After Width: | Height: | Size: 630 B |
43
src/main/resources/webapp/forum.html
Normal 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>
|
||||
124
src/main/resources/webapp/forumdisplay.html
Normal 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>
|
||||
BIN
src/main/resources/webapp/images/busy.gif
Normal file
|
After Width: | Height: | Size: 722 B |
BIN
src/main/resources/webapp/images/cat-line.gif
Normal file
|
After Width: | Height: | Size: 284 B |
BIN
src/main/resources/webapp/images/cellpic-fp-big.gif
Normal file
|
After Width: | Height: | Size: 315 B |
BIN
src/main/resources/webapp/images/cellpic-fp.gif
Normal file
|
After Width: | Height: | Size: 293 B |
BIN
src/main/resources/webapp/images/cernypetr.jpg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
src/main/resources/webapp/images/delete.gif
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
src/main/resources/webapp/images/edit.gif
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
src/main/resources/webapp/images/gif.gif
Normal file
|
After Width: | Height: | Size: 183 B |
BIN
src/main/resources/webapp/images/icon1.gif
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/main/resources/webapp/images/locked.gif
Normal file
|
After Width: | Height: | Size: 202 B |
BIN
src/main/resources/webapp/images/newthread.gif
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/main/resources/webapp/images/pm.gif
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/main/resources/webapp/images/pm_read.gif
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/main/resources/webapp/images/post_old.gif
Normal file
|
After Width: | Height: | Size: 522 B |
BIN
src/main/resources/webapp/images/quote.gif
Normal file
|
After Width: | Height: | Size: 794 B |
BIN
src/main/resources/webapp/images/reply.gif
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/main/resources/webapp/images/sortasc.gif
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
src/main/resources/webapp/images/sortdesc.gif
Normal file
|
After Width: | Height: | Size: 943 B |
BIN
src/main/resources/webapp/images/voting_no.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
src/main/resources/webapp/images/voting_yes.png
Normal file
|
After Width: | Height: | Size: 505 B |
BIN
src/main/resources/webapp/images/whos_online.gif
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
44
src/main/resources/webapp/login.html
Normal 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> </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>
|
||||
214
src/main/resources/webapp/member.html
Normal 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> </td></tr>
|
||||
<tr><td>Last Name:<br/><input type='text' class='bginput' name='lastname' size='25' maxlength='50' value='{{LAST_NAME}}'/></td><td> </td></tr>
|
||||
<tr><td>City:<br/><input type='text' class='bginput' name='city' size='25' maxlength='50' value='{{CITY}}'/></td><td> </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> </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>
|
||||
81
src/main/resources/webapp/message.html
Normal 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'> </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>
|
||||
64
src/main/resources/webapp/newpm.html
Normal 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'> </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> </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>
|
||||
62
src/main/resources/webapp/newthread.html
Normal 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> </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>
|
||||
BIN
src/main/resources/webapp/notif.wav
Normal file
109
src/main/resources/webapp/private.html
Normal 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>
|
||||
16
target/classes/app.properties
Normal 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
|
||||