commit 911853a183d7c67304ad7c6bbc57995651aed6e5 Author: Radek Davidek Date: Fri Mar 20 10:48:09 2026 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ebd831b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +servers +target +bin +.settings +.metadata +.classpath +.project + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..addbfe1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + cz.kamma.fabka + app-httpserver + 1.0-SNAPSHOT + FabkovaChata HttpServer + + + 11 + 11 + UTF-8 + + + + + org.mariadb.jdbc + mariadb-java-client + 3.5.3 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + false + + + cz.kamma.fabka.httpserver.HttpServerApplication + + + + + + + + + diff --git a/src/main/java/cz/kamma/fabka/httpserver/AppConfig.java b/src/main/java/cz/kamma/fabka/httpserver/AppConfig.java new file mode 100644 index 0000000..d51d30f --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/AppConfig.java @@ -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("-", "_"); + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/HttpServerApplication.java b/src/main/java/cz/kamma/fabka/httpserver/HttpServerApplication.java new file mode 100644 index 0000000..d391f1d --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/HttpServerApplication.java @@ -0,0 +1,1448 @@ +package cz.kamma.fabka.httpserver; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import com.sun.net.httpserver.HttpServer; + +import cz.kamma.fabka.httpserver.auth.AuthService; +import cz.kamma.fabka.httpserver.auth.AuthenticatedUser; +import cz.kamma.fabka.httpserver.auth.DatabaseAuthService; +import cz.kamma.fabka.httpserver.http.ClasspathStaticFileHandler; +import cz.kamma.fabka.httpserver.http.MultipartFormData; +import cz.kamma.fabka.httpserver.http.RequestContext; +import cz.kamma.fabka.httpserver.http.Responses; +import cz.kamma.fabka.httpserver.http.Router; +import cz.kamma.fabka.httpserver.repository.AttachmentData; +import cz.kamma.fabka.httpserver.repository.ChatLine; +import cz.kamma.fabka.httpserver.repository.ChatRepository; +import cz.kamma.fabka.httpserver.repository.ChatVoteStats; +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.ForumRepository; +import cz.kamma.fabka.httpserver.repository.ForumSummary; +import cz.kamma.fabka.httpserver.repository.MemberProfile; +import cz.kamma.fabka.httpserver.repository.MemberRepository; +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.PrivateMessageRepository; +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; +import cz.kamma.fabka.httpserver.repository.SettingsRepository; +import cz.kamma.fabka.httpserver.repository.UserIcon; +import cz.kamma.fabka.httpserver.repository.UserIconRepository; +import cz.kamma.fabka.httpserver.repository.VoteStats; +import cz.kamma.fabka.httpserver.session.SessionData; +import cz.kamma.fabka.httpserver.session.SessionManager; +import cz.kamma.fabka.httpserver.web.Pages; + + +public class HttpServerApplication { + private static final String AUTH_USER_KEY = "auth.username"; + private static final String AUTH_USER_ID_KEY = "auth.userId"; + private static final String CURRENT_PAGE_KEY = "app.currentPage"; + private static final String SETTINGS_SORT_TYPE_PREFIX = "SETTINGS_SORTING_TYPE_FORUMDISPLAY"; + private static final String SETTINGS_SORT_BY_PREFIX = "SETTINGS_SORTING_BY_FORUMDISPLAY"; + private static final String SETTINGS_PERPAGE_PREFIX = "SETTINGS_PERPAGE_RECORDS_FORUMDISPLAY"; + private static final String SETTINGS_SHOWIMG_PREFIX = "SETTINGS_SHOW_IMAGES_FORUMDISPLAY"; + private static final String SETTINGS_SHOWTYPE_PREFIX = "SETTINGS_SHOW_TYPE_FORUMDISPLAY"; + private static final String SETTINGS_PM_PRIVATE_ORDER = "SETTINGS_PM_PRIVATE_ORDER"; + private static final String SETTINGS_PM_PRIVATE_PERPAGE = "SETTINGS_PM_PRIVATE_PERPAGE"; + private static final String SETTINGS_PM_THREAD_PERPAGE = SETTINGS_PERPAGE_PREFIX + "_PM"; + private static final String SETTINGS_SOUND_ONOFF = "SETTINGS_SOUND_ONOFF"; + private static final String PM_ERROR_KEY = "pm.error"; + private static final String NEW_THREAD_ERROR_KEY = "newthread.error"; + private static final String MEMBER_ERROR_KEY = "member.error"; + + public static void main(String[] args) throws IOException { + int port = AppConfig.getInt("app.server.port", 8080); + int threads = AppConfig.getInt("app.server.threads", 24); + + SessionManager sessionManager = new SessionManager(Duration.ofMinutes(AppConfig.getInt("app.session.timeout.minutes", 30))); + String jdbcUrl = AppConfig.get("app.db.url", ""); + String dbUser = AppConfig.get("app.db.user", "fabkovachata"); + String dbPassword = AppConfig.get("app.db.password", ""); + if (dbPassword.isBlank()) { + throw new IllegalStateException("app.db.password must be set in app.properties or as environment variable APP_DB_PASSWORD"); + } + String mysqlAdminJdbcUrl = AppConfig.get("app.db.mysql_admin_url", ""); + + AuthService authService = new DatabaseAuthService(jdbcUrl, dbUser, dbPassword); + ForumRepository forumRepository = new ForumRepository(jdbcUrl, dbUser, dbPassword); + ChatRepository chatRepository = new ChatRepository(jdbcUrl, dbUser, dbPassword); + PrivateMessageRepository privateMessageRepository = new PrivateMessageRepository(jdbcUrl, dbUser, dbPassword); + UserIconRepository userIconRepository = new UserIconRepository(jdbcUrl, dbUser, dbPassword); + MemberRepository memberRepository = new MemberRepository(jdbcUrl, dbUser, dbPassword); + SettingsRepository settingsRepository = new SettingsRepository(jdbcUrl, dbUser, dbPassword); + MysqlClientRepository mysqlClientRepository = new MysqlClientRepository(mysqlAdminJdbcUrl, dbUser, dbPassword); + + Router router = new Router(sessionManager); + wireRoutes( + router, + sessionManager, + authService, + forumRepository, + chatRepository, + privateMessageRepository, + userIconRepository, + memberRepository, + settingsRepository, + mysqlClientRepository + ); + + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + ExecutorService executor = Executors.newFixedThreadPool(threads); + server.setExecutor(executor); + + server.createContext("/", router); + server.createContext("/css", new ClasspathStaticFileHandler("/css", "webapp/css")); + server.createContext("/images", new ClasspathStaticFileHandler("/images", "webapp/images")); + server.createContext("/client.js", new ClasspathStaticFileHandler("/client.js", "webapp/client.js")); + server.createContext("/notif.wav", new ClasspathStaticFileHandler("/notif.wav", "webapp/notif.wav")); + server.createContext("/favicon.ico", new ClasspathStaticFileHandler("/favicon.ico", "webapp/favicon.ico")); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + server.stop(0); + executor.shutdown(); + })); + + server.start(); + System.out.println("FabkovaChata HttpServer skeleton started on http://localhost:" + port); + } + + private static void wireRoutes( + Router router, + SessionManager sessionManager, + AuthService authService, + ForumRepository forumRepository, + ChatRepository chatRepository, + PrivateMessageRepository privateMessageRepository, + UserIconRepository userIconRepository, + MemberRepository memberRepository, + SettingsRepository settingsRepository, + MysqlClientRepository mysqlClientRepository + ) { + router.get("/", ctx -> { + if (isAuthenticated(ctx)) { + Responses.redirect(ctx.exchange(), "/forum"); + } else { + Responses.redirect(ctx.exchange(), "/login"); + } + }); + + router.get("/login", ctx -> { + boolean showError = "1".equals(ctx.queryParam("error")); + Responses.html(ctx.exchange(), 200, Pages.loginPage(showError)); + }); + router.get("/chat.jsp", ctx -> Responses.redirect(ctx.exchange(), "/chat")); + router.get("/private.jsp", ctx -> { + String oBy = ctx.queryParam("oBy"); + String perpage = ctx.queryParam("perpage"); + String iPageNo = ctx.queryParam("iPageNo"); + if (oBy == null || oBy.isBlank()) { + String extra = ""; + if (perpage != null && !perpage.isBlank()) { + extra += (extra.isEmpty() ? "?" : "&") + "perpage=" + URLEncoder.encode(perpage, StandardCharsets.UTF_8); + } + if (iPageNo != null && !iPageNo.isBlank()) { + extra += (extra.isEmpty() ? "?" : "&") + "iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8); + } + Responses.redirect(ctx.exchange(), "/private" + extra); + } else { + String location = "/private?oBy=" + URLEncoder.encode(oBy, StandardCharsets.UTF_8); + if (perpage != null && !perpage.isBlank()) { + location += "&perpage=" + URLEncoder.encode(perpage, StandardCharsets.UTF_8); + } + if (iPageNo != null && !iPageNo.isBlank()) { + location += "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8); + } + Responses.redirect(ctx.exchange(), location); + } + }); + router.get("/newpm.jsp", ctx -> { + String uid = ctx.queryParam("uid"); + if (uid == null || uid.isBlank()) { + Responses.redirect(ctx.exchange(), "/newpm"); + } else { + Responses.redirect(ctx.exchange(), "/newpm?uid=" + URLEncoder.encode(uid, StandardCharsets.UTF_8)); + } + }); + router.get("/newthread.jsp", ctx -> Responses.redirect(ctx.exchange(), "/newthread")); + router.get("/member.jsp", ctx -> Responses.redirect(ctx.exchange(), "/member")); + router.get("/message.jsp", ctx -> { + String pmid = ctx.queryParam("pmid"); + String perpage = ctx.queryParam("perpage"); + String iPageNo = ctx.queryParam("iPageNo"); + if (pmid == null || pmid.isBlank()) { + Responses.redirect(ctx.exchange(), "/private"); + } else { + String location = "/message?pmid=" + URLEncoder.encode(pmid, StandardCharsets.UTF_8); + if (perpage != null && !perpage.isBlank()) { + location += "&perpage=" + URLEncoder.encode(perpage, StandardCharsets.UTF_8); + } + if (iPageNo != null && !iPageNo.isBlank()) { + location += "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8); + } + Responses.redirect(ctx.exchange(), location); + } + }); + router.get("/mysqlc.jsp", ctx -> Responses.redirect(ctx.exchange(), "/mysqlc")); + router.get("/mysqlc", ctx -> handleMysqlClient(ctx, sessionManager, privateMessageRepository, mysqlClientRepository)); + router.post("/mysqlc", ctx -> handleMysqlClient(ctx, sessionManager, privateMessageRepository, mysqlClientRepository)); + router.post("/mysqlc.jsp", ctx -> handleMysqlClient(ctx, sessionManager, privateMessageRepository, mysqlClientRepository)); + + router.post("/process/login", ctx -> { + String username = ctx.formParam("uname"); + String password = ctx.formParam("passwd"); + if (username == null || password == null) { + Responses.redirect(ctx.exchange(), "/login?error=1"); + return; + } + + AuthenticatedUser user = authService.authenticate(username, password); + if (user == null) { + Responses.redirect(ctx.exchange(), "/login?error=1"); + return; + } + + SessionData session = ctx.getOrCreateSession(); + session.setAttribute(AUTH_USER_KEY, user.getUsername()); + session.setAttribute(AUTH_USER_ID_KEY, user.getUserId()); + Map userSettings = settingsRepository.loadUserSettings(user.getUserId()); + for (Map.Entry entry : userSettings.entrySet()) { + session.setAttribute(entry.getKey(), entry.getValue()); + } + Responses.redirect(ctx.exchange(), "/forum"); + }); + + router.post("/process/logout", ctx -> { + ctx.invalidateSession(); + Responses.redirect(ctx.exchange(), "/login"); + }); + + router.post("/process/replythread", ctx -> handleReplyThread(ctx, forumRepository)); + router.post("/process/replythread.jsp", ctx -> handleReplyThread(ctx, forumRepository)); + + router.post("/process/savemember", ctx -> handleSaveMember(ctx, memberRepository, settingsRepository)); + router.post("/process/savemember.jsp", ctx -> handleSaveMember(ctx, memberRepository, settingsRepository)); + router.post("/process/saveicon", ctx -> handleSaveIcon(ctx, userIconRepository)); + router.post("/process/saveicon.jsp", ctx -> handleSaveIcon(ctx, userIconRepository)); + + router.get("/process/setsticky.jsp", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long forumId = parseLong(ctx.queryParam("forumid"), -1L); + long postId = parseLong(ctx.queryParam("postid"), -1L); + if (postId > 0) { + forumRepository.toggleSticky(postId); + } + if (forumId > 0) { + Responses.redirect(ctx.exchange(), "/forumdisplay?f=" + forumId); + } else { + Responses.redirect(ctx.exchange(), "/forum"); + } + }); + + router.get("/process/deletepost.jsp", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + long forumId = parseLong(ctx.queryParam("forumid"), -1L); + long postId = parseLong(ctx.queryParam("postid"), -1L); + if (postId > 0 && userId > 0) { + forumRepository.deletePost(postId, userId); + } + if (forumId > 0) { + Responses.redirect(ctx.exchange(), "/forumdisplay?f=" + forumId); + } else { + Responses.redirect(ctx.exchange(), "/forum"); + } + }); + + router.get("/forum", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "forum"); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + forumRepository.putHistoryRecord(userId, 0L); + List forums = forumRepository.listActiveForums(); + Map newCounts = forumRepository.getNewMessagesCountByUserId(userId); + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html(ctx.exchange(), 200, Pages.forumPage(username, forums, newCounts, loggedUsersCount, loggedUsers, pmStats)); + }); + + router.get("/member", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "member"); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + MemberProfile profile = memberRepository.findByUserId(userId); + Object soundRaw = session.getAttribute(SETTINGS_SOUND_ONOFF); + String soundSetting = (soundRaw == null) ? "soundon" : String.valueOf(soundRaw); + String error = (String) session.getAttribute(MEMBER_ERROR_KEY); + session.setAttribute(MEMBER_ERROR_KEY, null); + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html( + ctx.exchange(), + 200, + Pages.memberPage( + username, + profile, + !"soundoff".equalsIgnoreCase(soundSetting), + error, + loggedUsersCount, + loggedUsers, + pmStats + ) + ); + }); + + router.get("/chat", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "chat"); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html(ctx.exchange(), 200, Pages.chatPage(username, loggedUsersCount, loggedUsers, pmStats)); + }); + + router.get("/private", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "private"); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + String orderBy = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_PM_PRIVATE_ORDER, "oBy", "readed desc, lastitem desc" + ); + String perPage = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_PM_PRIVATE_PERPAGE, "perpage", "25" + ); + int pageNo = Math.max(1, parseInt(ctx.queryParam("iPageNo"), 1)); + List allThreads = privateMessageRepository.listThreads(userId, orderBy); + int totalRows = allThreads.size(); + int pageSize; + if ("all".equalsIgnoreCase(perPage)) { + pageSize = Math.max(totalRows, 1); + } else { + int parsed = parseInt(perPage, 25); + pageSize = parsed <= 0 ? 25 : parsed; + } + int totalPages = Math.max(1, (int) Math.ceil(totalRows / (double) pageSize)); + int currentPage = Math.min(Math.max(1, pageNo), totalPages); + int fromIdx = totalRows == 0 ? 0 : (currentPage - 1) * pageSize; + int toIdx = totalRows == 0 ? 0 : Math.min(fromIdx + pageSize, totalRows); + List threads = totalRows == 0 ? List.of() : allThreads.subList(fromIdx, toIdx); + int rowsFrom = totalRows == 0 ? 0 : fromIdx + 1; + int rowsTo = toIdx; + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html( + ctx.exchange(), + 200, + Pages.privatePage( + username, + threads, + orderBy, + perPage, + currentPage, + totalPages, + rowsFrom, + rowsTo, + totalRows, + loggedUsersCount, + loggedUsers, + pmStats + ) + ); + }); + + router.post("/private", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String orderBy = valueOrDefault(ctx.formParam("oBy"), "readed desc, lastitem desc"); + String perPage = valueOrDefault(ctx.formParam("perpage"), "25"); + String iPageNo = valueOrDefault(ctx.formParam("iPageNo"), "1"); + session.setAttribute(SETTINGS_PM_PRIVATE_ORDER, orderBy); + session.setAttribute(SETTINGS_PM_PRIVATE_PERPAGE, perPage); + settingsRepository.upsertUserSetting(userId, SETTINGS_PM_PRIVATE_ORDER, orderBy); + settingsRepository.upsertUserSetting(userId, SETTINGS_PM_PRIVATE_PERPAGE, perPage); + String action = valueOrDefault(ctx.formParam("dowhat"), ""); + String idsCsv = valueOrDefault(ctx.formParam("pmidsCsv"), ""); + List ids = Arrays.stream(idsCsv.split(",")) + .map(String::trim) + .filter(s -> !s.isBlank()) + .map(s -> parseLong(s, -1L)) + .filter(id -> id > 0) + .collect(Collectors.toList()); + privateMessageRepository.bulkAction(userId, ids, action); + Responses.redirect( + ctx.exchange(), + "/private?oBy=" + URLEncoder.encode(orderBy, StandardCharsets.UTF_8) + + "&perpage=" + URLEncoder.encode(perPage, StandardCharsets.UTF_8) + + "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8) + ); + }); + + router.get("/newpm", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "newpm"); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + long toUser = parseLong(ctx.queryParam("uid"), 0L); + if (toUser <= 0) { + Responses.redirect(ctx.exchange(), "/private"); + return; + } + String toUsername = privateMessageRepository.usernameById(toUser); + String error = valueOrDefault((String) session.getAttribute(PM_ERROR_KEY), ""); + session.setAttribute(PM_ERROR_KEY, null); + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html(ctx.exchange(), 200, Pages.newPmPage(username, toUser, toUsername, error, loggedUsersCount, loggedUsers, pmStats)); + }); + + router.get("/newthread", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "newthread"); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + String error = valueOrDefault((String) session.getAttribute(NEW_THREAD_ERROR_KEY), ""); + session.setAttribute(NEW_THREAD_ERROR_KEY, null); + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html(ctx.exchange(), 200, Pages.newThreadPage(username, error, loggedUsersCount, loggedUsers, pmStats)); + }); + + router.post("/process/newthread", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String forumname = ctx.formParam("forumname"); + String description = ctx.formParam("description"); + String password = ctx.formParam("password"); + StringBuilder err = new StringBuilder(); + if (forumname == null || forumname.isBlank()) { + err.append("Thread name must be specified.
"); + } + if (description == null || description.isBlank()) { + err.append("Description must be specified.
"); + } + if (err.length() > 0) { + session.setAttribute(NEW_THREAD_ERROR_KEY, err.toString()); + Responses.redirect(ctx.exchange(), "/newthread"); + return; + } + forumRepository.createThread(userId, forumname, description, password); + Responses.redirect(ctx.exchange(), "/forum"); + }); + router.post("/process/newthread.jsp", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String forumname = ctx.formParam("forumname"); + String description = ctx.formParam("description"); + String password = ctx.formParam("password"); + StringBuilder err = new StringBuilder(); + if (forumname == null || forumname.isBlank()) { + err.append("Thread name must be specified.
"); + } + if (description == null || description.isBlank()) { + err.append("Description must be specified.
"); + } + if (err.length() > 0) { + session.setAttribute(NEW_THREAD_ERROR_KEY, err.toString()); + Responses.redirect(ctx.exchange(), "/newthread"); + return; + } + forumRepository.createThread(userId, forumname, description, password); + Responses.redirect(ctx.exchange(), "/forum"); + }); + + router.get("/message", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "message"); + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + long pmid = parseLong(ctx.queryParam("pmid"), -1L); + String perPage = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_PM_THREAD_PERPAGE, "perpage", "10" + ); + int pageNo = Math.max(1, parseInt(ctx.queryParam("iPageNo"), 1)); + PrivateThreadRoot root = privateMessageRepository.threadRoot(userId, pmid); + if (root == null) { + Responses.redirect(ctx.exchange(), "/private"); + return; + } + privateMessageRepository.markThreadRead(userId, pmid); + List allMessages = privateMessageRepository.threadMessages(userId, pmid); + int totalRows = allMessages.size(); + int pageSize; + if ("all".equalsIgnoreCase(perPage)) { + pageSize = Math.max(totalRows, 1); + } else { + int parsed = parseInt(perPage, 10); + pageSize = parsed <= 0 ? 10 : parsed; + } + int totalPages = Math.max(1, (int) Math.ceil(totalRows / (double) pageSize)); + int currentPage = Math.min(Math.max(1, pageNo), totalPages); + int fromIdx = totalRows == 0 ? 0 : (currentPage - 1) * pageSize; + int toIdx = totalRows == 0 ? 0 : Math.min(fromIdx + pageSize, totalRows); + List messages = totalRows == 0 ? List.of() : allMessages.subList(fromIdx, toIdx); + int rowsFrom = totalRows == 0 ? 0 : fromIdx + 1; + int rowsTo = toIdx; + String error = valueOrDefault((String) session.getAttribute(PM_ERROR_KEY), ""); + session.setAttribute(PM_ERROR_KEY, null); + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html( + ctx.exchange(), + 200, + Pages.messagePage( + username, + root, + messages, + perPage, + currentPage, + totalPages, + rowsFrom, + rowsTo, + totalRows, + error, + loggedUsersCount, + loggedUsers, + pmStats + ) + ); + }); + + router.post("/process/replymessage", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long fromUser = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + long toUser = parseLong(ctx.formParam("to_user"), -1L); + long pmid = parseLong(ctx.formParam("pmid"), -1L); + String perPage = valueOrDefault(ctx.formParam("perpage"), "10"); + String iPageNo = valueOrDefault(ctx.formParam("iPageNo"), "1"); + session.setAttribute(SETTINGS_PM_THREAD_PERPAGE, perPage); + settingsRepository.upsertUserSetting(fromUser, SETTINGS_PM_THREAD_PERPAGE, perPage); + String title = ctx.formParam("title"); + String message = ctx.formParam("message"); + StringBuilder err = new StringBuilder(); + if (message == null || message.isBlank()) { + err.append("Message text must be specified.
"); + } + if (title == null || title.isBlank()) { + err.append("Title must be specified.
"); + } + if (toUser <= 0) { + err.append("to_user must be specified.
"); + } + if (err.length() > 0) { + session.setAttribute(PM_ERROR_KEY, err.toString()); + if (pmid > 0) { + Responses.redirect( + ctx.exchange(), + "/message?pmid=" + pmid + + "&perpage=" + URLEncoder.encode(perPage, StandardCharsets.UTF_8) + + "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8) + ); + } else { + Responses.redirect(ctx.exchange(), "/newpm?uid=" + toUser); + } + return; + } + privateMessageRepository.send(fromUser, toUser, title, message, pmid > 0 ? pmid : null); + if (pmid > 0) { + Responses.redirect( + ctx.exchange(), + "/message?pmid=" + pmid + + "&perpage=" + URLEncoder.encode(perPage, StandardCharsets.UTF_8) + + "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8) + ); + } else { + Responses.redirect(ctx.exchange(), "/private"); + } + }); + router.post("/process/replymessage.jsp", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long fromUser = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + long toUser = parseLong(ctx.formParam("to_user"), -1L); + long pmid = parseLong(ctx.formParam("pmid"), -1L); + String perPage = valueOrDefault(ctx.formParam("perpage"), "10"); + String iPageNo = valueOrDefault(ctx.formParam("iPageNo"), "1"); + session.setAttribute(SETTINGS_PM_THREAD_PERPAGE, perPage); + settingsRepository.upsertUserSetting(fromUser, SETTINGS_PM_THREAD_PERPAGE, perPage); + String title = ctx.formParam("title"); + String message = ctx.formParam("message"); + StringBuilder err = new StringBuilder(); + if (message == null || message.isBlank()) { + err.append("Message text must be specified.
"); + } + if (title == null || title.isBlank()) { + err.append("Title must be specified.
"); + } + if (toUser <= 0) { + err.append("to_user must be specified.
"); + } + if (err.length() > 0) { + session.setAttribute(PM_ERROR_KEY, err.toString()); + if (pmid > 0) { + Responses.redirect( + ctx.exchange(), + "/message?pmid=" + pmid + + "&perpage=" + URLEncoder.encode(perPage, StandardCharsets.UTF_8) + + "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8) + ); + } else { + Responses.redirect(ctx.exchange(), "/newpm?uid=" + toUser); + } + return; + } + privateMessageRepository.send(fromUser, toUser, title, message, pmid > 0 ? pmid : null); + if (pmid > 0) { + Responses.redirect( + ctx.exchange(), + "/message?pmid=" + pmid + + "&perpage=" + URLEncoder.encode(perPage, StandardCharsets.UTF_8) + + "&iPageNo=" + URLEncoder.encode(iPageNo, StandardCharsets.UTF_8) + ); + } else { + Responses.redirect(ctx.exchange(), "/private"); + } + }); + + router.get("/forumdisplay", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + session.setAttribute(CURRENT_PAGE_KEY, "forumdisplay"); + + long forumId = parseLong(ctx.queryParam("f"), -1L); + if (forumId <= 0) { + Responses.redirect(ctx.exchange(), "/forum"); + return; + } + + ForumDetail forum = forumRepository.findForumById(forumId); + if (forum == null) { + Responses.redirect(ctx.exchange(), "/forum"); + return; + } + + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + long quoteItemId = parseLong(ctx.queryParam("q"), 0L); + QuotedTextItem quotedTextItem = quoteItemId > 0 ? forumRepository.findQuotedItem(quoteItemId) : null; + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + forumRepository.putHistoryRecord(userId, forumId); + String searchText = valueOrDefault(ctx.queryParam("stext"), ""); + String showType = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_SHOWTYPE_PREFIX + forumId, "showType", "full" + ); + String showImg = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_SHOWIMG_PREFIX + forumId, "showImg", "true" + ); + String sortBy = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_SORT_BY_PREFIX + forumId, "sortBy", "fi.id" + ); + String sortType = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_SORT_TYPE_PREFIX + forumId, "sortType", "desc" + ); + String perPage = readDisplaySetting( + ctx, session, settingsRepository, userId, + SETTINGS_PERPAGE_PREFIX + forumId, "perpage", "50" + ); + int pageNo = Math.max(1, parseInt(ctx.queryParam("iPageNo"), 1)); + + List messagesAll = forumRepository.listMessagesByForumId(forumId); + List filtered = filterAndSortMessages(messagesAll, searchText, sortBy, sortType); + if ("only".equalsIgnoreCase(showImg)) { + filtered = filtered.stream() + .filter(m -> m.getAttachments() != null && m.getAttachments().stream().anyMatch(ForumAttachment::isPicture)) + .collect(Collectors.toList()); + } + ForumDisplayView view = paginate(filtered, pageNo, perPage, searchText, showType, showImg, sortBy, sortType); + + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + MessageRenderSettings renderSettings = settingsRepository.loadMessageRenderSettings(); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html( + ctx.exchange(), + 200, + Pages.forumDisplayPage( + username, + userId, + forum, + view, + quoteItemId > 0 ? quoteItemId : null, + quotedTextItem, + loggedUsersCount, + loggedUsers, + renderSettings, + pmStats + ) + ); + }); + + router.get("/process/showimage", ctx -> { + if (!isAuthenticated(ctx)) { + Responses.text(ctx.exchange(), 401, "Unauthorized"); + return; + } + long attachmentId = parseLong(ctx.queryParam("attachmentid"), -1L); + if (attachmentId > 0) { + AttachmentData attachment = forumRepository.findAttachmentData(attachmentId); + if (attachment == null) { + Responses.text(ctx.exchange(), 404, "Attachment not found"); + return; + } + Responses.send(ctx.exchange(), 200, attachment.getContentType(), attachment.getData()); + return; + } + if (!"yes".equals(ctx.queryParam("userIcon"))) { + Responses.text(ctx.exchange(), 400, "Unsupported image request"); + return; + } + + SessionData session = ctx.getSession(); + long requestedUserId = parseLong(ctx.queryParam("uid"), -1L); + Object userIdRaw = session == null ? null : session.getAttribute(AUTH_USER_ID_KEY); + long sessionUserId = parseLong(userIdRaw, -1L); + long userId = requestedUserId > 0 ? requestedUserId : sessionUserId; + if (userId <= 0) { + Responses.text(ctx.exchange(), 404, "User icon not found"); + return; + } + + UserIcon icon = userIconRepository.findLatestByUserId(userId); + if (icon == null) { + Responses.text(ctx.exchange(), 404, "User icon not found"); + return; + } + + Responses.send(ctx.exchange(), 200, icon.getContentType(), icon.getData()); + }); + + router.get("/process/download", ctx -> { + if (!isAuthenticated(ctx)) { + Responses.text(ctx.exchange(), 401, "Unauthorized"); + return; + } + long attachmentId = parseLong(ctx.queryParam("attachmentid"), -1L); + AttachmentData attachment = forumRepository.findAttachmentData(attachmentId); + if (attachment == null) { + Responses.text(ctx.exchange(), 404, "Attachment not found"); + return; + } + String safeName = attachment.getName().replace("\"", ""); + ctx.exchange().getResponseHeaders().set( + "Content-Disposition", + "inline; filename=\"" + safeName + "\"; filename*=UTF-8''" + + URLEncoder.encode(safeName, StandardCharsets.UTF_8) + ); + Responses.send(ctx.exchange(), 200, attachment.getContentType(), attachment.getData()); + }); + + router.post("/process/ajaxreq.jsp", ctx -> { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.text(ctx.exchange(), 401, "invalid"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String ajaxMethod = ctx.formParam("ajaxMethod"); + long universalId = parseLong(ctx.formParam("universalId"), -1L); + if (userId <= 0 || ajaxMethod == null || ajaxMethod.isBlank()) { + Responses.text(ctx.exchange(), 400, "invalid"); + return; + } + if (ajaxMethod.startsWith("ajaxVoting")) { + if (universalId <= 0) { + Responses.text(ctx.exchange(), 400, "invalid"); + return; + } + int requestedVote = ajaxMethod.endsWith("_yes") ? 1 : -1; + Integer existingVote = forumRepository.getUserVote(userId, universalId); + if (existingVote == null) { + forumRepository.addVote(userId, universalId, requestedVote); + } else if (existingVote == requestedVote) { + forumRepository.deleteVote(userId, universalId); + } else { + forumRepository.updateVote(userId, universalId, requestedVote); + } + VoteStats stats = forumRepository.getVoteStats(universalId); + String xml = "\n" + + "\n" + + "" + universalId + "\n" + + "" + stats.getYes() + "\n" + + "" + stats.getNo() + "\n" + + ""; + Responses.send(ctx.exchange(), 200, "text/xml; charset=UTF-8", xml.getBytes(StandardCharsets.UTF_8)); + return; + } + + if (ajaxMethod.startsWith("ajaxChatVoting")) { + if (universalId <= 0) { + Responses.text(ctx.exchange(), 400, "invalid"); + return; + } + if (!chatRepository.alreadyChatVoted(userId, universalId)) { + int vote = ajaxMethod.endsWith("_yes") ? 1 : -1; + chatRepository.addChatVote(userId, universalId, vote); + } + ChatVoteStats stats = chatRepository.getChatVoteStats(universalId); + String xml = "\n" + + "\n" + + "" + universalId + "\n" + + "" + xmlEscape(stats.getThumbUpUsers()) + "\n" + + "" + xmlEscape(stats.getThumbDownUsers()) + "\n" + + ""; + Responses.send(ctx.exchange(), 200, "text/xml; charset=UTF-8", xml.getBytes(StandardCharsets.UTF_8)); + return; + } + + if (ajaxMethod.startsWith("asynchUpdate") || ajaxMethod.startsWith("chat_text_") || "firstUpdateChat".equals(ajaxMethod)) { + if (universalId > 0) { + chatRepository.confirmChatRead(userId, universalId); + } + if (ajaxMethod.startsWith("chat_text_")) { + String message = ajaxMethod.substring("chat_text_".length()).trim(); + chatRepository.addChatMessage(userId, message); + } + + StringBuilder resp = new StringBuilder("\n\n"); + resp.append(" \n"); + for (SessionData activeSession : sessionManager.activeSessions()) { + Object activeUsername = activeSession.getAttribute(AUTH_USER_KEY); + Object activeUserId = activeSession.getAttribute(AUTH_USER_ID_KEY); + if (activeUsername == null || activeUserId == null) { + continue; + } + long activeUid = parseLong(activeUserId, -1L); + if (activeUid <= 0) { + continue; + } + long inactiveMinutes = Math.max(0, (System.currentTimeMillis() - activeSession.lastAccessMillis()) / 60000L); + boolean inChat = "chat".equals(String.valueOf(activeSession.getAttribute(CURRENT_PAGE_KEY))); + resp.append(" ").append(activeUid).append("\n") + .append(" ").append(xmlEscape(String.valueOf(activeUsername))).append("\n") + .append(" ").append(inChat).append("\n") + .append(" ").append(inactiveMinutes).append("\n"); + } + resp.append(" \n"); + + Map newCounts = forumRepository.getNewMessagesCountByUserId(userId); + if (!newCounts.isEmpty()) { + resp.append(" \n"); + for (Map.Entry entry : newCounts.entrySet()) { + resp.append(" ").append(entry.getKey()).append("\n") + .append(" ").append(entry.getValue()).append("\n"); + } + resp.append(" \n"); + } + + long maxChatId = -1L; + boolean includeLines = "firstUpdateChat".equals(ajaxMethod) + || (("asynchUpdateChat".equals(ajaxMethod) || ajaxMethod.startsWith("chat_text_")) + && chatRepository.hasUserNewChatMessage(userId)); + if (includeLines) { + resp.append(" \n"); + List lines = chatRepository.listRecentChatLines(userId, 40); + for (ChatLine line : lines) { + resp.append(" \n") + .append(" ").append(xmlEscape(line.getFromName())).append("\n") + .append(" ").append(line.getId()).append("\n") + .append(" ").append(line.getNewMessage()).append("\n") + .append(" \n") + .append(" \n") + .append(" ").append(xmlEscape(line.getThumbUpUsers())).append("\n") + .append(" ").append(xmlEscape(line.getThumbDownUsers())).append("\n") + .append(" \n"); + if (line.getId() > maxChatId) { + maxChatId = line.getId(); + } + } + resp.append(" \n"); + } + + int unreadPm = chatRepository.getUnreadMessagesByUser(userId); + if (unreadPm > 0) { + resp.append(" ").append(unreadPm).append("\n"); + } + if (chatRepository.hasUserNewChatMessageOther(userId)) { + resp.append(" 1\n"); + } + resp.append(""); + Responses.send(ctx.exchange(), 200, "text/xml; charset=UTF-8", resp.toString().getBytes(StandardCharsets.UTF_8)); + if (maxChatId > -1L) { + chatRepository.confirmChatDownloaded(userId, maxChatId); + } + return; + } + + Responses.text(ctx.exchange(), 400, "invalid"); + }); + } + + private static boolean isAuthenticated(RequestContext ctx) { + SessionData session = ctx.getSession(); + return session != null && session.getAttribute(AUTH_USER_KEY) != null; + } + + private static void handleReplyThread( + RequestContext ctx, + ForumRepository forumRepository + ) throws IOException { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String contentType = ctx.exchange().getRequestHeaders().getFirst("Content-Type"); + boolean multipart = contentType != null && contentType.toLowerCase().startsWith("multipart/form-data"); + + long forumId; + long quoteItem; + String message; + List attachments; + + if (multipart) { + MultipartFormData data = MultipartFormData.parse(ctx.exchange(), AppConfig.getInt("app.multipart.max_bytes", 1024 * 1024 * 50), StandardCharsets.UTF_8); + forumId = parseLong(data.field("forumid"), -1L); + quoteItem = parseLong(data.field("quoteItem"), 0L); + message = valueOrDefault(data.field("message"), ""); + attachments = new ArrayList<>(); + attachments.addAll(data.files("attachment[]")); + attachments.addAll(data.files("attachment")); + } else { + forumId = parseLong(ctx.formParam("forumid"), -1L); + quoteItem = parseLong(ctx.formParam("quoteItem"), 0L); + message = valueOrDefault(ctx.formParam("message"), ""); + attachments = List.of(); + } + + boolean hasMessage = message != null && !message.isBlank(); + boolean hasAttachments = attachments != null && !attachments.isEmpty(); + if (forumId > 0 && userId > 0 && (hasMessage || hasAttachments)) { + long forumItemId = forumRepository.addReply(forumId, userId, message, quoteItem > 0 ? quoteItem : null); + if (forumItemId > 0 && hasAttachments) { + for (MultipartFormData.FileItem fileItem : attachments) { + forumRepository.addAttachment( + forumItemId, + fileItem.getFileName(), + fileItem.getData(), + fileItem.getContentType() + ); + } + } + } + + if (forumId > 0) { + Responses.redirect(ctx.exchange(), "/forumdisplay?f=" + forumId); + } else { + Responses.redirect(ctx.exchange(), "/forum"); + } + } + + private static void handleSaveIcon( + RequestContext ctx, + UserIconRepository userIconRepository + ) throws IOException { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + if (userId <= 0) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + + String contentType = ctx.exchange().getRequestHeaders().getFirst("Content-Type"); + boolean multipart = contentType != null && contentType.toLowerCase().startsWith("multipart/form-data"); + if (!multipart) { + session.setAttribute(MEMBER_ERROR_KEY, "Icon upload requires multipart/form-data."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + + try { + MultipartFormData data = MultipartFormData.parse(ctx.exchange(), AppConfig.getInt("app.multipart.icon_max_bytes", 1024 * 1024), StandardCharsets.UTF_8); + List icons = new ArrayList<>(); + icons.addAll(data.files("iconfile")); + icons.addAll(data.files("iconFile")); + icons.addAll(data.files("icon")); + if (!icons.isEmpty()) { + MultipartFormData.FileItem icon = icons.get(0); + boolean ok = userIconRepository.saveUserIcon(userId, icon.getData(), icon.getContentType()); + if (!ok) { + session.setAttribute(MEMBER_ERROR_KEY, "Error while saving icon."); + } else { + session.setAttribute(MEMBER_ERROR_KEY, null); + } + } + } catch (IOException ex) { + session.setAttribute(MEMBER_ERROR_KEY, "Icon upload is too large. Maximum is 1MB."); + } + Responses.redirect(ctx.exchange(), "/member"); + } + + private static void handleSaveMember( + RequestContext ctx, + MemberRepository memberRepository, + SettingsRepository settingsRepository + ) throws IOException { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + if (userId <= 0) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + + String changePwd = ctx.formParam("changePwd"); + String changeInfo = ctx.formParam("changeInfo"); + if (changePwd != null && !changePwd.isBlank()) { + String oldPassword = valueOrDefault(ctx.formParam("oldpassword"), ""); + String password = valueOrDefault(ctx.formParam("password"), ""); + String passwordConfirm = valueOrDefault(ctx.formParam("passwordconfirm"), ""); + if (oldPassword.isBlank() || password.isBlank() || passwordConfirm.isBlank()) { + session.setAttribute(MEMBER_ERROR_KEY, "If you want to change password, please fill out all password fields."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + if (!password.equals(passwordConfirm)) { + session.setAttribute(MEMBER_ERROR_KEY, "The entered passwords do not match."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + if (!memberRepository.changePassword(userId, oldPassword, password)) { + session.setAttribute(MEMBER_ERROR_KEY, "Old password does not match your current password."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + session.setAttribute(MEMBER_ERROR_KEY, null); + Responses.redirect(ctx.exchange(), "/forum"); + return; + } + + if (changeInfo != null && !changeInfo.isBlank()) { + String email = valueOrDefault(ctx.formParam("email"), ""); + String emailConfirm = valueOrDefault(ctx.formParam("emailconfirm"), ""); + String firstName = valueOrDefault(ctx.formParam("firstname"), ""); + String lastName = valueOrDefault(ctx.formParam("lastname"), ""); + String city = valueOrDefault(ctx.formParam("city"), ""); + if (email.isBlank() || firstName.isBlank() || lastName.isBlank() || city.isBlank()) { + session.setAttribute(MEMBER_ERROR_KEY, "Please fill all registration parameters."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + if (!email.equals(emailConfirm)) { + session.setAttribute(MEMBER_ERROR_KEY, "The entered emails do not match."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + if (!memberRepository.updatePersonalInfo(userId, email, firstName, lastName, city)) { + session.setAttribute(MEMBER_ERROR_KEY, "Error while saving personal info."); + Responses.redirect(ctx.exchange(), "/member"); + return; + } + String soundValue = "soundon".equals(ctx.formParam("soundonoff")) ? "soundon" : "soundoff"; + session.setAttribute(SETTINGS_SOUND_ONOFF, soundValue); + settingsRepository.upsertUserSetting(userId, SETTINGS_SOUND_ONOFF, soundValue); + session.setAttribute(MEMBER_ERROR_KEY, null); + Responses.redirect(ctx.exchange(), "/forum"); + return; + } + + Responses.redirect(ctx.exchange(), "/member"); + } + + private static void handleMysqlClient( + RequestContext ctx, + SessionManager sessionManager, + PrivateMessageRepository privateMessageRepository, + MysqlClientRepository mysqlClientRepository + ) throws IOException { + SessionData session = ctx.getSession(); + if (session == null || session.getAttribute(AUTH_USER_KEY) == null) { + Responses.redirect(ctx.exchange(), "/login"); + return; + } + + String username = String.valueOf(session.getAttribute(AUTH_USER_KEY)); + if (!isMysqlAdmin(username)) { + Responses.text(ctx.exchange(), 403, "MySQL Client is available only for admin user."); + return; + } + + long userId = parseLong(session.getAttribute(AUTH_USER_ID_KEY), -1L); + String database = "mysql"; + String tableName = ""; + String rowsToShow = "50"; + String myQuery = ""; + String tableSql = ""; + String dbNameForAction = ""; + String command = ""; + String info = ""; + String error = ""; + MysqlClientRepository.SqlExecution result = null; + + Map params = "POST".equalsIgnoreCase(ctx.method()) ? ctx.formParams() : ctx.queryParams(); + if (!params.isEmpty()) { + database = valueOrDefault(params.get("database"), database); + tableName = valueOrDefault(params.get("tablename"), tableName); + rowsToShow = valueOrDefault(params.get("rowsToShow"), rowsToShow); + myQuery = valueOrDefault(params.get("myQuery"), myQuery); + tableSql = valueOrDefault(params.get("tableSql"), tableSql); + dbNameForAction = valueOrDefault(params.get("databasename"), dbNameForAction); + command = valueOrDefault(params.get("command"), command); + } + + if (!command.isBlank()) { + try { + switch (command.toLowerCase()) { + case "changedatabase": + tableName = ""; + break; + case "changetable": + if (!database.isBlank() && !tableName.isBlank()) { + int limit = parseInt(rowsToShow, 50); + if (limit <= 0) { + limit = 50; + } + myQuery = "SELECT * FROM " + mysqlIdentifier(database) + "." + mysqlIdentifier(tableName) + + " LIMIT " + limit; + result = mysqlClientRepository.executeSql(database, myQuery); + info = "Query OK. Returned " + result.getRows().size() + " row(s)."; + } + break; + case "showcreatetable": + if (!database.isBlank() && !tableName.isBlank()) { + myQuery = mysqlClientRepository.showCreateTable(database, tableName); + info = "SHOW CREATE TABLE loaded into query textarea."; + } + break; + case "executemyquery": + if (!myQuery.isBlank()) { + result = mysqlClientRepository.executeSql(database, myQuery); + info = result.isResultSet() + ? "Query OK. Returned " + result.getRows().size() + " row(s)." + : "Statement OK. Affected rows: " + result.getUpdateCount() + "."; + } + break; + case "createdatabase": + if (!dbNameForAction.isBlank()) { + mysqlClientRepository.executeSql("", "CREATE DATABASE " + mysqlIdentifier(dbNameForAction)); + database = dbNameForAction; + info = "Database created: " + dbNameForAction; + } + break; + case "deletedatabase": + String targetDbToDrop = valueOrDefault(dbNameForAction, database); + if (!targetDbToDrop.isBlank()) { + mysqlClientRepository.executeSql("", "DROP DATABASE " + mysqlIdentifier(targetDbToDrop)); + if (targetDbToDrop.equalsIgnoreCase(database)) { + database = "mysql"; + tableName = ""; + } + info = "Database deleted: " + targetDbToDrop; + } + break; + case "deletetable": + if (!database.isBlank() && !tableName.isBlank()) { + mysqlClientRepository.executeSql(database, "DROP TABLE " + mysqlIdentifier(tableName)); + tableName = ""; + info = "Table deleted."; + } + break; + case "createtable": + String createSql = valueOrDefault(tableSql, myQuery); + if (!createSql.isBlank()) { + mysqlClientRepository.executeSql(database, createSql); + info = "CREATE TABLE statement executed."; + } + break; + default: + break; + } + } catch (Exception ex) { + error = ex.getMessage() == null ? "Unknown SQL error." : ex.getMessage(); + } + } + + List databases = mysqlClientRepository.listDatabases(); + if ((database == null || database.isBlank()) && !databases.isEmpty()) { + database = databases.get(0); + } + List tables = mysqlClientRepository.listTables(database); + if (!tableName.isBlank() && !tables.contains(tableName)) { + tableName = ""; + } + + int loggedUsersCount = sessionManager.countSessionsWithAttribute(AUTH_USER_KEY); + List loggedUsers = sessionManager.sessionAttributeValues(AUTH_USER_KEY); + PrivateMessageStats pmStats = privateMessageRepository.stats(userId); + Responses.html( + ctx.exchange(), + 200, + Pages.mysqlClientPage( + username, + databases, + tables, + database, + tableName, + rowsToShow, + myQuery, + tableSql, + command, + info, + error, + result, + loggedUsersCount, + loggedUsers, + pmStats + ) + ); + } + + private static long parseLong(Object raw, long defaultValue) { + if (raw == null) { + return defaultValue; + } + if (raw instanceof Number) { + return ((Number) raw).longValue(); + } + try { + return Long.parseLong(String.valueOf(raw)); + } catch (NumberFormatException ex) { + return defaultValue; + } + } + + private static int parseInt(String raw, int defaultValue) { + if (raw == null || raw.isBlank()) { + return defaultValue; + } + try { + return Integer.parseInt(raw); + } catch (NumberFormatException ex) { + return defaultValue; + } + } + + private static String readDisplaySetting( + RequestContext ctx, + SessionData session, + SettingsRepository settingsRepository, + long userId, + String key, + String paramName, + String defaultValue + ) { + String fromParam = ctx.queryParam(paramName); + if (fromParam != null && !fromParam.isBlank()) { + session.setAttribute(key, fromParam); + settingsRepository.upsertUserSetting(userId, key, fromParam); + return fromParam; + } + Object fromSession = session.getAttribute(key); + if (fromSession == null || String.valueOf(fromSession).isBlank()) { + session.setAttribute(key, defaultValue); + return defaultValue; + } + return String.valueOf(fromSession); + } + + private static List filterAndSortMessages( + List source, + String searchText, + String sortBy, + String sortType + ) { + List filtered = source == null ? new ArrayList<>() : source.stream() + .filter(m -> searchText == null || searchText.isBlank() + || m.getText().toLowerCase().contains(searchText.toLowerCase())) + .collect(Collectors.toCollection(ArrayList::new)); + + Comparator comparator; + if ("ua.username".equals(sortBy)) { + comparator = Comparator.comparing(m -> valueOrDefault(m.getAuthor(), "").toLowerCase()); + } else if ("vvalue".equals(sortBy)) { + comparator = Comparator.comparingInt(ForumMessage::getVoteValue); + } else { + comparator = Comparator.comparingLong(ForumMessage::getCreatedEpochMillis); + } + if (!"asc".equalsIgnoreCase(sortType)) { + comparator = comparator.reversed(); + } + comparator = Comparator.comparing(ForumMessage::isSticky).reversed().thenComparing(comparator); + filtered.sort(comparator); + return filtered; + } + + private static ForumDisplayView paginate( + List filtered, + int pageNo, + String perPage, + String searchText, + String showType, + String showImg, + String sortBy, + String sortType + ) { + int totalRows = filtered == null ? 0 : filtered.size(); + int pageSize; + if ("all".equalsIgnoreCase(perPage)) { + pageSize = Math.max(totalRows, 1); + } else { + int parsed = parseInt(perPage, 50); + pageSize = parsed <= 0 ? 50 : parsed; + } + int totalPages = Math.max(1, (int) Math.ceil(totalRows / (double) pageSize)); + int currentPage = Math.min(Math.max(1, pageNo), totalPages); + int fromIdx = totalRows == 0 ? 0 : (currentPage - 1) * pageSize; + int toIdx = totalRows == 0 ? 0 : Math.min(fromIdx + pageSize, totalRows); + List page = totalRows == 0 ? List.of() : filtered.subList(fromIdx, toIdx); + + int startRow = totalRows == 0 ? 0 : fromIdx + 1; + int endRow = toIdx; + + return new ForumDisplayView( + page, + totalRows, + startRow, + endRow, + totalPages, + currentPage, + valueOrDefault(searchText, ""), + valueOrDefault(showType, "full"), + valueOrDefault(showImg, "true"), + valueOrDefault(sortBy, "fi.id"), + valueOrDefault(sortType, "desc"), + valueOrDefault(perPage, "50") + ); + } + + private static String valueOrDefault(String value, String defaultValue) { + return value == null || value.isBlank() ? defaultValue : value; + } + + private static boolean isMysqlAdmin(String username) { + return username != null && "kamma".equalsIgnoreCase(username); + } + + private static String mysqlIdentifier(String value) { + return "`" + value.replace("`", "``") + "`"; + } + + private static String xmlEscape(String input) { + if (input == null) { + return ""; + } + return input.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + private static String toCdata(String input) { + if (input == null) { + return ""; + } + return input.replace("]]>", "]]]]>"); + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/auth/AuthService.java b/src/main/java/cz/kamma/fabka/httpserver/auth/AuthService.java new file mode 100644 index 0000000..e537e31 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/auth/AuthService.java @@ -0,0 +1,5 @@ +package cz.kamma.fabka.httpserver.auth; + +public interface AuthService { + AuthenticatedUser authenticate(String username, String password); +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.java b/src/main/java/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.java new file mode 100644 index 0000000..e176f64 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.java b/src/main/java/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.java new file mode 100644 index 0000000..206e1cc --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.java @@ -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(); + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/auth/EnvAuthService.java b/src/main/java/cz/kamma/fabka/httpserver/auth/EnvAuthService.java new file mode 100644 index 0000000..8c117c6 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/auth/EnvAuthService.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/crypto/Md5.java b/src/main/java/cz/kamma/fabka/httpserver/crypto/Md5.java new file mode 100644 index 0000000..ec98c95 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/crypto/Md5.java @@ -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); + } + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.java b/src/main/java/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.java new file mode 100644 index 0000000..c9e8af2 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.java @@ -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"; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/MultipartFormData.java b/src/main/java/cz/kamma/fabka/httpserver/http/MultipartFormData.java new file mode 100644 index 0000000..f69b662 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/MultipartFormData.java @@ -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 fields; + private final Map> files; + + private MultipartFormData(Map fields, Map> 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 fields = new HashMap<>(); + Map> 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 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; + } + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/RequestContext.java b/src/main/java/cz/kamma/fabka/httpserver/http/RequestContext.java new file mode 100644 index 0000000..9c7c149 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/RequestContext.java @@ -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 queryParams; + private Map formParams; + private Map 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 queryParams() { + if (queryParams == null) { + queryParams = parseEncodedParams(exchange.getRequestURI().getRawQuery()); + } + return queryParams; + } + + public Map 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 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 parseCookies(Headers headers) { + String rawCookie = headers.getFirst("Cookie"); + if (rawCookie == null || rawCookie.isBlank()) { + return Collections.emptyMap(); + } + Map 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 parseEncodedParams(String encoded) { + if (encoded == null || encoded.isBlank()) { + return Collections.emptyMap(); + } + Map 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/Responses.java b/src/main/java/cz/kamma/fabka/httpserver/http/Responses.java new file mode 100644 index 0000000..2e5788c --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/Responses.java @@ -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(); + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/RouteHandler.java b/src/main/java/cz/kamma/fabka/httpserver/http/RouteHandler.java new file mode 100644 index 0000000..caceed7 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/RouteHandler.java @@ -0,0 +1,6 @@ +package cz.kamma.fabka.httpserver.http; + +@FunctionalInterface +public interface RouteHandler { + void handle(RequestContext ctx) throws Exception; +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/Router.java b/src/main/java/cz/kamma/fabka/httpserver/http/Router.java new file mode 100644 index 0000000..69fe03c --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/Router.java @@ -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 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.java b/src/main/java/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.java new file mode 100644 index 0000000..42c2cfd --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.java @@ -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"; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/AttachmentData.java b/src/main/java/cz/kamma/fabka/httpserver/repository/AttachmentData.java new file mode 100644 index 0000000..f0b774f --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/AttachmentData.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ChatLine.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ChatLine.java new file mode 100644 index 0000000..1a659e1 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ChatLine.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ChatRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ChatRepository.java new file mode 100644 index 0000000..bffc663 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ChatRepository.java @@ -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 listRecentChatLines(long currentUserId, int limit) { + List 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ChatVoteStats.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ChatVoteStats.java new file mode 100644 index 0000000..0b43a3c --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ChatVoteStats.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ForumAttachment.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumAttachment.java new file mode 100644 index 0000000..695b48c --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumAttachment.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDetail.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDetail.java new file mode 100644 index 0000000..3c38d2d --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDetail.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDisplayView.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDisplayView.java new file mode 100644 index 0000000..9a8e544 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDisplayView.java @@ -0,0 +1,94 @@ +package cz.kamma.fabka.httpserver.repository; + +import java.util.List; + +public class ForumDisplayView { + private final List 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 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 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ForumMessage.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumMessage.java new file mode 100644 index 0000000..62bd4a9 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumMessage.java @@ -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 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 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 getAttachments() { + return attachments; + } + + public boolean isSticky() { + return sticky; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ForumRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumRepository.java new file mode 100644 index 0000000..3a15ef6 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumRepository.java @@ -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 listActiveForums() { + List 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 getNewMessagesCountByUserId(long userId) { + Map 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 listMessagesByForumId(long forumId) { + List 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 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 listAttachmentsByForumItemId(long forumItemId) { + List 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()); + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/ForumSummary.java b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumSummary.java new file mode 100644 index 0000000..0f0a292 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/ForumSummary.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/MemberProfile.java b/src/main/java/cz/kamma/fabka/httpserver/repository/MemberProfile.java new file mode 100644 index 0000000..cccbc51 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/MemberProfile.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/MemberRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/MemberRepository.java new file mode 100644 index 0000000..54a3662 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/MemberRepository.java @@ -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()); + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.java b/src/main/java/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.java new file mode 100644 index 0000000..be4b3b6 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.java new file mode 100644 index 0000000..0a403f5 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.java @@ -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 listDatabases() { + List 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 listTables(String database) { + List 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 columns = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + columns.add(rsmd.getColumnLabel(i)); + } + result.setColumns(columns); + + List> rows = new ArrayList<>(); + while (rs.next()) { + List 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 columns = List.of(); + private List> 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 getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns == null ? List.of() : columns; + } + + public List> getRows() { + return rows; + } + + public void setRows(List> rows) { + this.rows = rows == null ? List.of() : rows; + } + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.java b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.java new file mode 100644 index 0000000..6a3a870 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.java new file mode 100644 index 0000000..7305c44 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.java @@ -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 listThreads(long userId, String orderBy) { + List 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 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 threadMessages(long userId, long pmid) { + List 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.java b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.java new file mode 100644 index 0000000..46a09b9 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.java b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.java new file mode 100644 index 0000000..61123e9 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.java b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.java new file mode 100644 index 0000000..075b6b5 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/QuotedTextItem.java b/src/main/java/cz/kamma/fabka/httpserver/repository/QuotedTextItem.java new file mode 100644 index 0000000..568700a --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/QuotedTextItem.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/SettingsRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/SettingsRepository.java new file mode 100644 index 0000000..aed20ae --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/SettingsRepository.java @@ -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 loadUserSettings(long userId) { + Map 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/UserIcon.java b/src/main/java/cz/kamma/fabka/httpserver/repository/UserIcon.java new file mode 100644 index 0000000..a368d58 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/UserIcon.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/UserIconRepository.java b/src/main/java/cz/kamma/fabka/httpserver/repository/UserIconRepository.java new file mode 100644 index 0000000..fd39ace --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/UserIconRepository.java @@ -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; + } + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/repository/VoteStats.java b/src/main/java/cz/kamma/fabka/httpserver/repository/VoteStats.java new file mode 100644 index 0000000..1c2d7ac --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/repository/VoteStats.java @@ -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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/session/SessionData.java b/src/main/java/cz/kamma/fabka/httpserver/session/SessionData.java new file mode 100644 index 0000000..c24ddd7 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/session/SessionData.java @@ -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 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 attributes() { + return attributes; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/session/SessionManager.java b/src/main/java/cz/kamma/fabka/httpserver/session/SessionManager.java new file mode 100644 index 0000000..9eeb370 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/session/SessionManager.java @@ -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 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 sessionAttributeValues(String attributeKey) { + cleanupExpired(); + List 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 activeSessions() { + cleanupExpired(); + return new ArrayList<>(sessions.values()); + } + + private boolean isExpired(SessionData data) { + return System.currentTimeMillis() - data.lastAccessMillis() > timeoutMillis; + } + + private void cleanupExpired() { + for (Map.Entry entry : sessions.entrySet()) { + if (isExpired(entry.getValue())) { + sessions.remove(entry.getKey()); + } + } + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.java b/src/main/java/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.java new file mode 100644 index 0000000..0215e34 --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.java @@ -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, ""); + } else { + rendered = rendered.replace(part, "" + part + ""); + } + } + } + return rendered.replace("\r\n", "
").replace("\n", "
"); + } + + 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("")) { + part = rendered.substring(rendered.indexOf("")); + parts = part.split(" "); + } else if (rendered.contains("")); + 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; + } +} diff --git a/src/main/java/cz/kamma/fabka/httpserver/web/Pages.java b/src/main/java/cz/kamma/fabka/httpserver/web/Pages.java new file mode 100644 index 0000000..3f9b41a --- /dev/null +++ b/src/main/java/cz/kamma/fabka/httpserver/web/Pages.java @@ -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 + ? "\n" + + "
\n" + + "
Invalid user or password.
\n" + + "

\n" + : ""; + return LOGIN_TEMPLATE + .replace("{{COMMON_HEADER}}", "") + .replace("{{COMMON_FOOTER}}", renderLoginFooter()) + .replace("{{ERROR_BLOCK}}", error); + } + + public static String forumPage( + String username, + List forums, + Map newCounts, + int loggedUsersCount, + List loggedUsers, + PrivateMessageStats pmStats + ) { + StringBuilder forumRows = new StringBuilder(); + if (forums == null || forums.isEmpty()) { + forumRows.append("No forums found.\n"); + } else { + for (ForumSummary forum : forums) { + forumRows.append("") + .append("") + .append("
") + .append(escapeHtml(forum.getName())).append("") + .append(forum.isLocked() ? "" : "") + .append("") + .append(renderNewCountText(newCounts == null ? null : newCounts.get(forum.getId()))) + .append("") + .append("
") + .append("
").append(escapeHtml(forum.getDescription())).append("
") + .append("
").append(escapeHtml(forum.getCreatedAt())).append("
") + .append("") + .append("") + .append("
") + .append("
").append(escapeHtml(forum.getLastPostUser())).append("
") + .append("
").append(escapeHtml(forum.getLastPostAt())).append("
") + .append("
") + .append("") + .append("") + .append(forum.getPostsCount()) + .append("") + .append("\n"); + } + } + + String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty()) + ? "-" + : loggedUsers.stream() + .map(user -> "" + escapeHtml(user) + " (0)") + .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 loggedUsers, + MessageRenderSettings renderSettings, + PrivateMessageStats pmStats + ) { + StringBuilder messageRows = new StringBuilder(); + List messages = view == null ? List.of() : view.getMessages(); + boolean showFull = view == null || !"simple".equalsIgnoreCase(view.getShowType()); + if (messages == null || messages.isEmpty()) { + messageRows.append("") + .append("
No messages.
\n"); + } else { + for (ForumMessage message : messages) { + messageRows.append("") + .append("") + .append("") + .append(showFull + ? "" + : "") + .append("") + .append("
") + .append("
") + .append(escapeHtml(message.getCreatedAt())) + .append(" ") + .append("") + .append(message.getVoteYes()).append("") + .append("") + .append(message.getVoteNo()).append("") + .append("") + .append("") + .append(message.getAuthorId() == currentUserId + ? "" + + "" + : "") + .append(" ") + .append(message.isSticky() ? "[STICKY]" : "SET [STICKY]") + .append("") + .append("
" + + "" + + "" + + "" + + "" + + "" + + "
 
" + + "
Join Date: " + escapeHtml(message.getAuthorJoinDate()) + "
" + + "
Location: " + escapeHtml(message.getAuthorCity()) + "
" + + "
Posts: " + message.getAuthorPosts() + "
" + + "
" + + "
") + .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("
\n"); + } + } + + String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty()) + ? "-" + : loggedUsers.stream() + .map(user -> "" + escapeHtml(user) + " (0)") + .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 = "
" + + "
Quoting:
" + + "" + + "
" + + "
Originally Posted by " + escapeHtml(quotedTextItem.getAuthor()) + "
" + + "
" + escapeHtml(quotedTextItem.getText()).replace("\n", "
") + "
" + + "
" + + "

"; + } + + 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 "" + + "
" + + "
"; + } + + public static String chatPage( + String username, + int loggedUsersCount, + List loggedUsers, + PrivateMessageStats pmStats + ) { + String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty()) + ? "-" + : loggedUsers.stream() + .map(user -> "" + escapeHtml(user) + " (0)") + .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 threads, + String orderBy, + String perPage, + int currentPage, + int totalPages, + int rowsFrom, + int rowsTo, + int totalRows, + int loggedUsersCount, + List loggedUsers, + PrivateMessageStats pmStats + ) { + StringBuilder rows = new StringBuilder(); + if (threads == null || threads.isEmpty()) { + rows.append("No private messages."); + } else { + for (PrivateThreadSummary thread : threads) { + rows.append("") + .append("") + .append(" ") + .append("") + .append("
").append(escapeHtml(thread.getCreatedAt())).append("") + .append(thread.isAllRead() ? "" : "") + .append("").append(escapeHtml(thread.getTitle())).append("") + .append(thread.isAllRead() ? "" : "") + .append("
") + .append("
").append(escapeHtml(thread.getSenderName())).append(" -> ").append(escapeHtml(thread.getRecipientName())).append("
") + .append("
Replies: ").append(thread.getReplies()).append("
") + .append("") + .append("") + .append(""); + } + } + 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 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 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 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 messages, + String perPage, + int currentPage, + int totalPages, + int rowsFrom, + int rowsTo, + int totalRows, + String error, + int loggedUsersCount, + List loggedUsers, + PrivateMessageStats pmStats + ) { + StringBuilder rows = new StringBuilder(); + if (messages == null || messages.isEmpty()) { + rows.append("
No messages.
"); + } else { + for (PrivateMessageItem item : messages) { + rows.append("") + .append("") + .append("") + .append("
") + .append(escapeHtml(item.getCreatedAt())) + .append("
") + .append("") + .append("
 
") + .append("
Join Date: ").append(escapeHtml(item.getFromJoinDate())).append("
") + .append("
Posts: ").append(item.getFromPosts()).append("
") + .append("
") + .append(LegacyMessageFormatter.convertMessageToHtml(item.getMessage(), null, null, null)) + .append("
"); + } + } + + 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 databases, + List tables, + String selectedDatabase, + String selectedTable, + String rowsToShow, + String myQuery, + String tableSql, + String command, + String info, + String error, + MysqlClientRepository.SqlExecution result, + int loggedUsersCount, + List loggedUsers, + PrivateMessageStats pmStats + ) { + StringBuilder dbOptions = new StringBuilder(); + if (databases == null || databases.isEmpty()) { + dbOptions.append(""); + } else { + for (String db : databases) { + dbOptions.append(""); + } + } + + StringBuilder tableOptions = new StringBuilder(); + if (tables == null || tables.isEmpty()) { + tableOptions.append(""); + } else { + for (String table : tables) { + tableOptions.append(""); + } + } + + StringBuilder resultHtml = new StringBuilder(); + if (result != null) { + if (result.isResultSet()) { + resultHtml.append(""); + resultHtml.append(""); + resultHtml.append(""); + for (String column : result.getColumns()) { + resultHtml.append(""); + } + resultHtml.append(""); + for (List row : result.getRows()) { + resultHtml.append(""); + for (String value : row) { + resultHtml.append(""); + } + resultHtml.append(""); + } + if (result.getRows().isEmpty()) { + resultHtml.append(""); + } + resultHtml.append("
Result set
").append(escapeHtml(column)).append("
").append(escapeHtml(value == null ? "NULL" : value)).append("
No rows.
"); + } else { + resultHtml.append("") + .append("") + .append("
Affected rows: ").append(result.getUpdateCount()).append("
"); + } + } + + String infoBlock = (info == null || info.isBlank()) + ? "" + : "
" + escapeHtml(info) + "
"; + String errorBlock = (error == null || error.isBlank()) + ? "" + : "
" + escapeHtml(error) + "
"; + + String body = "" + + "" + + "" + + "" + + "kAmMa's Forum MySQL Client" + + "" + + "
" + + "
" + + renderCommonHeader(username, "MySQL Client", true, pmStats) + + "
" + + infoBlock + + errorBlock + + "
" + + "" + + "
" + + " " + + " " + + " " + + " " + + "" + + "
" + + "
" + + " " + + " " + + "Show rows: " + + " " + + "" + + "
" + + "
" + + "
" + + "
" + + "" + + "
" + + "
" + + "
" + + "" + + "
" + + "
" + + resultHtml + + "
" + + renderCommonFooter(Math.max(0, loggedUsersCount), loggedUsers) + + "
"; + 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 + ? " - " + sectionHtml + "" + : " - " + 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 + ? " You have unread Private Message" + : " "; + String mysqlLink = mysqlAdmin ? " | MySQL Client" : ""; + String rightBlock = authenticated + ? "" + + "User image" + + "" + + "" + + "

" + + "
Welcome, " + welcomeName + ".
" + + "You last visited: N/A
" + + "
Private Messages: Unread " + unread + ", Total " + total + ".
" + + "
Forum | Chat | " + + "Logout" + + mysqlLink + "
" + + "" + + "" + : "" + + "

" + + "
Welcome, " + welcomeName + ".
" + + "" + + ""; + return "" + + "
" + + "" + + "" + + rightBlock + + "
kAmMa's Forum" + + sectionPart + + "" + newPmBanner + "
" + + "
" + + ""; + } + + private static String renderCommonFooter(int loggedUsersCount, List loggedUsers) { + String loggedUsersHtml = (loggedUsers == null || loggedUsers.isEmpty()) + ? "-" + : loggedUsers.stream() + .map(user -> "" + escapeHtml(user) + " (0)") + .collect(Collectors.joining(", ")); + return "" + + "" + + "" + + "" + + "
Currently Logged Users: " + + Math.max(0, loggedUsersCount) + + "
Who's Online
" + + loggedUsersHtml + + "

" + + "
" + + "kAmMa's Forum System Version 2.43
Copyright ©2006-2021" + + "
" + + ""; + } + + private static String renderErrorBlock(String error) { + if (error == null || error.isBlank()) { + return ""; + } + return "" + + "
" + + "
" + error + "
" + + "

"; + } + + private static String renderLoginFooter() { + return "
" + + "kAmMa's Forum System Version 2.43
Copyright ©2006-2021" + + "
"; + } + + private static String renderQuotedBlock(QuotedTextItem quotedItem) { + if (quotedItem == null) { + return ""; + } + return "
" + + "
Quote:
" + + "" + + "
" + + "
Originally Posted by " + escapeHtml(quotedItem.getAuthor()) + "
" + + "
" + + LegacyMessageFormatter.convertMessageToHtml(quotedItem.getText(), null, null, null) + + "
"; + } + + private static String renderAttachments(List attachments, String showImg) { + if (attachments == null || attachments.isEmpty()) { + return ""; + } + String mode = showImg == null ? "true" : showImg.toLowerCase(); + StringBuilder sb = new StringBuilder(); + sb.append("
Attached Files"); + boolean first = true; + for (ForumAttachment a : attachments) { + if (!first) { + sb.append(""); + } + first = false; + sb.append(""); + } + sb.append("

"); + if (a.isPicture()) { + if ("true".equals(mode) || "yes".equals(mode) || "only".equals(mode)) { + sb.append("") + .append(escapeHtml(a.getName())).append("
") + .append(" 800 ? "width='800'" : "") + .append("/>"); + } else if ("thumbnail".equals(mode)) { + sb.append("") + .append(escapeHtml(a.getName())).append("
") + .append("") + .append(""); + } else { + sb.append("") + .append(escapeHtml(a.getName())).append(""); + } + } else { + sb.append("") + .append(escapeHtml(a.getName())).append(""); + } + sb.append("
"); + 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(""); + } + 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(" « Previous"); + } + + for (int i = startPage; i <= endPage; i++) { + if (i == currentPage) { + sb.append(" ").append(i).append(""); + } else { + sb.append(" ").append(i).append(""); + } + } + + if (endPage < totalPages) { + int nextBlockPage = endPage + 1; + sb.append(" Next »"); + } + return sb.toString(); + } + + private static String buildPageTds(String baseLink, int currentPage, int totalPages) { + if (totalPages <= 1) { + return "1"; + } + 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("« Previous"); + } + + for (int i = startPage; i <= endPage; i++) { + if (i == currentPage) { + sb.append("").append(i).append(""); + } else { + sb.append("") + .append(i).append(""); + } + } + + if (endPage < totalPages) { + int nextBlockPage = endPage + 1; + sb.append("Next »"); + } + 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); + } + } +} diff --git a/src/main/resources/app.properties b/src/main/resources/app.properties new file mode 100644 index 0000000..02d181a --- /dev/null +++ b/src/main/resources/app.properties @@ -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 diff --git a/src/main/resources/webapp/chat.html b/src/main/resources/webapp/chat.html new file mode 100644 index 0000000..b0b4106 --- /dev/null +++ b/src/main/resources/webapp/chat.html @@ -0,0 +1,103 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+ + + + + + + + + + + + +
Chat
+
+
Message:
+
+
+
+ + +
+
+
+
+
+
+

Loading messages...

+
+
+
+ +
+
+ + {{COMMON_FOOTER}} +
+
+ + + + diff --git a/src/main/resources/webapp/client.js b/src/main/resources/webapp/client.js new file mode 100644 index 0000000..a8b0ada --- /dev/null +++ b/src/main/resources/webapp/client.js @@ -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, '' + part + ''); + } + } + 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 + "" + userNames[i].firstChild.data + " [Chat] (" + inactives[i].firstChild.data + "), "; + else + resStr = resStr + "" + userNames[i].firstChild.data + " (" + inactives[i].firstChild.data + "), "; + } + + 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 = ' '+yesVotes+''+noVotes+''; + */ + var chatVotingLine = ''; + newdiv += '
['+times[i].firstChild.data+chatVotingLine+'] - '+fromNames[i].firstChild.data+': '+convertLinks(texts[i].firstChild.data)+'
'; + } + + 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 = " You have unread Private Message"; + 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 + */ \ No newline at end of file diff --git a/src/main/resources/webapp/css/all.css b/src/main/resources/webapp/css/all.css new file mode 100644 index 0000000..9d5d622 --- /dev/null +++ b/src/main/resources/webapp/css/all.css @@ -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} \ No newline at end of file diff --git a/src/main/resources/webapp/favicon.ico b/src/main/resources/webapp/favicon.ico new file mode 100644 index 0000000..1442c75 Binary files /dev/null and b/src/main/resources/webapp/favicon.ico differ diff --git a/src/main/resources/webapp/forum.html b/src/main/resources/webapp/forum.html new file mode 100644 index 0000000..d865182 --- /dev/null +++ b/src/main/resources/webapp/forum.html @@ -0,0 +1,43 @@ + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+
+ + + + +
+
+ Search in threads: + +
+
+
+ + + + + + + + {{FORUM_ROWS}} + +
ThreadLast PostPosts
+
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/src/main/resources/webapp/forumdisplay.html b/src/main/resources/webapp/forumdisplay.html new file mode 100644 index 0000000..6e3ff22 --- /dev/null +++ b/src/main/resources/webapp/forumdisplay.html @@ -0,0 +1,124 @@ + + + + + + + + kAmMa's Forum - {{FORUM_NAME}} + + +
+
+
+ {{COMMON_HEADER}} +
+ + + + + + +
{{FORUM_NAME}}
{{FORUM_DESCRIPTION}}
+
+ + +
+ + + + + + + + + + + +
Reply
+
+ {{QUOTE_BLOCK}} +
+
Message:
+ +
+ +
+
+ + +
+
+
+
+ {{COUNTDOWN_BLOCK}} + +
+ + + + + + + + + {{PAGE_TDS}} + +
+
+ + + + + + + + Search in thread: + +
+
+ Show type: + + + Show images: + + + Sort messages by: + + + + Records per page: + + Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}}
+
+
+ + {{MESSAGE_ROWS}} +
+ + {{COMMON_FOOTER}} +
+
+ + diff --git a/src/main/resources/webapp/images/busy.gif b/src/main/resources/webapp/images/busy.gif new file mode 100644 index 0000000..80ff48b Binary files /dev/null and b/src/main/resources/webapp/images/busy.gif differ diff --git a/src/main/resources/webapp/images/cat-line.gif b/src/main/resources/webapp/images/cat-line.gif new file mode 100644 index 0000000..a34f1b5 Binary files /dev/null and b/src/main/resources/webapp/images/cat-line.gif differ diff --git a/src/main/resources/webapp/images/cellpic-fp-big.gif b/src/main/resources/webapp/images/cellpic-fp-big.gif new file mode 100644 index 0000000..ed29c7a Binary files /dev/null and b/src/main/resources/webapp/images/cellpic-fp-big.gif differ diff --git a/src/main/resources/webapp/images/cellpic-fp.gif b/src/main/resources/webapp/images/cellpic-fp.gif new file mode 100644 index 0000000..147f209 Binary files /dev/null and b/src/main/resources/webapp/images/cellpic-fp.gif differ diff --git a/src/main/resources/webapp/images/cernypetr.jpg b/src/main/resources/webapp/images/cernypetr.jpg new file mode 100644 index 0000000..c814686 Binary files /dev/null and b/src/main/resources/webapp/images/cernypetr.jpg differ diff --git a/src/main/resources/webapp/images/delete.gif b/src/main/resources/webapp/images/delete.gif new file mode 100644 index 0000000..6775d0c Binary files /dev/null and b/src/main/resources/webapp/images/delete.gif differ diff --git a/src/main/resources/webapp/images/edit.gif b/src/main/resources/webapp/images/edit.gif new file mode 100644 index 0000000..91f28bc Binary files /dev/null and b/src/main/resources/webapp/images/edit.gif differ diff --git a/src/main/resources/webapp/images/gif.gif b/src/main/resources/webapp/images/gif.gif new file mode 100644 index 0000000..38da29d Binary files /dev/null and b/src/main/resources/webapp/images/gif.gif differ diff --git a/src/main/resources/webapp/images/icon1.gif b/src/main/resources/webapp/images/icon1.gif new file mode 100644 index 0000000..3253ff1 Binary files /dev/null and b/src/main/resources/webapp/images/icon1.gif differ diff --git a/src/main/resources/webapp/images/locked.gif b/src/main/resources/webapp/images/locked.gif new file mode 100644 index 0000000..ae18536 Binary files /dev/null and b/src/main/resources/webapp/images/locked.gif differ diff --git a/src/main/resources/webapp/images/newthread.gif b/src/main/resources/webapp/images/newthread.gif new file mode 100644 index 0000000..7f56063 Binary files /dev/null and b/src/main/resources/webapp/images/newthread.gif differ diff --git a/src/main/resources/webapp/images/pm.gif b/src/main/resources/webapp/images/pm.gif new file mode 100644 index 0000000..444825c Binary files /dev/null and b/src/main/resources/webapp/images/pm.gif differ diff --git a/src/main/resources/webapp/images/pm_read.gif b/src/main/resources/webapp/images/pm_read.gif new file mode 100644 index 0000000..e0cf467 Binary files /dev/null and b/src/main/resources/webapp/images/pm_read.gif differ diff --git a/src/main/resources/webapp/images/post_old.gif b/src/main/resources/webapp/images/post_old.gif new file mode 100644 index 0000000..b3097c6 Binary files /dev/null and b/src/main/resources/webapp/images/post_old.gif differ diff --git a/src/main/resources/webapp/images/quote.gif b/src/main/resources/webapp/images/quote.gif new file mode 100644 index 0000000..d386e0d Binary files /dev/null and b/src/main/resources/webapp/images/quote.gif differ diff --git a/src/main/resources/webapp/images/reply.gif b/src/main/resources/webapp/images/reply.gif new file mode 100644 index 0000000..9090957 Binary files /dev/null and b/src/main/resources/webapp/images/reply.gif differ diff --git a/src/main/resources/webapp/images/sortasc.gif b/src/main/resources/webapp/images/sortasc.gif new file mode 100644 index 0000000..603e7a4 Binary files /dev/null and b/src/main/resources/webapp/images/sortasc.gif differ diff --git a/src/main/resources/webapp/images/sortdesc.gif b/src/main/resources/webapp/images/sortdesc.gif new file mode 100644 index 0000000..7facfb5 Binary files /dev/null and b/src/main/resources/webapp/images/sortdesc.gif differ diff --git a/src/main/resources/webapp/images/voting_no.png b/src/main/resources/webapp/images/voting_no.png new file mode 100644 index 0000000..47bfe12 Binary files /dev/null and b/src/main/resources/webapp/images/voting_no.png differ diff --git a/src/main/resources/webapp/images/voting_yes.png b/src/main/resources/webapp/images/voting_yes.png new file mode 100644 index 0000000..8a59dbf Binary files /dev/null and b/src/main/resources/webapp/images/voting_yes.png differ diff --git a/src/main/resources/webapp/images/whos_online.gif b/src/main/resources/webapp/images/whos_online.gif new file mode 100644 index 0000000..a8e2343 Binary files /dev/null and b/src/main/resources/webapp/images/whos_online.gif differ diff --git a/src/main/resources/webapp/login.html b/src/main/resources/webapp/login.html new file mode 100644 index 0000000..dc664f3 --- /dev/null +++ b/src/main/resources/webapp/login.html @@ -0,0 +1,44 @@ + + + + + + + + kAmMa's Forum + + +
+
+
+
+ {{COMMON_HEADER}} +
+ + +
+
+ + + + + + + + + + + + + +
 
+
+
+
+ {{ERROR_BLOCK}} + {{COMMON_FOOTER}} +
+
+
+ + diff --git a/src/main/resources/webapp/member.html b/src/main/resources/webapp/member.html new file mode 100644 index 0000000..52543e4 --- /dev/null +++ b/src/main/resources/webapp/member.html @@ -0,0 +1,214 @@ + + + + + + + + + kAmMa's Forum - Member + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + + + + + + + + +
Member details
+
+
+
+ Account + + + + + +
Username: {{USERNAME}}
Join Date: {{JOIN_DATE}}
+
+
+
+
+ +
+ +
+ + + + + + + +
Change user icon
+
+
+
+ User Icon + + + + + + + + + +
+ Current user icon
+ + +
+ Browse your local disk for user icon
+ +
+
+
+ +
+
+
+
+
+ +
+ +
+ + + + + + + +
Change password
+
+
+
+ Old Password + + + + + +
Please enter your current password.
+
+
+ New Password + + + + + + + + +
Please enter a new password for your account.
New Password:
Confirm New Password:
+
+
+ + +
+
+
+
+
+ +
+ +
+ + + + + + + +
Change personal info
+
+
+
+ Email Address + + + + + + + + +
Please enter a valid email address.
Email Address:
Confirm Email Address:
+
+
+ Other informations + + + + + + +
First Name:
 
Last Name:
 
City:
 
+
+
+ Sounds + + + + +
Sound notifications:  
+
+
+ + +
+
+
+
+
+ +
+ {{COMMON_FOOTER}} +
+
+ + + + diff --git a/src/main/resources/webapp/message.html b/src/main/resources/webapp/message.html new file mode 100644 index 0000000..8e92335 --- /dev/null +++ b/src/main/resources/webapp/message.html @@ -0,0 +1,81 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + + + + + + + + + + +
Reply to {{TO_USERNAME}}
+
+
+
+
+
Message:
+
+
+
+ +
+
+
+
+ + + + + +
+
+
+
+
+
+
+
+ + + +
 
Private Messages: {{THREAD_TITLE}}
+
+ + + + +
+
+ + + Records per page: + +
+ Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}} {{PAGE_LINKS}} +
+
+
+ {{MESSAGE_ROWS}} +
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/src/main/resources/webapp/newpm.html b/src/main/resources/webapp/newpm.html new file mode 100644 index 0000000..3a093a2 --- /dev/null +++ b/src/main/resources/webapp/newpm.html @@ -0,0 +1,64 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + + + + + + + +
+
 
+ Private Message to : {{TO_USERNAME}} +
+ +
+ + + + + + + +
+
+
+ + + + + + + + +
Title:
  
+
Message text:
+
+ +
+
+
+
+
+
+
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/src/main/resources/webapp/newthread.html b/src/main/resources/webapp/newthread.html new file mode 100644 index 0000000..a3d7394 --- /dev/null +++ b/src/main/resources/webapp/newthread.html @@ -0,0 +1,62 @@ + + + + + + + + + kAmMa's Forum - New Thread + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + +
+ + + + + + + + + +
Post New Thread
+
+
+
Logged in as {{USERNAME}}
+ + + + + + + + +
Title:
  
+
Description:
+
+ +
+
+ Thread password +
+
+
+
+
+ +
+
+
+
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/src/main/resources/webapp/notif.wav b/src/main/resources/webapp/notif.wav new file mode 100644 index 0000000..3756db5 Binary files /dev/null and b/src/main/resources/webapp/notif.wav differ diff --git a/src/main/resources/webapp/private.html b/src/main/resources/webapp/private.html new file mode 100644 index 0000000..e931bc9 --- /dev/null +++ b/src/main/resources/webapp/private.html @@ -0,0 +1,109 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+
+ + + + +
+
+ + + Records per page: + +
+ Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}} {{PAGE_LINKS}} +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + {{THREAD_ROWS}} + + + + + + +
+ + + +
Private Messages
+
+ +
Title / Sender -> Recipient
+
+
+ Selected Messages: + + +
+
+
+
+ {{COMMON_FOOTER}} +
+
+ + + + diff --git a/target/classes/app.properties b/target/classes/app.properties new file mode 100644 index 0000000..02d181a --- /dev/null +++ b/target/classes/app.properties @@ -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 diff --git a/target/classes/cz/kamma/fabka/httpserver/AppConfig.class b/target/classes/cz/kamma/fabka/httpserver/AppConfig.class new file mode 100644 index 0000000..3c5853b Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/AppConfig.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/HttpServerApplication.class b/target/classes/cz/kamma/fabka/httpserver/HttpServerApplication.class new file mode 100644 index 0000000..4b68b40 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/HttpServerApplication.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/auth/AuthService.class b/target/classes/cz/kamma/fabka/httpserver/auth/AuthService.class new file mode 100644 index 0000000..20fdf3e Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/auth/AuthService.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.class b/target/classes/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.class new file mode 100644 index 0000000..81e1866 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.class b/target/classes/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.class new file mode 100644 index 0000000..38f41d7 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/auth/EnvAuthService.class b/target/classes/cz/kamma/fabka/httpserver/auth/EnvAuthService.class new file mode 100644 index 0000000..f023ee5 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/auth/EnvAuthService.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/crypto/Md5.class b/target/classes/cz/kamma/fabka/httpserver/crypto/Md5.class new file mode 100644 index 0000000..8d6ff3a Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/crypto/Md5.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.class b/target/classes/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.class new file mode 100644 index 0000000..c536aa6 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/MultipartFormData$FileItem.class b/target/classes/cz/kamma/fabka/httpserver/http/MultipartFormData$FileItem.class new file mode 100644 index 0000000..388b6ea Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/MultipartFormData$FileItem.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/MultipartFormData.class b/target/classes/cz/kamma/fabka/httpserver/http/MultipartFormData.class new file mode 100644 index 0000000..56ae049 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/MultipartFormData.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/RequestContext.class b/target/classes/cz/kamma/fabka/httpserver/http/RequestContext.class new file mode 100644 index 0000000..bcf87ab Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/RequestContext.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/Responses.class b/target/classes/cz/kamma/fabka/httpserver/http/Responses.class new file mode 100644 index 0000000..639a479 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/Responses.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/RouteHandler.class b/target/classes/cz/kamma/fabka/httpserver/http/RouteHandler.class new file mode 100644 index 0000000..921b600 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/RouteHandler.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/Router.class b/target/classes/cz/kamma/fabka/httpserver/http/Router.class new file mode 100644 index 0000000..c75079c Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/Router.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.class b/target/classes/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.class new file mode 100644 index 0000000..333ba15 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/AttachmentData.class b/target/classes/cz/kamma/fabka/httpserver/repository/AttachmentData.class new file mode 100644 index 0000000..7de3684 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/AttachmentData.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ChatLine.class b/target/classes/cz/kamma/fabka/httpserver/repository/ChatLine.class new file mode 100644 index 0000000..6bccd38 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ChatLine.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ChatRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/ChatRepository.class new file mode 100644 index 0000000..c33ecbd Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ChatRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ChatVoteStats.class b/target/classes/cz/kamma/fabka/httpserver/repository/ChatVoteStats.class new file mode 100644 index 0000000..6faaea2 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ChatVoteStats.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ForumAttachment.class b/target/classes/cz/kamma/fabka/httpserver/repository/ForumAttachment.class new file mode 100644 index 0000000..f61b883 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ForumAttachment.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ForumDetail.class b/target/classes/cz/kamma/fabka/httpserver/repository/ForumDetail.class new file mode 100644 index 0000000..a3a9fda Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ForumDetail.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ForumDisplayView.class b/target/classes/cz/kamma/fabka/httpserver/repository/ForumDisplayView.class new file mode 100644 index 0000000..88459f4 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ForumDisplayView.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ForumMessage.class b/target/classes/cz/kamma/fabka/httpserver/repository/ForumMessage.class new file mode 100644 index 0000000..9fd66ed Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ForumMessage.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ForumRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/ForumRepository.class new file mode 100644 index 0000000..08beb35 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ForumRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/ForumSummary.class b/target/classes/cz/kamma/fabka/httpserver/repository/ForumSummary.class new file mode 100644 index 0000000..bfb1507 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/ForumSummary.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/MemberProfile.class b/target/classes/cz/kamma/fabka/httpserver/repository/MemberProfile.class new file mode 100644 index 0000000..20e9b8c Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/MemberProfile.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/MemberRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/MemberRepository.class new file mode 100644 index 0000000..b1146f7 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/MemberRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.class b/target/classes/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.class new file mode 100644 index 0000000..167e681 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/MysqlClientRepository$SqlExecution.class b/target/classes/cz/kamma/fabka/httpserver/repository/MysqlClientRepository$SqlExecution.class new file mode 100644 index 0000000..d513bf6 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/MysqlClientRepository$SqlExecution.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.class new file mode 100644 index 0000000..ae141f6 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.class b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.class new file mode 100644 index 0000000..695ed94 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.class new file mode 100644 index 0000000..409e42f Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.class b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.class new file mode 100644 index 0000000..8ae4dd2 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.class b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.class new file mode 100644 index 0000000..64583a6 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.class b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.class new file mode 100644 index 0000000..186fb01 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/QuotedTextItem.class b/target/classes/cz/kamma/fabka/httpserver/repository/QuotedTextItem.class new file mode 100644 index 0000000..a9cdd11 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/QuotedTextItem.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/SettingsRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/SettingsRepository.class new file mode 100644 index 0000000..8f80eda Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/SettingsRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/UserIcon.class b/target/classes/cz/kamma/fabka/httpserver/repository/UserIcon.class new file mode 100644 index 0000000..9c4d2d6 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/UserIcon.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/UserIconRepository.class b/target/classes/cz/kamma/fabka/httpserver/repository/UserIconRepository.class new file mode 100644 index 0000000..ca6e8f1 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/UserIconRepository.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/repository/VoteStats.class b/target/classes/cz/kamma/fabka/httpserver/repository/VoteStats.class new file mode 100644 index 0000000..9e837b9 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/repository/VoteStats.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/session/SessionData.class b/target/classes/cz/kamma/fabka/httpserver/session/SessionData.class new file mode 100644 index 0000000..22555d5 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/session/SessionData.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/session/SessionManager.class b/target/classes/cz/kamma/fabka/httpserver/session/SessionManager.class new file mode 100644 index 0000000..ca35e82 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/session/SessionManager.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.class b/target/classes/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.class new file mode 100644 index 0000000..690a4ac Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.class differ diff --git a/target/classes/cz/kamma/fabka/httpserver/web/Pages.class b/target/classes/cz/kamma/fabka/httpserver/web/Pages.class new file mode 100644 index 0000000..2b39846 Binary files /dev/null and b/target/classes/cz/kamma/fabka/httpserver/web/Pages.class differ diff --git a/target/classes/webapp/chat.html b/target/classes/webapp/chat.html new file mode 100644 index 0000000..b0b4106 --- /dev/null +++ b/target/classes/webapp/chat.html @@ -0,0 +1,103 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+ + + + + + + + + + + + +
Chat
+
+
Message:
+
+
+
+ + +
+
+
+
+
+
+

Loading messages...

+
+
+
+ +
+
+ + {{COMMON_FOOTER}} +
+
+ + + + diff --git a/target/classes/webapp/client.js b/target/classes/webapp/client.js new file mode 100644 index 0000000..a8b0ada --- /dev/null +++ b/target/classes/webapp/client.js @@ -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, '
' + part + ''); + } + } + 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 + "" + userNames[i].firstChild.data + " [Chat] (" + inactives[i].firstChild.data + "), "; + else + resStr = resStr + "" + userNames[i].firstChild.data + " (" + inactives[i].firstChild.data + "), "; + } + + 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 = ' '+yesVotes+''+noVotes+''; + */ + var chatVotingLine = ''; + newdiv += '
['+times[i].firstChild.data+chatVotingLine+'] - '+fromNames[i].firstChild.data+': '+convertLinks(texts[i].firstChild.data)+'
'; + } + + 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 = " You have unread Private Message"; + 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 + */ \ No newline at end of file diff --git a/target/classes/webapp/css/all.css b/target/classes/webapp/css/all.css new file mode 100644 index 0000000..9d5d622 --- /dev/null +++ b/target/classes/webapp/css/all.css @@ -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} \ No newline at end of file diff --git a/target/classes/webapp/favicon.ico b/target/classes/webapp/favicon.ico new file mode 100644 index 0000000..1442c75 Binary files /dev/null and b/target/classes/webapp/favicon.ico differ diff --git a/target/classes/webapp/forum.html b/target/classes/webapp/forum.html new file mode 100644 index 0000000..d865182 --- /dev/null +++ b/target/classes/webapp/forum.html @@ -0,0 +1,43 @@ + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+
+ + + + +
+
+ Search in threads: + +
+
+
+ + + + + + + + {{FORUM_ROWS}} + +
ThreadLast PostPosts
+
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/target/classes/webapp/forumdisplay.html b/target/classes/webapp/forumdisplay.html new file mode 100644 index 0000000..6e3ff22 --- /dev/null +++ b/target/classes/webapp/forumdisplay.html @@ -0,0 +1,124 @@ + + + + + + + + kAmMa's Forum - {{FORUM_NAME}} + + +
+
+
+ {{COMMON_HEADER}} +
+ + + + + + +
{{FORUM_NAME}}
{{FORUM_DESCRIPTION}}
+
+ + +
+ + + + + + + + + + + +
Reply
+
+ {{QUOTE_BLOCK}} +
+
Message:
+ +
+ +
+
+ + +
+
+
+
+ {{COUNTDOWN_BLOCK}} + +
+ + + + + + + + + {{PAGE_TDS}} + +
+
+ + + + + + + + Search in thread: + +
+
+ Show type: + + + Show images: + + + Sort messages by: + + + + Records per page: + + Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}}
+
+
+ + {{MESSAGE_ROWS}} +
+ + {{COMMON_FOOTER}} +
+
+ + diff --git a/target/classes/webapp/images/busy.gif b/target/classes/webapp/images/busy.gif new file mode 100644 index 0000000..80ff48b Binary files /dev/null and b/target/classes/webapp/images/busy.gif differ diff --git a/target/classes/webapp/images/cat-line.gif b/target/classes/webapp/images/cat-line.gif new file mode 100644 index 0000000..a34f1b5 Binary files /dev/null and b/target/classes/webapp/images/cat-line.gif differ diff --git a/target/classes/webapp/images/cellpic-fp-big.gif b/target/classes/webapp/images/cellpic-fp-big.gif new file mode 100644 index 0000000..ed29c7a Binary files /dev/null and b/target/classes/webapp/images/cellpic-fp-big.gif differ diff --git a/target/classes/webapp/images/cellpic-fp.gif b/target/classes/webapp/images/cellpic-fp.gif new file mode 100644 index 0000000..147f209 Binary files /dev/null and b/target/classes/webapp/images/cellpic-fp.gif differ diff --git a/target/classes/webapp/images/cernypetr.jpg b/target/classes/webapp/images/cernypetr.jpg new file mode 100644 index 0000000..c814686 Binary files /dev/null and b/target/classes/webapp/images/cernypetr.jpg differ diff --git a/target/classes/webapp/images/delete.gif b/target/classes/webapp/images/delete.gif new file mode 100644 index 0000000..6775d0c Binary files /dev/null and b/target/classes/webapp/images/delete.gif differ diff --git a/target/classes/webapp/images/edit.gif b/target/classes/webapp/images/edit.gif new file mode 100644 index 0000000..91f28bc Binary files /dev/null and b/target/classes/webapp/images/edit.gif differ diff --git a/target/classes/webapp/images/gif.gif b/target/classes/webapp/images/gif.gif new file mode 100644 index 0000000..38da29d Binary files /dev/null and b/target/classes/webapp/images/gif.gif differ diff --git a/target/classes/webapp/images/icon1.gif b/target/classes/webapp/images/icon1.gif new file mode 100644 index 0000000..3253ff1 Binary files /dev/null and b/target/classes/webapp/images/icon1.gif differ diff --git a/target/classes/webapp/images/locked.gif b/target/classes/webapp/images/locked.gif new file mode 100644 index 0000000..ae18536 Binary files /dev/null and b/target/classes/webapp/images/locked.gif differ diff --git a/target/classes/webapp/images/newthread.gif b/target/classes/webapp/images/newthread.gif new file mode 100644 index 0000000..7f56063 Binary files /dev/null and b/target/classes/webapp/images/newthread.gif differ diff --git a/target/classes/webapp/images/pm.gif b/target/classes/webapp/images/pm.gif new file mode 100644 index 0000000..444825c Binary files /dev/null and b/target/classes/webapp/images/pm.gif differ diff --git a/target/classes/webapp/images/pm_read.gif b/target/classes/webapp/images/pm_read.gif new file mode 100644 index 0000000..e0cf467 Binary files /dev/null and b/target/classes/webapp/images/pm_read.gif differ diff --git a/target/classes/webapp/images/post_old.gif b/target/classes/webapp/images/post_old.gif new file mode 100644 index 0000000..b3097c6 Binary files /dev/null and b/target/classes/webapp/images/post_old.gif differ diff --git a/target/classes/webapp/images/quote.gif b/target/classes/webapp/images/quote.gif new file mode 100644 index 0000000..d386e0d Binary files /dev/null and b/target/classes/webapp/images/quote.gif differ diff --git a/target/classes/webapp/images/reply.gif b/target/classes/webapp/images/reply.gif new file mode 100644 index 0000000..9090957 Binary files /dev/null and b/target/classes/webapp/images/reply.gif differ diff --git a/target/classes/webapp/images/sortasc.gif b/target/classes/webapp/images/sortasc.gif new file mode 100644 index 0000000..603e7a4 Binary files /dev/null and b/target/classes/webapp/images/sortasc.gif differ diff --git a/target/classes/webapp/images/sortdesc.gif b/target/classes/webapp/images/sortdesc.gif new file mode 100644 index 0000000..7facfb5 Binary files /dev/null and b/target/classes/webapp/images/sortdesc.gif differ diff --git a/target/classes/webapp/images/voting_no.png b/target/classes/webapp/images/voting_no.png new file mode 100644 index 0000000..47bfe12 Binary files /dev/null and b/target/classes/webapp/images/voting_no.png differ diff --git a/target/classes/webapp/images/voting_yes.png b/target/classes/webapp/images/voting_yes.png new file mode 100644 index 0000000..8a59dbf Binary files /dev/null and b/target/classes/webapp/images/voting_yes.png differ diff --git a/target/classes/webapp/images/whos_online.gif b/target/classes/webapp/images/whos_online.gif new file mode 100644 index 0000000..a8e2343 Binary files /dev/null and b/target/classes/webapp/images/whos_online.gif differ diff --git a/target/classes/webapp/login.html b/target/classes/webapp/login.html new file mode 100644 index 0000000..dc664f3 --- /dev/null +++ b/target/classes/webapp/login.html @@ -0,0 +1,44 @@ + + + + + + + + kAmMa's Forum + + +
+
+
+
+ {{COMMON_HEADER}} +
+ + +
+
+ + + + + + + + + + + + + +
 
+
+
+
+ {{ERROR_BLOCK}} + {{COMMON_FOOTER}} +
+
+
+ + diff --git a/target/classes/webapp/member.html b/target/classes/webapp/member.html new file mode 100644 index 0000000..52543e4 --- /dev/null +++ b/target/classes/webapp/member.html @@ -0,0 +1,214 @@ + + + + + + + + + kAmMa's Forum - Member + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + + + + + + + + +
Member details
+
+
+
+ Account + + + + + +
Username: {{USERNAME}}
Join Date: {{JOIN_DATE}}
+
+
+
+
+ +
+ +
+ + + + + + + +
Change user icon
+
+
+
+ User Icon + + + + + + + + + +
+ Current user icon
+ + +
+ Browse your local disk for user icon
+ +
+
+
+ +
+
+
+
+
+ +
+ +
+ + + + + + + +
Change password
+
+
+
+ Old Password + + + + + +
Please enter your current password.
+
+
+ New Password + + + + + + + + +
Please enter a new password for your account.
New Password:
Confirm New Password:
+
+
+ + +
+
+
+
+
+ +
+ +
+ + + + + + + +
Change personal info
+
+
+
+ Email Address + + + + + + + + +
Please enter a valid email address.
Email Address:
Confirm Email Address:
+
+
+ Other informations + + + + + + +
First Name:
 
Last Name:
 
City:
 
+
+
+ Sounds + + + + +
Sound notifications:  
+
+
+ + +
+
+
+
+
+ +
+ {{COMMON_FOOTER}} +
+
+ + + + diff --git a/target/classes/webapp/message.html b/target/classes/webapp/message.html new file mode 100644 index 0000000..8e92335 --- /dev/null +++ b/target/classes/webapp/message.html @@ -0,0 +1,81 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + + + + + + + + + + +
Reply to {{TO_USERNAME}}
+
+
+
+
+
Message:
+
+
+
+ +
+
+
+
+ + + + + +
+
+
+
+
+
+
+
+ + + +
 
Private Messages: {{THREAD_TITLE}}
+
+ + + + +
+
+ + + Records per page: + +
+ Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}} {{PAGE_LINKS}} +
+
+
+ {{MESSAGE_ROWS}} +
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/target/classes/webapp/newpm.html b/target/classes/webapp/newpm.html new file mode 100644 index 0000000..3a093a2 --- /dev/null +++ b/target/classes/webapp/newpm.html @@ -0,0 +1,64 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + + + + + + + +
+
 
+ Private Message to : {{TO_USERNAME}} +
+ +
+ + + + + + + +
+
+
+ + + + + + + + +
Title:
  
+
Message text:
+
+ +
+
+
+
+
+
+
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/target/classes/webapp/newthread.html b/target/classes/webapp/newthread.html new file mode 100644 index 0000000..a3d7394 --- /dev/null +++ b/target/classes/webapp/newthread.html @@ -0,0 +1,62 @@ + + + + + + + + + kAmMa's Forum - New Thread + + +
+
+
+ {{COMMON_HEADER}} +
+ {{ERROR_BLOCK}} + +
+ + + + + + + + + +
Post New Thread
+
+
+
Logged in as {{USERNAME}}
+ + + + + + + + +
Title:
  
+
Description:
+
+ +
+
+ Thread password +
+
+
+
+
+ +
+
+
+
+ {{COMMON_FOOTER}} +
+
+ + diff --git a/target/classes/webapp/notif.wav b/target/classes/webapp/notif.wav new file mode 100644 index 0000000..3756db5 Binary files /dev/null and b/target/classes/webapp/notif.wav differ diff --git a/target/classes/webapp/private.html b/target/classes/webapp/private.html new file mode 100644 index 0000000..e931bc9 --- /dev/null +++ b/target/classes/webapp/private.html @@ -0,0 +1,109 @@ + + + + + + + + + kAmMa's Forum + + +
+
+
+ {{COMMON_HEADER}} +
+
+ + + + +
+
+ + + Records per page: + +
+ Rows {{ROWS_FROM}} - {{ROWS_TO}} of {{ROWS_TOTAL}} {{PAGE_LINKS}} +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + {{THREAD_ROWS}} + + + + + + +
+ + + +
Private Messages
+
+ +
Title / Sender -> Recipient
+
+
+ Selected Messages: + + +
+
+
+
+ {{COMMON_FOOTER}} +
+
+ + + + diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..0114e22 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,44 @@ +cz/kamma/fabka/httpserver/session/SessionData.class +cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.class +cz/kamma/fabka/httpserver/repository/MysqlClientRepository.class +cz/kamma/fabka/httpserver/http/Router.class +cz/kamma/fabka/httpserver/repository/PrivateMessageItem.class +cz/kamma/fabka/httpserver/http/Responses.class +cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.class +cz/kamma/fabka/httpserver/session/SessionManager.class +cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.class +cz/kamma/fabka/httpserver/repository/QuotedTextItem.class +cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.class +cz/kamma/fabka/httpserver/repository/MemberProfile.class +cz/kamma/fabka/httpserver/repository/ForumSummary.class +cz/kamma/fabka/httpserver/repository/MysqlClientRepository$SqlExecution.class +cz/kamma/fabka/httpserver/repository/ChatVoteStats.class +cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.class +cz/kamma/fabka/httpserver/auth/AuthService.class +cz/kamma/fabka/httpserver/HttpServerApplication.class +cz/kamma/fabka/httpserver/repository/UserIcon.class +cz/kamma/fabka/httpserver/auth/DatabaseAuthService.class +cz/kamma/fabka/httpserver/repository/MessageRenderSettings.class +cz/kamma/fabka/httpserver/repository/PrivateMessageStats.class +cz/kamma/fabka/httpserver/web/Pages.class +cz/kamma/fabka/httpserver/repository/ForumDisplayView.class +cz/kamma/fabka/httpserver/repository/ForumRepository.class +cz/kamma/fabka/httpserver/auth/EnvAuthService.class +cz/kamma/fabka/httpserver/repository/ForumMessage.class +cz/kamma/fabka/httpserver/http/RouteHandler.class +cz/kamma/fabka/httpserver/AppConfig.class +cz/kamma/fabka/httpserver/repository/SettingsRepository.class +cz/kamma/fabka/httpserver/repository/ChatLine.class +cz/kamma/fabka/httpserver/repository/ForumAttachment.class +cz/kamma/fabka/httpserver/repository/AttachmentData.class +cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.class +cz/kamma/fabka/httpserver/repository/MemberRepository.class +cz/kamma/fabka/httpserver/auth/AuthenticatedUser.class +cz/kamma/fabka/httpserver/repository/VoteStats.class +cz/kamma/fabka/httpserver/http/RequestContext.class +cz/kamma/fabka/httpserver/http/MultipartFormData.class +cz/kamma/fabka/httpserver/repository/UserIconRepository.class +cz/kamma/fabka/httpserver/repository/ChatRepository.class +cz/kamma/fabka/httpserver/http/MultipartFormData$FileItem.class +cz/kamma/fabka/httpserver/crypto/Md5.class +cz/kamma/fabka/httpserver/repository/ForumDetail.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..8f9e8d4 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,42 @@ +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/AppConfig.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/HttpServerApplication.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/auth/AuthService.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/auth/AuthenticatedUser.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/auth/DatabaseAuthService.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/auth/EnvAuthService.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/crypto/Md5.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/ClasspathStaticFileHandler.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/MultipartFormData.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/RequestContext.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/Responses.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/RouteHandler.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/Router.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/http/StaticFileHttpHandler.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/AttachmentData.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ChatLine.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ChatRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ChatVoteStats.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ForumAttachment.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDetail.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ForumDisplayView.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ForumMessage.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ForumRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/ForumSummary.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/MemberProfile.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/MemberRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/MessageRenderSettings.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/MysqlClientRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageItem.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateMessageStats.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadRoot.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/PrivateThreadSummary.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/QuotedTextItem.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/SettingsRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/UserIcon.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/UserIconRepository.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/repository/VoteStats.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/session/SessionData.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/session/SessionManager.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/web/LegacyMessageFormatter.java +/home/kamma/projects/FabkovaChata/new-version/app-httpserver/src/main/java/cz/kamma/fabka/httpserver/web/Pages.java