From 927f52fb60031c1dd5afe1f1543344399bd4af76 Mon Sep 17 00:00:00 2001 From: Repellent Date: Thu, 18 Dec 2025 09:51:12 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- class/comment.php | 221 ++++++++++++++++------------- js/comment.js | 237 +++++++++++++++++++------------- sql.php | 32 ++--- templates/admin_settings.tpl | 16 +++ templates/comments_tree.tpl | 78 +++++++---- templates/comments_tree_sub.tpl | 14 +- 6 files changed, 356 insertions(+), 242 deletions(-) diff --git a/class/comment.php b/class/comment.php index e1d7b48..c9cf524 100644 --- a/class/comment.php +++ b/class/comment.php @@ -338,51 +338,39 @@ function commentListShow($tpl_dir) { global $AVE_DB, $AVE_Template; - // Используем оператор объединения с null для PHP 8.4 $page = $_REQUEST['page'] ?? ''; $ajax = (isset($_REQUEST['ajax']) && $_REQUEST['ajax'] == 1); $user_group = UGROUP ?? 0; $secure_code = $_POST['securecode'] ?? ''; $session_captcha = $_SESSION['captcha_keystring'] ?? null; - // Получаем настройки модуля для проверки обязательных полей $settings = $this->_commentSettingsGet(); - // Если запрос пришел не ajax запросом, тогда формируем ссылку для последующего редиректа if (! $ajax) { $link = rewrite_link(base64_decode($page)); } - // --- ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ УНИВЕРСАЛЬНЫХ ПОЛЕЙ --- - // Проверка Поля 1 (хранится в website) + // --- ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ --- if ($settings['comment_show_f1'] == 1 && $settings['comment_req_f1'] == 1 && empty($_POST['comment_author_website'])) { if ($ajax) { echo 'error_req_f1'; exit; } else { header('Location:' . $link . '#end'); exit; } } - // Проверка Поля 2 (хранится в city) if ($settings['comment_show_f2'] == 1 && $settings['comment_req_f2'] == 1 && empty($_POST['comment_author_city'])) { if ($ajax) { echo 'error_req_f2'; exit; } else { header('Location:' . $link . '#end'); exit; } } - // ------------------------------------------------- - // Если в настройках модуля включено использование защитного кода, тогда + // --- АНТИСПАМ --- if ($settings['comment_use_antispam'] == 1) { - // Если введенный пользователем защитный код неверен, тогда выполняем обновление кода if (! (isset($session_captcha) && $session_captcha == $secure_code)) { unset($_SESSION['captcha_keystring']); - - if ($ajax) - { - echo 'wrong_securecode'; - } - else - { + if ($ajax) { echo 'wrong_securecode'; } + else { if(isset($GLOBALS['tmpl']))$GLOBALS['tmpl']->assign("wrongSecureCode", 1); header('Location:' . $link . '#end'); } @@ -391,47 +379,65 @@ function commentListShow($tpl_dir) unset($_SESSION['captcha_keystring']); } - // Определяем флаг модерации комментариев $comment_status = ($settings['comment_need_approve'] == 1) ? 0 : 1; - // Если комментарии разрешены, тогда получаем все данные, который пользователь указал в форме if ($settings['comment_active'] == 1 && !empty($_POST['comment_text']) && !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) + { + $upload_path = BASE_DIR . '/uploads/comments/'; + + // Создаем папку, если её нет + if (!is_dir($upload_path)) { + @mkdir($upload_path, 0775, true); + // Создаем пустой index.html для безопасности (запрет листинга папки) + @file_put_contents($upload_path . 'index.html', ''); + } + + $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 = ''; // Сбрасываем, если не удалось переместить + } + } + } + + // --- ПОДГОТОВКА ДАННЫХ ДЛЯ БД --- $new_comment['parent_id'] = (int)($_POST['parent_id'] ?? 0); $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['comment_author_email'] = addslashes(strip_tags($_POST['comment_author_email'] ?? '')); - - // Маппинг универсальных полей в существующие колонки БД - $new_comment['comment_author_city'] = addslashes(strip_tags($_POST['comment_author_city'] ?? '')); // Поле 2 - $new_comment['comment_author_website'] = addslashes(strip_tags($_POST['comment_author_website'] ?? '')); // Поле 1 - + $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; - - $comment_text_raw = $_POST['comment_text'] ?? ''; + $new_comment['comment_file'] = $comment_file_name; // НАША НОВАЯ КОЛОНКА + $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); - // --- !!! КОНЕЦ БЕЗОПАСНОСТИ !!! --- - - // Выполняем запрос к БД на добавление комментария + // Выполняем запрос $AVE_DB->Query(" INSERT INTO " . PREFIX . "_module_comment_info (`" . implode('`,`', array_keys($new_comment)) ."`) @@ -440,7 +446,7 @@ function commentListShow($tpl_dir) "); $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']; @@ -455,18 +461,9 @@ function commentListShow($tpl_dir) if ($ajax) { - if (isset($new_comment['comment_author_id']) && $new_comment['comment_author_id'] > 0) - { - $new_comment['avatar'] = getAvatar($new_comment['comment_author_id'], 48); - } - else - { - $new_comment['avatar'] = ''; - } + $new_comment['avatar'] = (isset($new_comment['comment_author_id']) && $new_comment['comment_author_id'] > 0) ? getAvatar($new_comment['comment_author_id'], 48) : ''; $new_comment['comment_changed'] = 0; $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; @@ -480,99 +477,111 @@ function commentListShow($tpl_dir) exit; } - /** +/** * Метод, предназначенный для редактирования комментария в Публичной части - * - * @param int $comment_id - идентификатор комментария + * с поддержкой удаления и замены изображения */ - function commentPostEdit($comment_id) +function commentPostEdit($comment_id) { global $AVE_DB; $user_id = $_SESSION['user_id'] ?? null; $user_group = UGROUP ?? 0; $post_text = $_POST['text'] ?? ''; - + + $delete_file = (isset($_POST['delete_image']) && $_POST['delete_image'] == 1); if (empty($user_id)) exit; $comment_id = intval(preg_replace('/\D/', '', $comment_id)); - // Выполняем запрос к БД и получаем всю информацию о комментарии, а также ряд значений из настроек модуля $row = $AVE_DB->Query(" SELECT msg.parent_id, msg.comment_text, + msg.comment_file, cmnt.comment_user_groups, cmnt.comment_max_chars, cmnt.comment_need_approve FROM " . PREFIX . "_module_comment_info AS msg, " . PREFIX . "_module_comments AS cmnt - WHERE comment_active = '1' + WHERE cmnt.comment_active = '1' AND msg.Id = '" . $comment_id . "' - " . (($user_group != 1) ? "AND comment_author_id = " . $user_id : '') . " + " . (($user_group != 1) ? "AND msg.comment_author_id = " . (int)$user_id : '') . " ")->FetchAssocArray(); - // Если данные получены if ($row !== false) { - - $comment_max_chars = ($row['comment_max_chars'] != '' && $row['comment_max_chars'] > 10) ? $row['comment_max_chars'] : 20; - + $comment_max_chars = ($row['comment_max_chars'] != '' && $row['comment_max_chars'] > 10) ? $row['comment_max_chars'] : 200; + + // --- ТВОЯ ОРИГИНАЛЬНАЯ ОБРАБОТКА ТЕКСТА --- $comment_text = $post_text; - - // --- !!! НАЧАЛО ИСПРАВЛЕНИЯ УСТАРЕВШИХ ОПЕРАТОРОВ (/e) !!! --- - - // Безопасная замена: преобразуем hex-сущности - $comment_text = preg_replace_callback('/&#x([0-9a-f]{1,7});/', function($matches) { - return chr(hexdec($matches[1])); - }, $comment_text); - - // Безопасная замена: преобразуем dec-сущности - $comment_text = preg_replace_callback('/&#([0-9]{1,7});/', function($matches) { - return chr($matches[1]); - }, $comment_text); - - // --- !!! КОНЕЦ ИСПРАВЛЕНИЯ УСТАРЕВШИХ ОПЕРАТОРОВ !!! --- - + $comment_text = preg_replace_callback('/&#x([0-9a-f]{1,7});/', function($matches) { return chr(hexdec($matches[1])); }, $comment_text); + $comment_text = preg_replace_callback('/&#([0-9]{1,7});/', function($matches) { return chr($matches[1]); }, $comment_text); $comment_text = stripslashes($comment_text); $comment_text = str_replace(array("
\n", "
\n", "
\n"), "\n", $comment_text); - - // Санитаризация: убираем теги перед обрезкой - $comment_text = strip_tags($comment_text); - - $comment_text = mb_substr($comment_text, 0, $comment_max_chars-1); - $message_length = mb_strlen($comment_text); - $comment_text .= ($message_length > $comment_max_chars) ? '…' : ''; + $comment_text = strip_tags($comment_text); + + $comment_text_cut = mb_substr($comment_text, 0, $comment_max_chars); + $message_length = mb_strlen($comment_text_cut); + if (mb_strlen($comment_text) > $comment_max_chars) $comment_text_cut .= '…'; - - // Если группа текущего пользователя совпадает с разрешенной группой в настройках модуля, тогда - // выполняем запрос к БД на обновление информации. - if (in_array($user_group, explode(',', $row['comment_user_groups'])) && $message_length > 3) + $is_admin = ($user_group == 1); + $is_allowed_group = in_array($user_group, explode(',', $row['comment_user_groups'])); + + if ($is_admin || ($is_allowed_group && $message_length > 3)) { + $upload_dir = BASE_DIR . '/uploads/comments/'; + $new_file_sql = ""; + + // 1. ЗАМЕНА ФАЙЛА + if (isset($_FILES['comment_image']) && $_FILES['comment_image']['error'] == UPLOAD_ERR_OK) { + if (!is_dir($upload_dir)) @mkdir($upload_dir, 0777, true); + + // Удаляем старый + if (!empty($row['comment_file'])) { + $old_file = $upload_dir . $row['comment_file']; + if (file_exists($old_file)) @unlink($old_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)) { + // Используем addslashes вместо Escape_String для надежности + $new_file_sql = ", comment_file = '" . addslashes($new_file_name) . "'"; + $delete_file = false; + } + } + // 2. УДАЛЕНИЕ ФАЙЛА + elseif ($delete_file && !empty($row['comment_file'])) { + $old_file = $upload_dir . $row['comment_file']; + if (file_exists($old_file)) @unlink($old_file); + $new_file_sql = ", comment_file = ''"; + } + + // ОБНОВЛЕНИЕ БАЗЫ $AVE_DB->Query(" UPDATE " . PREFIX . "_module_comment_info SET comment_changed = '" . time() . "', comment_status = '" . intval(!(bool)$row['comment_need_approve']) . "', - comment_text = '" . addslashes($comment_text) . "' + comment_text = '" . addslashes($comment_text_cut) . "' + $new_file_sql WHERE Id = '" . $comment_id . "' "); - // Преобразуем HTML теги в HTML сущности перед выводом (для XSS) - echo htmlspecialchars($comment_text, ENT_QUOTES); + echo htmlspecialchars($comment_text_cut, ENT_QUOTES); exit; } - - // Если редактирование не прошло, выводим оригинальный текст (экранированный) echo htmlspecialchars($row['comment_text'], ENT_QUOTES); } exit; } - /** +/** * Метод, предназначенный для удаления комментария. Если комментарий содержал какие-либо ответы на него, * то все ответы также будут удалены вместе с родительским комментарием. * @@ -582,7 +591,27 @@ function commentListShow($tpl_dir) { global $AVE_DB; - $comment_id = (int)$comment_id; // Убедимся, что это целое число + $comment_id = (int)$comment_id; + $upload_dir = BASE_DIR . '/uploads/comments/'; + + // --- 1. ОЧИСТКА ФАЙЛОВ ПЕРЕД УДАЛЕНИЕМ ИЗ БД --- + + // Ищем файлы основного комментария и всех вложенных ответов + $res = $AVE_DB->Query(" + SELECT comment_file + FROM " . PREFIX . "_module_comment_info + WHERE (Id = '" . $comment_id . "' OR parent_id = '" . $comment_id . "') + AND comment_file != '' + "); + + while ($row = $res->FetchAssocArray()) { + $file_path = $upload_dir . $row['comment_file']; + if (file_exists($file_path)) { + @unlink($file_path); // Удаляем файл с сервера + } + } + + // --- 2. ТВОЙ ОРИГИНАЛЬНЫЙ КОД УДАЛЕНИЯ ИЗ БД --- // Выполняем запрос к БД на удаление родительского комментария $AVE_DB->Query(" @@ -884,6 +913,7 @@ function commentListShow($tpl_dir) $AVE_Template->assign('content', $AVE_Template->fetch($tpl_dir . $this->_admin_edit_link_tpl)); } +/** /** /** * Метод, предназначенный для управления настройками модуля @@ -894,7 +924,6 @@ function commentListShow($tpl_dir) { global $AVE_DB, $AVE_Template; - // Используем оператор объединения с null для PHP 8.4 $request_sub = $_REQUEST['sub'] ?? ''; $post_max_chars = $_POST['comment_max_chars'] ?? 0; $post_user_groups = $_POST['comment_user_groups'] ?? array(); @@ -907,7 +936,10 @@ function commentListShow($tpl_dir) $post_use_page_nav = $_POST['comment_use_page_nav'] ?? 0; $post_page_nav_count = $_POST['comment_page_nav_count'] ?? 0; - // НОВОЕ: Получение данных для универсальных полей + // НОВОЕ: Настройка разрешения загрузки файлов + $post_allow_files = $_POST['comment_allow_files'] ?? 0; + + // Данные для универсальных полей $post_show_f1 = $_POST['comment_show_f1'] ?? 0; $post_req_f1 = $_POST['comment_req_f1'] ?? 0; $post_name_f1 = $_POST['comment_name_f1'] ?? ''; @@ -916,12 +948,10 @@ function commentListShow($tpl_dir) $post_req_f2 = $_POST['comment_req_f2'] ?? 0; $post_name_f2 = $_POST['comment_name_f2'] ?? ''; - // Если в запросе содержится подзапрос на сохранение данных if ($request_sub == 'save') { $max_chars = (empty($post_max_chars) || $post_max_chars < 50) ? 50 : $post_max_chars; - // Очищаем названия полей вручную, так как EscapeString не поддерживается $clean_name_f1 = htmlspecialchars(stripslashes($post_name_f1), ENT_QUOTES); $clean_name_f2 = htmlspecialchars(stripslashes($post_name_f2), ENT_QUOTES); @@ -936,6 +966,7 @@ function commentListShow($tpl_dir) comment_use_antispam = '" . (int)$post_use_antispam . "', comment_use_page_nav = '" . (int)$post_use_page_nav . "', comment_page_nav_count = '" . (int)$post_page_nav_count . "', + comment_allow_files = '" . (int)$post_allow_files . "', comment_show_f1 = '" . (int)$post_show_f1 . "', comment_req_f1 = '" . (int)$post_req_f1 . "', comment_name_f1 = '" . $clean_name_f1 . "', @@ -947,15 +978,11 @@ function commentListShow($tpl_dir) "); } - // Получаем список всех настроек модуля $row = $this->_commentSettingsGet(); - // Преобразуем поля с правами в массивы для удобства отображения в шаблоне $row['comment_user_groups'] = explode(',', $row['comment_user_groups']); $row['comment_user_groups_read'] = explode(',', $row['comment_user_groups_read']); - - // Передаем данные в шаблон $AVE_Template->assign($row); $AVE_Template->assign('content', $AVE_Template->fetch($tpl_dir . $this->_admin_settings_tpl)); } diff --git a/js/comment.js b/js/comment.js index 0386803..6130a73 100644 --- a/js/comment.js +++ b/js/comment.js @@ -3,22 +3,13 @@ ==================================================================== */ (function waitForJQuery() { if (typeof jQuery === 'undefined') { - // jQuery еще не загружен. Ждем 10 мс и проверяем снова. setTimeout(waitForJQuery, 10); } else { - // JQuery загружен, можно запускать основной код. - (function($){ - // ==================================================================== - // КОРРЕКЦИЯ: ПРИНУДИТЕЛЬНАЯ ФИКСАЦИЯ HTTPS (если требуется) - // ==================================================================== if (typeof aveabspath !== 'undefined') { - // ПРЕДУПРЕЖДЕНИЕ: Если aveabspath должен быть динамическим, - // удалите эту строку. // aveabspath = 'https://bag.local/'; } - // ==================================================================== /* Limit symbols */ (function($){ @@ -65,15 +56,12 @@ function cAction(obj, action){ var cid; - // --- Логика для действий с документом (open/close) --- if (action === 'open' || action === 'close') { cid = DOC_ID; } else { - // 1. Пытаемся найти ссылку и взять ID из атрибута rel var $link = $(obj).closest('a.mod_comment_answer'); cid = $link.attr('rel'); - // 2. Если rel не найден, берем ID из родительского блока if (typeof cid === 'undefined' || cid === false || cid === '') { cid = $(obj).parents('.mod_comment_box').attr('id'); } @@ -86,12 +74,10 @@ if (action === 'answer'){ $('#parent_id').val(cid); - // Перемещаем форму и показываем ее $('#mod_comment_new').insertBefore('#end' + cid).show(); return; } - // Действия администратора if (UGROUP == 1){ $.get(aveabspath + 'index.php', { module: 'comment', @@ -100,27 +86,21 @@ Id: cid }, function(){ if (action === 'delete'){ - // Удаляем блок комментария $(obj).parents('.mod_comment_comment').eq(0).remove(); } if (action === 'open'){ - // 1. Меняем ID и текст var $openButton = $('#mod_comment_open'); $openButton.attr('id', 'mod_comment_close').html(' ' + COMMENT_SITE_CLOSE); - // 2. МЕНЯЕМ КЛАССЫ СТИЛЕЙ $openButton.removeClass('btn-outline-success').addClass('btn-outline-danger'); } if (action === 'close'){ - // 1. Меняем ID и текст var $closeButton = $('#mod_comment_close'); $closeButton.attr('id', 'mod_comment_open').html(' ' + COMMENT_SITE_OPEN); - // 2. МЕНЯЕМ КЛАССЫ СТИЛЕЙ $closeButton.removeClass('btn-outline-danger').addClass('btn-outline-success'); } - // Динамическая смена иконки замка для отдельного комментария if (action === 'unlock'){ $(obj).removeClass('mod_comment_unlock text-success') .addClass('mod_comment_lock text-dark') @@ -139,27 +119,22 @@ } } -function validate(formData, jqForm, options){ + function validate(formData, jqForm, options){ $('.alert').remove(); var form = jqForm ? jqForm[0] : $('#mod_comment_new form')[0]; - // Проверка имени if (form.comment_author_name && !form.comment_author_name.value){ alert(COMMENT_ERROR_AUTHOR); $(form.comment_author_name).focus(); return false; } - // Проверка email if (form.comment_author_email && !form.comment_author_email.value){ alert(COMMENT_ERROR_EMAIL); $(form.comment_author_email).focus(); return false; } - // --- НОВОЕ: Проверка динамических полей --- - - // Проверка Поля №1 (website) if (typeof REQ_F1 !== 'undefined' && REQ_F1 == '1') { if (form.comment_author_website && !form.comment_author_website.value) { alert("Пожалуйста, заполните поле: " + NAME_F1); @@ -168,7 +143,6 @@ function validate(formData, jqForm, options){ } } - // Проверка Поля №2 (city) if (typeof REQ_F2 !== 'undefined' && REQ_F2 == '1') { if (form.comment_author_city && !form.comment_author_city.value) { alert("Пожалуйста, заполните поле: " + NAME_F2); @@ -176,16 +150,13 @@ function validate(formData, jqForm, options){ return false; } } - // ------------------------------------------ - // Проверка текста if (!form.comment_text || !form.comment_text.value){ alert(COMMENT_ERROR_TEXT); if (form.comment_text) $(form.comment_text).focus(); return false; } - // Проверка капчи (если требуется) if (IS_IM && form.securecode && !form.securecode.value){ alert(COMMENT_ERROR_CAPTCHA); $(form.securecode).focus(); @@ -195,35 +166,60 @@ function validate(formData, jqForm, options){ return true; } + // Функция для создания формы редактирования + function createEditForm(cid, revert, targetElement) { + var currentImg = $('#' + cid).find('.mod_comment_attached_image img').attr('src'); + var deletePhotoHtml = ''; + + if (currentImg) { + deletePhotoHtml = '
' + + '' + + '' + + '
'; + } + + // Блок для превью нового фото (добавлен класс edit-preview-img) + var editPreviewHtml = '
' + + '' + + '
'; + + // Поле для загрузки НОВОГО фото (добавлен класс edit-file-input и data-cid) + var uploadNewPhotoHtml = '
' + + '' + + '' + + editPreviewHtml + + '
'; + + var textarea = '

'; + var charsLeft = '

' + COMMENT_CHARS_LEFT + '

'; + var buttonSave = ' '; + var buttonReset = ''; + + targetElement.after( + '
' + COMMENT_EDIT_TITLE + + '' + textarea + deletePhotoHtml + uploadNewPhotoHtml + charsLeft + buttonSave + buttonReset + '
' + ).remove(); + + $('.saveButton').off('click').on('click', function(){saveChanges(this, false, cid);}); + $('.cancelButton').off('click').on('click', function(){saveChanges(this, revert, cid);}); + + $('#ta_' + cid).limit(MAX_CHARS, '#charsLeft_' + cid); + } + function setClickable(){ - // Отвязываем предыдущие обработчики, чтобы избежать множественных вызовов $('.editable_text').off('click'); $('.editable_text').click(function(){ var $this = $(this); var cid = $this.parents('.mod_comment_box').attr('id'); var revert = $this.html(); - - var textarea = '

'; - var charsLeft = '

' + COMMENT_CHARS_LEFT + '

'; - var buttonSave = ' '; - var buttonReset = ''; - - $this.after( - '
' + COMMENT_EDIT_TITLE - + '' + textarea + charsLeft + buttonSave + buttonReset + '
' - ).remove(); - - $('.saveButton').click(function(){saveChanges(this, false, cid);}); - $('.cancelButton').click(function(){saveChanges(this, revert, cid);}); - - $('#ta_' + cid).limit(MAX_CHARS, '#charsLeft_' + cid); + createEditForm(cid, revert, $this); }) .attr('title', COMMENT_EDIT_LINK) .mouseover(function(){$(this).addClass('editable');}) .mouseout(function(){$(this).removeClass('editable');}); - // Лимит символов для новой формы $('#in_message').limit(MAX_CHARS, '#charsLeft_new'); } @@ -232,26 +228,42 @@ function validate(formData, jqForm, options){ if (!cancel){ var t = $box.find('#ta_' + cid).val(); + var deleteImg = $('#del_img_' + cid).is(':checked') ? 1 : 0; + + var fd = new FormData(); + fd.append('module', 'comment'); + fd.append('action', 'edit'); + fd.append('Id', cid); + fd.append('text', t); + fd.append('delete_image', deleteImg); + + var fileInput = $('#new_file_' + cid)[0]; + if (fileInput && fileInput.files[0]) { + // Ключ 'comment_image' должен совпадать с PHP $_FILES['comment_image'] + fd.append('comment_image', fileInput.files[0]); + } $.ajax({ url: aveabspath + 'index.php?ajax=1', type: 'POST', - data: { - module: 'comment', - action: 'edit', - Id: cid, - text: t - }, + data: fd, + processData: false, + contentType: false, success: function(txt){ - $box.after('
' + txt + '
').remove(); - var now = new Date(); - var date = now.toLocaleString(); - $('#' + cid).find('.mod_comment_changed').html(' (' + COMMENT_TEXT_CHANGED + ' ' + date + ')'); - setClickable(); + // Если были операции с файлом, перезагружаем страницу + if (deleteImg == 1 || (fileInput && fileInput.files[0])) { + location.reload(); + } else { + $box.after('
' + txt + '
').remove(); + var now = new Date(); + var date = now.toLocaleString(); + $('#' + cid).find('.mod_comment_changed').html(' (' + COMMENT_TEXT_CHANGED + ' ' + date + ')'); + setClickable(); + } }, error: function(xhr, status, error) { console.error("AJAX Error (Edit):", status, error); - alert("Ошибка при редактировании комментария."); + alert("Ошибка при сохранении."); } }); } @@ -270,7 +282,9 @@ function validate(formData, jqForm, options){ $('#end' + $('#parent_id').val()).before(data); $('#parent_id').val(''); $('#mod_comment_new form')[0].reset(); - // Перемещаем форму обратно в конец + $('#image_preview_wrapper').addClass('d-none'); + $('#image_preview_img').attr('src', '#'); + $('#mod_comment_new').insertAfter('#end'); setClickable(); } @@ -281,109 +295,134 @@ function validate(formData, jqForm, options){ setClickable(); - // Смена капчи + // ЛОГИКА ПРЕДПРОСМОТРА ДЛЯ ФОРМ РЕДАКТИРОВАНИЯ (Делегирование) + $(document).on('change', '.edit-file-input', function() { + var cid = $(this).data('cid'); + var file = this.files[0]; + var previewWrapper = $('#edit_preview_wrapper_' + cid); + var previewImg = $('#edit_preview_img_' + cid); + + if (file) { + var reader = new FileReader(); + reader.onload = function(e) { + previewImg.attr('src', e.target.result); + previewWrapper.removeClass('d-none'); + } + reader.readAsDataURL(file); + } else { + previewWrapper.addClass('d-none'); + } + }); + + // ЛОГИКА ПРЕДПРОСМОТРА ДЛЯ НОВОГО КОММЕНТАРИЯ + $(document).on('change', '#comment_image', function() { + const file = this.files[0]; + const previewWrapper = $('#image_preview_wrapper'); + const previewImg = $('#image_preview_img'); + const errorDiv = $('#file_error'); + + if (errorDiv.length) errorDiv.addClass('d-none').text(''); + + if (file) { + if (file.size > 2 * 1024 * 1024) { + if (errorDiv.length) errorDiv.removeClass('d-none').text('Файл слишком большой (макс. 2Мб)'); + this.value = ''; + previewWrapper.addClass('d-none'); + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + previewImg.attr('src', e.target.result); + previewWrapper.removeClass('d-none'); + } + reader.readAsDataURL(file); + } + }); + + $(document).on('click', '#remove_image_btn', function() { + $('#comment_image').val(''); + $('#image_preview_wrapper').addClass('d-none'); + $('#image_preview_img').attr('src', '#'); + }); + $(document).on('click', '#captcha img', function(){getCaptha();}); - // Ответ на комментарий + $(document).on('click', '#reload_captcha', function(e){ + e.preventDefault(); + getCaptha(); + }); + $(document).on('click', '.mod_comment_answer', function(e){ e.preventDefault(); cAction(this, 'answer'); }); - // Редактирование (для администратора/автора) $(document).on('click', '.mod_comment_edit', function(e){ e.preventDefault(); var cid = $(this).parents('.mod_comment_box').attr('id'); var commentTextBlock = $('#' + cid).find('.mod_comment_text'); var revert = commentTextBlock.html(); - - var textarea = '

'; - var charsLeft = '

' + COMMENT_CHARS_LEFT + '

'; - var buttonSave = ' '; - var buttonReset = ''; - - commentTextBlock.after( - '
' + COMMENT_EDIT_TITLE - + '' + textarea + charsLeft + buttonSave + buttonReset + '
' - ).remove(); - - $('.saveButton').click(function(){saveChanges(this, false, cid);}); - $('.cancelButton').click(function(){saveChanges(this, revert, cid);}); - $('#ta_' + cid).limit(MAX_CHARS, '#charsLeft_' + cid); + createEditForm(cid, revert, commentTextBlock); }); if (UGROUP == 1) { - // Удаление (с использованием Bootstrap Modal) $(document).on('click', '.mod_comment_delete', function(e){ e.preventDefault(); var $deleteButton = $(this); var $modal = $('#deleteCommentModal'); - // Привязываем действие к кнопке подтверждения в модальном окне $('#confirmDeleteButton').off('click').on('click', function() { cAction($deleteButton[0], 'delete'); - // Закрываем модальное окно (совместимость с BS5) var modalInstance = bootstrap.Modal.getInstance($modal[0]) || new bootstrap.Modal($modal[0]); modalInstance.hide(); }); - // Открываем модальное окно (совместимость с BS5) var modal = new bootstrap.Modal($modal[0]); modal.show(); }); - // Блокировка/разблокировка $(document).on('click', '.mod_comment_lock', function(e){e.preventDefault(); cAction(this, 'lock');}); $(document).on('click', '.mod_comment_unlock', function(e){e.preventDefault(); cAction(this, 'unlock');}); - // Открыть/Закрыть комментарии для документа $(document).on('click', '#mod_comment_open', function(e){e.preventDefault(); cAction(this, 'open');}); $(document).on('click', '#mod_comment_close', function(e){e.preventDefault(); cAction(this, 'close');}); } - // Отправка формы (AJAX) $('#mod_comment_new form').on('submit', function(e){ e.preventDefault(); - var form = $(this); - if (!validate(null, form, null)) { - return false; - } + if (!validate(null, form, null)) return false; var submitButton = form.find('input[type="submit"], button[type="submit"]').first(); var originalButtonText = submitButton.text(); - if (!originalButtonText) originalButtonText = submitButton.val(); - - var formData = form.serialize(); + var formData = new FormData(this); $.ajax({ url: aveabspath + 'index.php?ajax=1', type: 'POST', data: formData, - dataType: 'html', - + processData: false, + contentType: false, beforeSend: function() { submitButton.prop('disabled', true).text('Отправка...'); }, - success: function(data) { displayNewComment(data); submitButton.prop('disabled', false).text(originalButtonText); }, - timeout: 3000, - error: function(xhr, status, error) { - console.error("AJAX Error (Creation):", status, error); - alert("Ошибка при отправке комментария."); + error: function() { + alert("Ошибка при отправке."); submitButton.prop('disabled', false).text(originalButtonText); } }); return false; }); - // Сброс формы $('#buttonReset').click(function(){ $('#parent_id').val(''); $('#mod_comment_new form')[0].reset(); + $('#image_preview_wrapper').addClass('d-none'); $('#mod_comment_new').insertAfter('#end'); }); }); diff --git a/sql.php b/sql.php index 06653e4..2721eb1 100644 --- a/sql.php +++ b/sql.php @@ -3,7 +3,7 @@ /** * AVE.cms - Модуль Комментарии * - * Обновленная структура с поддержкой произвольных полей + * Обновленная структура с поддержкой произвольных полей и загрузки изображений */ $module_sql_install = array(); @@ -26,13 +26,16 @@ $module_sql_install[] = "CREATE TABLE `%%PRFX%%_module_comments` ( `comment_use_antispam` enum('1','0') NOT NULL default '1', `comment_use_page_nav` enum('1','0') NOT NULL default '1', `comment_page_nav_count` varchar(5) NOT NULL, - /* НОВЫЕ УНИВЕРСАЛЬНЫЕ ПОЛЯ НАСТРОЕК */ + /* УНИВЕРСАЛЬНЫЕ ПОЛЯ */ `comment_show_f1` tinyint(1) NOT NULL default '1', `comment_req_f1` tinyint(1) NOT NULL default '0', `comment_name_f1` varchar(255) NOT NULL default '', `comment_show_f2` tinyint(1) NOT NULL default '1', `comment_req_f2` tinyint(1) NOT NULL default '0', `comment_name_f2` varchar(255) NOT NULL default '', + /* ПОЛЯ ДЛЯ ЗАГРУЗКИ ФАЙЛОВ */ + `comment_allow_files` tinyint(1) NOT NULL default '0', + `comment_file_max_size` int(10) NOT NULL default '2048', PRIMARY KEY (`Id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;"; @@ -43,22 +46,24 @@ $module_sql_install[] = "CREATE TABLE `%%PRFX%%_module_comment_info` ( `comment_author_name` varchar(255) NOT NULL, `comment_author_id` int(10) unsigned NOT NULL default '0', `comment_author_email` varchar(255) NOT NULL, - `comment_author_city` varchar(255) NOT NULL, /* Маппим Field 2 сюда */ - `comment_author_website` varchar(255) NOT NULL, /* Маппим Field 1 сюда */ + `comment_author_city` varchar(255) NOT NULL, + `comment_author_website` varchar(255) NOT NULL, `comment_author_ip` varchar(15) NOT NULL, `comment_published` int(10) unsigned NOT NULL default '0', `comment_changed` int(10) unsigned NOT NULL default '0', `comment_text` text NOT NULL, `comment_status` tinyint(1) unsigned NOT NULL default '1', `comments_close` tinyint(1) unsigned NOT NULL default '0', + /* ПУТЬ К ПРИКРЕПЛЕННОМУ ФАЙЛУ */ + `comment_file` varchar(255) NOT NULL default '', PRIMARY KEY (`Id`), KEY `document_id` (`document_id`), KEY `parent_id` (`parent_id`), KEY `comment_status` (`comment_status`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 PACK_KEYS=0;"; -// Начальные данные (добавлены дефолтные значения для новых полей в конце строки) -$module_sql_install[] = "INSERT INTO `%%PRFX%%_module_comments` VALUES (1, 1000, '1,3', '1,2,3,4', '0', '1', '1' , '0', '', 1, 0, '', 1, 0, '');"; +// Начальные данные (Добавлено: 0 для allow_files и 2048 для размера в конце) +$module_sql_install[] = "INSERT INTO `%%PRFX%%_module_comments` VALUES (1, 1000, '1,3', '1,2,3,4', '0', '1', '1' , '0', '', 1, 0, '', 1, 0, '', 0, 2048);"; // ================================================================================= // 2. ОБНОВЛЕНИЕ МОДУЛЯ (ALTER TABLE) @@ -74,16 +79,11 @@ $module_sql_update[] = " LIMIT 1; "; -// Добавление новых столбцов управления полями -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_show_f1` TINYINT(1) NOT NULL DEFAULT '1';"; -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_req_f1` TINYINT(1) NOT NULL DEFAULT '0';"; -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_name_f1` VARCHAR(255) NOT NULL DEFAULT '';"; +// Добавление поля для файла в основную таблицу (если модуль уже установлен) +$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comment_info` ADD `comment_file` VARCHAR(255) NOT NULL DEFAULT '' AFTER `comments_close`;"; -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_show_f2` TINYINT(1) NOT NULL DEFAULT '1';"; -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_req_f2` TINYINT(1) NOT NULL DEFAULT '0';"; -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_name_f2` VARCHAR(255) NOT NULL DEFAULT '';"; - -$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_user_groups_read` TEXT NOT NULL AFTER `comment_user_groups`;"; -$module_sql_update[] = "UPDATE `%%PRFX%%_module_comments` SET `comment_user_groups_read` = '1,2,3,4' WHERE `Id` = 1;"; +// Добавление настроек загрузки (если модуль уже установлен) +$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_allow_files` TINYINT(1) NOT NULL DEFAULT '0';"; +$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_file_max_size` INT(10) NOT NULL DEFAULT '2048';"; ?> \ No newline at end of file diff --git a/templates/admin_settings.tpl b/templates/admin_settings.tpl index 47438ca..bf6c798 100644 --- a/templates/admin_settings.tpl +++ b/templates/admin_settings.tpl @@ -111,6 +111,22 @@ + + + Настройки медиафайлов + + + Разрешить загрузку фото: + + + (позволяет прикреплять изображения к комментариям) + + Папка для хранения: + + /uploads/comments/ + + + diff --git a/templates/comments_tree.tpl b/templates/comments_tree.tpl index 940f87c..e2d3d40 100644 --- a/templates/comments_tree.tpl +++ b/templates/comments_tree.tpl @@ -39,7 +39,7 @@ {if isset($comments[0])} -
+
{include file="$subtpl" subcomments=$comments[0]}
{/if} @@ -61,14 +61,14 @@
-
+
Форма добавления комментария {* Блок для имени и email *} {if $smarty.session.user_group != '2'} - {* АВТОРИЗОВАННЫЙ ПОЛЬЗОВАТЕЛЬ: Отображаем поля как readonly *} + {* АВТОРИЗОВАННЫЙ ПОЛЬЗОВАТЕЛЬ *}
@@ -79,19 +79,16 @@
- {* Скрытые поля для отправки *} {else} - {* ГОСТЬ (требуется ввод имени) *} + {* ГОСТЬ *}
- - {* ГОСТЬ (email) *}
@@ -101,7 +98,6 @@ {* ДИНАМИЧЕСКИЕ ПОЛЯ (Field 1 и Field 2) *}
- {* Поле №1 *} {if $comment_show_f1 == 1}
{/if} - {* Поле №2 *} {if $comment_show_f2 == 1}