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
""".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);
}
}