diff --git a/ReadMe.txt b/ReadMe.txt
new file mode 100644
index 0000000..ed300e2
--- /dev/null
+++ b/ReadMe.txt
@@ -0,0 +1,49 @@
+Шаг 1: Назначение перемнных в шаблоне
+
+
+
+Шаг 2: Нужен плагин для jQuery - jquery.form
+http://malsup.com/jquery/form/
+
+
+
+/* ====================================================================
+ СТИЛИ ДЛЯ ДРЕВОВИДНОГО ОТОБРАЖЕНИЯ КОММЕНТАРИЕВ (Replies)
+ ==================================================================== */
+
+/* 1. Общий контейнер ответа, который находится внутри .mod_comment_comment
+ и имеет отступ ms-4 */
+.mod_comment_comment .mod_comment_comment.ms-4 {
+ /* Добавляем вертикальную линию слева */
+ border-left: 3px solid #dee2e6; /* Легкая граница */
+
+ /* Уменьшаем margin-left, который дает ms-4, чтобы освободить место
+ для линии и при этом не уходить слишком далеко вправо на каждом уровне. */
+ margin-left: 20px !important; /* Принудительно уменьшаем отступ */
+ padding-left: 15px; /* Отступ между линией и контентом ответа */
+
+ /* Опционально: немного отличающийся фон */
+ background-color: #f8f9fa;
+}
+
+/* 2. Отдельно стилизуем рамку для вложенных карточек, чтобы они не выглядели
+ громоздко внутри родителя */
+.mod_comment_comment .mod_comment_comment.ms-4 > .mod_comment_box {
+ border: none !important; /* Убираем стандартную рамку карточки, чтобы осталась только наша вертикальная линия */
+}
+
+/* 3. Убираем рамки у вложенных заголовков, чтобы линия была чистой */
+.mod_comment_comment .mod_comment_comment.ms-4 > .mod_comment_box > .card-header {
+ border-bottom: none !important;
+}
+
+/* 4. Более глубокий уровень вложенности (если ответов много) */
+/* Если ответ на ответ имеет дополнительный ms-4, стилизуем его дальше */
+.mod_comment_comment .mod_comment_comment.ms-4 .mod_comment_comment.ms-4 {
+ border-color: #adb5bd; /* Чуть темнее линия для следующего уровня */
+ background-color: #fff; /* Возвращаем белый фон для контраста */
+}
+
diff --git a/class/comment.php b/class/comment.php
new file mode 100644
index 0000000..ec1ddd4
--- /dev/null
+++ b/class/comment.php
@@ -0,0 +1,954 @@
+Query("
+ SELECT *
+ FROM " . PREFIX . "_module_comments
+ WHERE Id = '" . $this->_config_id . "'
+ ")->FetchAssocArray();
+ }
+
+ if ($param == '')
+ return $settings;
+
+ // В противном случае возвращаем уже имеющиеся значения
+ return (isset($settings[$param])
+ ? $settings[$param]
+ : null);
+ }
+
+ /**
+ * Метод, предназначенный для получения количества комментариев для определенного документа.
+ *
+ * @param int $document_id - идентификатор документа
+ * @return int - количество комментариев
+ */
+ function _commentPostCountGet($document_id)
+ {
+ global $AVE_DB;
+
+ // Определяем статический массив, который будет хранить количество комментариев для документов на
+ // протяжении всего срока жизни объекта.
+ static $comments = array();
+
+ // Если в массиве не найден ключ, который соответствует запрашиваемому документу, тогда выполняем
+ // запрос к БД на получение количества комментариев
+ if (! isset($comments[$document_id]))
+ {
+ $comments[$document_id] = $AVE_DB->Query("
+ SELECT COUNT(*)
+ FROM " . PREFIX . "_module_comment_info
+ WHERE document_id = '" . (int)$document_id . "'
+ ")->GetCell();
+ }
+
+ // Возвращаем количество комментариев для запрашиваемого документа
+ return $comments[$document_id];
+ }
+
+/**
+ * Внешние методы класса
+ */
+
+ /**
+ * Следующие методы описывают работу модуля в Публичной части сайта.
+ */
+
+ /**
+ * Метод, предназначенный для получения из БД всех комментариев, относящихся к указанному
+ * документу с последующим выводом в Публичной части.
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ *
+ * @todo Вывод информации о авторе комментария
+ */
+ function commentListShow($tpl_dir)
+ {
+ global $AVE_DB, $AVE_Template, $AVE_Core;
+
+ // Используем оператор объединения с null для PHP 8.4
+ $document_id = (int)($_REQUEST['id'] ?? 0);
+ $artpage = $_REQUEST['artpage'] ?? null;
+ $apage = $_REQUEST['apage'] ?? null;
+ $user_group = UGROUP ?? 0;
+
+
+ // Проверяем, что в настройках модуля разрешено комментирование документов
+ if ($this->_commentSettingsGet('comment_active') == 1)
+ {
+ // =================================================================================
+ // НОВОЕ: ПРОВЕРКА ПРАВ НА ПРОСМОТР КОММЕНТАРИЕВ (Read Permission)
+ // =================================================================================
+ $read_groups = explode(',', $this->_commentSettingsGet('comment_user_groups_read'));
+
+ $assign['no_read_permission'] = 0; // Флаг: 0 = права есть
+
+ // Если группа текущего пользователя НЕ в списке разрешенных для ЧТЕНИЯ,
+ if (!in_array($user_group, $read_groups))
+ {
+ // Устанавливаем флаг, что прав на чтение нет
+ $assign['no_read_permission'] = 1;
+ }
+ // =================================================================================
+
+ $assign['display_comments'] = 1;
+
+ // Если группа пользователя, который в текущий момент просматривает документ попадает в список
+ // разрешенных (в настройках модуля), тогда создаем флаг, который будет разрешать к показу
+ // форму для добавления нового комментария
+ if (in_array($user_group, explode(',', $this->_commentSettingsGet('comment_user_groups'))))
+ {
+ $assign['cancomment'] = 1;
+ }
+
+ // ЕСЛИ ЕСТЬ ПРАВА НА ЧТЕНИЕ, ПРОДОЛЖАЕМ ВЫБОРКУ И ВЫВОД
+ if ($assign['no_read_permission'] == 0)
+ {
+ $assign['comment_max_chars'] = $this->_commentSettingsGet('comment_max_chars');
+ $assign['im'] = $this->_commentSettingsGet('comment_use_antispam');
+
+ // Выполняем запрос к БД на получение количества комментариев для текущего документа
+ $comments = array();
+
+ if ($this->_commentSettingsGet('comment_use_page_nav') == 1)
+ {
+ $limit = $this->_commentSettingsGet('comment_page_nav_count');
+ $start = get_current_page() * $limit - $limit;
+
+ $num = $AVE_DB->Query("
+ SELECT COUNT(*)
+ FROM " . PREFIX . "_module_comment_info
+ WHERE document_id = '" . $document_id . "'
+ ")->GetCell();
+
+ $sql = $AVE_DB->Query("
+ SELECT *
+ FROM " . PREFIX . "_module_comment_info
+ WHERE document_id = '" . $document_id . "'
+ " . ($user_group == 1 ? '' : "AND comment_status = '1'") . "
+ ORDER BY comment_published ASC
+ LIMIT " . $start . "," . $limit . "
+ ");
+
+ $pages = @ceil($num / $limit);
+
+ if ($num > $limit)
+ {
+ $page_nav = '{t} ';
+ $page_nav = get_pagination(ceil($num / $limit), 'page', $page_nav, get_settings('navi_box'));
+ $page_nav = rewrite_link($page_nav);
+ $GLOBALS['page_id'][$document_id]['page']=($GLOBALS['page_id'][$document_id]['page']>ceil($num / $limit) ? $GLOBALS['page_id'][$document_id]['page'] : ceil($num / $limit));
+ }
+ else
+ {
+ $page_nav = '';
+ }
+
+ }
+ else
+ {
+ $sql = $AVE_DB->Query("
+ SELECT *
+ FROM " . PREFIX . "_module_comment_info
+ WHERE document_id = '" . $document_id . "'
+ " . ($user_group == 1 ? '' : "AND comment_status = '1'") . "
+ ORDER BY comment_published ASC
+ ");
+
+ $page_nav = '';
+ }
+
+ // Получаем формат даты, который указан в общих настройках системы и
+ // приводим дату создания комментария и дату редактирования к этому формату
+ $date_time_format = $AVE_Template->get_config_vars('COMMENT_DATE_TIME_FORMAT');
+ while ($row = $sql->FetchAssocArray())
+ {
+ $row['comment_published'] = ave_date_format($date_time_format, $row['comment_published']);
+ $row['comment_changed'] = ave_date_format($date_time_format, $row['comment_changed']);
+
+ $comments[$row['parent_id']][] = $row;
+ }
+
+ }
+ else
+ {
+ // Если нет прав, то эти переменные остаются пустыми/неопределенными
+ $comments = array();
+ $page_nav = '';
+ }
+
+
+ // Формируем ряд переменных для использования в шаблоне
+ $assign['closed'] = @$comments[0][0]['comments_close'];
+ $assign['comments'] = $comments;
+ $assign['theme'] = defined('THEME_FOLDER') ? THEME_FOLDER : DEFAULT_THEME_FOLDER;
+ $assign['doc_id'] = $document_id;
+ $assign['page'] = base64_encode(get_redirect_link());
+ $assign['subtpl'] = $tpl_dir . $this->_comments_tree_sub_tpl;
+
+ $AVE_Template->assign($assign);
+
+ $AVE_Template->assign('page_nav', $page_nav);
+
+ // Отображаем шаблон
+ $AVE_Template->display($tpl_dir . $this->_comments_tree_tpl);
+ }
+ }
+
+ /**
+ * Метод, предназначенный для отображения формы при добавлении нового комментария.
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ */
+ function commentPostFormShow($tpl_dir)
+ {
+ global $AVE_DB, $AVE_Template;
+
+ // Используем оператор объединения с null для PHP 8.4
+ $docid = (int)($_REQUEST['docid'] ?? 0);
+ $user_group = UGROUP ?? 0;
+
+ // Получаем список комментариев на которые запрещены ответы
+ $geschlossen = $AVE_DB->Query("
+ SELECT comments_close
+ FROM " . PREFIX . "_module_comment_info
+ WHERE document_id = '" . $docid . "'
+ LIMIT 1
+ ")->GetCell();
+
+ // Формируем ряд переменных для использования в шаблоне
+ $AVE_Template->assign('closed', $geschlossen);
+ $AVE_Template->assign('cancomment', ($this->_commentSettingsGet('comment_active') == 1 && in_array($user_group, explode(',', $this->_commentSettingsGet('comment_user_groups')))));
+ $AVE_Template->assign('comment_max_chars', $this->_commentSettingsGet('comment_max_chars'));
+ $AVE_Template->assign('theme', defined('THEME_FOLDER') ? THEME_FOLDER : DEFAULT_THEME_FOLDER);
+
+ // Отображаем форму для добавления комментария
+ $AVE_Template->display($tpl_dir . $this->_comment_form_tpl);
+ }
+
+ /**
+ * Метод, предназначенный для записи в БД нового комментария.
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ *
+ */
+ function commentPostNew($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;
+
+ // Если запрос пришел не ajax запросом, тогда формируем ссылку для последующего редиректа
+ if (! $ajax)
+ {
+ $link = rewrite_link(base64_decode($page));
+ }
+
+ // Если в настройках модуля включено использование защитного кода, тогда
+ if ($this->_commentSettingsGet('comment_use_antispam') == 1)
+ {
+ // Если введенный пользователем защитный код неверен, тогда выполняем обновление кода
+ if (! (isset($session_captcha) && $session_captcha == $secure_code))
+ {
+ unset($_SESSION['captcha_keystring']);
+
+ if ($ajax)
+ {
+ echo 'wrong_securecode';
+ }
+ else
+ {
+ if(isset($GLOBALS['tmpl']))$GLOBALS['tmpl']->assign("wrongSecureCode", 1);
+ header('Location:' . $link . '#end');
+ }
+ exit;
+ }
+ unset($_SESSION['captcha_keystring']);
+ }
+
+ // Определяем флаг модерации комментариев
+ $comment_status = ($this->_commentSettingsGet('comment_need_approve') == 1) ? 0 : 1;
+
+ // Если комментарии разрешены, тогда получаем все данные, который пользователь указал в форме
+ if ($this->_commentSettingsGet('comment_active') == 1
+ && !empty($_POST['comment_text'])
+ && !empty($_POST['comment_author_name'])
+ && in_array($user_group, explode(',', $this->_commentSettingsGet('comment_user_groups'))))
+ {
+ // --- !!! НАЧАЛО БЕЗОПАСНОСТИ: Санитаризация и Экранирование !!! ---
+
+ $new_comment['parent_id'] = (int)($_POST['parent_id'] ?? 0);
+ $new_comment['document_id'] = (int)($_POST['doc_id'] ?? 0);
+
+ // Экранирование для предотвращения SQLi:
+ // Санитаризация для предотвращения XSS в строковых полях:
+ $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'] ?? ''));
+ $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'] ?? '';
+
+ // Определяем максимальную длину символов для комментария
+ $comment_max_chars = $this->_commentSettingsGet('comment_max_chars');
+ $comment_max_chars = (!empty($comment_max_chars) && $comment_max_chars > 10) ? $comment_max_chars : 200;
+
+ // 1. Убираем HTML-теги для предотвращения XSS
+ $comment_text_clean = strip_tags(stripslashes($comment_text_raw));
+
+ // 2. Обрезка
+ $comment_text_cut = mb_substr($comment_text_clean, 0, $comment_max_chars);
+ $comment_text_cut .= (mb_strlen($comment_text_clean) > $comment_max_chars) ? '…' : '';
+
+ // 3. Экранирование текста перед вставкой в SQL
+ $new_comment['comment_text'] = addslashes($comment_text_cut);
+
+ // --- !!! КОНЕЦ БЕЗОПАСНОСТИ !!! ---
+
+ // Выполняем запрос к БД на добавление комментария
+ $AVE_DB->Query("
+ INSERT INTO " . PREFIX . "_module_comment_info
+ (`" . implode('`,`', array_keys($new_comment)) ."`)
+ VALUES
+ ('" . implode("','", $new_comment) . "')
+ ");
+ $new_comment['Id'] = $AVE_DB->InsertId();
+
+ // Получаем e-mail адрес из Общих настроек системы и формируем ссылку на комментарий в Публичной части
+ $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'];
+
+ // Формируем текст уведомления для отправки на e-mail
+ $mail_text = $AVE_Template->get_config_vars('COMMENT_MESSAGE_ADMIN');
+ // Используем stripslashes для очистки текста, который будет отправлен по почте
+ $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'
+ );
+
+ // Если данные были отправлены ajax-запросом, тогда выполняем автоматический показ комментария
+ // на странице.
+ if ($ajax)
+ {
+ $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']);
+ $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);
+ }
+ }
+
+ // Если же данные пришли НЕ ajax-запросом, тогда полностью обновляем страницу.
+ if (! $ajax) header('Location:' . str_replace("//", "", $link) . '#end');
+ exit;
+ }
+
+ /**
+ * Метод, предназначенный для редактирования комментария в Публичной части
+ *
+ * @param int $comment_id - идентификатор комментария
+ */
+ function commentPostEdit($comment_id)
+ {
+ global $AVE_DB;
+
+ $user_id = $_SESSION['user_id'] ?? null;
+ $user_group = UGROUP ?? 0;
+ $post_text = $_POST['text'] ?? '';
+
+
+ if (empty($user_id)) exit;
+
+ $comment_id = intval(preg_replace('/\D/', '', $comment_id));
+
+ // Выполняем запрос к БД и получаем всю информацию о комментарии, а также ряд значений из настроек модуля
+ $row = $AVE_DB->Query("
+ SELECT
+ msg.parent_id,
+ msg.comment_text,
+ 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'
+ AND msg.Id = '" . $comment_id . "'
+ " . (($user_group != 1) ? "AND comment_author_id = " . $user_id : '') . "
+ ")->FetchAssocArray();
+
+ // Если данные получены
+ if ($row !== false)
+ {
+
+ $comment_max_chars = ($row['comment_max_chars'] != '' && $row['comment_max_chars'] > 10) ? $row['comment_max_chars'] : 20;
+
+ $comment_text = $post_text;
+
+ // --- !!! НАЧАЛО ИСПРАВЛЕНИЯ УСТАРЕВШИХ ОПЕРАТОРОВ (/e) !!! ---
+
+ // Безопасная замена: преобразуем hex-сущности
+ $comment_text = preg_replace_callback('/([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 = 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) ? '…' : '';
+
+
+ // Если группа текущего пользователя совпадает с разрешенной группой в настройках модуля, тогда
+ // выполняем запрос к БД на обновление информации.
+ if (in_array($user_group, explode(',', $row['comment_user_groups'])) && $message_length > 3)
+ {
+ $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) . "'
+ WHERE
+ Id = '" . $comment_id . "'
+ ");
+
+ // Преобразуем HTML теги в HTML сущности перед выводом (для XSS)
+ echo htmlspecialchars($comment_text, ENT_QUOTES);
+ exit;
+ }
+
+ // Если редактирование не прошло, выводим оригинальный текст (экранированный)
+ echo htmlspecialchars($row['comment_text'], ENT_QUOTES);
+ }
+ exit;
+ }
+
+ /**
+ * Метод, предназначенный для удаления комментария. Если комментарий содержал какие-либо ответы на него,
+ * то все ответы также будут удалены вместе с родительским комментарием.
+ *
+ * @param int $comment_id - идентификатор комментария
+ */
+ function commentPostDelete($comment_id)
+ {
+ global $AVE_DB;
+
+ $comment_id = (int)$comment_id; // Убедимся, что это целое число
+
+ // Выполняем запрос к БД на удаление родительского комментария
+ $AVE_DB->Query("
+ DELETE
+ FROM " . PREFIX . "_module_comment_info
+ WHERE Id = '" . $comment_id . "'
+ ");
+
+ // Выполняем запрос к БД на удаление дочерних комментариев (ответов)
+ $AVE_DB->Query("
+ DELETE
+ FROM " . PREFIX . "_module_comment_info
+ WHERE parent_id = '" . $comment_id . "'
+ AND parent_id != 0
+ ");
+
+ exit;
+ }
+ function commentAdminDelete($comment_id)
+ {
+ global $AVE_DB;
+
+ $comment_id = (int)$comment_id; // Убедимся, что это целое число
+
+ // Выполняем запрос к БД на удаление родительского комментария
+ $AVE_DB->Query("
+ DELETE
+ FROM " . PREFIX . "_module_comment_info
+ WHERE Id = '" . $comment_id . "'
+ ");
+
+ // Выполняем запрос к БД на удаление дочерних комментариев (ответов)
+ $AVE_DB->Query("
+ DELETE
+ FROM " . PREFIX . "_module_comment_info
+ WHERE parent_id = '" . $comment_id . "'
+ AND parent_id != 0
+ ");
+
+ // Используем оператор объединения с null для PHP 8.4
+ $session_id = SESSION ?? '';
+
+ header('Location:index.php?do=modules&action=modedit&mod=comment&moduleaction=1&cp=' . $session_id);
+ exit;
+ }
+
+ /**
+ * Метод, предназначенный для вывода детальной информации об авторе комментария
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ */
+ function commentPostInfoShow($tpl_dir)
+ {
+ global $AVE_DB, $AVE_Template;
+
+ // Используем оператор объединения с null для PHP 8.4
+ $comment_id = (int)($_REQUEST['Id'] ?? 0);
+
+ // Получаем полную информацию о комментарии
+ $row = $AVE_DB->Query("
+ SELECT *
+ FROM " . PREFIX . "_module_comment_info
+ WHERE Id = '" . $comment_id . "'
+ ")->FetchAssocArray();
+
+ // Преобразуем адрес сайта к формату ссылки
+ $row['comment_author_website'] = str_replace('http://', '', $row['comment_author_website']);
+ $row['comment_author_website'] = ($row['comment_author_website'] != '')
+ ? '' . $row['comment_author_website'] .''
+ : '';
+
+ // Выполняем запрос к БД на получение количества всех комментариев, оставленных данным пользователем
+ $row['num'] = $AVE_DB->Query("
+ SELECT COUNT(*)
+ FROM " . PREFIX . "_module_comment_info
+ WHERE comment_author_id = '" . $row['comment_author_id'] . "'
+ AND comment_author_id != 0
+ ")->GetCell();
+
+ // Отображаем окно с информацией
+ $AVE_Template->assign('c', $row);
+ $AVE_Template->display($tpl_dir . $this->_postinfo_tpl);
+ }
+
+ /**
+ * Метод, предназначенный для управления запретом или разрешением отвечать на комментарии
+ *
+ * @param int $comment_id - идентификатор комментария
+ * @param string $comment_status - {lock|unlock} признак запрета/разрешения
+ */
+ function commentReplyStatusSet($comment_id, $comment_status = 'lock')
+ {
+ global $AVE_DB;
+
+ $comment_id = (int)$comment_id;
+
+ $AVE_DB->Query("
+ UPDATE " . PREFIX . "_module_comment_info
+ SET comment_status = '" . (($comment_status == 'lock') ? 0 : 1) . "'
+ WHERE Id = '" . $comment_id . "'
+ ");
+
+ exit;
+ }
+
+ /**
+ * Метод, предназначенный для управления запретом или разрешением комментировать документ
+ *
+ * @param int $document_id - идентификатор документа
+ * @param string $comment_status - {close|open} признак запрета/разрешения
+ */
+ function commentStatusSet($document_id, $comment_status = 'open')
+ {
+ global $AVE_DB;
+
+ $document_id = (int)$document_id;
+
+ $AVE_DB->Query("
+ UPDATE " . PREFIX . "_module_comment_info
+ SET comments_close = '" . (($comment_status == 'open') ? 0 : 1) . "'
+ WHERE document_id = '" . $document_id . "'
+ ");
+
+ exit;
+ }
+
+ /**
+ * Следующие методы описывают работу модуля в Административной части сайта.
+ */
+
+ /**
+ * Метод, предназначенный для вывода списка всех комментариев в Административной части.
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ */
+ function commentAdminListShow($tpl_dir)
+ {
+ global $AVE_DB, $AVE_Template;
+
+ // Используем оператор объединения с null для PHP 8.4
+ $request_sort = $_REQUEST['sort'] ?? '';
+ $session_id = SESSION ?? '';
+
+ // Получаем общее количество комментариев
+ $num = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info")->GetCell();
+
+ // Определяем количество страниц, учитывая параметр _limit, который опроеделяет количество
+ // комментариев отображаемых на одной странице
+ @$seiten = @ceil($num / $this->_limit);
+ $start = get_current_page() * $this->_limit - $this->_limit;
+
+ $docs = array();
+
+ $def_sort = 'ORDER BY doc.Id DESC';
+ $def_nav = '';
+
+ // Определяем условия сортировки комментариев
+ if (!empty($request_sort))
+ {
+ switch ($request_sort)
+ {
+ case 'document_desc':
+ $def_sort = 'ORDER BY doc.Id ASC'; // Предполагаю, что document_desc должен быть ASC
+ $def_nav = '&sort=document_desc';
+ break;
+
+ case 'document':
+ $def_sort = 'ORDER BY doc.Id DESC';
+ $def_nav = '&sort=document';
+ break;
+
+ case 'comment_desc':
+ $def_sort = 'ORDER BY cmnt.comment_text ASC';
+ $def_nav = '&sort=comment_desc';
+ break;
+
+ case 'comment':
+ $def_sort = 'ORDER BY cmnt.comment_text DESC';
+ $def_nav = '&sort=comment';
+ break;
+
+ case 'created_desc':
+ $def_sort = 'ORDER BY cmnt.comment_published ASC';
+ $def_nav = '&sort=created_desc';
+ break;
+
+ case 'created':
+ $def_sort = 'ORDER BY cmnt.comment_published DESC';
+ $def_nav = '&sort=created';
+ break;
+ }
+ }
+
+ // Выполняем запрос к БД на получение комметариев с учетом параметров сортировки и лимита.
+ $sql = $AVE_DB->Query("
+ SELECT
+ doc.Id,
+ doc.document_title,
+ cmnt.Id AS CId,
+ cmnt.document_id,
+ cmnt.comment_text,
+ cmnt.comment_published,
+ cmnt.comment_status
+ FROM
+ " . PREFIX . "_module_comment_info AS cmnt
+ JOIN
+ " . PREFIX . "_documents AS doc
+ ON doc.Id = cmnt.document_id
+ " . $def_sort . "
+ LIMIT " . $start . "," . $this->_limit
+ );
+
+ while ($row = $sql->FetchAssocArray())
+ {
+ $row['Comments'] = $this->_commentPostCountGet($row['Id']);
+ array_push($docs, $row);
+ }
+
+ // Если количество комментариев полученных из БД превышает допустимое на странице, тогда формируем
+ // меню постраницной навигации
+ if ($num > $this->_limit)
+ {
+ $page_nav = ' {t} ';
+ $page_nav = get_pagination($seiten, 'page', $page_nav);
+ $AVE_Template->assign('page_nav', $page_nav);
+ }
+
+ // Передаем данные в шаблон для вывода и отображаем шаблон
+ $AVE_Template->assign('docs', $docs);
+ $AVE_Template->assign('content', $AVE_Template->fetch($tpl_dir . $this->_admin_comments_tpl));
+ }
+
+ /**
+ * Метод, предназначенный для редактирования комментариев в Административной части.
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ */
+ function commentAdminPostEdit($tpl_dir)
+ {
+ global $AVE_DB, $AVE_Template;
+
+ // Используем оператор объединения с null для PHP 8.4
+ $post_sub = $_POST['sub'] ?? '';
+ $request_id = (int)($_REQUEST['Id'] ?? 0);
+ $request_docid = (int)($_REQUEST['docid'] ?? 0);
+
+ // Выполняем запрос к БД на получение информации о редактируемом комментарии
+ $row = $AVE_DB->Query("
+ SELECT *
+ FROM " . PREFIX . "_module_comment_info
+ WHERE Id = '" . $request_id . "'
+ LIMIT 1
+ ")->FetchAssocArray();
+
+ // Если в запросе содержится подзапрос на сохранение данных (пользователь уже отредактировал комментарий
+ // и нажал кнопку сохранить изменения), тогда выполняем запрос к БД на обновление информации.
+ if ($post_sub == 'send' && false != $row)
+ {
+ // --- !!! БЕЗОПАСНОСТЬ: Экранирование данных перед сохранением !!! ---
+ $AVE_DB->Query("
+ UPDATE " . PREFIX . "_module_comment_info
+ SET
+ comment_author_name = '" . addslashes(htmlspecialchars($_POST['comment_author_name'] ?? '')) . "',
+ comment_author_email = '" . addslashes(htmlspecialchars($_POST['comment_author_email'] ?? '')) . "',
+ comment_author_city = '" . addslashes(htmlspecialchars($_POST['comment_author_city'] ?? '')) . "',
+ comment_author_website = '" . addslashes(htmlspecialchars($_POST['comment_author_website'] ?? '')) . "',
+ comment_text = '" . addslashes(htmlspecialchars($_POST['comment_text'] ?? '')) . "',
+ comment_changed = '" . time() . "'
+ WHERE
+ Id = '" . (int)($_POST['Id'] ?? 0) . "'
+ ");
+
+ echo '';
+
+ return;
+ }
+
+ // Если в первой выборке из БД мы получили нулевой результат, тогда генерируем сообщение с ошибкой
+ if ($row == false)
+ {
+ $AVE_Template->assign('editfalse', 1);
+ }
+ // в противном случае получаем список комментариев, у которых стоит запрет на ответы
+ else
+ {
+ $closed = $AVE_DB->Query("
+ SELECT comments_close
+ FROM " . PREFIX . "_module_comment_info
+ WHERE document_id = '" . $request_docid . "'
+ LIMIT 1
+ ")->GetCell();
+
+ $AVE_Template->assign('closed', $closed);
+ $AVE_Template->assign('row', $row);
+ $AVE_Template->assign('comment_max_chars', $this->_commentSettingsGet('comment_max_chars'));
+ }
+
+ // Отображаем шаблон
+ $AVE_Template->assign('content', $AVE_Template->fetch($tpl_dir . $this->_admin_edit_link_tpl));
+ }
+
+ /**
+ * Метод, предназначенный для управления настройками модуля
+ *
+ * @param string $tpl_dir - путь к шаблонам модуля
+ */
+ function commentAdminSettingsEdit($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();
+ // НОВОЕ: Получение данных для групп, которым разрешен просмотр
+ $post_user_groups_read = $_POST['comment_user_groups_read'] ?? array();
+ // КОНЕЦ НОВОГО
+ $post_need_approve = $_POST['comment_need_approve'] ?? 0;
+ $post_active = $_POST['comment_active'] ?? 0;
+ $post_use_antispam = $_POST['comment_use_antispam'] ?? 0;
+ $post_use_page_nav = $_POST['comment_use_page_nav'] ?? 0;
+ $post_page_nav_count = $_POST['comment_page_nav_count'] ?? 0;
+
+ // Если в запросе содержится подзапрос на сохранение данных (пользователь нажал кнопку
+ // сохранить изменения), тогда выполняем запрос к БД на обновление информации.
+
+ if ($request_sub == 'save')
+ {
+ $max_chars = (empty($post_max_chars) || $post_max_chars < 50) ? 50 : $post_max_chars;
+
+ $AVE_DB->Query("
+ UPDATE " . PREFIX . "_module_comments
+ SET
+ comment_max_chars = '" . (int)$max_chars . "',
+ comment_user_groups = '" . implode(',', $post_user_groups) . "',
+ comment_user_groups_read = '" . implode(',', $post_user_groups_read) . "', /* <-- СОХРАНЕНИЕ НОВОГО ПОЛЯ */
+ comment_need_approve = '" . (int)$post_need_approve . "',
+ comment_active = '" . (int)$post_active . "',
+ 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 . "'
+ WHERE
+ Id = 1
+ ");
+ }
+
+ // Получаем список всех настроек модуля
+ $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));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..9c20174
--- /dev/null
+++ b/index.php
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/info.php b/info.php
new file mode 100644
index 0000000..f2f2cfa
--- /dev/null
+++ b/info.php
@@ -0,0 +1,20 @@
+ 'comment',
+ 'ModuleVersion' => '1.26.2a',
+ 'ModuleAutor' => 'AVE.cms Team',
+ 'ModuleCopyright' => '© 2007-' . date('Y') . ' AVE.cms',
+ 'ModuleStatus' => 1,
+ 'ModuleIsFunction' => 1,
+ 'ModuleTemplate' => 0,
+ 'ModuleAdminEdit' => 1,
+ 'ModuleFunction' => 'mod_comment',
+ 'ModuleTag' => '[mod_comment]',
+ 'ModuleTagLink' => null,
+ 'ModuleAveTag' => '#\\\[mod_comment]#',
+ 'ModulePHPTag' => ''
+ );
+?>
\ No newline at end of file
diff --git a/js/comment.js b/js/comment.js
new file mode 100644
index 0000000..f71f457
--- /dev/null
+++ b/js/comment.js
@@ -0,0 +1,297 @@
+/* ====================================================================
+ ОБЕРТКА ДЛЯ ОЖИДАНИЯ JQUERY
+ ==================================================================== */
+(function waitForJQuery() {
+ if (typeof jQuery === 'undefined') {
+ // jQuery еще не загружен. Ждем 10 мс и проверяем снова.
+ setTimeout(waitForJQuery, 10);
+ } else {
+ // JQuery загружен, можно запускать основной код.
+
+ (function($){
+
+ // ====================================================================
+ // КОРРЕКЦИЯ: ПРИНУДИТЕЛЬНАЯ ФИКСАЦИЯ HTTPS
+ // ====================================================================
+ if (typeof aveabspath !== 'undefined') {
+ aveabspath = 'https://bag.local/';
+ }
+ // ====================================================================
+
+ /*Limit symbols*/
+ (function($){$.fn.extend({limit:function(limit,element){var interval,f;var self=$(this);$(this).focus(function(){interval=window.setInterval(substring,100)});$(this).blur(function(){clearInterval(interval);substring()});substringFunction="function substring(){ var val = $(self).val();var length = val.length;if(length > limit){$(self).val($(self).val().substring(0,limit));}";if(typeof element!='undefined')substringFunction+="if($(element).html() != limit-length){$(element).html((limit-length<=0)?'0':limit-length);}";substringFunction+="}";eval(substringFunction);substring()}})})(jQuery);
+
+ function getCaptha(){
+ now = new Date();
+ $('#captcha img').attr('src', aveabspath+'inc/captcha.php?cd=' + now);
+ };
+
+ // ИСПРАВЛЕННАЯ ФУНКЦИЯ cAction
+ 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');
+ }
+ }
+
+ if (typeof cid === 'undefined' || cid === false || cid === '') {
+ console.error("Comment ID not found for action: " + action);
+ return;
+ }
+
+ if (action=='answer'){
+ $('#parent_id').val(cid);
+ // --- ИСПРАВЛЕНИЕ: Добавлено .show() для отображения формы после перемещения ---
+ $('#mod_comment_new').insertBefore('#end'+cid).show();
+ return;
+ }
+
+ // ... (остальной код для admin actions: delete, lock, open/close)
+ if (UGROUP==1){
+ $.get(aveabspath+'index.php',{
+ module: 'comment',
+ action: action,
+ docid: DOC_ID,
+ Id: cid
+ },function(){
+ if (action=='delete'){
+ $(obj).parents('.mod_comment_comment').eq(0).remove();
+ }
+ if (action=='open'){
+ $('#mod_comment_open').attr('id', 'mod_comment_close').html(COMMENT_SITE_CLOSE);
+ }
+ if (action=='close'){
+ $('#mod_comment_close').attr('id', 'mod_comment_open').html(COMMENT_SITE_OPEN);
+ }
+ // ====================================================================
+ // ИСПРАВЛЕНИЕ: ДИНАМИЧЕСКАЯ СМЕНА ИКОНКИ ЗАМКА (Font Awesome)
+ // ====================================================================
+ if (action=='unlock'){
+ // Меняем классы: mod_comment_unlock (открыт) -> mod_comment_lock (закрыт)
+ // Меняем цвет: text-success (зеленый) -> text-dark (темный)
+ $(obj).removeClass('mod_comment_unlock text-success')
+ .addClass('mod_comment_lock text-dark')
+ .attr('title',COMMENT_LOCK_LINK)
+ .find('i.fa') // Ищем иконку FA
+ .removeClass('fa-unlock').addClass('fa-lock'); // Меняем иконку
+ }
+ if (action=='lock'){
+ // Меняем классы: mod_comment_lock (закрыт) -> mod_comment_unlock (открыт)
+ // Меняем цвет: text-dark (темный) -> text-success (зеленый)
+ $(obj).removeClass('mod_comment_lock text-dark')
+ .addClass('mod_comment_unlock text-success')
+ .attr('title',COMMENT_UNLOCK_LINK)
+ .find('i.fa') // Ищем иконку FA
+ .removeClass('fa-lock').addClass('fa-unlock'); // Меняем иконку
+ }
+ // ====================================================================
+ });
+ }
+ };
+
+ // ИСПРАВЛЕННАЯ ФУНКЦИЯ VALIDATE: Устойчива к отсутствию полей
+ 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;
+ }
+
+ if (form.comment_author_email && !form.comment_author_email.value){
+ alert(COMMENT_ERROR_EMAIL);
+ $(form.comment_author_email).focus();
+ 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();
+ return false;
+ }
+
+ return true;
+ };
+
+ function setClickable(){
+ $('.editable_text').click(function(){
+ var cid = $(this).parents('.mod_comment_box').attr('id');
+ var revert = $(this).html();
+ var textarea = '
'+COMMENT_CHARS_LEFT+'
'; + var buttonSave = ' '; + var buttonReset = ''; + $(this).after(''+COMMENT_CHARS_LEFT+'
'; + var buttonSave = ' '; + var buttonReset = ''; + + commentTextBlock.after( + '+ + {else} + {if $editfalse==1} + {#COMMENT_EDIT_FALSE#} + {else} + + {/if} + {/if} +
+ + {else} + {if !$cancomment} +
{#COMMENT_NEW_FALSE#}
++ + {else} + + {/if} + {/if} +
| {#COMMENT_USER_NAME#} | +{$c.comment_author_name|stripslashes|escape} | +
| {#COMMENT_DATE_CREATE#} | +{$c.comment_published|date_format:$TIME_FORMAT|pretty_date} | +
| {#COMMENT_USER_EMAIL#} | ++ {assign var=comment_author_email value=$c.comment_author_email} + {mailto address="$comment_author_email" encode="javascript_charcode"} + | +
| {#COMMENT_USER_SITE#} | +{$c.comment_author_website|default:'-'} | +
| {#COMMENT_USER_FROM#} | +{$c.comment_author_city|stripslashes|escape|default:'-'} | +
| {#COMMENT_USER_COMMENTS#} | +{$c.num|default:'-'} | +
| + + + {if $smarty.const.UGROUP==1} + + {/if} + | +
| {$comment_text} | +
{$JsAfter}
+ +|
+
+
+
+
+
+ {#COMMENT_ANSWER_LINK#}
+
+
+ {if $smarty.const.UGROUP==1 || $c.comment_author_id==$smarty.session.user_id}
+
+ {#COMMENT_EDIT_LINK#}
+ {/if}
+
+ {if $smarty.const.UGROUP==1}
+
+ {#COMMENT_DELETE_LINK#}
+
+ {if $c.comment_status!=1}
+
+ {#COMMENT_UNLOCK_LINK#}
+
+ {else}
+
+ {#COMMENT_LOCK_LINK#}
+
+ {/if}
+ {/if}
+ |
+
|
+ {$c.comment_text}
+ {if $c.comment_changed > 1} {#COMMENT_TEXT_CHANGED#} {$c.comment_changed}{/if} + |
+
| + + + + | +
|
+ {$sc.comment_text}
+ {if $sc.comment_changed > 1} {#COMMENT_TEXT_CHANGED#} {$sc.comment_changed}{/if} + |
+