Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-04-15 19:11:02 +02:00
parent bafddee288
commit d66871234c
20 changed files with 3854 additions and 451 deletions

View File

@@ -0,0 +1,336 @@
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 <link rel="icon">-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 <link rel="icon">} HTML-Tag für den {@code <head>}-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 "<link rel='icon' type='" + mime + "' href='/static/" + escHtml(safe) + "'>";
}
// Standard-SVG als Data-URI (kein externes File nötig)
String svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'>"
+ "<rect width='48' height='48' rx='10' fill='%231d4ed8'/>"
+ "<rect x='8' y='14' width='32' height='6' rx='2' fill='white' opacity='.9'/>"
+ "<rect x='8' y='23' width='20' height='4' rx='2' fill='white' opacity='.6'/>"
+ "<rect x='8' y='30' width='14' height='4' rx='2' fill='white' opacity='.4'/>"
+ "<circle cx='37' cy='32' r='6' fill='white' opacity='.15'/>"
+ "<path d='M34 32l2 2 4-4' stroke='white' stroke-width='1.8'"
+ " stroke-linecap='round' stroke-linejoin='round'/>"
+ "</svg>";
String b64 = Base64.getEncoder().encodeToString(svg.getBytes(StandardCharsets.UTF_8));
return "<link rel='icon' type='image/svg+xml' href='data:image/svg+xml;base64," + b64 + "'>";
}
// ─────────────────────────── 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<String, String> parseQuery(String query) {
Map<String, String> 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<String, String> parseBody(HttpExchange ex) throws IOException {
try (InputStream is = ex.getRequestBody()) {
String body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
return parseQuery(body);
}
}
protected Map<String, String> parseCookies(HttpExchange ex) {
Map<String, String> 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()
? "<a href='/faq' class='nav-link'>FAQ</a>" : "";
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()
? "<a href='/faq' class='nav-link'>" + escHtml(wl(plugin, "nav-faq")) + "</a>" : "";
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 "<!DOCTYPE html>"
+ "<html lang='de'>"
+ "<head>"
+ "<meta charset='UTF-8'>"
+ "<meta name='viewport' content='width=device-width,initial-scale=1'>"
+ "<title>" + escHtml(title) + " TicketSystem Panel</title>"
+ faviconTag
+ "<link rel='preconnect' href='https://fonts.googleapis.com'>"
+ "<link href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap' rel='stylesheet'>"
+ "<link rel='stylesheet' href='/static/style.css'>"
+ "</head>"
+ "<body>"
+ "<nav class='navbar'>"
+ "<div class='nav-brand'>🎫 TicketSystem</div>"
+ "<div class='nav-links'>"
+ "<a href='/dashboard' class='nav-link'>" + escHtml(navDashboard) + "</a>"
+ "<a href='/tickets' class='nav-link'>" + escHtml(navTickets) + "</a>"
+ faqNav
+ "</div>"
+ "<div class='nav-user'>"
+ "<div class='nav-avatar'>" + escHtml(initial) + "</div>"
+ "<span class='nav-username'>" + escHtml(username) + "</span>"
+ "<span class='badge badge-" + roleClass + "'>" + escHtml(roleLabel) + "</span>"
+ "<a href='/logout' class='btn btn-sm btn-secondary'>" + escHtml(navLogout) + "</a>"
+ "</div>"
+ "</nav>"
+ "<main class='container'>"
+ content
+ "</main>"
+ "<script src='/static/panel.js'></script>"
+ "</body>"
+ "</html>";
}
/**
* 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 """
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">
<title>%s</title><link rel="stylesheet" href="/static/style.css"></head>
<body><div class="container" style="margin-top:4rem;text-align:center">
<h1>%s</h1><p>%s</p><a href="/dashboard" class="btn btn-primary">Zur\u00fcck</a>
</div></body></html>
""".formatted(title, title, message);
}
/**
* Einfache Fehlerseite mit lokalisiertem Zurück-Button.
*/
protected String errorPage(String title, String message, TicketPlugin plugin) {
return """
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">
<title>%s</title><link rel="stylesheet" href="/static/style.css"></head>
<body><div class="container" style="margin-top:4rem;text-align:center">
<h1>%s</h1><p>%s</p><a href="/dashboard" class="btn btn-primary">%s</a>
</div></body></html>
""".formatted(title, title, message, wl(plugin, "btn-back"));
}
protected String escHtml(String s) {
if (s == null) return "";
return s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
private String decode(String s) {
return URLDecoder.decode(s, StandardCharsets.UTF_8);
}
}