Der 3D-Traceroute benötigt WebGL-Unterstützung. Bitte verwenden Sie einen aktuellen Browser oder aktivieren Sie WebGL in den Einstellungen.
-diff --git a/write.php b/chat.php similarity index 61% rename from write.php rename to chat.php index 0655580..e22f2e8 100644 --- a/write.php +++ b/chat.php @@ -30,6 +30,8 @@ define('LOG_RETENTION_MONTHS', 6); define('ONLINE_TIMEOUT_SECONDS', 30); define('SSE_RETRY_MS', 500); define('MAX_MESSAGES_PER_FETCH', 200); +define('MAX_ATTACHMENT_SIZE', 200 * 1024); // 200 KB +define('UPLOAD_DIR', __DIR__ . '/uploads'); // Rate Limiting define('MAX_MESSAGES_PER_MINUTE', 10); @@ -71,141 +73,184 @@ $PROFANITY_FILTER = [ // ═══════════════════════════════════════════════════════════ function getDB() { - $db = new SQLite3(DB_FILE); - $db->busyTimeout(5000); - - // 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, - FOREIGN KEY (from_user_id) REFERENCES users(id), - FOREIGN KEY (to_user_id) REFERENCES users(id) - ) - '); - - // 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) - ) - '); + static $db = null; + static $initialized = false; - // 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) - ) - '); + if ($db === null) { + if (!is_dir(UPLOAD_DIR)) { + @mkdir(UPLOAD_DIR, 0755, true); + } - // 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(); + $db = new SQLite3(DB_FILE); + $db->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 + ) + '); + + // Settings Table (Feature-Flags) + $db->exec(' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + '); + + initializeDefaultSettings($db); + + // 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; } - - // 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)'); return $db; } @@ -229,6 +274,129 @@ function getAgeGroup($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 initializeDefaultSettings(SQLite3 $db) { + static $defaultsInitialized = false; + + if ($defaultsInitialized) { + return; + } + + $defaultsInitialized = true; + + $defaults = [ + 'age_filter_enabled' => '0', + 'keyword_filter_enabled' => '0', + 'profanity_filter_enabled' => '0', + 'link_filter_enabled' => '0', + ]; + + $stmt = $db->prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (:key, :value)'); + + foreach ($defaults as $key => $value) { + $stmt->bindValue(':key', $key, SQLITE3_TEXT); + $stmt->bindValue(':value', $value, SQLITE3_TEXT); + $stmt->execute(); + } +} + +function settingsCache($forceReload = false) { + static $cache = null; + + if ($forceReload) { + $cache = null; + } + + if ($cache === null) { + $cache = []; + $db = getDB(); + $result = $db->query('SELECT key, value FROM settings'); + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $cache[$row['key']] = $row['value']; + } + } + + return $cache; +} + +function getSetting($key, $default = null) { + $cache = settingsCache(); + return array_key_exists($key, $cache) ? $cache[$key] : $default; +} + +function setSettingValue($key, $value) { + $db = getDB(); + $stmt = $db->prepare(' + INSERT OR REPLACE INTO settings (key, value, updated_at) + VALUES (:key, :value, CURRENT_TIMESTAMP) + '); + $stmt->bindValue(':key', $key, SQLITE3_TEXT); + $stmt->bindValue(':value', $value, SQLITE3_TEXT); + $stmt->execute(); + + settingsCache(true); +} + +function normalizeBooleanFlag($value) { + if (is_bool($value)) { + return $value; + } + + if (is_int($value)) { + return $value === 1; + } + + $stringValue = is_string($value) ? strtolower(trim($value)) : ''; + if ($stringValue === '') { + return false; + } + + return in_array($stringValue, ['1', 'true', 'yes', 'on'], true); +} + +function isFeatureEnabled($settingKey, $default = false) { + $defaultValue = $default ? '1' : '0'; + return normalizeBooleanFlag(getSetting($settingKey, $defaultValue)); +} + +function isAgeFilterEnabled() { + return isFeatureEnabled('age_filter_enabled', false); +} + +function isKeywordFilterEnabled() { + return isFeatureEnabled('keyword_filter_enabled', false); +} + +function isProfanityFilterEnabled() { + return isFeatureEnabled('profanity_filter_enabled', false); +} + +function isLinkFilterEnabled() { + return isFeatureEnabled('link_filter_enabled', false); +} + +function getFeatureSettings() { + return [ + 'age_filter_enabled' => isAgeFilterEnabled(), + 'keyword_filter_enabled' => isKeywordFilterEnabled(), + 'profanity_filter_enabled' => isProfanityFilterEnabled(), + 'link_filter_enabled' => isLinkFilterEnabled(), + ]; +} + function checkKeywordBlacklist($message) { global $KEYWORD_BLACKLIST; @@ -352,6 +520,10 @@ function logSecurityEvent($userId, $action, $details = '') { } function canUsersChatByAge($ageGroupA, $ageGroupB) { + if (!isAgeFilterEnabled()) { + return true; + } + if (!$ageGroupA || !$ageGroupB) { return false; } @@ -383,9 +555,27 @@ function isBlocked($userId, $otherUserId) { 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 @@ -451,7 +641,7 @@ function generateSessionToken() { return bin2hex(random_bytes(32)); } -function startUserSession($userId) { +function startUserSession($userId, $force = false) { if (!$userId) { return ['allowed' => false, 'error' => 'Ungültige Benutzer-ID']; } @@ -462,17 +652,24 @@ function startUserSession($userId) { $result = $stmt->execute(); $existing = $result->fetchArray(SQLITE3_ASSOC); - if ($existing && !empty($existing['last_seen'])) { + 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. Bitte dort zuerst ausloggen oder kurz warten.' + '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(' @@ -568,7 +765,7 @@ if (isset($_POST['action']) || isset($_GET['action'])) { $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']); @@ -665,7 +862,85 @@ if (isset($_POST['action']) || isset($_GET['action'])) { ]); 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 // ─────────────────────────────────────────────────────── @@ -744,11 +1019,14 @@ if (isset($_POST['action']) || isset($_GET['action'])) { $currentUserId = getCurrentUserId(); $currentAgeGroup = getCurrentAgeGroup(); + $ageFilterEnabled = isAgeFilterEnabled(); + $query = ' SELECT u.id, u.username, u.user_id as display_id, + u.birthdate, u.age_group, u.last_seen, CASE @@ -782,10 +1060,12 @@ if (isset($_POST['action']) || isset($_GET['action'])) { AND u.is_banned = 0 '; - if ($currentAgeGroup === 'U18') { - $query .= ' AND u.age_group = :allowed_group'; - } else { - $query .= ' AND u.age_group != :blocked_group'; + if ($ageFilterEnabled) { + 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'; @@ -793,32 +1073,65 @@ if (isset($_POST['action']) || isset($_GET['action'])) { $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); + if ($ageFilterEnabled) { + 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' => $row['age_group'], + 'age_group' => $resolvedAgeGroup, 'is_online' => $row['is_online'], 'unread_count' => $row['unread_count'] ]; } - - echo json_encode(['success' => true, 'users' => $users]); + + if ($rawCount > 0 && count($users) === 0) { + logSecurityEvent( + $currentUserId, + 'GET_USERS_FILTERED_EMPTY', + sprintf( + 'Raw: %d | AgeGroup: %s | AgeFilter: %s', + $rawCount, + $currentAgeGroup, + $ageFilterEnabled ? 'on' : 'off' + ) + ); + } + + echo json_encode([ + 'success' => true, + 'users' => $users, + 'diagnostics' => [ + 'raw_count' => $rawCount, + 'filtered_count' => count($users), + 'current_age_group' => $currentAgeGroup, + 'feature_flags' => getFeatureSettings() + ] + ]); exit; } @@ -869,6 +1182,9 @@ if (isset($_POST['action']) || isset($_GET['action'])) { 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 @@ -899,6 +1215,9 @@ if (isset($_POST['action']) || isset($_GET['action'])) { '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'] ]; @@ -914,28 +1233,81 @@ if (isset($_POST['action']) || isset($_GET['action'])) { 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 (empty($message)) { - echo json_encode(['success' => false, 'error' => 'Nachricht darf nicht leer sein']); + + 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(); @@ -963,91 +1335,125 @@ if (isset($_POST['action']) || isset($_GET['action'])) { echo json_encode(['success' => false, 'error' => $rateLimitCheck['reason']]); exit; } - - // 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; + + if ($message !== '') { + if (isKeywordFilterEnabled()) { + // 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; + } + } + + if (isProfanityFilterEnabled()) { + // 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; + } + } + + if (isLinkFilterEnabled()) { + // 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; + } + } } - - // 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 = ''; - - // Check for repeated characters (AAAAAAA) - if (preg_match('/(.)\1{5,}/', $message)) { - $isFlagged = 1; - $flagReason = 'Repeated characters'; + + 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'; + } } - - // Check for all caps (min 20 chars) - if (strlen($message) > 20 && $message === strtoupper($message)) { - $isFlagged = 1; - $flagReason = 'All caps'; + + $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; } - - // Check for excessive emojis - $emojiCount = preg_match_all('/[\x{1F600}-\x{1F64F}]/u', $message); - if ($emojiCount > 10) { - $isFlagged = 1; - $flagReason = 'Excessive emojis'; - } - + // Insert message $stmt = $db->prepare(' - INSERT INTO messages (from_user_id, to_user_id, message, is_flagged, flag_reason) - VALUES (:from_user_id, :to_user_id, :message, :is_flagged, :flag_reason) + 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') + 'timestamp' => date('Y-m-d H:i:s'), + 'attachment_url' => $attachmentPath ]); exit; } @@ -1370,19 +1776,51 @@ if (isset($_POST['action']) || isset($_GET['action'])) { 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 + 'total_users' => (int)$totalUsers, + 'u18_users' => (int)$u18Users, + 'o18_users' => (int)$o18Users, + 'online_users' => (int)$onlineUsers, + 'total_messages' => (int)$totalMessages, + 'flagged_messages' => (int)$flaggedMessages, + 'pending_reports' => (int)$pendingReports, + 'banned_users' => (int)$bannedUsers ] ]); exit; } + if ($action === 'admin_get_settings') { + echo json_encode([ + 'success' => true, + 'settings' => getFeatureSettings() + ]); + exit; + } + + if ($action === 'admin_update_settings') { + $allowedKeys = [ + 'age_filter_enabled', + 'keyword_filter_enabled', + 'profanity_filter_enabled', + 'link_filter_enabled' + ]; + + $updatedValues = []; + foreach ($allowedKeys as $key) { + $value = normalizeBooleanFlag($_POST[$key] ?? '0') ? '1' : '0'; + setSettingValue($key, $value); + $updatedValues[$key] = $value; + } + + logSecurityEvent(null, 'ADMIN_UPDATE_SETTINGS', json_encode($updatedValues)); + + echo json_encode([ + 'success' => true, + 'settings' => getFeatureSettings() + ]); + exit; + } + if ($action === 'admin_get_banned_users') { $db = getDB(); @@ -1495,23 +1933,106 @@ if (isset($_POST['action']) || isset($_GET['action'])) { // ─────────────────────────────────────────────────────── 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; } @@ -1549,63 +2070,85 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { 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, - 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; + $lastPingTime = time(); + + while (true) { + if (connection_aborted()) { + break; } - $messages[] = [ - 'id' => $row['id'], - 'from_user_id' => $row['from_user_id'], - 'to_user_id' => $row['to_user_id'], - 'message' => $row['message'], - 'timestamp' => $row['timestamp'], - 'from_username' => $row['from_username'], - 'from_display_name' => $row['from_username'] . '#' . $row['from_display_id'] - ]; + $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); } - - 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; } @@ -1646,6 +2189,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: linear-gradient(135deg, #fef08a 0%, #f97316 100%); + background-color: #fff9db; min-height: 100vh; display: flex; align-items: center; @@ -1962,6 +2506,48 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { color: var(--sun-700); } + .admin-settings-form { + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 10px; + } + + .admin-settings-toggle { + display: flex; + align-items: flex-start; + gap: 12px; + } + + .admin-settings-toggle input { + margin-top: 4px; + width: 20px; + height: 20px; + cursor: pointer; + } + + .admin-settings-toggle strong { + display: block; + font-size: 15px; + color: var(--sun-800); + } + + .admin-settings-description { + font-size: 13px; + color: rgba(120, 53, 15, 0.75); + margin-top: 2px; + } + + .admin-settings-status { + margin-top: 12px; + font-size: 13px; + color: #2563eb; + } + + .admin-settings-status.error { + color: #dc2626; + } + .admin-table-wrapper { overflow-x: auto; } @@ -2051,7 +2637,70 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { display: none; font-size: 14px; } - + + .auth-toggle { + display: flex; + gap: 8px; + margin-bottom: 18px; + background: rgba(255, 221, 87, 0.2); + padding: 6px; + border-radius: 999px; + } + + .auth-toggle button { + flex: 1; + border: none; + border-radius: 999px; + padding: 10px 12px; + font-weight: 600; + cursor: pointer; + background: transparent; + color: #92400e; + transition: background 0.2s ease, color 0.2s ease; + } + + .auth-toggle button.active { + background: linear-gradient(135deg, var(--sun-400), var(--sun-500)); + color: #ffffff; + box-shadow: 0 8px 16px rgba(188, 118, 0, 0.25); + } + + .auth-form.hidden { + display: none; + } + + .form-helper { + font-size: 13px; + color: #92400e; + margin-top: -8px; + margin-bottom: 16px; + } + + .force-login-box { + background: #fff7ed; + border: 1px solid rgba(217, 119, 6, 0.25); + border-radius: 12px; + padding: 12px; + margin-bottom: 16px; + display: none; + } + + .force-login-box p { + margin: 0 0 12px; + font-size: 13px; + color: #92400e; + } + + .force-login-box button { + background: var(--sun-500); + color: #fff; + border: none; + padding: 10px 16px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + } + .success-message { background: #d4edda; color: #155724; @@ -2403,11 +3052,12 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { border-top: 1px solid rgba(188, 118, 0, 0.18); display: flex; gap: 12px; - align-items: center; + align-items: flex-end; + flex-wrap: wrap; } .chat-input { - flex: 1; + flex: 1 1 auto; padding: 12px 16px; border: 1px solid rgba(188, 118, 0, 0.28); border-radius: 28px; @@ -2425,6 +3075,54 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { box-shadow: 0 0 0 3px rgba(240, 180, 0, 0.18); } + .chat-input-tools { + display: flex; + align-items: center; + gap: 10px; + } + + .attach-button { + background: white; + border: 1px dashed rgba(240, 180, 0, 0.6); + color: var(--sun-700); + padding: 10px 16px; + border-radius: 24px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + } + + .attach-button:hover { + transform: translateY(-1px); + border-color: var(--sun-700); + box-shadow: 0 10px 18px rgba(240, 180, 0, 0.18); + } + + .attachment-info { + display: flex; + align-items: center; + gap: 8px; + background: rgba(251, 191, 36, 0.18); + border: 1px solid rgba(240, 180, 0, 0.35); + border-radius: 20px; + padding: 6px 12px; + font-size: 13px; + color: var(--sun-800); + } + + .attachment-remove { + background: none; + border: none; + color: #b91c1c; + font-size: 14px; + cursor: pointer; + padding: 0; + } + + .attachment-remove:hover { + color: #7f1d1d; + } + .send-button { background: linear-gradient(135deg, var(--sun-500) 0%, var(--sun-700) 100%); color: white; @@ -2448,6 +3146,30 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { box-shadow: none; } + .attachment-warning { + margin: 0 24px 12px; + color: #b91c1c; + font-size: 13px; + } + + .message-attachment { + margin-top: 6px; + } + + .message-attachment a { + display: inline-block; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 6px 18px rgba(60, 42, 0, 0.22); + background: rgba(255, 255, 255, 0.85); + } + + .message-attachment img { + display: block; + max-width: 220px; + height: auto; + } + .empty-user-list, .empty-messages, .loading-state, @@ -2471,6 +3193,14 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') { color: #c2410c; } + .user-list-error-banner { + margin: 12px 16px; + border-radius: 10px; + background: rgba(255, 237, 213, 0.85); + border: 1px solid rgba(251, 146, 60, 0.35); + padding: 12px 14px; + } + .chat-state-message.hidden { display: none; } @@ -2502,6 +3232,51 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
Sicherer Chat mit Altersverifikation
- - - - - + + + @@ -2637,9 +3439,18 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {Der 3D-Traceroute benötigt WebGL-Unterstützung. Bitte verwenden Sie einen aktuellen Browser oder aktivieren Sie WebGL in den Einstellungen.
-Minimalistisches Wohnen im Einklang mit der Natur – wir verbinden Hersteller, Planer:innen und Menschen, die erschwingliche, hochwertige Tiny Houses suchen.
+ Jetzt Projekt einreichen +Unsere Plattform bündelt Anbieter, die nachhaltige Materialien und ökologische Bauweisen priorisieren.
+Smarter Grundriss, effiziente Energie, faire Preise: TinyHomes machen Wohnen wieder erschwinglich.
+Vom Wochenend-Retreat bis zum ganzjährigen Zuhause – konfigurieren Sie Ihr TinyHome passend zu Ihrem Lebensstil.
+Sie erhalten maßgeschneiderte Angebote von geprüften Hersteller:innen aus der DACH-Region.
+Übermitteln Sie uns Ihr TinyHome-Konzept – inklusive Banner-Link, damit wir Bestellungen eindeutig zuordnen können. Wir melden uns persönlich bei Ihnen.
+ + + + + + +diff --git a/tiny.php b/tiny.php new file mode 100644 index 0000000..a357b26 --- /dev/null +++ b/tiny.php @@ -0,0 +1,454 @@ +file($file['tmp_name']); + $allowedTypes = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/webp' => 'webp', + 'image/gif' => 'gif' + ]; + + if (!array_key_exists($mimeType, $allowedTypes)) { + $errors[] = 'Die Datei "' . htmlspecialchars($file['name']) . '" ist kein unterstütztes Bildformat.'; + return null; + } + + ensureDirectory($targetDir); + $extension = $allowedTypes[$mimeType]; + $filename = sanitizeFileName(pathinfo($file['name'], PATHINFO_FILENAME)); + if ($filename === '') { + $filename = 'upload_' . time(); + } + $destination = rtrim($targetDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename . '_' . uniqid() . '.' . $extension; + + if (!move_uploaded_file($file['tmp_name'], $destination)) { + $errors[] = 'Die Datei "' . htmlspecialchars($file['name']) . '" konnte nicht gespeichert werden.'; + return null; + } + + return [ + 'path' => $destination, + 'original' => $file['name'], + 'type' => $mimeType + ]; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $company = trim($_POST['company'] ?? ''); + $contactEmail = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL) ? $_POST['email'] : ''; + $website = trim($_POST['website'] ?? ''); + $message = trim($_POST['message'] ?? ''); + $bannerLink = trim($_POST['banner_link'] ?? ''); + + if ($company === '') { + $errors[] = 'Bitte geben Sie den Namen Ihres Unternehmens oder Projekts an.'; + } + + if ($contactEmail === '') { + $errors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.'; + } + + if ($bannerLink === '') { + $errors[] = 'Bitte geben Sie den Link an, der Ihrem Banner zugeordnet werden soll.'; + } + + $uploadedImage = handleUpload('project_image', __DIR__ . '/uploads/images', $errors); + $uploadedBanner = handleUpload('banner_image', __DIR__ . '/uploads/banners', $errors); + + if (!$errors) { + $boundary = '=_TinyHome_' . md5((string) microtime(true)); + $headers = []; + $fromAddress = $contactEmail ?: 'no-reply@tinyhome.local'; + $headers[] = 'From: ' . $fromAddress; + $headers[] = 'Reply-To: ' . $fromAddress; + $headers[] = 'MIME-Version: 1.0'; + $headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"'; + + $bodyParts = []; + $text = "Neue TinyHome-Anfrage\n\n"; + $text .= "Unternehmen/Projekt: $company\n"; + $text .= "Kontakt-E-Mail: $contactEmail\n"; + if ($website !== '') { + $text .= "Website: $website\n"; + } + $text .= "Banner-Link: $bannerLink\n\n"; + if ($message !== '') { + $text .= "Nachricht:\n$message\n\n"; + } + if ($uploadedImage) { + $text .= 'Projektbild gespeichert unter: ' . $uploadedImage['path'] . "\n"; + } + if ($uploadedBanner) { + $text .= 'Banner gespeichert unter: ' . $uploadedBanner['path'] . "\n"; + } + + $bodyParts[] = '--' . $boundary; + $bodyParts[] = 'Content-Type: text/plain; charset="UTF-8"'; + $bodyParts[] = 'Content-Transfer-Encoding: 8bit'; + $bodyParts[] = ''; + $bodyParts[] = $text; + + foreach ([$uploadedImage, $uploadedBanner] as $upload) { + if (!$upload) { + continue; + } + $fileContent = file_get_contents($upload['path']); + if ($fileContent === false) { + $errors[] = 'Die Datei "' . htmlspecialchars($upload['original']) . '" konnte nicht für den Mailversand gelesen werden.'; + continue; + } + $bodyParts[] = '--' . $boundary; + $bodyParts[] = 'Content-Type: ' . $upload['type'] . '; name="' . sanitizeFileName($upload['original']) . '"'; + $bodyParts[] = 'Content-Transfer-Encoding: base64'; + $bodyParts[] = 'Content-Disposition: attachment; filename="' . sanitizeFileName($upload['original']) . '"'; + $bodyParts[] = ''; + $bodyParts[] = chunk_split(base64_encode($fileContent)); + } + + $bodyParts[] = '--' . $boundary . '--'; + $body = implode("\r\n", $bodyParts); + + if (!$errors) { + $mailSent = mail( + $recipient, + 'Neue TinyHome-Anfrage von ' . $company, + $body, + implode("\r\n", $headers) + ); + + if ($mailSent) { + $successMessage = 'Vielen Dank! Ihre Anfrage wurde erfolgreich übermittelt.'; + } else { + $errors[] = 'Ihre Anfrage konnte leider nicht versendet werden. Bitte versuchen Sie es später erneut.'; + } + } + } +} +?> + + +
+ + +
+ + +
+