From 85bc016bc7e5039c80ef1072d3d0701cbcfd32d3 Mon Sep 17 00:00:00 2001 From: Repellent Date: Mon, 29 Dec 2025 21:52:35 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BC=D0=B0=D1=81=D1=81=D0=BE=D0=B2=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- class/comment.php | 281 +++++++++++----------- js/comment.js | 402 +++++++++++++++++++------------- templates/comments_tree.tpl | 11 +- templates/comments_tree_sub.tpl | 20 +- 4 files changed, 398 insertions(+), 316 deletions(-) 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 = ` +
+ + +
`; + $previewWrapper.append(imgHtml); + } + reader.readAsDataURL(file); + } else { + $(input).val('').addClass('is-invalid'); + $previewWrapper.addClass('d-none'); + } + }); } }); - $doc.on('click', '#remove_image_btn', function() { - $('#comment_image').val(''); - $('#image_preview_wrapper').addClass('d-none'); - $('#image_preview_img').attr('src', '#'); - }); +// старый обработчик, доработанный под мультизагрузку +$doc.on('click', '#remove_image_btn', function() { + $('#comment_image').val(''); // Очищаем выбор файлов + $('#image_preview_wrapper').addClass('d-none').empty(); // Скрываем и полностью очищаем блок превью + // Строка с #image_preview_img больше не нужна, так как картинок теперь много +}); + +// удаление картинок по одной (крестиком на самой картинке) +$doc.on('click', '.remove-new-img', function() { + $(this).closest('.preview-item').remove(); // Удаляем только этот контейнер с картинкой + + // Если после удаления картинок внутри не осталось — скрываем обертку и чистим инпут + if ($('#image_preview_wrapper').children().length === 0) { + $('#image_preview_wrapper').addClass('d-none'); + $('#comment_image').val(''); + } +}); // --- ЛОГИКА ВЫБОРА РЕЙТИНГА В ФОРМЕ --- $doc.on('mouseenter', '.star-choice', function() { @@ -404,155 +432,186 @@ $doc.on('mouseleave', '.rating-edit-block, #rating_wrapper', function() { cAction(this, $(this).hasClass('mod_comment_lock') ? 'lock' : 'unlock'); }); - // --- РЕДАКТИРОВАНИЕ (С ПРЕДПРОСМОТРОМ ТЕКУЩЕГО ФОТО) --- - $doc.off('click', '.mod_comment_edit').on('click', '.mod_comment_edit', function(e) { - e.preventDefault(); - var cid = $(this).data('id'); - var $wrapper = $('#comment_wrapper_' + cid); - var $textBlock = $wrapper.find('.mod_comment_text').first(); - - if ($wrapper.find('.edit-form-container').length > 0) return; - var isMyOwn = $wrapper.attr('data-is-own') == '1'; - var currentRating = parseInt($wrapper.attr('data-user-rating')) || 0; - $('.edit-form-container').remove(); - $('.mod_comment_text').show(); - - var cleanText = $textBlock.html().replace(//mg, "\n").trim(); - var currentImg = $wrapper.find('.mod_comment_attached_image img').first().attr('src'); - - var starsEditBlock = ''; - - // --- ИЗМЕНЕНИЕ 2: Проверка запрета звезд для ответов при редактировании --- - // Добавлена более строгая проверка data-parent - var parentIdVal = parseInt($wrapper.attr('data-parent')) || 0; - var isReply = parentIdVal > 0; - var ratingForbiddenForReply = (isReply && typeof SHOW_USER_RATING_REPLIES !== 'undefined' && SHOW_USER_RATING_REPLIES == '0'); - - if (typeof SHOW_USER_RATING !== 'undefined' && SHOW_USER_RATING == '1' && isMyOwn && !ratingForbiddenForReply) { - if (typeof UGROUP !== 'undefined' && (UGROUP != '2' || RATING_ANON_SET == '1')) { - var starsHtml = ''; - for(var i=1; i<=5; i++) { - var starClass = (i <= currentRating) ? 'bi-star-fill' : 'bi-star'; - starsHtml += ``; - } - starsEditBlock = ` -
- -
-
${starsHtml}
- - Сбросить - -
- -
`; - } - } - // --- КОНЕЦ ИЗМЕНЕНИЯ 2 --- - // Обертка для ТЕКУЩЕГО изображения с крестиком (как при загрузке) - var currentImgPreview = ''; - if (currentImg) { - currentImgPreview = ` -
- - - -
`; - } -var fileInputHtml = ''; -if (typeof UGROUP !== 'undefined' && (UGROUP != '2' || (typeof ALLOW_FILES_ANON !== 'undefined' && ALLOW_FILES_ANON == '1'))) { - // Получаем значения для подсказки (те же, что и в основной форме) - var allowedText = (typeof ALLOWED_EXTENSIONS !== 'undefined') ? ALLOWED_EXTENSIONS : 'jpg,jpeg,png,gif,webp'; - var sizeText = (typeof MAX_FILE_SIZE_KB !== 'undefined') ? MAX_FILE_SIZE_KB : '2048'; - fileInputHtml = ` -
- + +// НАЧАЛО $doc.off('click', '.mod_comment_edit') + +$doc.off('click', '.mod_comment_edit').on('click', '.mod_comment_edit', function(e) { + e.preventDefault(); + var cid = $(this).data('id'); + var $wrapper = $('#comment_wrapper_' + cid); + + if ($wrapper.length === 0) { + console.error("Обертка #comment_wrapper_" + cid + " не найдена!"); + return; + } + + var $textBlock = $wrapper.find('.mod_comment_text').first(); + + // Пытаемся найти блок с картинками + var $attachedImagesBlock = $wrapper.find('.mod_comment_attached_image'); + if ($attachedImagesBlock.length === 0) { + $attachedImagesBlock = $wrapper.find('.comment-files, .attached-images'); + } + + if ($wrapper.find('.edit-form-container').length > 0) return; + + var isMyOwn = $wrapper.attr('data-is-own') == '1'; + var currentRating = parseInt($wrapper.attr('data-user-rating')) || 0; + + $('.edit-form-container').remove(); + $('.mod_comment_text').show(); + $('.mod_comment_attached_image, .comment-files').show(); + + var cleanText = $textBlock.html().replace(//mg, "\n").trim(); + + // --- СБОР ТЕКУЩИХ КАРТИНОК (ТОТ САМЫЙ РАБОЧИЙ КОД) --- + var existingImagesHtml = ''; + var $foundImgs = $wrapper.find('img').filter(function() { + return !$(this).hasClass('star-choice') && !$(this).hasClass('avatar') && !$(this).closest('.stars').length; + }); + + if ($foundImgs.length > 0) { + existingImagesHtml = '
'; + $foundImgs.each(function(index) { + var imgSrc = $(this).attr('src'); + var fileName = imgSrc.split('/').pop(); -
- Разрешены: ${allowedText}. - Макс. размер: ${sizeText} Кб. + existingImagesHtml += ` +
+ + +
`; + }); + existingImagesHtml += '
'; + } + + // --- РЕЙТИНГ (ВОЗВРАЩЕНА ПОЛНАЯ ОРИГИНАЛЬНАЯ ЛОГИКА) --- + var starsEditBlock = ''; + var parentIdVal = parseInt($wrapper.attr('data-parent')) || 0; + var isReply = parentIdVal > 0; + var ratingForbiddenForReply = (isReply && typeof SHOW_USER_RATING_REPLIES !== 'undefined' && SHOW_USER_RATING_REPLIES == '0'); + + if (typeof SHOW_USER_RATING !== 'undefined' && SHOW_USER_RATING == '1' && isMyOwn && !ratingForbiddenForReply) { + if (typeof UGROUP !== 'undefined' && (UGROUP != '2' || RATING_ANON_SET == '1')) { + var starsHtml = ''; + for(var i=1; i<=5; i++) { + var starClass = (i <= currentRating) ? 'bi-star-fill' : 'bi-star'; + starsHtml += ``; + } + starsEditBlock = ` +
+ +
+
${starsHtml}
+ + Сбросить + +
+ +
`; + } + } + + // --- СБОРКА ФОРМЫ --- + var editHtml = ` +
+ + ${starsEditBlock} + +

