\'\') AS has_file, m.file_name, m.file_mime, m.file_size, ' . 'r.username AS reply_username, r.message AS reply_message, r.created_at AS reply_created_at ' . 'FROM admin_chat_messages m ' . 'LEFT JOIN admin_chat_messages r ON r.id = m.reply_to_id '; } function admin_chat_normalize_row(array $row): array { $row['id'] = isset($row['id']) ? (int)$row['id'] : 0; $row['user_id'] = isset($row['user_id']) ? (int)$row['user_id'] : 0; $row['has_file'] = (bool)((int)($row['has_file'] ?? 0)); $row['reply_to_id'] = isset($row['reply_to_id']) ? (int)$row['reply_to_id'] : null; $row['file_size'] = isset($row['file_size']) && $row['file_size'] !== null ? (int)$row['file_size'] : null; $row['updated_at_ts'] = isset($row['updated_at_ts']) && $row['updated_at_ts'] !== null ? (int)$row['updated_at_ts'] : null; $row['is_hearted'] = (bool)((int)($row['is_hearted'] ?? 0)); $row['hearted_by_user_id'] = isset($row['hearted_by_user_id']) && $row['hearted_by_user_id'] !== null ? (int)$row['hearted_by_user_id'] : null; $row['hearted_by_username'] = isset($row['hearted_by_username']) && $row['hearted_by_username'] !== null ? (string)$row['hearted_by_username'] : null; return $row; } function admin_chat_normalize_rows(array $rows): array { foreach ($rows as $k => $r) { if (is_array($r)) { $rows[$k] = admin_chat_normalize_row($r); } } return $rows; } if ($method === 'GET') { $beforeId = isset($_GET['before_id']) ? (int)$_GET['before_id'] : 0; $afterId = isset($_GET['after_id']) ? (int)$_GET['after_id'] : 0; $updatedAfter = isset($_GET['updated_after']) ? (int)$_GET['updated_after'] : 0; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 100; $limit = max(1, min(200, $limit)); try { if ($afterId > 0) { // Nowe wiadomości (do dopisywania na dole) $stmt = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id > :after_id ' . 'ORDER BY m.id ASC ' . 'LIMIT :limit' ); $stmt->bindValue(':after_id', $afterId, PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); $rows = admin_chat_normalize_rows($stmt->fetchAll(PDO::FETCH_ASSOC)); admin_json_response([ 'success' => true, 'data' => $rows, 'count' => count($rows), ]); } if ($updatedAfter > 0) { // Zmienione (edytowane) wiadomości do odświeżenia w UI $stmt = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.updated_at IS NOT NULL AND m.updated_at > FROM_UNIXTIME(:updated_after) ' . 'ORDER BY m.id ASC ' . 'LIMIT :limit' ); $stmt->bindValue(':updated_after', $updatedAfter, PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); $rows = admin_chat_normalize_rows($stmt->fetchAll(PDO::FETCH_ASSOC)); admin_json_response([ 'success' => true, 'data' => $rows, 'count' => count($rows), ]); } if ($beforeId > 0) { // Starsze wiadomości (do wczytywania przy scrollu w górę) $stmt = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id < :before_id ' . 'ORDER BY m.id DESC ' . 'LIMIT :limit' ); $stmt->bindValue(':before_id', $beforeId, PDO::PARAM_INT); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); $rowsDesc = admin_chat_normalize_rows($stmt->fetchAll(PDO::FETCH_ASSOC)); admin_json_response([ 'success' => true, 'data' => $rowsDesc, 'count' => count($rowsDesc), 'hasMore' => count($rowsDesc) === $limit, ]); } // Początkowe wczytanie: 100 najnowszych $stmt = $pdo->prepare( admin_chat_select_sql() . 'ORDER BY m.id DESC ' . 'LIMIT :limit' ); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); $rowsDesc = admin_chat_normalize_rows($stmt->fetchAll(PDO::FETCH_ASSOC)); admin_json_response([ 'success' => true, 'data' => $rowsDesc, 'count' => count($rowsDesc), 'hasMore' => count($rowsDesc) === $limit, ]); } catch (Throwable $e) { $msg = (string)$e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, "doesn't exist") !== false) { admin_json_error('Brak wymaganych kolumn/tabel (np. updated_at). Uruchom /administration/install_notes_chat.php', 500); } admin_json_error('Błąd pobierania wiadomości', 500); } } if ($method === 'POST') { $contentType = (string)($_SERVER['CONTENT_TYPE'] ?? ''); $isJson = stripos($contentType, 'application/json') !== false; $RECALLED_TEXT = 'Wiadomość cofnięta'; $message = ''; $replyToId = null; $fileName = null; $fileMime = null; $fileSize = null; $fileData = null; if ($isJson) { $payload = admin_read_json_body(); $action = isset($payload['action']) ? (string)$payload['action'] : ''; // Recall existing message (revoke own) if ($action === 'recall') { $id = isset($payload['id']) ? (int)$payload['id'] : 0; if ($id <= 0) { admin_json_error('Nieprawidłowe id', 422); } try { $stmt = $pdo->prepare('SELECT id, user_id FROM admin_chat_messages WHERE id = :id'); $stmt->execute([':id' => $id]); $cur = $stmt->fetch(PDO::FETCH_ASSOC); if (!$cur) { admin_json_error('Nie znaleziono wiadomości', 404); } if ((int)$cur['user_id'] !== (int)$auth['user_id']) { admin_json_error('Nie możesz cofnąć cudzej wiadomości', 403); } $up = $pdo->prepare( 'UPDATE admin_chat_messages ' . 'SET message = :message, file_name = NULL, file_mime = NULL, file_size = NULL, file_data = NULL, updated_at = NOW() ' . 'WHERE id = :id' ); $up->execute([':message' => $RECALLED_TEXT, ':id' => $id]); $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $id]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response(['success' => true, 'data' => $row]); } catch (Throwable $e) { $msg = (string)$e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, "doesn't exist") !== false) { admin_json_error('Brak wymaganych kolumn/tabel (np. updated_at). Uruchom /administration/install_notes_chat.php', 500); } admin_json_error('Błąd cofania wiadomości', 500); } } // Update existing message (edit own) if ($action === 'update') { $id = isset($payload['id']) ? (int)$payload['id'] : 0; $newMessage = isset($payload['message']) ? trim((string)$payload['message']) : ''; if ($id <= 0) { admin_json_error('Nieprawidłowe id', 422); } if ($newMessage === '') { admin_json_error('Wiadomość nie może być pusta', 422); } if (mb_strlen($newMessage) > 1500) { admin_json_error('Wiadomość jest zbyt długa (max 1500 znaków)', 422); } try { $stmt = $pdo->prepare('SELECT id, user_id, message FROM admin_chat_messages WHERE id = :id'); $stmt->execute([':id' => $id]); $cur = $stmt->fetch(PDO::FETCH_ASSOC); if (!$cur) { admin_json_error('Nie znaleziono wiadomości', 404); } if ((int)$cur['user_id'] !== (int)$auth['user_id']) { admin_json_error('Nie możesz edytować cudzej wiadomości', 403); } if (((string)($cur['message'] ?? '')) === $RECALLED_TEXT) { admin_json_error('Nie możesz edytować cofniętej wiadomości', 422); } $up = $pdo->prepare('UPDATE admin_chat_messages SET message = :message, updated_at = NOW() WHERE id = :id'); $up->execute([':message' => $newMessage, ':id' => $id]); $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $id]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response(['success' => true, 'data' => $row]); } catch (Throwable $e) { $msg = (string)$e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, "doesn't exist") !== false) { admin_json_error('Brak wymaganych kolumn/tabel (np. updated_at). Uruchom /administration/install_notes_chat.php', 500); } admin_json_error('Błąd edycji wiadomości', 500); } } if ($action === 'toggle_heart') { $id = isset($payload['id']) ? (int)$payload['id'] : 0; if ($id <= 0) { admin_json_error('Nieprawidłowe id', 422); } try { $stmt = $pdo->prepare('SELECT id, is_hearted FROM admin_chat_messages WHERE id = :id'); $stmt->execute([':id' => $id]); $cur = $stmt->fetch(PDO::FETCH_ASSOC); if (!$cur) { admin_json_error('Nie znaleziono wiadomości', 404); } $isHearted = (int)($cur['is_hearted'] ?? 0) === 1; if ($isHearted) { $up = $pdo->prepare( 'UPDATE admin_chat_messages ' . 'SET is_hearted = 0, hearted_by_user_id = NULL, hearted_by_username = NULL, hearted_at = NULL, updated_at = NOW() ' . 'WHERE id = :id' ); $up->execute([':id' => $id]); } else { $up = $pdo->prepare( 'UPDATE admin_chat_messages ' . 'SET is_hearted = 1, hearted_by_user_id = :hearted_by_user_id, hearted_by_username = :hearted_by_username, hearted_at = NOW(), updated_at = NOW() ' . 'WHERE id = :id' ); $up->execute([ ':id' => $id, ':hearted_by_user_id' => (int)$auth['user_id'], ':hearted_by_username' => (string)$auth['username'], ]); } $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $id]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response(['success' => true, 'data' => $row]); } catch (Throwable $e) { $msg = (string)$e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, "doesn't exist") !== false) { admin_json_error('Brak wymaganych kolumn/tabel (np. is_hearted). Uruchom /administration/install_notes_chat.php', 500); } admin_json_error('Błąd zmiany serduszka', 500); } } $message = isset($payload['message']) ? trim((string)$payload['message']) : ''; $replyToId = isset($payload['reply_to_id']) ? (int)$payload['reply_to_id'] : null; } else { $action = isset($_POST['action']) ? (string)$_POST['action'] : ''; $message = isset($_POST['message']) ? trim((string)$_POST['message']) : ''; $replyToId = isset($_POST['reply_to_id']) && $_POST['reply_to_id'] !== '' ? (int)$_POST['reply_to_id'] : null; $clearFile = isset($_POST['clear_file']) ? (bool)((int)$_POST['clear_file']) : false; // Recall existing message (revoke own) if ($action === 'recall') { $id = isset($_POST['id']) ? (int)$_POST['id'] : 0; if ($id <= 0) { admin_json_error('Nieprawidłowe id', 422); } try { $stmt = $pdo->prepare('SELECT id, user_id FROM admin_chat_messages WHERE id = :id'); $stmt->execute([':id' => $id]); $cur = $stmt->fetch(PDO::FETCH_ASSOC); if (!$cur) { admin_json_error('Nie znaleziono wiadomości', 404); } if ((int)$cur['user_id'] !== (int)$auth['user_id']) { admin_json_error('Nie możesz cofnąć cudzej wiadomości', 403); } $up = $pdo->prepare( 'UPDATE admin_chat_messages ' . 'SET message = :message, file_name = NULL, file_mime = NULL, file_size = NULL, file_data = NULL, updated_at = NOW() ' . 'WHERE id = :id' ); $up->execute([':message' => $RECALLED_TEXT, ':id' => $id]); $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $id]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response(['success' => true, 'data' => $row]); } catch (Throwable $e) { $msg = (string)$e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, "doesn't exist") !== false) { admin_json_error('Brak wymaganych kolumn/tabel (np. updated_at). Uruchom /administration/install_notes_chat.php', 500); } admin_json_error('Błąd cofania wiadomości', 500); } } if (!empty($_FILES['file']) && is_array($_FILES['file'])) { $upload = $_FILES['file']; if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) { if (($upload['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) { admin_json_error('Błąd uploadu pliku (kod: ' . (int)$upload['error'] . ')', 422); } if (empty($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name'])) { admin_json_error('Nieprawidłowy upload pliku', 422); } $fileSize = isset($upload['size']) ? (int)$upload['size'] : null; if ($fileSize !== null && $fileSize > 5 * 1024 * 1024) { admin_json_error('Plik jest za duży (max 5MB)', 422); } $fileName = (string)($upload['name'] ?? 'plik'); // Rozpoznaj MIME bezpieczniej niż "type" z przeglądarki $detectedMime = null; if (function_exists('finfo_open')) { $fi = finfo_open(FILEINFO_MIME_TYPE); if ($fi) { $detectedMime = finfo_file($fi, $upload['tmp_name']); finfo_close($fi); } } $fileMime = (string)($detectedMime ?: ($upload['type'] ?? 'application/octet-stream')); $allowed = false; if (stripos($fileMime, 'image/') === 0) { $allowed = true; } if (in_array($fileMime, ['application/pdf', 'text/plain'], true)) { $allowed = true; } if (!$allowed) { admin_json_error('Niedozwolony typ pliku: ' . $fileMime, 422); } $fileData = file_get_contents($upload['tmp_name']); if ($fileData === false) { admin_json_error('Nie udało się odczytać pliku', 500); } } } // If we store attachments in DB, we must respect MySQL max_allowed_packet. if ($fileData !== null) { $dataSize = $fileSize !== null ? (int)$fileSize : (int)strlen($fileData); try { $maxPacket = (int)($pdo->query('SELECT @@max_allowed_packet')->fetchColumn() ?: 0); if ($maxPacket > 0) { // Leave margin for SQL/metadata overhead. $margin = 128 * 1024; if ($dataSize + $margin >= $maxPacket) { admin_json_error( 'Plik jest za duży dla konfiguracji MySQL (max_allowed_packet=' . $maxPacket . ' bajtów). Zmniejsz plik lub zwiększ max_allowed_packet na serwerze.', 422, ['max_allowed_packet' => $maxPacket, 'file_size' => $dataSize] ); } } } catch (Throwable $e) { // ignore and let DB enforce limits } } // Update existing message (edit own) with optional file changes if ($action === 'update') { $id = isset($_POST['id']) ? (int)$_POST['id'] : 0; $newMessage = $message; if ($id <= 0) { admin_json_error('Nieprawidłowe id', 422); } if ($newMessage === '') { admin_json_error('Wiadomość nie może być pusta', 422); } if (mb_strlen($newMessage) > 1500) { admin_json_error('Wiadomość jest zbyt długa (max 1500 znaków)', 422); } try { $stmt = $pdo->prepare('SELECT id, user_id, message FROM admin_chat_messages WHERE id = :id'); $stmt->execute([':id' => $id]); $cur = $stmt->fetch(PDO::FETCH_ASSOC); if (!$cur) { admin_json_error('Nie znaleziono wiadomości', 404); } if ((int)$cur['user_id'] !== (int)$auth['user_id']) { admin_json_error('Nie możesz edytować cudzej wiadomości', 403); } if (((string)($cur['message'] ?? '')) === $RECALLED_TEXT) { admin_json_error('Nie możesz edytować cofniętej wiadomości', 422); } $fields = ['message = :message', 'updated_at = NOW()']; $params = [':message' => $newMessage, ':id' => $id]; if ($clearFile) { $fields[] = 'file_name = NULL'; $fields[] = 'file_mime = NULL'; $fields[] = 'file_size = NULL'; $fields[] = 'file_data = NULL'; } elseif ($fileData !== null) { $fields[] = 'file_name = :file_name'; $fields[] = 'file_mime = :file_mime'; $fields[] = 'file_size = :file_size'; $fields[] = 'file_data = :file_data'; $params[':file_name'] = $fileName; $params[':file_mime'] = $fileMime; $params[':file_size'] = $fileSize; $params[':file_data'] = $fileData; } $sql = 'UPDATE admin_chat_messages SET ' . implode(', ', $fields) . ' WHERE id = :id'; $up = $pdo->prepare($sql); $up->bindValue(':message', $params[':message'], PDO::PARAM_STR); $up->bindValue(':id', (int)$params[':id'], PDO::PARAM_INT); if (array_key_exists(':file_name', $params)) { $up->bindValue(':file_name', (string)$params[':file_name'], PDO::PARAM_STR); $up->bindValue(':file_mime', (string)$params[':file_mime'], PDO::PARAM_STR); $up->bindValue(':file_size', $params[':file_size'] !== null ? (int)$params[':file_size'] : null, $params[':file_size'] !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); $up->bindValue(':file_data', $params[':file_data'], PDO::PARAM_LOB); } $up->execute(); $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $id]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response(['success' => true, 'data' => $row]); } catch (Throwable $e) { $msg = (string)$e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, "doesn't exist") !== false) { admin_json_error('Brak wymaganych kolumn/tabel (np. updated_at). Uruchom /administration/install_notes_chat.php', 500); } admin_json_error('Błąd edycji wiadomości', 500); } } } if ($replyToId !== null && $replyToId <= 0) { $replyToId = null; } if ($message === '' && $fileData === null) { admin_json_error('Wiadomość lub plik są wymagane', 422); } if ($message !== '' && mb_strlen($message) > 1500) { admin_json_error('Wiadomość jest zbyt długa (max 1500 znaków)', 422); } try { // Server-side dedupe: jeśli user kliknie "Wyślij" kilka razy, nie duplikuj tego samego wpisu $dedupeStmt = $pdo->prepare( 'SELECT id FROM admin_chat_messages ' . 'WHERE user_id = :uid ' . 'AND (message <=> :message) ' . 'AND (reply_to_id <=> :reply_to_id) ' . 'AND (file_name <=> :file_name) ' . 'AND (file_size <=> :file_size) ' . 'AND created_at >= (NOW() - INTERVAL 3 SECOND) ' . 'ORDER BY id DESC LIMIT 1' ); $dedupeStmt->bindValue(':uid', (int)$auth['user_id'], PDO::PARAM_INT); // Keep compatibility with schemas where message is NOT NULL: use empty string instead of NULL. $dedupeStmt->bindValue(':message', $message, PDO::PARAM_STR); $dedupeStmt->bindValue(':reply_to_id', $replyToId, $replyToId !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); $dedupeStmt->bindValue(':file_name', $fileName, $fileName !== null ? PDO::PARAM_STR : PDO::PARAM_NULL); $dedupeStmt->bindValue(':file_size', $fileSize, $fileSize !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); $dedupeStmt->execute(); $existingId = (int)($dedupeStmt->fetchColumn() ?: 0); if ($existingId > 0) { $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $existingId]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response([ 'success' => true, 'deduped' => true, 'data' => $row, ]); } $stmt = $pdo->prepare( 'INSERT INTO admin_chat_messages (user_id, username, message, reply_to_id, file_name, file_mime, file_size, file_data) ' . 'VALUES (:user_id, :username, :message, :reply_to_id, :file_name, :file_mime, :file_size, :file_data)' ); $stmt->bindValue(':user_id', (int)$auth['user_id'], PDO::PARAM_INT); $stmt->bindValue(':username', (string)$auth['username'], PDO::PARAM_STR); // Keep compatibility with schemas where message is NOT NULL: use empty string instead of NULL. $stmt->bindValue(':message', $message, PDO::PARAM_STR); $stmt->bindValue(':reply_to_id', $replyToId, $replyToId !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); $stmt->bindValue(':file_name', $fileName, $fileName !== null ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':file_mime', $fileMime, $fileMime !== null ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':file_size', $fileSize, $fileSize !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); if ($fileData !== null) { $stmt->bindValue(':file_data', $fileData, PDO::PARAM_LOB); } else { $stmt->bindValue(':file_data', null, PDO::PARAM_NULL); } $stmt->execute(); $id = (int)$pdo->lastInsertId(); $select = $pdo->prepare( admin_chat_select_sql() . 'WHERE m.id = :id LIMIT 1' ); $select->execute([':id' => $id]); $row = $select->fetch(PDO::FETCH_ASSOC); if (is_array($row)) { $row = admin_chat_normalize_row($row); } admin_json_response([ 'success' => true, 'data' => $row ?: [ 'id' => $id, 'user_id' => (int)$auth['user_id'], 'username' => (string)$auth['username'], 'message' => $message, 'created_at' => date('Y-m-d H:i:s'), ], ], 201); } catch (PDOException $e) { $msg = $e->getMessage(); if (stripos($msg, 'Unknown column') !== false || stripos($msg, 'doesn\'t exist') !== false) { admin_json_error('Brak wymaganych kolumn/tabel. Uruchom /administration/install_notes_chat.php', 500); } if (stripos($msg, 'max_allowed_packet') !== false || stripos($msg, 'packet') !== false || stripos($msg, 'server has gone away') !== false) { admin_json_error('Błąd zapisu wiadomości: prawdopodobnie limit max_allowed_packet w MySQL. Zmniejsz plik lub zwiększ max_allowed_packet na serwerze.', 500); } $compact = preg_replace('/\s+/', ' ', (string)$msg); $compact = mb_substr($compact, 0, 240); if ($fileData !== null) { admin_json_error('Błąd zapisu wiadomości (DB): ' . $compact, 500); } admin_json_error('Błąd zapisu wiadomości', 500); } catch (Throwable $e) { admin_json_error('Błąd zapisu wiadomości', 500); } } admin_json_error('Metoda niedozwolona', 405);