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 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) { 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 && !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. Bitte dort zuerst ausloggen oder kurz warten.' ]; } } $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; } // ─────────────────────────────────────────────────────── // 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.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'; } $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 = []; while ($row = $result->fetchArray(SQLITE3_ASSOC)) { // 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' => $row['age_group'], 'is_online' => $row['is_online'], 'unread_count' => $row['unread_count'] ]; } echo json_encode(['success' => true, 'users' => $users]); 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; } 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(); $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'] ]; } if (!empty($messages)) { echo "data: " . json_encode(['type' => 'messages', 'messages' => $messages]) . "\n\n"; flush(); } else { echo "data: " . json_encode(['type' => 'ping']) . "\n\n"; flush(); } 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