package de.ticketsystem.web.handlers; import com.sun.net.httpserver.HttpExchange; import de.ticketsystem.TicketPlugin; import de.ticketsystem.web.SessionManager; import de.ticketsystem.web.WebSession; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; /** * Gemeinsame Hilfsmethoden für alle HTTP-Handler. * * Neue Methoden für i18n und Favicon: * wl(plugin, key) – Liest einen Web-Panel-Text aus der aktiven Sprachdatei (web.{key}). * buildFaviconHtml(plugin) – Erzeugt das -Tag (Logo oder Standard-SVG). * layout(title, content, session, plugin) – Layout mit lokalisierten Nav-Texten + Favicon. * requireAdmin(ex, session, plugin) – 403-Fehlerseite mit lokalisierten Texten. * errorPage(title, message, plugin) – Fehlerseite mit lokalisiertem Zurück-Button. * * Die parameterlos-Varianten von layout(), requireAdmin() und errorPage() bleiben * unverändert erhalten (Rückwärtskompatibilität für StaticHandler o. Ä.). */ public abstract class BaseHandler { // ─────────────────────────── Lang-Hilfsmethode ───────────────────────── /** * Liest einen Web-Panel-Text aus der aktiven Sprachdatei (web.{key}). * Kein Minecraft-Farbcode-Parsing – reiner Plaintext für HTML. */ protected String wl(TicketPlugin plugin, String key) { return plugin.lang().getRaw("web." + key); } // ─────────────────────────── Favicon ─────────────────────────────────── /** * Erzeugt das {@code } HTML-Tag für den {@code }-Bereich. * * Wenn {@code web-panel.logo-file} in der config.yml gesetzt ist, wird das Logo * als Favicon verwendet (wird bereits als /static/{datei} serviert). * Ansonsten wird ein eingebettetes SVG-Icon als Data-URI erzeugt. * * Unterstützte Formate: png, jpg, jpeg, gif, webp, svg, ico. */ protected String buildFaviconHtml(TicketPlugin plugin) { String logoFile = plugin.getConfig().getString("web-panel.logo-file", "").trim(); if (!logoFile.isEmpty()) { String safe = logoFile.replaceAll("[^a-zA-Z0-9._\\-]", "_"); String ext = safe.contains(".") ? safe.substring(safe.lastIndexOf('.') + 1).toLowerCase() : "png"; String mime = switch (ext) { case "svg" -> "image/svg+xml"; case "jpg", "jpeg" -> "image/jpeg"; case "gif" -> "image/gif"; case "webp" -> "image/webp"; case "ico" -> "image/x-icon"; default -> "image/png"; }; return ""; } // Standard-SVG als Data-URI (kein externes File nötig) String svg = "" + "" + "" + "" + "" + "" + "" + ""; String b64 = Base64.getEncoder().encodeToString(svg.getBytes(StandardCharsets.UTF_8)); return ""; } // ─────────────────────────── Response ────────────────────────────────── protected void sendHtml(HttpExchange ex, int status, String html) throws IOException { byte[] bytes = html.getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8"); ex.sendResponseHeaders(status, bytes.length); try (OutputStream os = ex.getResponseBody()) { os.write(bytes); } } protected void sendJson(HttpExchange ex, int status, String json) throws IOException { byte[] bytes = json.getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8"); ex.sendResponseHeaders(status, bytes.length); try (OutputStream os = ex.getResponseBody()) { os.write(bytes); } } protected void sendRedirect(HttpExchange ex, String location) throws IOException { ex.getResponseHeaders().set("Location", location); ex.sendResponseHeaders(302, -1); ex.getResponseBody().close(); } // ─────────────────────────── Request-Parsing ─────────────────────────── protected Map parseQuery(String query) { Map map = new HashMap<>(); if (query == null || query.isEmpty()) return map; for (String pair : query.split("&")) { String[] kv = pair.split("=", 2); if (kv.length == 2) { map.put(decode(kv[0]), decode(kv[1])); } else if (kv.length == 1) { map.put(decode(kv[0]), ""); } } return map; } protected Map parseBody(HttpExchange ex) throws IOException { try (InputStream is = ex.getRequestBody()) { String body = new String(is.readAllBytes(), StandardCharsets.UTF_8); return parseQuery(body); } } protected Map parseCookies(HttpExchange ex) { Map cookies = new HashMap<>(); String header = ex.getRequestHeaders().getFirst("Cookie"); if (header == null) return cookies; for (String part : header.split(";")) { String[] kv = part.trim().split("=", 2); if (kv.length == 2) cookies.put(kv[0].trim(), kv[1].trim()); } return cookies; } protected String getCookieToken(HttpExchange ex) { return parseCookies(ex).get("ts_session"); } protected void setSessionCookie(HttpExchange ex, String token, long timeoutMinutes) { String cookie = "ts_session=" + token + "; Path=/" + "; HttpOnly" + "; Max-Age=" + (timeoutMinutes * 60); ex.getResponseHeaders().add("Set-Cookie", cookie); } protected void clearSessionCookie(HttpExchange ex) { ex.getResponseHeaders().add("Set-Cookie", "ts_session=; Path=/; HttpOnly; Max-Age=0"); } // ─────────────────────────── Auth ────────────────────────────────────── /** * Gibt die aktuelle Session zurück oder null. * Leitet NICHT automatisch um – das macht der Aufrufer. */ protected WebSession getSession(HttpExchange ex, SessionManager sessionManager) { String token = getCookieToken(ex); return sessionManager.getSession(token); } /** * Prüft ob eine gültige Session vorliegt. * Bei ungültiger Session: Redirect zu /login, gibt null zurück. */ protected WebSession requireSession(HttpExchange ex, SessionManager sessionManager) throws IOException { WebSession session = getSession(ex, sessionManager); if (session == null) { sendRedirect(ex, "/login"); return null; } return session; } /** * Prüft ob Admin-Rolle vorhanden ist (legacy – ohne i18n). * Bei fehlender Rolle: 403-Fehler mit hart kodierten Texten. * * @deprecated Nutze {@link #requireAdmin(HttpExchange, WebSession, TicketPlugin)}. */ @Deprecated protected boolean requireAdmin(HttpExchange ex, WebSession session) throws IOException { if (!session.isAdmin()) { sendHtml(ex, 403, errorPage("403 – Kein Zugriff", "Diese Seite ist nur für Administratoren zugänglich.")); return false; } return true; } /** * Prüft ob Admin-Rolle vorhanden ist. * Bei fehlender Rolle: 403-Fehler mit lokalisierten Texten aus der Sprachdatei. */ protected boolean requireAdmin(HttpExchange ex, WebSession session, TicketPlugin plugin) throws IOException { if (!session.isAdmin()) { sendHtml(ex, 403, errorPage( wl(plugin, "error-403-title"), wl(plugin, "error-403-message"), plugin)); return false; } return true; } // ─────────────────────────── HTML-Hilfsmethoden ──────────────────────── /** * Gibt das gemeinsame Seiten-Layout zurück (legacy – ohne i18n). * Nav-Texte und Rollenlabels sind hart kodiert (Deutsch). * * @deprecated Nutze {@link #layout(String, String, WebSession, TicketPlugin)}. */ @Deprecated protected String layout(String title, String content, WebSession session) { String username = session != null ? session.getUsername() : ""; String roleLabel = session != null ? (session.isAdmin() ? "Admin" : "Supporter") : ""; String roleClass = session != null ? (session.isAdmin() ? "admin" : "supporter") : "supporter"; String initial = username.isEmpty() ? "?" : String.valueOf(username.charAt(0)).toUpperCase(); String faqNav = session != null && session.isAdmin() ? "FAQ" : ""; return buildLayoutHtml(title, content, session, username, roleLabel, roleClass, initial, faqNav, "Dashboard", "Tickets", "Abmelden", ""); } /** * Gibt das gemeinsame Seiten-Layout zurück. * Nav-Texte, Rollenlabels und Favicon werden aus Sprachdatei / Logo-Config gelesen. */ protected String layout(String title, String content, WebSession session, TicketPlugin plugin) { String username = session != null ? session.getUsername() : ""; String roleLabel = session != null ? (session.isAdmin() ? wl(plugin, "role-admin") : wl(plugin, "role-supporter")) : ""; String roleClass = session != null ? (session.isAdmin() ? "admin" : "supporter") : "supporter"; String initial = username.isEmpty() ? "?" : String.valueOf(username.charAt(0)).toUpperCase(); String faqNav = session != null && session.isAdmin() ? "" + escHtml(wl(plugin, "nav-faq")) + "" : ""; return buildLayoutHtml(title, content, session, username, roleLabel, roleClass, initial, faqNav, wl(plugin, "nav-dashboard"), wl(plugin, "nav-tickets"), wl(plugin, "nav-logout"), buildFaviconHtml(plugin)); } /** Interner Builder – beide layout()-Varianten landen hier. */ private String buildLayoutHtml(String title, String content, WebSession session, String username, String roleLabel, String roleClass, String initial, String faqNav, String navDashboard, String navTickets, String navLogout, String faviconTag) { return "" + "" + "" + "" + "" + "" + escHtml(title) + " – TicketSystem Panel" + faviconTag + "" + "" + "" + "" + "" + "" + "
" + content + "
" + "" + "" + ""; } /** * Einfache Fehlerseite (legacy – Zurück-Button auf Deutsch hart kodiert). * * @deprecated Nutze {@link #errorPage(String, String, TicketPlugin)}. */ @Deprecated protected String errorPage(String title, String message) { return """ %s """.formatted(title, title, message); } /** * Einfache Fehlerseite mit lokalisiertem Zurück-Button. */ protected String errorPage(String title, String message, TicketPlugin plugin) { return """ %s

%s

%s

%s
""".formatted(title, title, message, wl(plugin, "btn-back")); } protected String escHtml(String s) { if (s == null) return ""; return s.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """); } private String decode(String s) { return URLDecoder.decode(s, StandardCharsets.UTF_8); } }