Загруженные файлы:

+ ${existingImagesHtml} + +
+ + +
+ + +
+ + + Осталось:
- - -
-
-
- Превью - -
-
-
`; -} + + $textBlock.hide(); + $attachedImagesBlock.hide(); + $foundImgs.hide(); + + $textBlock.after(editHtml); + + // Добавил поддержку лимита символов + if ($.fn.limit) { + $('#ta_' + cid).limit(1000, '#charsLeft_' + cid); + } + $('#ta_' + cid).focus(); +}); - var editHtml = ` -
- - ${starsEditBlock} - ${currentImgPreview} - ${fileInputHtml} -
- - - Осталось: -
-
`; - - $textBlock.hide().after(editHtml); - $('#ta_' + cid).limit(1000, '#charsLeft_' + cid).focus(); - }); - // Удаление ТЕКУЩЕГО изображения при редактировании - $doc.on('click', '.remove-existing-img', function() { - var cid = $(this).data('id'); - $('#existing_preview_wrapper_' + cid).addClass('d-none'); - $('#del_img_' + cid).prop('checked', true); // Помечаем скрытый чекбокс на удаление - }); +// КОНЕЦ $doc.off('click', '.mod_comment_edit') + + +$doc.on('click', '.remove-existing-img', function() { + var cid = $(this).data('id'); + var imgPath = $(this).data('img-path'); // Имя файла + var targetId = $(this).data('target'); + + // Добавляем имя файла в скрытый инпут для сервера + var $deleteInput = $('#delete_files_' + cid); + var currentDeleteList = $deleteInput.val() ? $deleteInput.val().split(',') : []; + + if (currentDeleteList.indexOf(imgPath) === -1) { + currentDeleteList.push(imgPath); + $deleteInput.val(currentDeleteList.join(',')); + } + + // Скрываем блок с картинкой + $('#' + targetId).fadeOut(300); +}); // Обработка выбора НОВОГО файла при редактировании $doc.on('change', 'input[id^="new_file_"]', function() { var cid = $(this).attr('id').replace('new_file_', ''); var input = this; - - // Создаем или находим блок для ошибки - if ($('#file_error_' + cid).length === 0) { - $(input).after('
'); - } var $errorBox = $('#file_error_' + cid); + var $previewWrapper = $('#edit_preview_wrapper_' + cid); - // Сбрасываем старые ошибки + // Очищаем превью перед новым выбором + $previewWrapper.find('.preview-item-new').remove(); $errorBox.addClass('d-none').text(''); - $(input).removeClass('is-invalid'); - if (input.files && input.files[0]) { - // 1. Проверяем файл функцией checkFileConsistency - if (!checkFileConsistency(input.files[0], $errorBox)) { - $(input).val('').addClass('is-invalid'); - $('#edit_preview_wrapper_' + cid).addClass('d-none'); - $errorBox.removeClass('d-none'); - return; - } + if (input.files && input.files.length > 0) { + $previewWrapper.removeClass('d-none'); + + Array.from(input.files).forEach(function(file) { + if (checkFileConsistency(file, $errorBox)) { + var reader = new FileReader(); + reader.onload = function(e) { + // Создаем новое превью для каждого файла + var item = ` +
+ +
`; + $previewWrapper.append(item); + } + reader.readAsDataURL(file); + } + }); - // 2. Если всё ОК — показываем превью - var reader = new FileReader(); - reader.onload = function(e) { - $('#edit_preview_img_' + cid).attr('src', e.target.result); - $('#edit_preview_wrapper_' + cid).removeClass('d-none'); - $('#existing_preview_wrapper_' + cid).addClass('d-none'); - $('#del_img_' + cid).prop('checked', true); - } - reader.readAsDataURL(input.files[0]); + // Прячем старое фото и помечаем на удаление + $('#existing_preview_wrapper_' + cid).addClass('d-none'); + $('#del_img_' + cid).prop('checked', true); } }); @@ -569,23 +628,28 @@ $doc.on('change', 'input[id^="new_file_"]', function() { $container.remove(); }); - $doc.on('click', '.saveButton', function() { +$doc.on('click', '.saveButton', function() { var $btn = $(this); var cid = $btn.data('id'); var fd = new FormData(); fd.append('module', 'comment'); fd.append('action', 'edit'); fd.append('Id', cid); fd.append('text', $('#ta_' + cid).val()); + var rInput = $('#rating_input_' + cid); if (rInput.length) fd.append('user_rating', rInput.val()); - fd.append('delete_image', $('#del_img_' + cid).is(':checked') ? 1 : 0); + fd.append('delete_files', $('#delete_files_' + cid).val()); + // --- ИЗМЕНЕННЫЙ БЛОК ДЛЯ MULTIPLE --- var fileInput = $('#new_file_' + cid); - if (fileInput.length) { - var file = fileInput[0].files[0]; - if (file) fd.append('comment_image', file); + if (fileInput.length && fileInput[0].files.length > 0) { + for (var i = 0; i < fileInput[0].files.length; i++) { + // Добавляем каждый файл в массив comment_image[] + fd.append('comment_image[]', fileInput[0].files[i]); + } } + // ------------------------------------ $.ajax({ url: aveabspath + 'index.php?ajax=1', @@ -606,7 +670,7 @@ $doc.on('change', 'input[id^="new_file_"]', function() { }); }); - // Отправка новой формы (или ответа) +// Отправка новой формы (или ответа) $doc.on('submit', '#mod_comment_new form', function(e) { e.preventDefault(); if (!validate(this)) return false; @@ -615,10 +679,24 @@ $doc.on('change', 'input[id^="new_file_"]', function() { var $btn = $form.find('[type="submit"]'); var originalBtnText = $btn.text(); + // --- ДОБАВЛЕНА ЛОГИКА ДЛЯ МНОЖЕСТВЕННЫХ ФАЙЛОВ --- + var formData = new FormData(this); + var fileInput = $form.find('#comment_image')[0]; + + if (fileInput && fileInput.files.length > 0) { + // Удаляем одиночное поле, чтобы заменить его массивом + formData.delete('comment_image'); + // Добавляем каждый файл в массив comment_image[] + for (var i = 0; i < fileInput.files.length; i++) { + formData.append('comment_image[]', fileInput.files[i]); + } + } + // ------------------------------------------------ + $.ajax({ url: aveabspath + 'index.php?ajax=1', type: 'POST', - data: new FormData(this), + data: formData, // Используем formData с поддержкой нескольких файлов processData: false, contentType: false, xhr: function() { diff --git a/templates/comments_tree.tpl b/templates/comments_tree.tpl index b69ad5e..3ccc6c4 100644 --- a/templates/comments_tree.tpl +++ b/templates/comments_tree.tpl @@ -145,7 +145,7 @@ {if $smarty.session.user_group != '2' || $comment_allow_files_anon == 1}
@@ -153,11 +153,12 @@ Макс. размер: {$comment_max_file_size|default:'2048'} Кб.
- + {* Ключевое изменение: name="comment_image[]" и атрибут multiple *} + -
- Предпросмотр - + {* Контейнер для нескольких превью *} +
+ {* Сюда JS будет добавлять превью *}
diff --git a/templates/comments_tree_sub.tpl b/templates/comments_tree_sub.tpl index f696c1f..25b7c80 100644 --- a/templates/comments_tree_sub.tpl +++ b/templates/comments_tree_sub.tpl @@ -54,12 +54,22 @@
{/if} - {* Вывод изображения *} +{* Вывод изображения (ИСПРАВЛЕНО ДЛЯ MULTIPLE) *} {if !empty($c.comment_file)} -
- - Изображение - +
+ {assign var="photos" value=","|explode:$c.comment_file} + {foreach from=$photos item=photo} + {if $photo|trim} +
+ + Изображение + +
+ {/if} + {/foreach}
{/if}