diff --git a/class/comment.php b/class/comment.php index 6584053..c00166d 100644 --- a/class/comment.php +++ b/class/comment.php @@ -478,154 +478,119 @@ function commentListShow($tpl_dir) && !empty($_POST['comment_author_name']) && in_array($user_group, explode(',', $settings['comment_user_groups']))) { - // --- ОБРАБОТКА ЗАГРУЗКИ ИЗОБРАЖЕНИЯ --- - $comment_file_name = ''; - if ($settings['comment_allow_files'] == 1 && isset($_FILES['comment_image']) && $_FILES['comment_image']['error'] == UPLOAD_ERR_OK) + // --- ОБРАБОТКА ЗАГРУЗКИ ИЗОБРАЖЕНИЯ (С ЗАЩИТОЙ ОТ ДУБЛЕЙ) --- + $uploaded_files = []; + if ($settings['comment_allow_files'] == 1 && isset($_FILES['comment_image'])) { $upload_path = BASE_DIR . '/uploads/comments/'; - - // Создаем папку, если её нет if (!is_dir($upload_path)) { @mkdir($upload_path, 0775, true); - - // Содержимое для защитного файла index.php - $index_content = ""; - - // Создаем index.php для безопасности (запрет листинга папки и редирект) + $index_content = ""; @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))); + $files_to_process = []; + if (is_array($_FILES['comment_image']['name'])) { + foreach ($_FILES['comment_image']['name'] as $k => $v) { + if (!empty($v) && $_FILES['comment_image']['error'][$k] == UPLOAD_ERR_OK) { + $files_to_process[] = [ + 'name' => $_FILES['comment_image']['name'][$k], + 'tmp_name' => $_FILES['comment_image']['tmp_name'][$k], + 'size' => $_FILES['comment_image']['size'][$k] + ]; + } + } + } elseif (!empty($_FILES['comment_image']['name']) && $_FILES['comment_image']['error'] == UPLOAD_ERR_OK) { + $files_to_process[] = $_FILES['comment_image']; + } -$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']; + $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_bytes = (int)($settings['comment_max_file_size'] ?? 2048) * 1024; -// Дополнительная проверка: действительно ли это изображение по содержимому -$finfo = finfo_open(FILEINFO_MIME_TYPE); -$mime = finfo_file($finfo, $_FILES['comment_image']['tmp_name']); -finfo_close($finfo); + // Защита: не даем загрузить одно и то же содержимое дважды в одном запросе + $processed_hashes = []; -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; } -} + foreach ($files_to_process as $file) { + $file_hash = md5_file($file['tmp_name']); + if (in_array($file_hash, $processed_hashes)) continue; + + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + if (in_array($ext, $allowed_extensions) + && $file['size'] > 0 + && $file['size'] <= $max_file_size_bytes + && strpos($mime, 'image/') === 0) + { + $new_name = time() . '_' . rand(100, 999) . '.' . $ext; + if (move_uploaded_file($file['tmp_name'], $upload_path . $new_name)) { + $uploaded_files[] = $new_name; + $processed_hashes[] = $file_hash; + } + } + } } // --- ПОДГОТОВКА ДАННЫХ ДЛЯ БД --- - - // ПРОВЕРКА РОДИТЕЛЯ: Если родитель не найден в БД, сбрасываем в 0 (корень) $parent_id = (int)($_POST['parent_id'] ?? 0); if ($parent_id > 0) { $parent_exists = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE Id = '" . $parent_id . "'")->GetCell(); if (!$parent_exists) $parent_id = 0; } $new_comment['parent_id'] = $parent_id; - $new_comment['document_id'] = (int)($_POST['doc_id'] ?? 0); $new_comment['comment_author_name'] = addslashes(strip_tags($_POST['comment_author_name'] ?? '')); $new_comment['comment_author_id'] = empty($_SESSION['user_id']) ? 0 : (int)$_SESSION['user_id']; - - // ПРИВЯЗКА КЛЮЧА К КОММЕНТАРИЮ $new_comment['anon_key'] = ($new_comment['comment_author_id'] == 0) ? $anon_key : ''; - $new_comment['comment_author_email'] = addslashes(strip_tags($_POST['comment_author_email'] ?? '')); $new_comment['comment_author_city'] = addslashes(strip_tags($_POST['comment_author_city'] ?? '')); $new_comment['comment_author_website'] = addslashes(strip_tags($_POST['comment_author_website'] ?? '')); $new_comment['comment_author_ip'] = $_SERVER['REMOTE_ADDR']; $new_comment['comment_published'] = time(); $new_comment['comment_status'] = $comment_status; - $new_comment['comment_file'] = $comment_file_name; + + // Пишем список файлов + $new_comment['comment_file'] = !empty($uploaded_files) ? implode(',', $uploaded_files) : ''; - // --- ПОЛУЧАЕМ РЕЙТИНГ ИЗ ФОРМЫ --- $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; $comment_text_raw = $_POST['comment_text'] ?? ''; - $comment_max_chars = $settings['comment_max_chars']; - $comment_max_chars = (!empty($comment_max_chars) && $comment_max_chars > 10) ? $comment_max_chars : 200; - $comment_text_clean = strip_tags(stripslashes($comment_text_raw)); - $comment_text_cut = mb_substr($comment_text_clean, 0, $comment_max_chars); - $comment_text_cut .= (mb_strlen($comment_text_clean) > $comment_max_chars) ? '…' : ''; - $new_comment['comment_text'] = addslashes($comment_text_cut); + $new_comment['comment_text'] = addslashes($comment_text_clean); - // Выполняем запрос - $AVE_DB->Query(" - INSERT INTO " . PREFIX . "_module_comment_info - (`" . implode('`,`', array_keys($new_comment)) ."`) - VALUES - ('" . implode("','", $new_comment) . "') - "); + $AVE_DB->Query("INSERT INTO " . PREFIX . "_module_comment_info (`" . implode('`,`', array_keys($new_comment)) ."`) VALUES ('" . implode("','", $new_comment) . "')"); $new_comment['Id'] = $AVE_DB->InsertId(); // --- УВЕДОМЛЕНИЕ АДМИНА --- $mail_from = get_settings('mail_from'); $mail_from_name = get_settings('mail_from_name'); - $page_link = get_home_link() . urldecode(base64_decode($page)) . '&subaction=showonly&comment_id=' . $new_comment['Id'] . '#' . $new_comment['Id']; - - $mail_text = $AVE_Template->get_config_vars('COMMENT_MESSAGE_ADMIN'); - $mail_text = str_replace('%COMMENT%', stripslashes($new_comment['comment_text']), $mail_text); - $mail_text = str_replace('%N%', "\n", $mail_text); - $mail_text = str_replace('%PAGE%', $page_link, $mail_text); - $mail_text = str_replace('&', '&', $mail_text); - - send_mail($mail_from, $mail_text, $AVE_Template->get_config_vars('COMMENT_SUBJECT_MAIL'), $mail_from, $mail_from_name, 'text'); + $page_link = get_home_link() . urldecode(base64_decode($page)) . '&comment_id=' . $new_comment['Id'] . '#' . $new_comment['Id']; + $mail_text = "Новый комментарий:\n" . stripslashes($new_comment['comment_text']) . "\n\nСсылка: " . $page_link; + send_mail($mail_from, $mail_text, "Новый комментарий на сайте", $mail_from, $mail_from_name, 'text'); 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) : ''; - - // 2. Очищаем, если это системный user.png - if (!empty($new_comment['avatar']) && strpos($new_comment['avatar'], 'user.png') !== false) - { - $new_comment['avatar'] = ''; - } - - // 3. Если аватара нет — генерируем данные для буквы - if (empty($new_comment['avatar'])) - { + if (empty($new_comment['avatar']) || strpos($new_comment['avatar'], 'user.png') !== false) { $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; - - // Передаем флаги для мгновенной активации управления в шаблоне $new_comment['can_edit'] = 1; $new_comment['edit_time_left'] = $this->conf_edit_time; - $new_comment['comment_published'] = ave_date_format($AVE_Template->get_config_vars('COMMENT_DATE_TIME_FORMAT'), $new_comment['comment_published']); - $new_comment['settings'] = $settings; - + $subcomments[] = $new_comment; $AVE_Template->assign('subcomments', $subcomments); - $AVE_Template->assign('theme', defined('THEME_FOLDER') ? THEME_FOLDER : DEFAULT_THEME_FOLDER); $AVE_Template->display($tpl_dir . $this->_comments_tree_sub_tpl); } } @@ -647,7 +612,6 @@ function commentPostEdit($comment_id) if ($comment_id <= 0 || $user_group <= 0) exit('INVALID_ID'); // 2. Получаем данные комментария и настройки модуля (JOIN) - // ДОБАВИЛ: выборку новых полей (allowed_extensions и max_file_size) $row = $AVE_DB->Query(" SELECT msg.*, @@ -698,50 +662,69 @@ function commentPostEdit($comment_id) if (mb_strlen($comment_text) > $max) $comment_text_cut .= '…'; -// 5. Работа с изображениями (С МАКСИМАЛЬНОЙ ЗАЩИТОЙ) -$new_file_sql = ""; -$upload_dir = BASE_DIR . '/uploads/comments/'; + // 5. Работа с изображениями (МУЛЬТИЗАГРУЗКА И УДАЛЕНИЕ) + $upload_dir = BASE_DIR . '/uploads/comments/'; + // Получаем текущие файлы из БД в массив + $current_files = !empty($row['comment_file']) ? explode(',', $row['comment_file']) : []; -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']; + // --- А. Обработка выборочного удаления --- + if (!empty($_POST['delete_files'])) { + $files_to_remove = explode(',', $_POST['delete_files']); + foreach ($files_to_remove as $rem_file) { + $rem_file = trim($rem_file); + if (empty($rem_file)) continue; - // --- НОВОЕ: Проверка 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']); + // Удаляем физически + if (file_exists($upload_dir . $rem_file)) { + @unlink($upload_dir . $rem_file); + } + // Удаляем из нашего массива для БД + $current_files = array_filter($current_files, function($v) use ($rem_file) { + return trim($v) !== $rem_file; + }); } - - // Генерируем новое имя - $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) . "'"; - } - } else { - // Если файл не прошел серверную проверку - echo "FILE_ERROR"; - exit; } -} + + // --- Б. Загрузка новых файлов (если есть) --- + // Проверяем, что пришел массив файлов + if (isset($_FILES['comment_image']) && is_array($_FILES['comment_image']['name'])) { + + $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); + + // Перебираем загруженные файлы + foreach ($_FILES['comment_image']['name'] as $i => $fname) { + if ($_FILES['comment_image']['error'][$i] == UPLOAD_ERR_OK) { + + $tmp_name = $_FILES['comment_image']['tmp_name'][$i]; + $file_ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION)); + $file_size = $_FILES['comment_image']['size'][$i]; + + // Проверка MIME-типа + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $tmp_name); + finfo_close($finfo); + + 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); + + $new_file_name = 'comm_' . time() . '_' . rand(1000, 9999) . '.' . $file_ext; + + if (move_uploaded_file($tmp_name, $upload_dir . $new_file_name)) { + $current_files[] = $new_file_name; // Добавляем новый файл к списку + } + } + } + } + } + + // Собираем итоговую строку файлов для БД через запятую + $final_files_str = implode(',', array_unique(array_filter($current_files))); $user_rating = isset($_POST['user_rating']) ? (int)$_POST['user_rating'] : (int)$row['user_rating']; @@ -754,8 +737,8 @@ if (isset($_FILES['comment_image']) && $_FILES['comment_image']['error'] == UPLO comment_changed = '" . time() . "', comment_text = '" . addslashes($comment_text_cut) . "', comment_status = '" . $new_status . "', - user_rating = '" . $user_rating . "' - $new_file_sql + user_rating = '" . $user_rating . "', + comment_file = '" . addslashes($final_files_str) . "' WHERE Id = '" . $comment_id . "' "); @@ -774,8 +757,6 @@ function commentPostDelete($comment_id) if ($comment_id <= 0) die('Ошибка: Неверный ID'); // --- ШАГ 0. ПОЛУЧЕНИЕ ДАННЫХ И ПРОВЕРКА ПРАВ --- - - // ГАРАНТИРУЕМ, что настройки загружены из БД в свойства класса $this->_commentSettingsGet(); $comment_data = $AVE_DB->Query(" @@ -798,7 +779,6 @@ function commentPostDelete($comment_id) $is_author = ($current_user_id > 0 && $current_user_id == $comment_data['comment_author_id']) || ($comment_data['comment_author_id'] == 0 && !empty($comment_data['anon_key']) && $comment_data['anon_key'] == $anon_key); - // Теперь $this->conf_edit_time точно содержит значение из БД (благодаря вызову выше) $is_time_ok = (time() - (int)$comment_data['comment_published'] < (int)$this->conf_edit_time); if ($is_author && $is_time_ok) { @@ -811,29 +791,36 @@ function commentPostDelete($comment_id) die('Доступ запрещен'); } - // --- ШАГ 1. ПРОВЕРКА НАЛИЧИЯ ОТВЕТОВ (без изменений) --- $has_children = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info WHERE parent_id = '" . $comment_id . "'")->GetCell(); $upload_dir = BASE_DIR . '/uploads/comments/'; - // Мягкое удаление ТОЛЬКО для авторов, если есть ответы + // --- ВАРИАНТ А: МЯГКОЕ УДАЛЕНИЕ (если есть ответы и удаляет не админ) --- if ($has_children > 0 && $user_group != 1) { + // Удаляем файлы физически, если они есть if (!empty($comment_data['comment_file'])) { - if (file_exists($upload_dir . $comment_data['comment_file'])) @unlink($upload_dir . $comment_data['comment_file']); + $files_to_del = explode(',', $comment_data['comment_file']); + foreach ($files_to_del as $f_name) { + $f_path = $upload_dir . trim($f_name); + if (!empty($f_name) && file_exists($f_path)) @unlink($f_path); + } } $del_text = "Комментарий удален автором."; $AVE_DB->Query(" UPDATE " . PREFIX . "_module_comment_info - SET comment_text = '" . addslashes($del_text) . "', comment_file = '', anon_key = '', comment_changed = '" . time() . "' + SET comment_text = '" . addslashes($del_text) . "', + comment_file = '', + anon_key = '', + comment_changed = '" . time() . "' WHERE Id = '" . $comment_id . "' "); echo "OK_SOFT"; } else { - // --- ВАРИАНТ Б: ПОЛНОЕ УДАЛЕНИЕ (Для админа или если нет детей) --- - + // --- ВАРИАНТ Б: ПОЛНОЕ УДАЛЕНИЕ (Админ или нет вложенных ответов) --- $ids_to_delete = [$comment_id]; + // Если админ — собираем всю ветку детей для удаления if ($user_group == 1) { $all_child_ids = []; $parent_ids = [$comment_id]; @@ -852,14 +839,20 @@ function commentPostDelete($comment_id) } $final_ids_str = implode(',', $ids_to_delete); + + // Массовое удаление всех файлов во всех удаляемых комментариях $files_res = $AVE_DB->Query("SELECT comment_file FROM " . PREFIX . "_module_comment_info WHERE Id IN ($final_ids_str)"); while ($f = $files_res->FetchAssocArray()) { if (!empty($f['comment_file'])) { - $f_path = $upload_dir . $f['comment_file']; - if (file_exists($f_path)) @unlink($f_path); + $files_to_del = explode(',', $f['comment_file']); + foreach ($files_to_del as $f_name) { + $f_path = $upload_dir . trim($f_name); + if (!empty($f_name) && file_exists($f_path)) @unlink($f_path); + } } } + // Удаляем записи из БД $AVE_DB->Query("DELETE FROM " . PREFIX . "_module_comment_info WHERE Id IN ($final_ids_str)"); echo "OK"; diff --git a/js/comment.js b/js/comment.js index 7dcd031..31b2906 100644 --- a/js/comment.js +++ b/js/comment.js @@ -155,13 +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 ($fileInput.length && $fileInput[0].files.length > 0) { + var files = $fileInput[0].files; + for (var i = 0; i < files.length; i++) { + if (!checkFileConsistency(files[i], $('#file_error'))) { + $fileInput.addClass('is-invalid'); + isValid = false; + break; + } } } @@ -230,31 +233,56 @@ if ($fileInput.length && $fileInput[0].files[0]) { $doc.on('change', '#comment_image', function() { var input = this; var $errorBox = $('#file_error'); + var $previewWrapper = $('#image_preview_wrapper'); - if (input.files && input.files[0]) { - // Сначала проверяем файл - if (!checkFileConsistency(input.files[0], $errorBox)) { - $(input).val('').addClass('is-invalid'); - $('#image_preview_wrapper').addClass('d-none'); - return; - } + $previewWrapper.empty().addClass('d-none'); + $errorBox.addClass('d-none'); + $(input).removeClass('is-invalid'); - // Если файл прошел проверку — показываем превью - 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]); + if (input.files && input.files.length > 0) { + var files = Array.from(input.files); + $previewWrapper.removeClass('d-none').addClass('d-flex flex-wrap gap-2'); + + files.forEach(function(file) { + if (checkFileConsistency(file, $errorBox)) { + var reader = new FileReader(); + reader.onload = function(e) { + var imgHtml = ` +
Загруженные файлы:
+ ${existingImagesHtml} + +