+
{$title}
+
{$message}
";
+
+ if (Config::isEnvironment('development') && isset($error['file'])) {
+ echo "
+ File: {$error['file']}
+ Line: {$error['line']}
+ Type: " . ($error['error_type'] ?? $error['type'] ?? 'unknown') . "
+
";
+ }
+
+ echo "
+
+";
+ }
+
+ /**
+ * Perform cleanup tasks on shutdown
+ */
+ private static function performCleanup()
+ {
+ // Clean up expired sessions or temporary files if needed
+ // This could be expanded based on application needs
+
+ // Example: Clean up old temp files
+ $tempDir = sys_get_temp_dir();
+ $pattern = $tempDir . '/dodgers_*';
+
+ foreach (glob($pattern) as $file) {
+ // Remove files older than 1 hour
+ if (filemtime($file) < time() - 3600) {
+ @unlink($file);
+ }
+ }
+ }
+
+ /**
+ * Handle API errors with JSON response
+ */
+ public static function apiError($message, $code = 500, $details = null)
+ {
+ $error = [
+ 'success' => false,
+ 'error' => $message,
+ 'code' => $code,
+ 'timestamp' => time()
+ ];
+
+ if (Config::isEnvironment('development') && $details) {
+ $error['details'] = $details;
+ }
+
+ http_response_code($code);
+ header('Content-Type: application/json');
+ echo json_encode($error);
+ }
+
+ /**
+ * Log security events
+ */
+ public static function logSecurityEvent($event, $details = [])
+ {
+ $logEntry = [
+ 'type' => 'security_event',
+ 'event' => $event,
+ 'details' => $details,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'remote_ip' => Security::getClientIP(),
+ 'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ 'session_id' => session_id()
+ ];
+
+ self::logError($logEntry);
+ }
+
+ /**
+ * Log performance metrics
+ */
+ public static function logPerformance($operation, $duration, $details = [])
+ {
+ $logEntry = [
+ 'type' => 'performance',
+ 'operation' => $operation,
+ 'duration' => $duration,
+ 'details' => $details,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'remote_ip' => Security::getClientIP(),
+ 'session_id' => session_id()
+ ];
+
+ self::logError($logEntry);
+ }
+}
diff --git a/includes/autoloader.php b/includes/autoloader.php
new file mode 100644
index 0000000..566d2f8
--- /dev/null
+++ b/includes/autoloader.php
@@ -0,0 +1,103 @@
+ __DIR__ . '/../app/',
+ 'Models\\' => __DIR__ . '/../models/',
+ 'Controllers\\' => __DIR__ . '/../controllers/',
+ 'Utils\\' => __DIR__ . '/../utils/',
+ 'Services\\' => __DIR__ . '/../services/',
+ 'Middleware\\' => __DIR__ . '/../middleware/'
+ ];
+
+ // Check for exact class match first (for legacy classes)
+ $legacyMappings = [
+ 'Config' => __DIR__ . '/Config.php',
+ 'Security' => __DIR__ . '/../utils/Security.php',
+ 'Validation' => __DIR__ . '/../utils/Validation.php',
+ 'Database' => __DIR__ . '/Database.php',
+ 'UserModel' => __DIR__ . '/../models/UserModel.php',
+ 'ChatMessageModel' => __DIR__ . '/../models/ChatMessageModel.php',
+ 'ActiveViewerModel' => __DIR__ . '/../models/ActiveViewerModel.php'
+ ];
+
+ // First check legacy mappings
+ if (isset($legacyMappings[$className])) {
+ $file = $legacyMappings[$className];
+ if (file_exists($file)) {
+ require_once $file;
+ return;
+ }
+ }
+
+ // Check PSR-4 mappings
+ foreach ($prefixes as $prefix => $baseDir) {
+ $len = strlen($prefix);
+ if (strncmp($prefix, $className, $len) !== 0) {
+ continue;
+ }
+
+ $relativeClass = substr($className, $len);
+ $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
+
+ if (file_exists($file)) {
+ require_once $file;
+ if (Config::isDebug()) {
+ error_log("Autoloaded: {$className} from {$file}");
+ }
+ return;
+ }
+ }
+
+ // Class not found - this will throw an exception from spl_autoload_register
+ if (Config::isDebug()) {
+ error_log("Autoloader: Class {$className} not found in any mapping");
+ }
+});
+
+/**
+ * Optional: Load additional helper functions
+ */
+if (file_exists(__DIR__ . '/helpers.php')) {
+ require_once __DIR__ . '/helpers.php';
+}
+
+/**
+ * Optional: Load composer autoloader if it exists (for future dependencies)
+ */
+$composerAutoloader = __DIR__ . '/../vendor/autoload.php';
+if (file_exists($composerAutoloader)) {
+ require_once $composerAutoloader;
+}
+
+// Verify critical classes are loaded
+$criticalClasses = [
+ 'Config',
+ 'Security',
+ 'Validation',
+ 'Database'
+];
+
+foreach ($criticalClasses as $class) {
+ if (!class_exists($class, false)) {
+ // Try to load manually
+ $legacyPaths = [
+ 'Config' => 'includes/Config.php',
+ 'Security' => 'utils/Security.php',
+ 'Validation' => 'utils/Validation.php',
+ 'Database' => 'includes/Database.php'
+ ];
+
+ if (isset($legacyPaths[$class])) {
+ $path = __DIR__ . '/../' . $legacyPaths[$class];
+ if (file_exists($path)) {
+ require_once $path;
+ }
+ }
+ }
+}
diff --git a/index.php b/index.php
index 35104f0..208a903 100644
--- a/index.php
+++ b/index.php
@@ -1,26 +1,35 @@
handleSSE($_GET['user_id']);
+ exit;
+}
// Clean up old viewers (inactive for more than 10 seconds)
function cleanupViewers() {
@@ -42,26 +51,38 @@ function cleanupViewers() {
// Handle API requests for stream status
if (isset($_GET['api']) && $_GET['api'] === 'stream_status') {
header('Content-Type: application/json');
- header('Access-Control-Allow-Origin: *');
+ header('Cache-Control: no-cache, no-store, must-revalidate');
+ header('Pragma: no-cache');
+ header('Expires: 0');
- $streamUrl = 'http://38.64.28.91:23456/stream.m3u8';
+ $corsOrigins = Config::get('cors.allowed_origins', []);
+ if (in_array($_SERVER['HTTP_ORIGIN'] ?? '', $corsOrigins)) {
+ header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
+ }
+
+ $streamUrl = $streamBaseUrl . '/stream.m3u8';
$online = false;
- $context = stream_context_create([
- 'http' => [
- 'method' => 'GET',
- 'header' => "User-Agent: Mozilla/5.0\r\n",
- 'timeout' => 5 // Quick check
- ]
- ]);
+ if (Security::checkRateLimit(Security::getClientIP(), 'stream_status', 10, 60)) {
+ $context = stream_context_create([
+ 'http' => [
+ 'method' => 'GET',
+ 'header' => "User-Agent: Mozilla/5.0\r\n",
+ 'timeout' => 5 // Quick check
+ ]
+ ]);
- $content = @file_get_contents($streamUrl, false, $context);
+ $content = @file_get_contents($streamUrl, false, $context);
+ Security::logSecurityEvent('stream_status_check', ['online' => $online]);
- if ($content !== false && !empty($content)) {
- // Check if it looks like a valid m3u8
- if (str_starts_with($content, '#EXTM3U')) {
- $online = true;
+ if ($content !== false && !empty($content)) {
+ // Check if it looks like a valid m3u8
+ if (str_starts_with($content, '#EXTM3U')) {
+ $online = true;
+ }
}
+ } else {
+ Security::logSecurityEvent('stream_status_rate_limited');
}
echo json_encode(['online' => $online]);
@@ -70,14 +91,29 @@ if (isset($_GET['api']) && $_GET['api'] === 'stream_status') {
// Handle proxy requests for the stream
if (isset($_GET['proxy']) && $_GET['proxy'] === 'stream') {
- $streamUrl = 'http://38.64.28.91:23456/stream.m3u8';
-
+ // Check rate limiting
+ if (!Security::checkRateLimit(Security::getClientIP(), 'proxy_stream')) {
+ Security::logSecurityEvent('proxy_stream_rate_limited');
+ http_response_code(429);
+ echo json_encode(['error' => 'Too many requests. Please try again later.']);
+ exit;
+ }
+
+ $streamUrl = $streamBaseUrl . '/stream.m3u8';
+
// Set appropriate headers for m3u8 content
header('Content-Type: application/vnd.apple.mpegurl');
- header('Access-Control-Allow-Origin: *');
- header('Access-Control-Allow-Methods: GET, OPTIONS');
- header('Access-Control-Allow-Headers: Range');
-
+ header('Cache-Control: no-cache, no-store, must-revalidate');
+ header('Pragma: no-cache');
+ header('Expires: 0');
+
+ $corsOrigins = Config::get('cors.allowed_origins', []);
+ if (in_array($_SERVER['HTTP_ORIGIN'] ?? '', $corsOrigins)) {
+ header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
+ header('Access-Control-Allow-Methods: GET, OPTIONS');
+ header('Access-Control-Allow-Headers: Range');
+ }
+
// Fetch and output the m3u8 content
$context = stream_context_create([
'http' => [
@@ -86,33 +122,39 @@ if (isset($_GET['proxy']) && $_GET['proxy'] === 'stream') {
'timeout' => 10
]
]);
-
+
$content = @file_get_contents($streamUrl, false, $context);
-
+
if ($content !== false) {
// Parse and update the m3u8 content to use our proxy for .ts segments
$lines = explode("\n", $content);
$updatedContent = [];
-
+
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line) && !str_starts_with($line, '#')) {
// This is a .ts segment URL
if (strpos($line, 'http') === 0) {
- // Absolute URL
- $updatedContent[] = '?proxy=segment&url=' . urlencode($line);
+ // Absolute URL - validate it first
+ if (Security::isValidStreamUrl($line)) {
+ $updatedContent[] = '?proxy=segment&url=' . urlencode($line);
+ }
} else {
// Relative URL
- $baseUrl = 'http://38.64.28.91:23456/';
- $updatedContent[] = '?proxy=segment&url=' . urlencode($baseUrl . $line);
+ $segmentUrl = $streamBaseUrl . '/' . $line;
+ if (Security::isValidStreamUrl($segmentUrl)) {
+ $updatedContent[] = '?proxy=segment&url=' . urlencode($segmentUrl);
+ }
}
} else {
$updatedContent[] = $line;
}
}
-
+
+ Security::logSecurityEvent('proxy_stream_success');
echo implode("\n", $updatedContent);
} else {
+ Security::logSecurityEvent('proxy_stream_failed', ['url' => $streamUrl]);
http_response_code(500);
echo "Failed to fetch stream";
}
@@ -121,17 +163,31 @@ if (isset($_GET['proxy']) && $_GET['proxy'] === 'stream') {
// Handle proxy requests for .ts segments
if (isset($_GET['proxy']) && $_GET['proxy'] === 'segment' && isset($_GET['url'])) {
- $segmentUrl = urldecode($_GET['url']);
-
- // Validate URL to prevent abuse
- if (strpos($segmentUrl, 'http://38.64.28.91:23456/') !== 0) {
- http_response_code(403);
+ // Check rate limiting
+ if (!Security::checkRateLimit(Security::getClientIP(), 'proxy_segment')) {
+ Security::logSecurityEvent('proxy_segment_rate_limited');
+ http_response_code(429);
exit;
}
-
+
+ $segmentUrl = Security::sanitizeInput($_GET['url'], 'url');
+
+ // Validate URL to prevent SSRF attacks
+ if (!$segmentUrl || !Security::isValidStreamUrl($segmentUrl)) {
+ Security::logSecurityEvent('proxy_segment_invalid_url', ['url' => $_GET['url'] ?? '']);
+ http_response_code(403);
+ echo "Invalid segment URL";
+ exit;
+ }
+
header('Content-Type: video/mp2t');
- header('Access-Control-Allow-Origin: *');
-
+ header('Cache-Control: public, max-age=3600'); // Cache segments for 1 hour
+
+ $corsOrigins = Config::get('cors.allowed_origins', []);
+ if (in_array($_SERVER['HTTP_ORIGIN'] ?? '', $corsOrigins)) {
+ header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
+ }
+
$context = stream_context_create([
'http' => [
'method' => 'GET',
@@ -139,13 +195,16 @@ if (isset($_GET['proxy']) && $_GET['proxy'] === 'segment' && isset($_GET['url'])
'timeout' => 10
]
]);
-
+
$content = @file_get_contents($segmentUrl, false, $context);
-
+
if ($content !== false) {
+ Security::logSecurityEvent('proxy_segment_success');
echo $content;
} else {
+ Security::logSecurityEvent('proxy_segment_failed', ['url' => $segmentUrl]);
http_response_code(500);
+ echo "Failed to fetch segment";
}
exit;
}
@@ -153,151 +212,223 @@ if (isset($_GET['proxy']) && $_GET['proxy'] === 'segment' && isset($_GET['url'])
// Handle chat actions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
header('Content-Type: application/json');
-
- // Admin actions
- if ($_POST['action'] === 'delete_message' && $isAdmin && isset($_POST['message_id'])) {
- $messages = file_exists($chatFile) ? json_decode(file_get_contents($chatFile), true) : [];
- $messages = array_filter($messages, function($msg) {
- return $msg['id'] !== $_POST['message_id'];
- });
- $messages = array_values($messages); // Re-index array
- file_put_contents($chatFile, json_encode($messages));
- echo json_encode(['success' => true]);
+
+ $action = $_POST['action'];
+
+ // Validate the action parameter
+ if (!preg_match('/^[a-z_]+$/', $action)) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Invalid action parameter']);
+ Security::logSecurityEvent('invalid_chat_action', ['action' => $action]);
exit;
}
-
- if ($_POST['action'] === 'clear_chat' && $isAdmin) {
- file_put_contents($chatFile, json_encode([]));
- echo json_encode(['success' => true]);
- exit;
- }
-
- if ($_POST['action'] === 'ban_user' && $isAdmin && isset($_POST['user_id'])) {
- $banned = file_exists($bannedFile) ? json_decode(file_get_contents($bannedFile), true) : [];
- if (!in_array($_POST['user_id'], $banned)) {
- $banned[] = $_POST['user_id'];
- file_put_contents($bannedFile, json_encode($banned));
- }
- echo json_encode(['success' => true]);
- exit;
- }
-
- if ($_POST['action'] === 'heartbeat') {
- $userId = $_SESSION['user_id'];
- $nickname = isset($_POST['nickname']) ? htmlspecialchars(substr($_POST['nickname'], 0, 20)) : 'Anonymous';
-
- $viewers = file_exists($viewersFile) ? json_decode(file_get_contents($viewersFile), true) : [];
-
- // Update or add viewer
- $found = false;
- foreach ($viewers as &$viewer) {
- if ($viewer['user_id'] === $userId) {
- $viewer['last_seen'] = time();
- $viewer['nickname'] = $nickname;
- $viewer['is_admin'] = $isAdmin;
- $found = true;
- break;
- }
- }
-
- if (!$found) {
- $viewers[] = [
- 'user_id' => $userId,
- 'nickname' => $nickname,
- 'last_seen' => time(),
- 'is_admin' => $isAdmin
- ];
- }
-
- file_put_contents($viewersFile, json_encode($viewers));
- $viewerCount = cleanupViewers();
-
- echo json_encode(['success' => true, 'viewer_count' => $viewerCount]);
- exit;
- }
-
- if ($_POST['action'] === 'send' && isset($_POST['message']) && isset($_POST['nickname'])) {
- $nickname = htmlspecialchars(substr($_POST['nickname'], 0, 20));
- $message = htmlspecialchars(substr($_POST['message'], 0, 1000));
- $userId = $_SESSION['user_id'];
-
- // Check if user is banned
- $banned = file_exists($bannedFile) ? json_decode(file_get_contents($bannedFile), true) : [];
- if (in_array($userId, $banned)) {
- echo json_encode(['success' => false, 'error' => 'You are banned from chat']);
+
+ // Admin-only actions
+ if (in_array($action, ['delete_message', 'clear_chat', 'ban_user'])) {
+ if (!$isAdmin) {
+ http_response_code(403);
+ echo json_encode(['success' => false, 'error' => 'Admin access required']);
+ Security::logSecurityEvent('unauthorized_admin_action', ['action' => $action, 'ip' => Security::getClientIP()]);
exit;
}
-
- if (!empty($nickname) && !empty($message)) {
+
+ // Rate limiting for admin actions
+ if (!Security::checkRateLimit(Security::getClientIP(), 'admin_actions', 10, 60)) {
+ http_response_code(429);
+ echo json_encode(['success' => false, 'error' => 'Too many admin actions. Please wait.']);
+ Security::logSecurityEvent('admin_rate_limited');
+ exit;
+ }
+ }
+
+ // Handle different actions
+ switch ($action) {
+ case 'delete_message':
+ if (!isset($_POST['message_id']) || !preg_match('/^[a-zA-Z0-9]+$/', $_POST['message_id'])) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Invalid message ID']);
+ exit;
+ }
+
+ $messageIdToDelete = $_POST['message_id'];
$messages = file_exists($chatFile) ? json_decode(file_get_contents($chatFile), true) : [];
-
+ $filteredMessages = array_filter($messages, function($msg) use ($messageIdToDelete) {
+ return $msg['id'] !== $messageIdToDelete;
+ });
+ $filteredMessages = array_values($filteredMessages);
+ file_put_contents($chatFile, json_encode($filteredMessages));
+
+ Security::logSecurityEvent('message_deleted', ['message_id' => $messageIdToDelete]);
+ echo json_encode(['success' => true]);
+ exit;
+
+ case 'clear_chat':
+ file_put_contents($chatFile, json_encode([]));
+ Security::logSecurityEvent('chat_cleared');
+ echo json_encode(['success' => true]);
+ exit;
+
+ case 'ban_user':
+ if (!isset($_POST['user_id'])) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'User ID required']);
+ exit;
+ }
+
+ $bannedUsers = file_exists($bannedFile) ? json_decode(file_get_contents($bannedFile), true) : [];
+ if (!in_array($_POST['user_id'], $bannedUsers)) {
+ $bannedUsers[] = $_POST['user_id'];
+ file_put_contents($bannedFile, json_encode($bannedUsers));
+ }
+
+ Security::logSecurityEvent('user_banned', ['user_id' => $_POST['user_id']]);
+ echo json_encode(['success' => true]);
+ exit;
+
+ case 'heartbeat':
+ // Validate heartbeat data
+ $heartbeatValidation = Validation::validateHeartbeat($_POST);
+ if (!$heartbeatValidation['valid']) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Invalid heartbeat data']);
+ exit;
+ }
+
+ $userId = $_SESSION['user_id'];
+ $nickname = $heartbeatValidation['validated']['nickname'] ?? 'Anonymous';
+
+ $viewers = file_exists($viewersFile) ? json_decode(file_get_contents($viewersFile), true) : [];
+
+ // Update or add viewer
+ $found = false;
+ foreach ($viewers as &$viewer) {
+ if ($viewer['user_id'] === $userId) {
+ $viewer['last_seen'] = time();
+ $viewer['nickname'] = $nickname;
+ $viewer['is_admin'] = $isAdmin;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $viewers[] = [
+ 'user_id' => $userId,
+ 'nickname' => $nickname,
+ 'last_seen' => time(),
+ 'is_admin' => $isAdmin
+ ];
+ }
+
+ file_put_contents($viewersFile, json_encode($viewers));
+ $viewerCount = cleanupViewers();
+
+ echo json_encode(['success' => true, 'viewer_count' => $viewerCount]);
+ exit;
+
+ case 'send':
+ if (!isset($_POST['message']) || !isset($_POST['nickname'])) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Message and nickname required']);
+ exit;
+ }
+
+ // Rate limiting for message sending
+ if (!Security::checkRateLimit($_SESSION['user_id'], 'send_message', 5, 60)) {
+ echo json_encode(['success' => false, 'error' => 'Too many messages. Please wait before sending another.']);
+ exit;
+ }
+
+ // Validate message data
+ $messageValidation = Validation::validateMessageSend($_POST);
+ if (!$messageValidation['valid']) {
+ echo json_encode(['success' => false, 'error' => 'Validation failed: ' . implode(', ', array_values($messageValidation['errors']))]);
+ exit;
+ }
+
+ $userId = $_SESSION['user_id'];
+ $nickname = $messageValidation['validated']['nickname'];
+ $message = $messageValidation['validated']['message'];
+
+ // Check if user is banned
+ $bannedUsers = file_exists($bannedFile) ? json_decode(file_get_contents($bannedFile), true) : [];
+ if (in_array($userId, $bannedUsers)) {
+ echo json_encode(['success' => false, 'error' => 'You are banned from chat']);
+ exit;
+ }
+
+ $messages = file_exists($chatFile) ? json_decode(file_get_contents($chatFile), true) : [];
+
$newMessage = [
- 'id' => uniqid(),
+ 'id' => Security::generateSecureToken(8), // More secure than uniqid()
'user_id' => $userId,
- 'nickname' => $nickname,
- 'message' => $message,
+ 'nickname' => htmlspecialchars($nickname, ENT_QUOTES, 'UTF-8'),
+ 'message' => htmlspecialchars($message, ENT_QUOTES, 'UTF-8'),
'timestamp' => time(),
'time' => date('M j, H:i'),
'is_admin' => $isAdmin
];
-
- array_push($messages, $newMessage);
-
+
+ $messages[] = $newMessage;
+
// Keep only last N messages
if (count($messages) > $maxMessages) {
$messages = array_slice($messages, -$maxMessages);
}
-
+
file_put_contents($chatFile, json_encode($messages));
+
+ Security::logSecurityEvent('message_sent', ['message_id' => $newMessage['id']]);
echo json_encode(['success' => true, 'message' => $newMessage]);
- } else {
- echo json_encode(['success' => false, 'error' => 'Invalid input']);
- }
- exit;
- }
-
- if ($_POST['action'] === 'fetch') {
- $lastId = isset($_POST['last_id']) ? $_POST['last_id'] : '';
- $messages = file_exists($chatFile) ? json_decode(file_get_contents($chatFile), true) : [];
-
- // Find new messages only
- $newMessages = [];
- $foundLast = empty($lastId);
-
- foreach ($messages as $msg) {
- if ($foundLast) {
- $newMessages[] = $msg;
+ exit;
+
+ case 'fetch':
+ $lastId = isset($_POST['last_id']) ? trim($_POST['last_id']) : '';
+ $messages = file_exists($chatFile) ? json_decode(file_get_contents($chatFile), true) : [];
+
+ // Find new messages only
+ $newMessages = [];
+ $foundLast = empty($lastId);
+
+ foreach ($messages as $msg) {
+ if ($foundLast) {
+ $newMessages[] = $msg;
+ }
+ if ($msg['id'] === $lastId) {
+ $foundLast = true;
+ }
}
- if ($msg['id'] === $lastId) {
- $foundLast = true;
+
+ // If lastId wasn't found, return all messages (initial load or refresh)
+ if (!$foundLast && !empty($lastId)) {
+ $newMessages = $messages;
}
- }
-
- // If lastId wasn't found, return all messages (initial load or refresh)
- if (!$foundLast && !empty($lastId)) {
- $newMessages = $messages;
- }
-
- // Get viewer count
- $viewerCount = cleanupViewers();
-
- // Determine if we should send all messages (for initial load or after admin actions)
- $sendAllMessages = empty($lastId) || !$foundLast;
-
- echo json_encode([
- 'success' => true,
- 'messages' => $newMessages,
- 'all_messages' => $sendAllMessages ? $messages : null,
- 'message_count' => count($messages),
- 'viewer_count' => $viewerCount,
- 'is_admin' => $isAdmin
- ]);
- exit;
- }
-
- if ($_POST['action'] === 'get_user_id') {
- echo json_encode(['success' => true, 'user_id' => $_SESSION['user_id'], 'is_admin' => $isAdmin]);
- exit;
+
+ // Get viewer count
+ $viewerCount = cleanupViewers();
+
+ // Determine if we should send all messages (for initial load or after admin actions)
+ $sendAllMessages = empty($lastId) || !$foundLast;
+
+ echo json_encode([
+ 'success' => true,
+ 'messages' => $newMessages,
+ 'all_messages' => $sendAllMessages ? $messages : null,
+ 'message_count' => count($messages),
+ 'viewer_count' => $viewerCount,
+ 'is_admin' => $isAdmin
+ ]);
+ exit;
+
+ case 'get_user_id':
+ echo json_encode(['success' => true, 'user_id' => $_SESSION['user_id'], 'is_admin' => $isAdmin]);
+ exit;
+
+ default:
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Unknown action']);
+ Security::logSecurityEvent('unknown_chat_action', ['action' => $action]);
+ exit;
}
}
?>
@@ -307,6 +438,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {