diff --git a/class/comment.php b/class/comment.php index 76fa546..6584053 100644 --- a/class/comment.php +++ b/class/comment.php @@ -255,7 +255,8 @@ function commentListShow($tpl_dir) { $assign['comment_max_chars'] = $settings['comment_max_chars']; $assign['im'] = $settings['comment_use_antispam']; - + $assign['comment_allowed_extensions'] = $settings['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif'; + $assign['comment_max_file_size'] = $settings['comment_max_file_size'] ?? 2048; $comments = array(); if ($settings['comment_use_page_nav'] == 1) @@ -483,12 +484,12 @@ function commentListShow($tpl_dir) { $upload_path = BASE_DIR . '/uploads/comments/'; -// Создаем папку, если её нет -if (!is_dir($upload_path)) { - @mkdir($upload_path, 0775, true); + // Создаем папку, если её нет + if (!is_dir($upload_path)) { + @mkdir($upload_path, 0775, true); - // Содержимое для защитного файла index.php - $index_content = ""; - // Создаем index.php для безопасности (запрет листинга папки и редирект) - @file_put_contents($upload_path . 'index.php', $index_content); -} - - $allowed_mime = ['image/jpeg', 'image/png', 'image/gif']; - $file_info = @getimagesize($_FILES['comment_image']['tmp_name']); - $file_size = $_FILES['comment_image']['size']; - - // Проверка: это реально картинка и её размер не более 2Мб - if ($file_info && in_array($file_info['mime'], $allowed_mime) && $file_size <= 2 * 1024 * 1024) - { - $ext = pathinfo($_FILES['comment_image']['name'], PATHINFO_EXTENSION); - $comment_file_name = time() . '_' . rand(100, 999) . '.' . $ext; - - if (!move_uploaded_file($_FILES['comment_image']['tmp_name'], $upload_path . $comment_file_name)) { - $comment_file_name = ''; // Сбрасываем, если не удалось переместить - } + // Создаем index.php для безопасности (запрет листинга папки и редирект) + @file_put_contents($upload_path . 'index.php', $index_content); } + + // ---ДИНАМИЧЕСКАЯ ПРОВЕРКА РАСШИРЕНИЯ И РАЗМЕРА --- +$ext = strtolower(pathinfo($_FILES['comment_image']['name'], PATHINFO_EXTENSION)); +$allowed_ext_str = $settings['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif,webp'; +$allowed_extensions = array_map('trim', explode(',', strtolower($allowed_ext_str))); + +$max_file_size_kb = (int)($settings['comment_max_file_size'] ?? 2048); +$max_file_size_bytes = $max_file_size_kb * 1024; +$file_size = $_FILES['comment_image']['size']; + +// Дополнительная проверка: действительно ли это изображение по содержимому +$finfo = finfo_open(FILEINFO_MIME_TYPE); +$mime = finfo_file($finfo, $_FILES['comment_image']['tmp_name']); +finfo_close($finfo); + +if (in_array($ext, $allowed_extensions) + && $file_size > 0 + && $file_size <= $max_file_size_bytes + && strpos($mime, 'image/') === 0) // Проверяем, что MIME начинается с image/ +{ + $comment_file_name = time() . '_' . rand(100, 999) . '.' . $ext; + if (!move_uploaded_file($_FILES['comment_image']['tmp_name'], $upload_path . $comment_file_name)) { + $comment_file_name = ''; + } +} else { + // Если файл не прошел проверку, можно выдать ошибку (для AJAX) + if ($ajax) { echo 'error_file_validation'; exit; } +} } // --- ПОДГОТОВКА ДАННЫХ ДЛЯ БД --- @@ -546,7 +560,7 @@ exit; $user_rating = (int)($_POST['comment_user_rating'] ?? 0); // Если это ответ (parent_id > 0) И в настройках выключено "Использовать в ответах" if ($parent_id > 0 && (!isset($settings['comment_show_user_rating_replies']) || $settings['comment_show_user_rating_replies'] == 0)) { - $user_rating = 0; // Принудительно сбрасываем оценку для ответов + $user_rating = 0; // Принудительно сбрасываем оценку для ответов } if ($user_rating < 0) $user_rating = 0; if ($user_rating > 5) $user_rating = 5; $new_comment['user_rating'] = $user_rating; @@ -585,21 +599,21 @@ exit; if ($ajax) { // 1. Получаем аватар как обычно -$new_comment['avatar'] = (isset($new_comment['comment_author_id']) && $new_comment['comment_author_id'] > 0) ? getAvatar($new_comment['comment_author_id'], 48) : ''; + $new_comment['avatar'] = (isset($new_comment['comment_author_id']) && $new_comment['comment_author_id'] > 0) ? getAvatar($new_comment['comment_author_id'], 48) : ''; -// 2. Очищаем, если это системный user.png -if (!empty($new_comment['avatar']) && strpos($new_comment['avatar'], 'user.png') !== false) -{ - $new_comment['avatar'] = ''; -} + // 2. Очищаем, если это системный user.png + if (!empty($new_comment['avatar']) && strpos($new_comment['avatar'], 'user.png') !== false) + { + $new_comment['avatar'] = ''; + } -// 3. Если аватара нет — генерируем данные для буквы -if (empty($new_comment['avatar'])) -{ - $name = !empty($new_comment['comment_author_name']) ? stripslashes($new_comment['comment_author_name']) : 'Guest'; - $new_comment['first_letter'] = mb_substr(trim($name), 0, 1, 'UTF-8'); - $new_comment['avatar_color_index'] = (abs(crc32($name)) % 12) + 1; -} + // 3. Если аватара нет — генерируем данные для буквы + if (empty($new_comment['avatar'])) + { + $name = !empty($new_comment['comment_author_name']) ? stripslashes($new_comment['comment_author_name']) : 'Guest'; + $new_comment['first_letter'] = mb_substr(trim($name), 0, 1, 'UTF-8'); + $new_comment['avatar_color_index'] = (abs(crc32($name)) % 12) + 1; + } $new_comment['comment_changed'] = 0; // Передаем флаги для мгновенной активации управления в шаблоне @@ -633,14 +647,16 @@ function commentPostEdit($comment_id) if ($comment_id <= 0 || $user_group <= 0) exit('INVALID_ID'); // 2. Получаем данные комментария и настройки модуля (JOIN) - // ДОБАВИЛ: cmnt.comment_edit_time в выборку SQL + // ДОБАВИЛ: выборку новых полей (allowed_extensions и max_file_size) $row = $AVE_DB->Query(" SELECT msg.*, cmnt.comment_max_chars, cmnt.comment_need_approve, cmnt.comment_user_groups, - cmnt.comment_edit_time + cmnt.comment_edit_time, + cmnt.comment_allowed_extensions, + cmnt.comment_max_file_size FROM " . PREFIX . "_module_comment_info AS msg JOIN " . PREFIX . "_module_comments AS cmnt ON cmnt.Id = 1 WHERE msg.Id = '" . $comment_id . "' @@ -655,12 +671,10 @@ function commentPostEdit($comment_id) $is_author = ($user_id > 0 && $user_id == $row['comment_author_id']) || ($row['comment_author_id'] == 0 && !empty($row['anon_key']) && $row['anon_key'] == $anon_key); - // ИСПРАВЛЕНО: Теперь берем лимит времени ПРЯМО из БД ($row), а не из переменной класса $time_limit = (isset($row['comment_edit_time'])) ? (int)$row['comment_edit_time'] : 60; $time_passed = time() - (int)$row['comment_published']; $is_time_ok = ($time_passed < $time_limit); - // Если не админ, проверяем: автор ли это и не вышло ли время if (!$is_admin) { if (!$is_author) exit('NOT_AUTHOR'); if (!$is_time_ok) { @@ -669,7 +683,7 @@ function commentPostEdit($comment_id) } } - // 4. Обработка текста (без изменений...) + // 4. Обработка текста $comment_text = $_POST['text'] ?? ''; $comment_text = preg_replace_callback('/([0-9a-f]{1,7});/i', function($matches) { return chr(hexdec($matches[1])); }, $comment_text); @@ -683,30 +697,51 @@ function commentPostEdit($comment_id) $comment_text_cut = mb_substr($comment_text, 0, $max); if (mb_strlen($comment_text) > $max) $comment_text_cut .= '…'; - // 5. Работа с изображениями (без изменений...) - $new_file_sql = ""; - $upload_dir = BASE_DIR . '/uploads/comments/'; - if (isset($_FILES['comment_image']) && $_FILES['comment_image']['error'] == UPLOAD_ERR_OK) { +// 5. Работа с изображениями (С МАКСИМАЛЬНОЙ ЗАЩИТОЙ) +$new_file_sql = ""; +$upload_dir = BASE_DIR . '/uploads/comments/'; + +if (isset($_FILES['comment_image']) && $_FILES['comment_image']['error'] == UPLOAD_ERR_OK) { + + // Настройки из БД + $file_ext = strtolower(pathinfo($_FILES['comment_image']['name'], PATHINFO_EXTENSION)); + $allowed_ext_str = $row['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif,webp'; + $allowed_extensions = array_map('trim', explode(',', strtolower($allowed_ext_str))); + + $max_kb = (int)($row['comment_max_file_size'] ?? 2048); + $file_size = $_FILES['comment_image']['size']; + + // --- НОВОЕ: Проверка MIME-типа (защита от переименованных скриптов) --- + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $_FILES['comment_image']['tmp_name']); + finfo_close($finfo); + + // ПРОВЕРКА: расширение + размер + MIME-тип + if (in_array($file_ext, $allowed_extensions) + && $file_size > 0 + && $file_size <= ($max_kb * 1024) + && strpos($mime, 'image/') === 0) // Файл действительно картинка + { if (!is_dir($upload_dir)) @mkdir($upload_dir, 0775, true); + // Удаляем старый файл, если он был if (!empty($row['comment_file']) && file_exists($upload_dir . $row['comment_file'])) { @unlink($upload_dir . $row['comment_file']); } - $file_ext = strtolower(pathinfo($_FILES['comment_image']['name'], PATHINFO_EXTENSION)); + // Генерируем новое имя $new_file_name = 'comm_' . time() . '_' . rand(100, 999) . '.' . $file_ext; if (move_uploaded_file($_FILES['comment_image']['tmp_name'], $upload_dir . $new_file_name)) { $new_file_sql = ", comment_file = '" . addslashes($new_file_name) . "'"; } - } - elseif (isset($_POST['delete_image']) && $_POST['delete_image'] == 1) { - if (!empty($row['comment_file']) && file_exists($upload_dir . $row['comment_file'])) { - @unlink($upload_dir . $row['comment_file']); - } - $new_file_sql = ", comment_file = ''"; + } else { + // Если файл не прошел серверную проверку + echo "FILE_ERROR"; + exit; } +} $user_rating = isset($_POST['user_rating']) ? (int)$_POST['user_rating'] : (int)$row['user_rating']; @@ -1281,6 +1316,11 @@ function commentAdminSettingsEdit($tpl_dir) // Настройки рейтинга и файлов $post_allow_files = $_POST['comment_allow_files'] ?? 0; $post_allow_files_anon = $_POST['comment_allow_files_anon'] ?? 0; + + /* НОВОЕ: Настройки расширений и максимального размера файла */ + $post_allowed_extensions = $_POST['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif'; + $post_max_file_size = $_POST['comment_max_file_size'] ?? 2048; + $post_rating_type = $_POST['comment_rating_type'] ?? 0; $post_show_user_rating = $_POST['comment_show_user_rating'] ?? 0; @@ -1308,9 +1348,12 @@ function commentAdminSettingsEdit($tpl_dir) { $max_chars = (empty($post_max_chars) || $post_max_chars < 50) ? 50 : $post_max_chars; - // Подготовка имен полей + // Подготовка имен полей и настроек файлов $clean_name_f1 = htmlspecialchars(stripslashes($post_name_f1), ENT_QUOTES); $clean_name_f2 = htmlspecialchars(stripslashes($post_name_f2), ENT_QUOTES); + + // Очищаем строку расширений от пробелов и лишних запятых + $clean_extensions = implode(',', array_filter(array_map('trim', explode(',', $post_allowed_extensions)))); $AVE_DB->Query(" UPDATE " . PREFIX . "_module_comments @@ -1325,6 +1368,11 @@ function commentAdminSettingsEdit($tpl_dir) comment_page_nav_count = '" . (int)$post_page_nav_count . "', comment_allow_files = '" . (int)$post_allow_files . "', comment_allow_files_anon = '" . (int)$post_allow_files_anon . "', + + /* СОХРАНЕНИЕ НОВЫХ ПОЛЕЙ */ + comment_allowed_extensions = '" . addslashes($clean_extensions) . "', + comment_max_file_size = '" . (int)$post_max_file_size . "', + comment_rating_type = '" . (int)$post_rating_type . "', comment_show_user_rating = '" . (int)$post_show_user_rating . "', comment_show_user_rating_replies = '" . (int)$post_show_user_rating_replies . "', diff --git a/js/comment.js b/js/comment.js index 38d32b9..7dcd031 100644 --- a/js/comment.js +++ b/js/comment.js @@ -89,6 +89,34 @@ function initCommentTimers() { // ВАЛИДАЦИЯ ПОЛЕЙ + // Функция для проверки файла (расширение и размер) + function checkFileConsistency(file, $errorDisplay) { + if (!file) return true; + + // Получаем настройки из шаблона (те переменные, что мы добавили в тег script) + var allowedExts = (typeof ALLOWED_EXTENSIONS !== 'undefined') ? ALLOWED_EXTENSIONS.toLowerCase().split(',') : ['jpg', 'jpeg', 'png', 'gif']; + var maxSizeKb = (typeof MAX_FILE_SIZE_KB !== 'undefined') ? parseInt(MAX_FILE_SIZE_KB) : 2048; + + var fileName = file.name.toLowerCase(); + var fileExt = fileName.split('.').pop(); + var fileSizeKb = file.size / 1024; + + // Проверка расширения + if ($.inArray(fileExt, allowedExts) === -1) { + $errorDisplay.text('Тип файла .' + fileExt + ' не разрешен.').removeClass('d-none'); + return false; + } + + // Проверка размера + if (fileSizeKb > maxSizeKb) { + $errorDisplay.text('Файл слишком большой (' + Math.round(fileSizeKb) + ' Кб). Максимум: ' + maxSizeKb + ' Кб').removeClass('d-none'); + return false; + } + + $errorDisplay.addClass('d-none').text(''); + return true; +} + function validate(form) { var isValid = true; @@ -127,6 +155,16 @@ function initCommentTimers() { // Проверка Текста setInvalid($(form.comment_text), !form.comment_text.value.trim(), "Введите текст комментария"); + // Проверка файла в основной форме +var $fileInput = $(form).find('#comment_image'); +if ($fileInput.length && $fileInput[0].files[0]) { + var isFileOk = checkFileConsistency($fileInput[0].files[0], $('#file_error')); + if (!isFileOk) { + $fileInput.addClass('is-invalid'); + isValid = false; + } +} + if (!isValid) { // Скроллим к первой ошибке $('html, body').animate({ @@ -189,18 +227,28 @@ function initCommentTimers() { // Счетчик: Количество оставшихся символов 1000 для формы создания комментария $('#in_message').limit(1000, '#charsLeft_new'); - // --- ЛОГИКА ПРЕДПРОСМОТРА ИЗОБРАЖЕНИЯ (НОВАЯ ФОРМА) --- - $doc.on('change', '#comment_image', function() { - var input = this; - if (input.files && input.files[0]) { - var reader = new FileReader(); - reader.onload = function(e) { - $('#image_preview_img').attr('src', e.target.result); - $('#image_preview_wrapper').removeClass('d-none'); - } - reader.readAsDataURL(input.files[0]); - } - }); +$doc.on('change', '#comment_image', function() { + var input = this; + var $errorBox = $('#file_error'); + + if (input.files && input.files[0]) { + // Сначала проверяем файл + if (!checkFileConsistency(input.files[0], $errorBox)) { + $(input).val('').addClass('is-invalid'); + $('#image_preview_wrapper').addClass('d-none'); + return; + } + + // Если файл прошел проверку — показываем превью + var reader = new FileReader(); + reader.onload = function(e) { + $('#image_preview_img').attr('src', e.target.result); + $('#image_preview_wrapper').removeClass('d-none'); + $(input).removeClass('is-invalid'); + } + reader.readAsDataURL(input.files[0]); + } +}); $doc.on('click', '#remove_image_btn', function() { $('#comment_image').val(''); @@ -418,24 +466,35 @@ $doc.on('mouseleave', '.rating-edit-block, #rating_wrapper', function() { `; } - var fileInputHtml = ''; - if (typeof UGROUP !== 'undefined' && (UGROUP != '2' || (typeof ALLOW_FILES_ANON !== 'undefined' && ALLOW_FILES_ANON == '1'))) { - fileInputHtml = ` -