From ab20136bac5eba5889e9cb3b838f299c05deb7fe Mon Sep 17 00:00:00 2001 From: Metacube Date: Tue, 4 Nov 2025 10:04:40 +0100 Subject: [PATCH] Create chat.php --- chat.php | 4570 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4570 insertions(+) create mode 100644 chat.php diff --git a/chat.php b/chat.php new file mode 100644 index 0000000..6f013ca --- /dev/null +++ b/chat.php @@ -0,0 +1,4570 @@ +busyTimeout(5000); + } + + if (!$initialized) { + // Users Table (mit Geburtsdatum und User-ID) + $db->exec(' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + user_id TEXT UNIQUE NOT NULL, + birthdate DATE NOT NULL, + age_group TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + is_banned INTEGER DEFAULT 0, + ban_reason TEXT + ) + '); + + // Messages Table + $db->exec(' + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_user_id INTEGER NOT NULL, + to_user_id INTEGER NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + is_read INTEGER DEFAULT 0, + is_flagged INTEGER DEFAULT 0, + flag_reason TEXT, + attachment_path TEXT, + attachment_type TEXT, + attachment_size INTEGER, + FOREIGN KEY (from_user_id) REFERENCES users(id), + FOREIGN KEY (to_user_id) REFERENCES users(id) + ) + '); + + // Ensure attachment columns exist for older installations + $messagesInfo = $db->query('PRAGMA table_info(messages)'); + $messageColumns = []; + while ($column = $messagesInfo->fetchArray(SQLITE3_ASSOC)) { + $messageColumns[$column['name']] = true; + } + if (!isset($messageColumns['attachment_path'])) { + $db->exec('ALTER TABLE messages ADD COLUMN attachment_path TEXT'); + } + if (!isset($messageColumns['attachment_type'])) { + $db->exec('ALTER TABLE messages ADD COLUMN attachment_type TEXT'); + } + if (!isset($messageColumns['attachment_size'])) { + $db->exec('ALTER TABLE messages ADD COLUMN attachment_size INTEGER'); + } + + // Online Status Table + $db->exec(' + CREATE TABLE IF NOT EXISTS online_status ( + user_id INTEGER PRIMARY KEY, + last_ping DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); + + // User Sessions Table + $db->exec(' + CREATE TABLE IF NOT EXISTS user_sessions ( + user_id INTEGER PRIMARY KEY, + session_token TEXT NOT NULL, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); + + // Reports Table + $db->exec(' + CREATE TABLE IF NOT EXISTS reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + reporter_id INTEGER NOT NULL, + reported_user_id INTEGER NOT NULL, + reason TEXT NOT NULL, + message_id INTEGER, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT "pending", + FOREIGN KEY (reporter_id) REFERENCES users(id), + FOREIGN KEY (reported_user_id) REFERENCES users(id), + FOREIGN KEY (message_id) REFERENCES messages(id) + ) + '); + + // Blocks Table + $db->exec(' + CREATE TABLE IF NOT EXISTS blocks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + blocker_id INTEGER NOT NULL, + blocked_id INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (blocker_id) REFERENCES users(id), + FOREIGN KEY (blocked_id) REFERENCES users(id), + UNIQUE(blocker_id, blocked_id) + ) + '); + + // Rate Limiting Table + $db->exec(' + CREATE TABLE IF NOT EXISTS rate_limits ( + user_id INTEGER NOT NULL, + action_type TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); + + // Logs Table (für Behörden) + $db->exec(' + CREATE TABLE IF NOT EXISTS security_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + action TEXT NOT NULL, + details TEXT, + ip_address TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + '); + + // Admin Table + $db->exec(' + CREATE TABLE IF NOT EXISTS admins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + '); + + // Create default admin if not exists + $stmt = $db->prepare('SELECT COUNT(*) as count FROM admins WHERE username = :username'); + $stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + if ($row['count'] == 0) { + $stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)'); + $stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT); + $stmt->bindValue(':password', ADMIN_PASSWORD, SQLITE3_TEXT); + $stmt->execute(); + } + + // Create indexes + $db->exec('CREATE INDEX IF NOT EXISTS idx_messages_users ON messages(from_user_id, to_user_id)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_users_age_group ON users(age_group)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_blocks ON blocks(blocker_id, blocked_id)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status)'); + $db->exec('CREATE INDEX IF NOT EXISTS idx_user_sessions_last_seen ON user_sessions(last_seen)'); + + $initialized = true; + } + + return $db; +} + +// ═══════════════════════════════════════════════════════════ +// SECURITY FUNCTIONS +// ═══════════════════════════════════════════════════════════ + +function generateUserId() { + return str_pad(mt_rand(1000, 9999), 4, '0', STR_PAD_LEFT); +} + +function calculateAge($birthdate) { + $birth = new DateTime($birthdate); + $today = new DateTime(); + return $birth->diff($today)->y; +} + +function getAgeGroup($birthdate) { + $age = calculateAge($birthdate); + return $age < 18 ? 'U18' : 'O18'; +} + +function resolveStoredAgeGroup($storedAgeGroup, $birthdate) { + $storedAgeGroup = is_string($storedAgeGroup) ? strtoupper(trim($storedAgeGroup)) : ''; + + if ($storedAgeGroup === 'U18' || $storedAgeGroup === 'O18') { + return $storedAgeGroup; + } + + if (!empty($birthdate)) { + return getAgeGroup($birthdate); + } + + return 'O18'; +} + +function checkKeywordBlacklist($message) { + global $KEYWORD_BLACKLIST; + + $message_lower = mb_strtolower($message, 'UTF-8'); + + foreach ($KEYWORD_BLACKLIST as $keyword) { + if (stripos($message_lower, $keyword) !== false) { + return ['blocked' => true, 'keyword' => $keyword]; + } + } + + return ['blocked' => false]; +} + +function checkProfanityFilter($message) { + global $PROFANITY_FILTER; + + $message_lower = mb_strtolower($message, 'UTF-8'); + + foreach ($PROFANITY_FILTER as $word) { + if (stripos($message_lower, $word) !== false) { + return ['blocked' => true, 'word' => $word]; + } + } + + return ['blocked' => false]; +} + +function checkLinkFilter($message) { + $patterns = [ + '/https?:\/\//i', + '/www\./i', + '/[a-z0-9-]+\.(com|de|ch|net|org|info)/i' + ]; + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $message)) { + return ['blocked' => true]; + } + } + + return ['blocked' => false]; +} + +function checkRateLimit($userId, $ageGroup) { + $db = getDB(); + + // Check messages per minute + $stmt = $db->prepare(" + SELECT COUNT(*) as count + FROM rate_limits + WHERE user_id = :user_id + AND action_type = 'message' + AND timestamp > datetime('now', '-1 minute') + "); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + if ($row['count'] >= MAX_MESSAGES_PER_MINUTE) { + return ['allowed' => false, 'reason' => 'Zu viele Nachrichten pro Minute (max ' . MAX_MESSAGES_PER_MINUTE . ')']; + } + + // Check messages per hour + $stmt = $db->prepare(" + SELECT COUNT(*) as count + FROM rate_limits + WHERE user_id = :user_id + AND action_type = 'message' + AND timestamp > datetime('now', '-1 hour') + "); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + if ($row['count'] >= MAX_MESSAGES_PER_HOUR) { + return ['allowed' => false, 'reason' => 'Zu viele Nachrichten pro Stunde (max ' . MAX_MESSAGES_PER_HOUR . ')']; + } + + // Check messages per day for U18 + if ($ageGroup === 'U18') { + $stmt = $db->prepare(" + SELECT COUNT(*) as count + FROM rate_limits + WHERE user_id = :user_id + AND action_type = 'message' + AND timestamp > datetime('now', '-1 day') + "); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + if ($row['count'] >= MAX_MESSAGES_PER_DAY_U18) { + return ['allowed' => false, 'reason' => 'Tages-Limit erreicht (max ' . MAX_MESSAGES_PER_DAY_U18 . ' für U18)']; + } + } + + return ['allowed' => true]; +} + +function logRateLimit($userId) { + $db = getDB(); + $stmt = $db->prepare("INSERT INTO rate_limits (user_id, action_type) VALUES (:user_id, 'message')"); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->execute(); +} + +function logSecurityEvent($userId, $action, $details = '') { + $db = getDB(); + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + + $stmt = $db->prepare(' + INSERT INTO security_logs (user_id, action, details, ip_address) + VALUES (:user_id, :action, :details, :ip) + '); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->bindValue(':action', $action, SQLITE3_TEXT); + $stmt->bindValue(':details', $details, SQLITE3_TEXT); + $stmt->bindValue(':ip', $ip, SQLITE3_TEXT); + $stmt->execute(); +} + +function canUsersChatByAge($ageGroupA, $ageGroupB) { + if (!$ageGroupA || !$ageGroupB) { + return false; + } + + // Wenn einer minderjährig ist, müssen beide minderjährig sein + if ($ageGroupA === 'U18' || $ageGroupB === 'U18') { + return $ageGroupA === 'U18' && $ageGroupB === 'U18'; + } + + // Volljährige dürfen miteinander chatten + return true; +} + +function isBlocked($userId, $otherUserId) { + $db = getDB(); + $stmt = $db->prepare(' + SELECT COUNT(*) as count + FROM blocks + WHERE (blocker_id = :user1 AND blocked_id = :user2) + OR (blocker_id = :user2 AND blocked_id = :user1) + '); + $stmt->bindValue(':user1', $userId, SQLITE3_INTEGER); + $stmt->bindValue(':user2', $otherUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + return $row['count'] > 0; +} + +function cleanupOldData() { + $db = getDB(); + + // Delete old messages + $hours = MESSAGE_RETENTION_HOURS; + $attachmentResult = $db->query("SELECT attachment_path FROM messages WHERE attachment_path IS NOT NULL AND timestamp < datetime('now', '-{$hours} hours')"); + while ($attachmentRow = $attachmentResult->fetchArray(SQLITE3_ASSOC)) { + $relativePath = $attachmentRow['attachment_path'] ?? ''; + if (!$relativePath) { + continue; + } + + $normalizedPath = str_replace('\\', '/', $relativePath); + if (strpos($normalizedPath, 'uploads/') !== 0) { + continue; + } + + $fullPath = __DIR__ . '/' . $normalizedPath; + if (is_file($fullPath)) { + @unlink($fullPath); + } + } + + $db->exec("DELETE FROM messages WHERE timestamp < datetime('now', '-{$hours} hours')"); + + // Delete old rate limits + $db->exec("DELETE FROM rate_limits WHERE timestamp < datetime('now', '-1 day')"); + + // Delete old logs (keep 6 months) + $months = LOG_RETENTION_MONTHS; + $db->exec("DELETE FROM security_logs WHERE timestamp < datetime('now', '-{$months} months')"); + + // Remove stale session placeholders + $db->exec("DELETE FROM user_sessions WHERE last_seen < datetime('now', '-5 minutes')"); +} + +// ═══════════════════════════════════════════════════════════ +// SESSION & AUTH +// ═══════════════════════════════════════════════════════════ + +session_start(); +$isAdminPage = isset($_GET['admin']); + +function isLoggedIn() { + return isset($_SESSION['user_id']) && isset($_SESSION['username']); +} + +function isAdmin() { + return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true; +} + +function getCurrentUserId() { + return $_SESSION['user_id'] ?? null; +} + +function getCurrentUsername() { + return $_SESSION['username'] ?? null; +} + +function getCurrentUserDisplayName() { + if (!isLoggedIn()) return null; + return $_SESSION['username'] . '#' . $_SESSION['user_display_id']; +} + +function getCurrentAgeGroup() { + return $_SESSION['age_group'] ?? null; +} + +function updateOnlineStatus($userId) { + $db = getDB(); + $stmt = $db->prepare(' + INSERT OR REPLACE INTO online_status (user_id, last_ping) + VALUES (:user_id, CURRENT_TIMESTAMP) + '); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->execute(); + + $stmt = $db->prepare('UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = :user_id'); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->execute(); + + touchUserSession($userId); +} + +function generateSessionToken() { + return bin2hex(random_bytes(32)); +} + +function startUserSession($userId, $force = false) { + if (!$userId) { + return ['allowed' => false, 'error' => 'Ungültige Benutzer-ID']; + } + + $db = getDB(); + $stmt = $db->prepare('SELECT session_token, last_seen FROM user_sessions WHERE user_id = :user_id'); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $existing = $result->fetchArray(SQLITE3_ASSOC); + + if ($existing && !$force && !empty($existing['last_seen'])) { + $secondsSinceLastSeen = time() - strtotime($existing['last_seen']); + + if ($secondsSinceLastSeen < ONLINE_TIMEOUT_SECONDS) { + return [ + 'allowed' => false, + 'error' => 'Du bist bereits auf einem anderen Gerät eingeloggt. Übernimm die Sitzung nur, wenn du wirklich ausgeloggt bist.', + 'can_force' => true + ]; + } + } + + if ($force && $existing) { + $stmt = $db->prepare('DELETE FROM user_sessions WHERE user_id = :user_id'); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->execute(); + } + + $token = generateSessionToken(); + + $stmt = $db->prepare(' + INSERT OR REPLACE INTO user_sessions (user_id, session_token, last_seen) + VALUES (:user_id, :token, CURRENT_TIMESTAMP) + '); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->bindValue(':token', $token, SQLITE3_TEXT); + $stmt->execute(); + + $_SESSION['session_token'] = $token; + + return ['allowed' => true, 'token' => $token]; +} + +function touchUserSession($userId) { + if (!$userId || empty($_SESSION['session_token'])) { + return; + } + + $db = getDB(); + $stmt = $db->prepare(' + UPDATE user_sessions + SET last_seen = CURRENT_TIMESTAMP + WHERE user_id = :user_id AND session_token = :token + '); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->bindValue(':token', $_SESSION['session_token'], SQLITE3_TEXT); + $stmt->execute(); +} + +function clearUserSession($userId) { + if (!$userId) { + return; + } + + $db = getDB(); + $stmt = $db->prepare('DELETE FROM user_sessions WHERE user_id = :user_id'); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->execute(); + + unset($_SESSION['session_token']); +} + +function validateActiveSession() { + if (!isLoggedIn()) { + return false; + } + + $token = $_SESSION['session_token'] ?? null; + if (!$token) { + return false; + } + + $db = getDB(); + $stmt = $db->prepare('SELECT session_token, last_seen FROM user_sessions WHERE user_id = :user_id'); + $stmt->bindValue(':user_id', getCurrentUserId(), SQLITE3_INTEGER); + $result = $stmt->execute(); + $session = $result->fetchArray(SQLITE3_ASSOC); + + if (!$session) { + return false; + } + + if (!hash_equals($session['session_token'], $token)) { + return false; + } + + if (!empty($session['last_seen'])) { + $secondsSinceLastSeen = time() - strtotime($session['last_seen']); + + if ($secondsSinceLastSeen > ONLINE_TIMEOUT_SECONDS * 3) { + return false; + } + } + + return true; +} + +// ═══════════════════════════════════════════════════════════ +// AJAX API HANDLER +// ═══════════════════════════════════════════════════════════ + +if (isset($_POST['action']) || isset($_GET['action'])) { + header('Content-Type: application/json'); + + $action = $_POST['action'] ?? $_GET['action']; + + // ─────────────────────────────────────────────────────── + // REGISTER + // ─────────────────────────────────────────────────────── + if ($action === 'register') { + $username = trim($_POST['username'] ?? ''); + $birthdate = trim($_POST['birthdate'] ?? ''); + $agreed_terms = isset($_POST['agreed_terms']) && $_POST['agreed_terms'] === 'true'; + + // Validierung + if (empty($username) || empty($birthdate)) { + echo json_encode(['success' => false, 'error' => 'Username und Geburtsdatum erforderlich']); + exit; + } + + if (!$agreed_terms) { + echo json_encode(['success' => false, 'error' => 'Bitte akzeptiere die Nutzungsbedingungen']); + exit; + } + + if (strlen($username) < 3 || strlen($username) > 15) { + echo json_encode(['success' => false, 'error' => 'Username muss 3-15 Zeichen lang sein']); + exit; + } + + if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) { + echo json_encode(['success' => false, 'error' => 'Nur Buchstaben, Zahlen und _ erlaubt']); + exit; + } + + // Verbotene Usernamen + $forbidden = ['admin', 'moderator', 'support', 'system', 'root']; + if (in_array(strtolower($username), $forbidden)) { + echo json_encode(['success' => false, 'error' => 'Dieser Username ist nicht erlaubt']); + exit; + } + + // Alter prüfen + $age = calculateAge($birthdate); + if ($age < 13) { + echo json_encode(['success' => false, 'error' => 'Du musst mindestens 13 Jahre alt sein']); + logSecurityEvent(null, 'REGISTER_UNDERAGE', "Username: $username, Age: $age"); + exit; + } + + $ageGroup = getAgeGroup($birthdate); + + $db = getDB(); + + // Generate unique user_id + $userId = generateUserId(); + $attempts = 0; + while ($attempts < 10) { + $stmt = $db->prepare('SELECT COUNT(*) as count FROM users WHERE user_id = :user_id'); + $stmt->bindValue(':user_id', $userId, SQLITE3_TEXT); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + + if ($row['count'] == 0) break; + + $userId = generateUserId(); + $attempts++; + } + + // Create user + $stmt = $db->prepare(' + INSERT INTO users (username, user_id, birthdate, age_group) + VALUES (:username, :user_id, :birthdate, :age_group) + '); + $stmt->bindValue(':username', $username, SQLITE3_TEXT); + $stmt->bindValue(':user_id', $userId, SQLITE3_TEXT); + $stmt->bindValue(':birthdate', $birthdate, SQLITE3_TEXT); + $stmt->bindValue(':age_group', $ageGroup, SQLITE3_TEXT); + $stmt->execute(); + + $dbUserId = $db->lastInsertRowID(); + + $sessionResult = startUserSession($dbUserId); + if (!$sessionResult['allowed']) { + logSecurityEvent($dbUserId, 'LOGIN_BLOCKED_DUPLICATE_SESSION', 'REGISTER'); + echo json_encode([ + 'success' => false, + 'error' => $sessionResult['error'] + ]); + exit; + } + + $_SESSION['user_id'] = $dbUserId; + $_SESSION['username'] = $username; + $_SESSION['user_display_id'] = $userId; + $_SESSION['age_group'] = $ageGroup; + $_SESSION['birthdate'] = $birthdate; + + updateOnlineStatus($dbUserId); + logSecurityEvent($dbUserId, 'REGISTER', "Age group: $ageGroup"); + + echo json_encode([ + 'success' => true, + 'user_id' => $dbUserId, + 'username' => $username, + 'display_name' => $username . '#' . $userId, + 'age_group' => $ageGroup + ]); + exit; + } + + // ─────────────────────────────────────────────────────── + // LOGIN + // ─────────────────────────────────────────────────────── + if ($action === 'login') { + $username = trim($_POST['username'] ?? ''); + $birthdate = trim($_POST['birthdate'] ?? ''); + $forceLogin = in_array(($_POST['force_login'] ?? '0'), ['1', 'true', 'TRUE'], true); + + if ($username === '' || $birthdate === '') { + echo json_encode(['success' => false, 'error' => 'Bitte gib Username und Geburtsdatum ein.']); + exit; + } + + $db = getDB(); + $stmt = $db->prepare(' + SELECT id, username, user_id as display_id, birthdate, age_group, is_banned, ban_reason + FROM users + WHERE LOWER(username) = LOWER(:username) + LIMIT 1 + '); + $stmt->bindValue(':username', $username, SQLITE3_TEXT); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if (!$user) { + echo json_encode(['success' => false, 'error' => 'Account wurde nicht gefunden.']); + exit; + } + + if ((int)$user['is_banned'] === 1) { + $reason = $user['ban_reason'] ? (string)$user['ban_reason'] : 'Verstoß gegen Regeln'; + echo json_encode(['success' => false, 'error' => 'Dein Account ist gesperrt: ' . $reason]); + exit; + } + + if ($user['birthdate'] !== $birthdate) { + logSecurityEvent($user['id'], 'LOGIN_FAILED', 'Falsches Geburtsdatum'); + echo json_encode(['success' => false, 'error' => 'Daten stimmen nicht überein.']); + exit; + } + + $sessionResult = startUserSession($user['id'], $forceLogin); + if (!$sessionResult['allowed']) { + $response = [ + 'success' => false, + 'error' => $sessionResult['error'] ?? 'Anmeldung nicht möglich.' + ]; + + if (!empty($sessionResult['can_force'])) { + $response['can_force'] = true; + } + + echo json_encode($response); + exit; + } + + if ($forceLogin) { + logSecurityEvent($user['id'], 'LOGIN_FORCE', 'Sitzung übernommen'); + } + + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['user_display_id'] = $user['display_id']; + $_SESSION['age_group'] = $user['age_group']; + $_SESSION['birthdate'] = $user['birthdate']; + + updateOnlineStatus($user['id']); + logSecurityEvent($user['id'], 'LOGIN', 'Erfolgreiche Anmeldung'); + + echo json_encode([ + 'success' => true, + 'user_id' => $user['id'], + 'display_name' => $user['username'] . '#' . $user['display_id'], + 'age_group' => $user['age_group'] + ]); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN LOGIN + // ─────────────────────────────────────────────────────── + if ($action === 'admin_login') { + $username = trim($_POST['username'] ?? ''); + $password = trim($_POST['password'] ?? ''); + + $db = getDB(); + $stmt = $db->prepare('SELECT * FROM admins WHERE username = :username'); + $stmt->bindValue(':username', $username, SQLITE3_TEXT); + $result = $stmt->execute(); + $admin = $result->fetchArray(SQLITE3_ASSOC); + + if ($admin && password_verify($password, $admin['password_hash'])) { + $_SESSION['is_admin'] = true; + $_SESSION['admin_id'] = $admin['id']; + $_SESSION['admin_username'] = $admin['username']; + + logSecurityEvent(null, 'ADMIN_LOGIN', "Admin: $username"); + + echo json_encode(['success' => true]); + } else { + logSecurityEvent(null, 'ADMIN_LOGIN_FAILED', "Username: $username"); + echo json_encode(['success' => false, 'error' => 'Ungültige Zugangsdaten']); + } + exit; + } + + // ─────────────────────────────────────────────────────── + // LOGOUT + // ─────────────────────────────────────────────────────── + if ($action === 'logout') { + if (isLoggedIn()) { + $currentUserId = getCurrentUserId(); + logSecurityEvent($currentUserId, 'LOGOUT', ''); + clearUserSession($currentUserId); + } + session_destroy(); + echo json_encode(['success' => true]); + exit; + } + + // All other actions require login + if (!isLoggedIn() && !isAdmin()) { + echo json_encode(['success' => false, 'error' => 'Nicht eingeloggt']); + exit; + } + + if (isLoggedIn() && !isAdmin() && !validateActiveSession()) { + $userId = getCurrentUserId(); + clearUserSession($userId); + session_destroy(); + echo json_encode(['success' => false, 'error' => 'Deine Sitzung ist nicht mehr gültig. Bitte erneut einloggen.']); + exit; + } + + if (isLoggedIn() && !isAdmin()) { + touchUserSession(getCurrentUserId()); + } + + // ─────────────────────────────────────────────────────── + // PING (UPDATE ONLINE STATUS) + // ─────────────────────────────────────────────────────── + if ($action === 'ping') { + updateOnlineStatus(getCurrentUserId()); + cleanupOldData(); + echo json_encode(['success' => true]); + exit; + } + + // ─────────────────────────────────────────────────────── + // GET USERS + // ─────────────────────────────────────────────────────── + if ($action === 'get_users') { + $db = getDB(); + $currentUserId = getCurrentUserId(); + $currentAgeGroup = getCurrentAgeGroup(); + + $query = ' + SELECT + u.id, + u.username, + u.user_id as display_id, + u.birthdate, + u.age_group, + u.last_seen, + CASE + WHEN os.last_ping IS NOT NULL + AND (julianday("now") - julianday(os.last_ping)) * 86400 < ' . ONLINE_TIMEOUT_SECONDS . ' + THEN 1 + ELSE 0 + END as is_online, + ( + SELECT COUNT(*) + FROM messages + WHERE from_user_id = u.id + AND to_user_id = :current_user_id + AND is_read = 0 + ) as unread_count, + ( + SELECT COUNT(*) + FROM blocks + WHERE blocker_id = :current_user_id + AND blocked_id = u.id + ) as is_blocked_by_me, + ( + SELECT COUNT(*) + FROM blocks + WHERE blocker_id = u.id + AND blocked_id = :current_user_id + ) as has_blocked_me + FROM users u + LEFT JOIN online_status os ON u.id = os.user_id + WHERE u.id != :current_user_id + AND u.is_banned = 0 + '; + + if ($currentAgeGroup === 'U18') { + $query .= ' AND (u.age_group = :allowed_group)'; + } else { + $query .= ' AND (u.age_group != :blocked_group OR u.age_group IS NULL)'; + } + + $query .= ' ORDER BY is_online DESC, u.username ASC'; + + $stmt = $db->prepare($query); + $stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER); + + if ($currentAgeGroup === 'U18') { + $stmt->bindValue(':allowed_group', 'U18', SQLITE3_TEXT); + } else { + $stmt->bindValue(':blocked_group', 'U18', SQLITE3_TEXT); + } + $result = $stmt->execute(); + + $users = []; + $rawCount = 0; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $rawCount++; + + $resolvedAgeGroup = resolveStoredAgeGroup($row['age_group'] ?? '', $row['birthdate'] ?? ''); + + if (!canUsersChatByAge($currentAgeGroup, $resolvedAgeGroup)) { + continue; + } + + // Don't show users who blocked me or I blocked + if ($row['is_blocked_by_me'] > 0 || $row['has_blocked_me'] > 0) { + continue; + } + + $users[] = [ + 'id' => $row['id'], + 'username' => $row['username'], + 'display_id' => $row['display_id'], + 'display_name' => $row['username'] . '#' . $row['display_id'], + 'age_group' => $resolvedAgeGroup, + 'is_online' => $row['is_online'], + 'unread_count' => $row['unread_count'] + ]; + } + + if ($rawCount > 0 && count($users) === 0) { + logSecurityEvent( + $currentUserId, + 'GET_USERS_FILTERED_EMPTY', + sprintf( + 'Raw: %d | AgeGroup: %s', + $rawCount, + $currentAgeGroup + ) + ); + } + + echo json_encode([ + 'success' => true, + 'users' => $users, + 'diagnostics' => [ + 'raw_count' => $rawCount, + 'filtered_count' => count($users), + 'current_age_group' => $currentAgeGroup + ] + ]); + exit; + } + + // ─────────────────────────────────────────────────────── + // GET MESSAGES + // ─────────────────────────────────────────────────────── + if ($action === 'get_messages') { + $otherUserId = intval($_GET['user_id'] ?? 0); + + if ($otherUserId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + // Check if blocked + if (isBlocked(getCurrentUserId(), $otherUserId)) { + echo json_encode(['success' => false, 'error' => 'Chat nicht verfügbar']); + exit; + } + + $db = getDB(); + $currentUserId = getCurrentUserId(); + $currentAgeGroup = getCurrentAgeGroup(); + + $stmt = $db->prepare('SELECT age_group FROM users WHERE id = :user_id AND is_banned = 0'); + $stmt->bindValue(':user_id', $otherUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $otherUser = $result->fetchArray(SQLITE3_ASSOC); + + if (!$otherUser) { + echo json_encode(['success' => false, 'error' => 'Benutzer nicht gefunden']); + exit; + } + + if (!canUsersChatByAge($currentAgeGroup, $otherUser['age_group'])) { + logSecurityEvent($currentUserId, 'AGE_RESTRICTION_BLOCKED', "GET_MESSAGES -> User $otherUserId"); + echo json_encode(['success' => false, 'error' => 'Chat zwischen Altersgruppen nicht erlaubt']); + exit; + } + + $query = ' + SELECT * FROM ( + SELECT + m.id, + m.from_user_id, + m.to_user_id, + m.message, + m.timestamp, + m.is_read, + m.is_flagged, + m.attachment_path, + m.attachment_type, + m.attachment_size, + u.username as from_username, + u.user_id as from_display_id + FROM messages m + JOIN users u ON m.from_user_id = u.id + WHERE + (m.from_user_id = :current_user_id AND m.to_user_id = :other_user_id) + OR + (m.from_user_id = :other_user_id AND m.to_user_id = :current_user_id) + ORDER BY m.id DESC + LIMIT :limit + ) + ORDER BY id ASC + '; + + $stmt = $db->prepare($query); + $stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER); + $stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER); + $stmt->bindValue(':limit', MAX_MESSAGES_PER_FETCH, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $messages = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $messages[] = [ + 'id' => $row['id'], + 'from_user_id' => $row['from_user_id'], + 'to_user_id' => $row['to_user_id'], + 'message' => $row['message'], + 'timestamp' => $row['timestamp'], + 'is_read' => $row['is_read'], + 'is_flagged' => $row['is_flagged'], + 'attachment_url' => $row['attachment_path'] ?: null, + 'attachment_type' => $row['attachment_type'] ?: null, + 'attachment_size' => $row['attachment_size'] !== null ? (int)$row['attachment_size'] : null, + 'from_username' => $row['from_username'], + 'from_display_name' => $row['from_username'] . '#' . $row['from_display_id'] + ]; + } + + echo json_encode(['success' => true, 'messages' => $messages]); + exit; + } + + // ─────────────────────────────────────────────────────── + // SEND MESSAGE + // ─────────────────────────────────────────────────────── + if ($action === 'send_message') { + $toUserId = intval($_POST['to_user_id'] ?? 0); + $message = trim($_POST['message'] ?? ''); + $hasAttachment = isset($_FILES['attachment']) && ($_FILES['attachment']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE; + + if ($toUserId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + if (!$hasAttachment && $message === '') { + echo json_encode(['success' => false, 'error' => 'Nachricht oder Bild erforderlich']); + exit; + } + + if (strlen($message) > 1000) { + echo json_encode(['success' => false, 'error' => 'Nachricht zu lang (max 1000 Zeichen)']); + exit; + } + + $attachmentFile = $hasAttachment ? $_FILES['attachment'] : null; + $attachmentMime = null; + $attachmentSize = null; + + if ($hasAttachment && $attachmentFile) { + if (!is_uploaded_file($attachmentFile['tmp_name'])) { + echo json_encode(['success' => false, 'error' => 'Ungültiger Datei-Upload']); + exit; + } + + if ($attachmentFile['error'] !== UPLOAD_ERR_OK) { + echo json_encode(['success' => false, 'error' => 'Bild konnte nicht hochgeladen werden']); + exit; + } + + if ($attachmentFile['size'] > MAX_ATTACHMENT_SIZE) { + echo json_encode(['success' => false, 'error' => 'Bild ist zu groß (max. 200 KB)']); + exit; + } + + $attachmentSize = (int)$attachmentFile['size']; + + $mime = null; + if (function_exists('finfo_open')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + if ($finfo) { + $mime = finfo_file($finfo, $attachmentFile['tmp_name']); + finfo_close($finfo); + } + } + if (!$mime && function_exists('mime_content_type')) { + $mime = mime_content_type($attachmentFile['tmp_name']); + } + if (!$mime && isset($attachmentFile['type'])) { + $mime = $attachmentFile['type']; + } + + $mime = strtolower((string)$mime); + if (!in_array($mime, ['image/jpeg', 'image/pjpeg', 'image/jpg'], true)) { + echo json_encode(['success' => false, 'error' => 'Nur JPG-Bilder sind erlaubt']); + exit; + } + + $imageInfo = @getimagesize($attachmentFile['tmp_name']); + if ($imageInfo === false || !in_array($imageInfo[2], [IMAGETYPE_JPEG], true)) { + echo json_encode(['success' => false, 'error' => 'Bilddatei konnte nicht verifiziert werden']); + exit; + } + + $attachmentMime = 'image/jpeg'; + } + + // Check if blocked + if (isBlocked(getCurrentUserId(), $toUserId)) { + echo json_encode(['success' => false, 'error' => 'Nachricht kann nicht gesendet werden']); + exit; + } + + $db = getDB(); + $currentUserId = getCurrentUserId(); + $currentAgeGroup = getCurrentAgeGroup(); + + $stmt = $db->prepare('SELECT age_group FROM users WHERE id = :user_id AND is_banned = 0'); + $stmt->bindValue(':user_id', $toUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $targetUser = $result->fetchArray(SQLITE3_ASSOC); + + if (!$targetUser) { + echo json_encode(['success' => false, 'error' => 'Empfänger nicht gefunden']); + exit; + } + + if (!canUsersChatByAge($currentAgeGroup, $targetUser['age_group'])) { + logSecurityEvent($currentUserId, 'AGE_RESTRICTION_BLOCKED', "SEND_MESSAGE -> User $toUserId"); + echo json_encode(['success' => false, 'error' => 'Nachrichten zwischen Altersgruppen nicht erlaubt']); + exit; + } + + // Rate Limiting + $rateLimitCheck = checkRateLimit($currentUserId, $currentAgeGroup); + if (!$rateLimitCheck['allowed']) { + logSecurityEvent($currentUserId, 'RATE_LIMIT_EXCEEDED', $rateLimitCheck['reason']); + echo json_encode(['success' => false, 'error' => $rateLimitCheck['reason']]); + exit; + } + + if ($message !== '') { + // Keyword Blacklist + $keywordCheck = checkKeywordBlacklist($message); + if ($keywordCheck['blocked']) { + logSecurityEvent($currentUserId, 'KEYWORD_BLOCKED', "Keyword: {$keywordCheck['keyword']}"); + echo json_encode([ + 'success' => false, + 'error' => 'Deine Nachricht enthält nicht erlaubte Inhalte', + 'details' => 'Verbotenes Wort erkannt: ' . $keywordCheck['keyword'] + ]); + exit; + } + + // Profanity Filter + $profanityCheck = checkProfanityFilter($message); + if ($profanityCheck['blocked']) { + logSecurityEvent($currentUserId, 'PROFANITY_BLOCKED', "Word: {$profanityCheck['word']}"); + echo json_encode([ + 'success' => false, + 'error' => 'Deine Nachricht enthält Schimpfwörter', + 'details' => 'Bitte verwende eine angemessene Sprache' + ]); + exit; + } + + // Link Filter + $linkCheck = checkLinkFilter($message); + if ($linkCheck['blocked']) { + logSecurityEvent($currentUserId, 'LINK_BLOCKED', "Message: $message"); + echo json_encode([ + 'success' => false, + 'error' => 'Links sind nicht erlaubt', + 'details' => 'Aus Sicherheitsgründen können keine URLs gesendet werden' + ]); + exit; + } + } + + // Auto-Flagging (verdächtige Muster) + $isFlagged = 0; + $flagReason = ''; + + if ($message !== '') { + if (preg_match('/(.)\1{5,}/', $message)) { + $isFlagged = 1; + $flagReason = 'Repeated characters'; + } + + if (strlen($message) > 20 && $message === strtoupper($message)) { + $isFlagged = 1; + $flagReason = 'All caps'; + } + + $emojiCount = preg_match_all('/[\x{1F600}-\x{1F64F}]/u', $message); + if ($emojiCount > 10) { + $isFlagged = 1; + $flagReason = 'Excessive emojis'; + } + } + + $attachmentPath = null; + if ($hasAttachment && $attachmentFile) { + $randomName = bin2hex(random_bytes(16)) . '.jpg'; + $destination = rtrim(UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $randomName; + + if (!move_uploaded_file($attachmentFile['tmp_name'], $destination)) { + echo json_encode(['success' => false, 'error' => 'Bild konnte nicht gespeichert werden']); + exit; + } + + $attachmentPath = 'uploads/' . $randomName; + } + + // Insert message + $stmt = $db->prepare(' + INSERT INTO messages (from_user_id, to_user_id, message, is_flagged, flag_reason, attachment_path, attachment_type, attachment_size) + VALUES (:from_user_id, :to_user_id, :message, :is_flagged, :flag_reason, :attachment_path, :attachment_type, :attachment_size) + '); + $stmt->bindValue(':from_user_id', $currentUserId, SQLITE3_INTEGER); + $stmt->bindValue(':to_user_id', $toUserId, SQLITE3_INTEGER); + $stmt->bindValue(':message', $message, SQLITE3_TEXT); + $stmt->bindValue(':is_flagged', $isFlagged, SQLITE3_INTEGER); + $stmt->bindValue(':flag_reason', $flagReason, SQLITE3_TEXT); + if ($attachmentPath) { + $stmt->bindValue(':attachment_path', $attachmentPath, SQLITE3_TEXT); + $stmt->bindValue(':attachment_type', $attachmentMime, SQLITE3_TEXT); + $stmt->bindValue(':attachment_size', $attachmentSize, SQLITE3_INTEGER); + } else { + $stmt->bindValue(':attachment_path', null, SQLITE3_NULL); + $stmt->bindValue(':attachment_type', null, SQLITE3_NULL); + $stmt->bindValue(':attachment_size', null, SQLITE3_NULL); + } + $stmt->execute(); + + $messageId = $db->lastInsertRowID(); + + // Log rate limit + logRateLimit($currentUserId); + + if ($isFlagged) { + logSecurityEvent($currentUserId, 'MESSAGE_FLAGGED', "Reason: $flagReason, Message ID: $messageId"); + } + + if ($attachmentPath) { + logSecurityEvent($currentUserId, 'ATTACHMENT_UPLOADED', "Message ID: $messageId, Size: $attachmentSize"); + } + + echo json_encode([ + 'success' => true, + 'message_id' => $messageId, + 'timestamp' => date('Y-m-d H:i:s'), + 'attachment_url' => $attachmentPath + ]); + exit; + } + + // ─────────────────────────────────────────────────────── + // MARK AS READ + // ─────────────────────────────────────────────────────── + if ($action === 'mark_read') { + $otherUserId = intval($_POST['user_id'] ?? 0); + + if ($otherUserId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + $db = getDB(); + $currentUserId = getCurrentUserId(); + $currentAgeGroup = getCurrentAgeGroup(); + + $stmt = $db->prepare('SELECT age_group FROM users WHERE id = :user_id AND is_banned = 0'); + $stmt->bindValue(':user_id', $otherUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $otherUser = $result->fetchArray(SQLITE3_ASSOC); + + if (!$otherUser) { + echo json_encode(['success' => false, 'error' => 'Benutzer nicht gefunden']); + exit; + } + + if (!canUsersChatByAge($currentAgeGroup, $otherUser['age_group'])) { + logSecurityEvent($currentUserId, 'AGE_RESTRICTION_BLOCKED', "MARK_READ -> User $otherUserId"); + echo json_encode(['success' => false, 'error' => 'Aktion zwischen Altersgruppen nicht erlaubt']); + exit; + } + + $stmt = $db->prepare(' + UPDATE messages + SET is_read = 1 + WHERE from_user_id = :other_user_id + AND to_user_id = :current_user_id + AND is_read = 0 + '); + $stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER); + $stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER); + $stmt->execute(); + + echo json_encode(['success' => true]); + exit; + } + + // ─────────────────────────────────────────────────────── + // BLOCK USER + // ─────────────────────────────────────────────────────── + if ($action === 'block_user') { + $blockedUserId = intval($_POST['user_id'] ?? 0); + + if ($blockedUserId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + $db = getDB(); + $currentUserId = getCurrentUserId(); + + $stmt = $db->prepare(' + INSERT OR IGNORE INTO blocks (blocker_id, blocked_id) + VALUES (:blocker_id, :blocked_id) + '); + $stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER); + $stmt->bindValue(':blocked_id', $blockedUserId, SQLITE3_INTEGER); + $stmt->execute(); + + logSecurityEvent($currentUserId, 'USER_BLOCKED', "Blocked user ID: $blockedUserId"); + + echo json_encode(['success' => true]); + exit; + } + + // ─────────────────────────────────────────────────────── + // UNBLOCK USER + // ─────────────────────────────────────────────────────── + if ($action === 'unblock_user') { + $blockedUserId = intval($_POST['user_id'] ?? 0); + + if ($blockedUserId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + $db = getDB(); + $currentUserId = getCurrentUserId(); + + $stmt = $db->prepare(' + DELETE FROM blocks + WHERE blocker_id = :blocker_id + AND blocked_id = :blocked_id + '); + $stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER); + $stmt->bindValue(':blocked_id', $blockedUserId, SQLITE3_INTEGER); + $stmt->execute(); + + logSecurityEvent($currentUserId, 'USER_UNBLOCKED', "Unblocked user ID: $blockedUserId"); + + echo json_encode(['success' => true]); + exit; + } + + // ─────────────────────────────────────────────────────── + // REPORT USER + // ─────────────────────────────────────────────────────── + if ($action === 'report_user') { + $reportedUserId = intval($_POST['user_id'] ?? 0); + $reason = trim($_POST['reason'] ?? ''); + $messageId = intval($_POST['message_id'] ?? 0); + + if ($reportedUserId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + if (empty($reason)) { + echo json_encode(['success' => false, 'error' => 'Bitte gib einen Grund an']); + exit; + } + + $db = getDB(); + $currentUserId = getCurrentUserId(); + + $stmt = $db->prepare(' + INSERT INTO reports (reporter_id, reported_user_id, reason, message_id) + VALUES (:reporter_id, :reported_user_id, :reason, :message_id) + '); + $stmt->bindValue(':reporter_id', $currentUserId, SQLITE3_INTEGER); + $stmt->bindValue(':reported_user_id', $reportedUserId, SQLITE3_INTEGER); + $stmt->bindValue(':reason', $reason, SQLITE3_TEXT); + $stmt->bindValue(':message_id', $messageId > 0 ? $messageId : null, SQLITE3_INTEGER); + $stmt->execute(); + + logSecurityEvent($currentUserId, 'USER_REPORTED', "Reported user ID: $reportedUserId, Reason: $reason"); + + echo json_encode(['success' => true, 'message' => 'Meldung wurde erfolgreich gesendet']); + exit; + } + + // ─────────────────────────────────────────────────────── + // GET BLOCKED USERS + // ─────────────────────────────────────────────────────── + if ($action === 'get_blocked_users') { + $db = getDB(); + $currentUserId = getCurrentUserId(); + + $query = ' + SELECT + u.id, + u.username, + u.user_id as display_id, + b.timestamp as blocked_at + FROM blocks b + JOIN users u ON b.blocked_id = u.id + WHERE b.blocker_id = :current_user_id + ORDER BY b.timestamp DESC + '; + + $stmt = $db->prepare($query); + $stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $blockedUsers = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $blockedUsers[] = [ + 'id' => $row['id'], + 'display_name' => $row['username'] . '#' . $row['display_id'], + 'blocked_at' => $row['blocked_at'] + ]; + } + + echo json_encode(['success' => true, 'blocked_users' => $blockedUsers]); + exit; + } + + // ═══════════════════════════════════════════════════════════ + // ADMIN ACTIONS + // ═══════════════════════════════════════════════════════════ + + if (!isAdmin()) { + echo json_encode(['success' => false, 'error' => 'Admin-Rechte erforderlich']); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: GET REPORTS + // ─────────────────────────────────────────────────────── + if ($action === 'admin_get_reports') { + $db = getDB(); + + $query = ' + SELECT + r.id, + r.reason, + r.timestamp, + r.status, + r.message_id, + reporter.username as reporter_name, + reporter.user_id as reporter_display_id, + reported.username as reported_name, + reported.user_id as reported_display_id, + reported.id as reported_user_id, + m.message as message_content + FROM reports r + JOIN users reporter ON r.reporter_id = reporter.id + JOIN users reported ON r.reported_user_id = reported.id + LEFT JOIN messages m ON r.message_id = m.id + WHERE r.status = "pending" + ORDER BY r.timestamp DESC + LIMIT 50 + '; + + $result = $db->query($query); + + $reports = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $reports[] = [ + 'id' => $row['id'], + 'reporter' => $row['reporter_name'] . '#' . $row['reporter_display_id'], + 'reported' => $row['reported_name'] . '#' . $row['reported_display_id'], + 'reported_user_id' => $row['reported_user_id'], + 'reason' => $row['reason'], + 'message' => $row['message_content'], + 'timestamp' => $row['timestamp'], + 'status' => $row['status'] + ]; + } + + echo json_encode(['success' => true, 'reports' => $reports]); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: GET FLAGGED MESSAGES + // ─────────────────────────────────────────────────────── + if ($action === 'admin_get_flagged') { + $db = getDB(); + + $query = ' + SELECT + m.id, + m.message, + m.flag_reason, + m.timestamp, + u.username, + u.user_id as display_id, + u.id as user_db_id + FROM messages m + JOIN users u ON m.from_user_id = u.id + WHERE m.is_flagged = 1 + ORDER BY m.timestamp DESC + LIMIT 50 + '; + + $result = $db->query($query); + + $flagged = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $flagged[] = [ + 'id' => $row['id'], + 'user' => $row['username'] . '#' . $row['display_id'], + 'user_id' => $row['user_db_id'], + 'message' => $row['message'], + 'reason' => $row['flag_reason'], + 'timestamp' => $row['timestamp'] + ]; + } + + echo json_encode(['success' => true, 'flagged' => $flagged]); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: GET STATISTICS + // ─────────────────────────────────────────────────────── + if ($action === 'admin_get_stats') { + $db = getDB(); + + // Total users + $result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 0'); + $totalUsers = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // U18 users + $result = $db->query('SELECT COUNT(*) as count FROM users WHERE age_group = "U18" AND is_banned = 0'); + $u18Users = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // O18 users + $result = $db->query('SELECT COUNT(*) as count FROM users WHERE age_group = "O18" AND is_banned = 0'); + $o18Users = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // Online users + $result = $db->query(' + SELECT COUNT(*) as count + FROM online_status + WHERE (julianday("now") - julianday(last_ping)) * 86400 < ' . ONLINE_TIMEOUT_SECONDS + ); + $onlineUsers = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // Total messages (24h) + $result = $db->query('SELECT COUNT(*) as count FROM messages'); + $totalMessages = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // Flagged messages + $result = $db->query('SELECT COUNT(*) as count FROM messages WHERE is_flagged = 1'); + $flaggedMessages = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // Pending reports + $result = $db->query('SELECT COUNT(*) as count FROM reports WHERE status = "pending"'); + $pendingReports = $result->fetchArray(SQLITE3_ASSOC)['count']; + + // Banned users + $result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 1'); + $bannedUsers = $result->fetchArray(SQLITE3_ASSOC)['count']; + + echo json_encode([ + 'success' => true, + 'stats' => [ + 'total_users' => $totalUsers, + 'u18_users' => $u18Users, + 'o18_users' => $o18Users, + 'online_users' => $onlineUsers, + 'total_messages' => $totalMessages, + 'flagged_messages' => $flaggedMessages, + 'pending_reports' => $pendingReports, + 'banned_users' => $bannedUsers + ] + ]); + exit; + } + + if ($action === 'admin_get_banned_users') { + $db = getDB(); + + $result = $db->query(' + SELECT id, username, user_id as display_id, ban_reason, last_seen + FROM users + WHERE is_banned = 1 + ORDER BY last_seen DESC + LIMIT 100 + '); + + $banned = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $banned[] = [ + 'id' => $row['id'], + 'display_name' => $row['username'] . '#' . $row['display_id'], + 'reason' => $row['ban_reason'] ?? 'keine Angabe', + 'last_seen' => $row['last_seen'] + ]; + } + + echo json_encode(['success' => true, 'banned' => $banned]); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: BAN USER + // ─────────────────────────────────────────────────────── + if ($action === 'admin_ban_user') { + $userId = intval($_POST['user_id'] ?? 0); + $reason = trim($_POST['reason'] ?? 'Verstoß gegen Nutzungsbedingungen'); + + if ($userId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + $db = getDB(); + $stmt = $db->prepare(' + UPDATE users + SET is_banned = 1, ban_reason = :reason + WHERE id = :user_id + '); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->bindValue(':reason', $reason, SQLITE3_TEXT); + $stmt->execute(); + + logSecurityEvent(null, 'ADMIN_BAN_USER', "User ID: $userId, Reason: $reason"); + + echo json_encode(['success' => true, 'message' => 'User wurde gesperrt']); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: UNBAN USER + // ─────────────────────────────────────────────────────── + if ($action === 'admin_unban_user') { + $userId = intval($_POST['user_id'] ?? 0); + + if ($userId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']); + exit; + } + + $db = getDB(); + $stmt = $db->prepare(' + UPDATE users + SET is_banned = 0, ban_reason = NULL + WHERE id = :user_id + '); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->execute(); + + logSecurityEvent(null, 'ADMIN_UNBAN_USER', "User ID: $userId"); + + echo json_encode(['success' => true, 'message' => 'Sperre wurde aufgehoben']); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: RESOLVE REPORT + // ─────────────────────────────────────────────────────── + if ($action === 'admin_resolve_report') { + $reportId = intval($_POST['report_id'] ?? 0); + $action_taken = trim($_POST['action_taken'] ?? 'resolved'); + + if ($reportId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige Report-ID']); + exit; + } + + $db = getDB(); + $stmt = $db->prepare(' + UPDATE reports + SET status = :status + WHERE id = :report_id + '); + $stmt->bindValue(':report_id', $reportId, SQLITE3_INTEGER); + $stmt->bindValue(':status', $action_taken, SQLITE3_TEXT); + $stmt->execute(); + + logSecurityEvent(null, 'ADMIN_RESOLVE_REPORT', "Report ID: $reportId, Action: $action_taken"); + + echo json_encode(['success' => true, 'message' => 'Report wurde bearbeitet']); + exit; + } + + // ─────────────────────────────────────────────────────── + // ADMIN: DELETE MESSAGE + // ─────────────────────────────────────────────────────── + if ($action === 'admin_delete_message') { + $messageId = intval($_POST['message_id'] ?? 0); + + if ($messageId <= 0) { + echo json_encode(['success' => false, 'error' => 'Ungültige Message-ID']); + exit; + } + + $db = getDB(); + $stmt = $db->prepare('DELETE FROM messages WHERE id = :message_id'); + $stmt->bindValue(':message_id', $messageId, SQLITE3_INTEGER); + $stmt->execute(); + + logSecurityEvent(null, 'ADMIN_DELETE_MESSAGE', "Message ID: $messageId"); + + echo json_encode(['success' => true, 'message' => 'Nachricht wurde gelöscht']); + exit; + } + + if ($action === 'poll_updates') { + if (!isLoggedIn()) { + echo json_encode(['success' => false, 'error' => 'Nicht angemeldet']); + exit; + } + + if (!validateActiveSession()) { + echo json_encode(['success' => false, 'error' => 'Sitzung ungültig']); + exit; + } + + $lastMessageId = intval($_POST['last_message_id'] ?? $_GET['last_message_id'] ?? 0); + + $db = getDB(); + $currentUserId = getCurrentUserId(); + $currentAgeGroup = getCurrentAgeGroup(); + + touchUserSession($currentUserId); + + $stmt = $db->prepare(' + SELECT + m.id, + m.from_user_id, + m.to_user_id, + m.message, + m.timestamp, + m.attachment_path, + m.attachment_type, + m.attachment_size, + uf.username as from_username, + uf.user_id as from_display_id, + uf.age_group as from_age_group, + ut.age_group as to_age_group + FROM messages m + JOIN users uf ON m.from_user_id = uf.id + JOIN users ut ON m.to_user_id = ut.id + WHERE m.id > :last_message_id + AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id) + AND NOT EXISTS ( + SELECT 1 FROM blocks + WHERE (blocker_id = :current_user_id AND blocked_id = m.from_user_id) + OR (blocker_id = m.from_user_id AND blocked_id = :current_user_id) + ) + ORDER BY m.id ASC + '); + $stmt->bindValue(':last_message_id', $lastMessageId, SQLITE3_INTEGER); + $stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $messages = []; + $maxId = $lastMessageId; + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $otherAgeGroup = $row['from_user_id'] === $currentUserId ? $row['to_age_group'] : $row['from_age_group']; + + if (!canUsersChatByAge($currentAgeGroup, $otherAgeGroup)) { + continue; + } + + $maxId = max($maxId, (int)$row['id']); + + $messages[] = [ + 'id' => $row['id'], + 'from_user_id' => $row['from_user_id'], + 'to_user_id' => $row['to_user_id'], + 'message' => $row['message'], + 'timestamp' => $row['timestamp'], + 'attachment_url' => $row['attachment_path'] ?: null, + 'attachment_type' => $row['attachment_type'] ?: null, + 'attachment_size' => $row['attachment_size'] !== null ? (int)$row['attachment_size'] : null, + 'from_username' => $row['from_username'], + 'from_display_name' => $row['from_username'] . '#' . $row['from_display_id'] + ]; + } + + echo json_encode([ + 'success' => true, + 'messages' => $messages, + 'last_message_id' => $maxId + ]); + exit; + } + + echo json_encode(['success' => false, 'error' => 'Unbekannte Aktion']); + exit; +} + +// ═══════════════════════════════════════════════════════════ +// SSE STREAM (ECHTZEIT) +// ═══════════════════════════════════════════════════════════ + +if (isset($_GET['stream']) && $_GET['stream'] === 'events') { + if (!isLoggedIn()) { + exit; + } + + if (!validateActiveSession()) { + exit; + } + + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + header('X-Accel-Buffering: no'); + + $currentUserId = getCurrentUserId(); + $currentAgeGroup = getCurrentAgeGroup(); + $lastMessageId = intval($_GET['last_message_id'] ?? 0); + + touchUserSession($currentUserId); + + set_time_limit(0); + ob_implicit_flush(true); + while (ob_get_level() > 0) { + @ob_end_flush(); + } + + echo ": connected\n\n"; + echo "retry: " . SSE_RETRY_MS . "\n\n"; + flush(); + + $lastPingTime = time(); + + while (true) { + if (connection_aborted()) { + break; + } + + $db = getDB(); + + $stmt = $db->prepare(' + SELECT + m.id, + m.from_user_id, + m.to_user_id, + m.message, + m.timestamp, + m.attachment_path, + m.attachment_type, + m.attachment_size, + uf.username as from_username, + uf.user_id as from_display_id, + uf.age_group as from_age_group, + ut.age_group as to_age_group + FROM messages m + JOIN users uf ON m.from_user_id = uf.id + JOIN users ut ON m.to_user_id = ut.id + WHERE m.id > :last_message_id + AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id) + AND NOT EXISTS ( + SELECT 1 FROM blocks + WHERE (blocker_id = :current_user_id AND blocked_id = m.from_user_id) + OR (blocker_id = m.from_user_id AND blocked_id = :current_user_id) + ) + ORDER BY m.id ASC + '); + $stmt->bindValue(':last_message_id', $lastMessageId, SQLITE3_INTEGER); + $stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $messages = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $otherAgeGroup = $row['from_user_id'] === $currentUserId ? $row['to_age_group'] : $row['from_age_group']; + + if (!canUsersChatByAge($currentAgeGroup, $otherAgeGroup)) { + continue; + } + + $messages[] = [ + 'id' => $row['id'], + 'from_user_id' => $row['from_user_id'], + 'to_user_id' => $row['to_user_id'], + 'message' => $row['message'], + 'timestamp' => $row['timestamp'], + 'attachment_url' => $row['attachment_path'] ?: null, + 'attachment_type' => $row['attachment_type'] ?: null, + 'attachment_size' => $row['attachment_size'] !== null ? (int)$row['attachment_size'] : null, + 'from_username' => $row['from_username'], + 'from_display_name' => $row['from_username'] . '#' . $row['from_display_id'] + ]; + + $lastMessageId = max($lastMessageId, (int)$row['id']); + } + + if (!empty($messages)) { + echo "data: " . json_encode(['type' => 'messages', 'messages' => $messages]) . "\n\n"; + flush(); + } + + if (time() - $lastPingTime >= 15) { + echo "data: " . json_encode(['type' => 'ping']) . "\n\n"; + flush(); + $lastPingTime = time(); + touchUserSession($currentUserId); + } + + usleep(500000); + } + + exit; +} + +// ═══════════════════════════════════════════════════════════ +// HTML OUTPUT +// ═══════════════════════════════════════════════════════════ +?> + + + + + + + 💬 Secure Private Chat + + + + + + +
+
+

🔐 Admin-Dashboard

+ +
+ +
+ +
+ +
+
+
+

🚨 Offene Meldungen

+
+
+
Lade Meldungen…
+
+
+ +
+

🚩 Markierte Nachrichten

+
+
Lade Nachrichten…
+
+
+ +
+

🚫 Gesperrte Nutzer

+
+
Lade Nutzer…
+
+
+
+
+ + +
+

🔐 Admin-Login

+

Zugriff nur für autorisierte Moderatoren.

+ +
+ +
+
+ + +
+ +
+ + +
+ + +
+ + +
+ + + +
+

💬 Secure Private Chat

+

Sicherer Chat mit Altersverifikation

+ +
+ + +
+ +
+
+ +
+
+ + +
+ +
+ + +
+ +
+

⚠️ Wichtige Regeln

+
    +
  • Gib NIEMALS persönliche Daten weiter (Adresse, Telefon, etc.)
  • +
  • Treffe dich NICHT mit Fremden
  • +
  • Bleibe respektvoll und freundlich
  • +
  • Keine Links oder externe Kontakte teilen
  • +
  • Bei verdächtigem Verhalten: Melde den User!
  • +
+
+ +
+ + +
+ + +
+ + + + +
+ + + +
+ +
+

💬 Secure Private Chat

+ +
+ + + + + +
+
+
💬
+
Wähle einen Benutzer aus der Liste
+
+ +
+
+ +
+ + +
+ +
+ +
+
+ + + +
+ + +
+ +
+
+
+ + + + +