2503 lines
106 KiB
PHP
2503 lines
106 KiB
PHP
<?php
|
||
/**
|
||
* Класс, включающий все свойства и методы для управления комментариями как в
|
||
* Публичной части сайта, так и в Панели управления.
|
||
*
|
||
* @package AVE4cms
|
||
* @author Александр Сальников (Repellent)
|
||
* @copyright 2026
|
||
* @subpackage module_Comment
|
||
* @since 3.34
|
||
* @filesource
|
||
*/
|
||
class Comment
|
||
{
|
||
|
||
/**
|
||
* Свойства класса
|
||
*/
|
||
|
||
/**
|
||
* Идентификатор записи с настройками модуля Комментарии
|
||
*
|
||
* @var int
|
||
*/
|
||
private $_config_id = 1;
|
||
|
||
/**
|
||
* Количество комментариев на странице в административной части
|
||
*
|
||
* @var int
|
||
*/
|
||
private $_limit = 15;
|
||
|
||
/**
|
||
* Имя файла с шаблоном для вывода блока комментариев
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_comments_tree_tpl = 'comments_tree.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном для рекурсивного вывода иерархии комментариев
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_comments_tree_sub_tpl = 'comments_tree_sub.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном формы добавления комментария
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_comment_form_tpl = 'comment_form.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном нового комментария
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_comment_new_tpl = 'comment_new.tpl';
|
||
|
||
/**
|
||
* Имя файлаа с шаблоном уведомляющим о успешном выполнении операции
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_comment_thankyou_tpl = 'comment_thankyou.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном редактирования шаблона в административной части
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_admin_edit_link_tpl = 'admin_edit.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном списка комментариев в административной части
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_admin_comments_tpl = 'admin_comments.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном редактирования настроек модуля в административной части
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_admin_settings_tpl = 'admin_settings.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном редактирования шаблона в публичной части
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_edit_link_tpl = 'comment_edit.tpl';
|
||
|
||
/**
|
||
* Имя файла с шаблоном для вывода информации об авторе комментария
|
||
*
|
||
* @var string
|
||
*/
|
||
public $_postinfo_tpl = 'comment_info.tpl';
|
||
|
||
/**
|
||
* Настройки времени (подгружаются из БД)
|
||
*/
|
||
public $conf_edit_time;
|
||
public $conf_cookie_life;
|
||
private $_anon_cookie_name = 'ave_anon_comment_key';
|
||
|
||
/**
|
||
* Редактирование Авторской оценки (1-разрешено, 0-запрещено)
|
||
*/
|
||
private $_edit_avtor_rating = 0;
|
||
|
||
/**
|
||
* Конструктор класса
|
||
*
|
||
*/
|
||
function __construct()
|
||
{
|
||
// Как только класс создан идем в базу за настройками
|
||
$this->_commentSettingsGet();
|
||
}
|
||
|
||
/**
|
||
* Внутренние методы класса
|
||
*/
|
||
|
||
|
||
/**
|
||
* Получение или создание уникального ключа для анонимного пользователя
|
||
*/
|
||
private function _getAnonKey()
|
||
{
|
||
if (isset($_COOKIE[$this->_anon_cookie_name])) {
|
||
return preg_replace('/[^a-f0-9]/', '', $_COOKIE[$this->_anon_cookie_name]);
|
||
} else {
|
||
$new_key = md5(uniqid(rand(), true));
|
||
// Устанавливаем куку на заданный срок
|
||
setcookie($this->_anon_cookie_name, $new_key, time() + (86400 * $this->conf_cookie_life), "/");
|
||
$_COOKIE[$this->_anon_cookie_name] = $new_key; // Чтобы сразу было доступно в текущем скрипте
|
||
return $new_key;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получение всех уникальных имен, использованных анонимом
|
||
* * @param string $anon_key уникальный ключ
|
||
* @param string $exclude_name исключить это имя из результата (текущее)
|
||
* @return array
|
||
*/
|
||
private function _getAnonNamesHistory($anon_key, $exclude_name = '')
|
||
{
|
||
global $AVE_DB;
|
||
$names = [];
|
||
|
||
$exclude_sql = !empty($exclude_name) ? " AND comment_author_name != '" . addslashes($exclude_name) . "'" : "";
|
||
|
||
$sql = $AVE_DB->Query("
|
||
SELECT DISTINCT comment_author_name
|
||
FROM " . PREFIX . "_module_comment_info
|
||
WHERE anon_key = '" . addslashes($anon_key) . "'
|
||
" . $exclude_sql . "
|
||
ORDER BY Id DESC
|
||
");
|
||
|
||
while ($row = $sql->FetchAssocArray()) {
|
||
$names[] = stripslashes($row['comment_author_name']);
|
||
}
|
||
return $names;
|
||
}
|
||
|
||
|
||
function _commentSettingsGet($param = '')
|
||
{
|
||
global $AVE_DB;
|
||
|
||
static $settings = null;
|
||
|
||
if ($settings === null)
|
||
{
|
||
$settings = $AVE_DB->Query("
|
||
SELECT *
|
||
FROM " . PREFIX . "_module_comments
|
||
WHERE Id = '" . $this->_config_id . "'
|
||
")->FetchAssocArray();
|
||
|
||
// ОБНОВЛЯЕМ ПЕРЕМЕННЫЕ КЛАССА ЗНАЧЕНИЯМИ ИЗ БАЗЫ
|
||
if ($settings) {
|
||
// Если в базе есть настройка, перезаписываем стандартные 60 секунд
|
||
if (isset($settings['comment_edit_time'])) {
|
||
$this->conf_edit_time = (int)$settings['comment_edit_time'];
|
||
}
|
||
|
||
// Если в базе есть настройка куки, перезаписываем стандартные 30 дней
|
||
if (isset($settings['comment_cookie_life'])) {
|
||
$this->conf_cookie_life = (int)$settings['comment_cookie_life'];
|
||
}
|
||
}
|
||
}
|
||
|
||
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];
|
||
}
|
||
/**
|
||
* Логирование подозрительной активности при загрузке файлов
|
||
*/
|
||
private function _logUploadSecurity($status, $file_info, $context = [])
|
||
{
|
||
// Читаем настройки из базы
|
||
$settings = $this->_commentSettingsGet();
|
||
|
||
// ПРОВЕРКА: Если лог выключен в админке — просто выходим
|
||
if (empty($settings['comment_log_dangerous']) || $settings['comment_log_dangerous'] == 0) {
|
||
return;
|
||
}
|
||
$log_dir = BASE_DIR . '/modules/comment/logs/';
|
||
if (!is_dir($log_dir)) @mkdir($log_dir, 0775, true);
|
||
|
||
$log_file = $log_dir . 'security_upload.log';
|
||
$date = date('Y-m-d H:i:s');
|
||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||
$user_id = $_SESSION['user_id'] ?? 0;
|
||
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'none';
|
||
|
||
// Формируем строку контекста: ID сообщения, имя автора и т.д.
|
||
$ctx_info = "";
|
||
if (!empty($context)) {
|
||
foreach ($context as $key => $val) {
|
||
$ctx_info .= " | $key: $val";
|
||
}
|
||
}
|
||
|
||
$message = "[$date] IP: $ip | UID: $user_id{$ctx_info} | STATUS: $status | FILE: {$file_info['name']} | MIME: {$file_info['mime']} | SIZE: {$file_info['size']} bytes | UA: $user_agent\n";
|
||
|
||
@file_put_contents($log_file, $message, FILE_APPEND);
|
||
}
|
||
|
||
/**
|
||
* Следующие методы описывают работу модуля в Публичной части сайта.
|
||
*/
|
||
|
||
/**
|
||
* Метод, предназначенный для получения из БД всех комментариев, относящихся к указанному
|
||
* документу с последующим выводом в Публичной части.
|
||
*
|
||
* @param string $tpl_dir - путь к шаблонам модуля
|
||
*/
|
||
function commentListShow($tpl_dir)
|
||
{
|
||
global $AVE_DB, $AVE_Template, $AVE_Core;
|
||
|
||
$document_id = (int)($_REQUEST['id'] ?? 0);
|
||
$artpage = $_REQUEST['artpage'] ?? null;
|
||
$apage = $_REQUEST['apage'] ?? null;
|
||
$user_group = (int)(UGROUP ?? 0);
|
||
|
||
// 1. Подключаем CSS
|
||
$GLOBALS['user_header']['comment_css'] = '<link rel="stylesheet" href="' . ABS_PATH . 'modules/comment/css/mod_comment_styles.css" type="text/css" />';
|
||
|
||
// --- ОПРЕДЕЛЯЕМ ТЕКУЩЕГО ПОЛЬЗОВАТЕЛЯ ---
|
||
$current_user_id = (int)($_SESSION['user_id'] ?? 0);
|
||
$anon_key = $this->_getAnonKey();
|
||
|
||
// --- УСЛОВИЕ ВИДИМОСТИ (Свои неодобренные + Все одобренные) ---
|
||
if ($user_group == 1) {
|
||
$where_visibility = ""; // Админ видит всё
|
||
} else {
|
||
$cond = "comment_status = '1'";
|
||
if ($current_user_id > 0) {
|
||
$cond .= " OR (comment_status = '0' AND comment_author_id = '$current_user_id')";
|
||
}
|
||
if (!empty($anon_key)) {
|
||
$cond .= " OR (comment_status = '0' AND anon_key = '$anon_key')";
|
||
}
|
||
$where_visibility = "AND (" . $cond . ")";
|
||
}
|
||
|
||
// Получаем ВСЕ настройки модуля разом (из БД)
|
||
$settings = $this->_commentSettingsGet();
|
||
|
||
// Проверяем, что в настройках модуля разрешено комментирование
|
||
if (isset($settings['comment_active']) && $settings['comment_active'] == 1)
|
||
{
|
||
$read_groups = explode(',', $settings['comment_user_groups_read']);
|
||
$assign['no_read_permission'] = 0;
|
||
|
||
// Чтобы Smarty не ругался, создаем пустую заглушку
|
||
$assign['saved_anon'] = ['name' => '', 'email' => '', 'exists' => false];
|
||
|
||
if (!in_array($user_group, $read_groups))
|
||
{
|
||
$assign['no_read_permission'] = 1;
|
||
}
|
||
|
||
$assign['display_comments'] = 1;
|
||
|
||
if (in_array($user_group, explode(',', $settings['comment_user_groups'])))
|
||
{
|
||
$assign['cancomment'] = 1;
|
||
}
|
||
|
||
if ($assign['no_read_permission'] == 0)
|
||
{
|
||
// ПЕРЕКЛЮЧАЕМ РЕЖИМ ЧТЕНИЯ НА utf8mb4
|
||
$AVE_DB->Query("SET NAMES 'utf8mb4'");
|
||
$assign['comment_max_chars'] = $settings['comment_max_chars'];
|
||
$assign['im'] = $settings['comment_use_antispam'];
|
||
$assign['comment_allowed_extensions'] = $settings['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif,webp';
|
||
$assign['comment_max_file_size'] = $settings['comment_max_file_size'] ?? 2048;
|
||
$assign['comment_max_files'] = (int)($settings['comment_max_files'] ?? 5);
|
||
$assign['ajax_replies_limit'] = (int)($settings['comment_ajax_replies_limit'] ?? 0);
|
||
$assign['comment_allow_self_answer'] = (int)($settings['comment_allow_self_answer'] ?? 0);
|
||
$comments = array();
|
||
|
||
// --- ВЫБОРКА ИЗ БД ---
|
||
// Логика формирования SQL-сортировки
|
||
$sort_setting = $settings['comment_sort_order'] ?? 'ASC';
|
||
switch ($sort_setting) {
|
||
case 'USER_RATING':
|
||
// Сначала самые высокие оценки автора (5 звезд), затем дата
|
||
$sql_sort = "user_rating DESC, comment_published DESC";
|
||
break;
|
||
case 'RATING':
|
||
// Сначала самые залайканные пользователями, затем дата
|
||
$sql_sort = "rating_sum DESC, comment_published DESC";
|
||
break;
|
||
case 'DESC':
|
||
$sql_sort = "comment_published DESC";
|
||
break;
|
||
case 'ASC':
|
||
default:
|
||
$sql_sort = "comment_published ASC";
|
||
break;
|
||
}
|
||
|
||
// ОПРЕДЕЛЯЕМ НАПРАВЛЕНИЕ ДЛЯ ID (чтобы не было конфликта при одинаковых датах)
|
||
$final_direction = (stripos($sql_sort, 'DESC') !== false) ? 'DESC' : 'ASC';
|
||
|
||
if ($settings['comment_use_page_nav'] == 1)
|
||
{
|
||
$limit = (int)$settings['comment_page_nav_count'];
|
||
|
||
if ($limit <= 0)
|
||
{
|
||
$sql = $AVE_DB->Query("SELECT * FROM " . PREFIX . "_module_comment_info WHERE document_id = '" . $document_id . "' " . $where_visibility . " ORDER BY " . $sql_sort . ", Id " . $final_direction);
|
||
$page_nav = '';
|
||
}
|
||
else
|
||
{
|
||
$start = get_current_page() * $limit - $limit;
|
||
|
||
// Считаем только РОДИТЕЛЕЙ для пагинации
|
||
$num = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info WHERE document_id = '" . $document_id . "' AND parent_id = '0' " . $where_visibility)->GetCell();
|
||
|
||
// Основной запрос (сортировка применяется к родителям в подзапросе)
|
||
$sql = $AVE_DB->Query("
|
||
SELECT * FROM " . PREFIX . "_module_comment_info
|
||
WHERE document_id = '" . $document_id . "'
|
||
AND (
|
||
id IN (SELECT id FROM (SELECT id FROM " . PREFIX . "_module_comment_info WHERE document_id = '" . $document_id . "' AND parent_id = '0' " . $where_visibility . " ORDER BY " . $sql_sort . " LIMIT " . (int)$start . "," . (int)$limit . ") as tmp)
|
||
OR
|
||
parent_id != '0'
|
||
)
|
||
" . $where_visibility . "
|
||
ORDER BY " . $sql_sort . ", Id " . $final_direction . "
|
||
");
|
||
|
||
if ($num > $limit)
|
||
{
|
||
$page_nav = '<a class="page_nav" href="index.php?id=' . $AVE_Core->curentdoc->Id
|
||
. '&doc=' . (empty($AVE_Core->curentdoc->document_alias) ? prepare_url($AVE_Core->curentdoc->document_title) : $AVE_Core->curentdoc->document_alias)
|
||
. ((isset($artpage) && is_numeric($artpage)) ? '&artpage=' . $artpage : '')
|
||
. ((isset($apage) && is_numeric($apage)) ? '&apage=' . $apage : '')
|
||
. '&page={s}">{t}</a> ';
|
||
|
||
$total_pages = ceil($num / $limit);
|
||
$page_nav = get_pagination($total_pages, 'page', $page_nav, get_settings('navi_box'));
|
||
$page_nav = str_ireplace('"//"', '"/"', str_ireplace('///', '/', rewrite_link($page_nav)));
|
||
$page_nav = preg_replace('/(?<!:)\/\//', '/', $page_nav);
|
||
$GLOBALS['page_id'][$document_id]['page'] = ($GLOBALS['page_id'][$document_id]['page'] > $total_pages ? $GLOBALS['page_id'][$document_id]['page'] : $total_pages);
|
||
}
|
||
else { $page_nav = ''; }
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Если навигация отключена
|
||
$sql = $AVE_DB->Query("SELECT * FROM " . PREFIX . "_module_comment_info WHERE document_id = '" . $document_id . "' " . $where_visibility . " ORDER BY " . $sql_sort . ", Id " . $final_direction);
|
||
$page_nav = '';
|
||
}
|
||
|
||
$date_time_format = $AVE_Template->get_config_vars('COMMENT_DATE_TIME_FORMAT');
|
||
$now = time();
|
||
$conf_limit = (int)($settings['comment_edit_time'] ?? 0);
|
||
|
||
while ($row = $sql->FetchAssocArray())
|
||
{
|
||
if ($assign['saved_anon']['exists'] == false && !empty($row['anon_key']) && $row['anon_key'] == $anon_key) {
|
||
$assign['saved_anon'] = [
|
||
'name' => stripslashes($row['comment_author_name']),
|
||
'email' => stripslashes($row['comment_author_email']),
|
||
'exists' => true
|
||
];
|
||
}
|
||
|
||
// Аватар
|
||
if (isset($row['comment_author_id']) && $row['comment_author_id'] > 0) {
|
||
$row['avatar'] = getAvatar($row['comment_author_id'], 48);
|
||
} else {
|
||
$row['avatar'] = '';
|
||
}
|
||
|
||
if (!empty($row['avatar']) && strpos($row['avatar'], 'user.png') !== false) {
|
||
$row['avatar'] = '';
|
||
}
|
||
|
||
if (empty($row['avatar'])) {
|
||
$name = !empty($row['comment_author_name']) ? stripslashes($row['comment_author_name']) : 'Guest';
|
||
$row['first_letter'] = mb_substr(trim($name), 0, 1, 'UTF-8');
|
||
$row['avatar_color_index'] = (abs(crc32($name)) % 12) + 1;
|
||
}
|
||
|
||
// --- ТАЙМЕР И ПРАВА ---
|
||
$row['can_edit'] = 0;
|
||
$is_admin = ($user_group === 1);
|
||
$is_author = ($current_user_id > 0 && $current_user_id == $row['comment_author_id']) ||
|
||
($row['comment_author_id'] == 0 && !empty($row['anon_key']) && $row['anon_key'] == $anon_key);
|
||
|
||
if ($is_admin) {
|
||
$row['can_edit'] = 1;
|
||
$row['edit_time_left'] = 0;
|
||
} else {
|
||
$elapsed = $now - (int)$row['comment_published'];
|
||
$time_left = $conf_limit - $elapsed;
|
||
$row['edit_time_left'] = ($time_left > 0) ? $time_left : 0;
|
||
|
||
if ($is_author && $row['edit_time_left'] > 0) {
|
||
$row['can_edit'] = 1;
|
||
}
|
||
}
|
||
$row['is_my_own'] = $is_author;
|
||
|
||
// История имен
|
||
$row['past_names'] = [];
|
||
if (!empty($row['anon_key'])) {
|
||
$row['past_names'] = $this->_getAnonNamesHistory($row['anon_key'], $row['comment_author_name']);
|
||
}
|
||
|
||
$row['comment_published_raw'] = $row['comment_published'];
|
||
$row['comment_published'] = ave_date_format($date_time_format, $row['comment_published']);
|
||
|
||
if ($row['comment_changed'] > 0) {
|
||
$row['comment_changed'] = ave_date_format($date_time_format, $row['comment_changed']);
|
||
} else {
|
||
$row['comment_changed'] = 0;
|
||
}
|
||
|
||
$comments[$row['parent_id']][] = $row;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$comments = array();
|
||
$page_nav = '';
|
||
}
|
||
|
||
// --- СНАЧАЛА ДЕТИ ВСЕГДА ОТ СТАРЫХ К НОВЫМ (Причесываем всю очередь) ---
|
||
if (!empty($comments)) {
|
||
foreach ($comments as $parentId => &$subList) {
|
||
if ($parentId > 0) {
|
||
usort($subList, function($a, $b) {
|
||
return $a['comment_published_raw'] <=> $b['comment_published_raw'];
|
||
});
|
||
}
|
||
}
|
||
unset($subList);
|
||
}
|
||
|
||
// --- И ТОЛЬКО ПОТОМ AJAX ЛИМИТЫ (Отрезаем лишнее от уже правильной очереди) ---
|
||
$assign['more_counts'] = [];
|
||
$requested_branch = (int)($_REQUEST['ajax_load_branch'] ?? 0);
|
||
|
||
if ($assign['ajax_replies_limit'] > 0 && !empty($comments)) {
|
||
foreach ($comments as $parentId => $subList) {
|
||
if ($parentId > 0 && count($subList) > $assign['ajax_replies_limit']) {
|
||
if ($requested_branch == $parentId) {
|
||
continue;
|
||
}
|
||
$assign['more_counts'][$parentId] = count($subList) - $assign['ajax_replies_limit'];
|
||
// Теперь array_slice заберет именно ПЕРВЫЕ (старые) ответы
|
||
$comments[$parentId] = array_slice($subList, 0, $assign['ajax_replies_limit']);
|
||
}
|
||
}
|
||
}
|
||
|
||
$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('anon_key', $anon_key);
|
||
$AVE_Template->assign($settings);
|
||
$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;
|
||
|
||
$docid = (int)($_REQUEST['docid'] ?? 0);
|
||
$user_group = UGROUP ?? 0;
|
||
|
||
// --- Автоподстановка для анонима ---
|
||
$anon_data = ['name' => '', 'email' => '', 'exists' => false];
|
||
if ($user_group == 2) { // Если гость
|
||
$anon_key = $this->_getAnonKey();
|
||
$last_post = $AVE_DB->Query("
|
||
SELECT comment_author_name, comment_author_email
|
||
FROM " . PREFIX . "_module_comment_info
|
||
WHERE anon_key = '" . $anon_key . "'
|
||
ORDER BY Id DESC LIMIT 1
|
||
")->FetchAssocArray();
|
||
|
||
if ($last_post) {
|
||
$anon_data['name'] = stripslashes($last_post['comment_author_name']);
|
||
$anon_data['email'] = stripslashes($last_post['comment_author_email']);
|
||
$anon_data['exists'] = true;
|
||
}
|
||
}
|
||
$AVE_Template->assign('saved_anon', $anon_data);
|
||
|
||
$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;
|
||
|
||
$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();
|
||
|
||
// --- ПОЛУЧАЕМ КЛЮЧ АНОНИМА ---
|
||
$anon_key = $this->_getAnonKey();
|
||
|
||
if (! $ajax)
|
||
{
|
||
$link = rewrite_link(base64_decode($page));
|
||
}
|
||
|
||
// --- ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ---
|
||
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; }
|
||
}
|
||
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(isset($GLOBALS['tmpl']))$GLOBALS['tmpl']->assign("wrongSecureCode", 1);
|
||
header('Location:' . $link . '#end');
|
||
}
|
||
exit;
|
||
}
|
||
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'])))
|
||
{
|
||
// --- ОБРАБОТКА ЗАГРУЗКИ ИЗОБРАЖЕНИЯ (С ЗАЩИТОЙ ОТ ДУБЛЕЙ) ---
|
||
$uploaded_files = [];
|
||
if ($settings['comment_allow_files'] == 1 && isset($_FILES['comment_image']))
|
||
{
|
||
// --- ПРОВЕРКА ЛИМИТА КОЛИЧЕСТВА ---
|
||
$max_files_limit = (int)($settings['comment_max_files'] ?? 5);
|
||
$total_incoming = 0;
|
||
if (is_array($_FILES['comment_image']['name'])) {
|
||
foreach($_FILES['comment_image']['name'] as $fname) if(!empty($fname)) $total_incoming++;
|
||
} elseif(!empty($_FILES['comment_image']['name'])) {
|
||
$total_incoming = 1;
|
||
}
|
||
|
||
if ($total_incoming > $max_files_limit) {
|
||
if ($ajax) { echo 'error_max_files'; exit; }
|
||
else { header('Location:' . $link . '#end'); exit; }
|
||
}
|
||
// --- КОНЕЦ ПРОВЕРКИ ---
|
||
|
||
$upload_path = BASE_DIR . '/uploads/comments/';
|
||
if (!is_dir($upload_path)) {
|
||
@mkdir($upload_path, 0775, true);
|
||
$index_content = "<?php\n/**\n * Файл-заглушка...\n */\nheader('Location:/');\nexit;\n?>";
|
||
@file_put_contents($upload_path . 'index.php', $index_content);
|
||
}
|
||
|
||
$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'];
|
||
}
|
||
|
||
$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;
|
||
|
||
// не даем загрузить одно и то же содержимое дважды в одном запросе
|
||
$processed_hashes = [];
|
||
|
||
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);
|
||
|
||
// Определяем списки расширений для разной логики проверок
|
||
$img_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
|
||
$is_image_ext = in_array($ext, $img_exts);
|
||
|
||
// 1. Проверки расширения и базовой безопасности
|
||
$is_allowed_ext = in_array($ext, $allowed_extensions);
|
||
$is_dangerous = in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi', 'html', 'js']);
|
||
|
||
// 2. Проверка на "лже-картинку" (только если расширение из списка изображений)
|
||
$mime_is_valid = true;
|
||
if ($is_image_ext && strpos($mime, 'image/') !== 0) {
|
||
$mime_is_valid = false; // Попытка просунуть скрипт под видом картинки
|
||
}
|
||
|
||
// 3. Защита от опасных MIME-типов (даже если админ разрешил такое расширение)
|
||
$is_dangerous_mime = in_array($mime, [
|
||
'text/php', 'text/x-php', 'application/x-php',
|
||
'application/x-httpd-php', 'application/x-executable',
|
||
'text/html', 'text/javascript'
|
||
]);
|
||
|
||
// Итоговое условие: разрешено в админке + не опасно + валидный MIME + размер
|
||
if ($is_allowed_ext && !$is_dangerous && !$is_dangerous_mime && $mime_is_valid && $file['size'] > 0 && $file['size'] <= $max_file_size_bytes)
|
||
{
|
||
// 1. Берем имя
|
||
$original_basename = pathinfo($file['name'], PATHINFO_FILENAME);
|
||
|
||
// 2. Очищаем
|
||
$clean_name = prepare_fname($original_basename);
|
||
|
||
// 3. Если пусто - дефолт
|
||
if (empty($clean_name)) {
|
||
$clean_name = 'file';
|
||
}
|
||
|
||
// 4. Собираем имя
|
||
$new_name = $clean_name . '_' . time() . '.' . $ext;
|
||
|
||
if (move_uploaded_file($file['tmp_name'], $upload_path . $new_name)) {
|
||
$uploaded_files[] = $new_name;
|
||
$processed_hashes[] = $file_hash;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 1. Формируем причину отказа
|
||
$reason = "ОТКАЗ ПРИ СОЗДАНИИ КОММЕНТАРИЯ:";
|
||
if (!$is_allowed_ext) $reason .= " Запрещенное расширение($ext);";
|
||
if ($is_dangerous) $reason .= " Опасный формат файла;";
|
||
if ($is_dangerous_mime) $reason .= " Вредоносный тип контента($mime);";
|
||
if (!$mime_is_valid) $reason .= " Обнаружена подмена (Fake Image);";
|
||
if ($file['size'] > $max_file_size_bytes) $reason .= " Файл слишком большой;";
|
||
if ($file['size'] <= 0) $reason .= " Пустой файл;";
|
||
|
||
// 2. Собираем контекст (кто, куда и под каким именем писал)
|
||
// Берём данные из $_POST, так как запись в БД ещё не создана
|
||
$context = [
|
||
'AUTHOR' => $_POST['comment_author_name'] ?? 'Неизвестно',
|
||
'EMAIL' => $_POST['comment_author_email'] ?? '-',
|
||
'DOC_ID' => (int)($_POST['doc_id'] ?? 0)
|
||
];
|
||
|
||
// 3. Записываем в лог с контекстом
|
||
$this->_logUploadSecurity($reason, [
|
||
'name' => $file['name'],
|
||
'mime' => $mime,
|
||
'size' => $file['size']
|
||
], $context);
|
||
|
||
// 4. Защита от дублей в логе (если один и тот же файл пришел дважды)
|
||
$processed_hashes[] = $file_hash;
|
||
}
|
||
|
||
|
||
|
||
|
||
}
|
||
}
|
||
|
||
// --- ПОДГОТОВКА ДАННЫХ ДЛЯ БД ---
|
||
$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'] = !empty($uploaded_files) ? implode(',', $uploaded_files) : '';
|
||
|
||
$user_rating = (int)($_POST['comment_user_rating'] ?? 0);
|
||
if ($parent_id > 0 && (!isset($settings['comment_show_user_rating_replies']) || $settings['comment_show_user_rating_replies'] == 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_text_clean = strip_tags(stripslashes($comment_text_raw));
|
||
//$new_comment['comment_text'] = addslashes($comment_text_clean);
|
||
|
||
// --- ПОДГОТОВКА И ОБРАБОТКА ТЕКСТА КОММЕНАТРИЯ МЯГКАЯ (с соранением в виде специальных сущностей) ---
|
||
// 1. Получаем сырой текст
|
||
$comment_text_raw = $_POST['comment_text'] ?? '';
|
||
|
||
// 2. Убираем лишние слеши
|
||
$comment_text_raw = stripslashes($comment_text_raw);
|
||
|
||
// 3. ПРЕВРАЩАЕМ теги в текст (экранируем), а не вырезаем их
|
||
// Теперь <?php станет <?php и просто отобразится как текст
|
||
$comment_text_safe = htmlspecialchars($comment_text_raw, ENT_QUOTES, 'UTF-8');
|
||
|
||
// 4. Подготавливаем для вставки в SQL запрос
|
||
$new_comment['comment_text'] = addslashes($comment_text_safe);
|
||
|
||
$AVE_DB->Query("SET NAMES 'utf8mb4'");
|
||
|
||
$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)) . '&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)
|
||
{
|
||
$new_comment['avatar'] = (isset($new_comment['comment_author_id']) && $new_comment['comment_author_id'] > 0) ? getAvatar($new_comment['comment_author_id'], 48) : '';
|
||
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']);
|
||
|
||
$subcomments[] = $new_comment;
|
||
$AVE_Template->assign('subcomments', $subcomments);
|
||
$AVE_Template->display($tpl_dir . $this->_comments_tree_sub_tpl);
|
||
}
|
||
}
|
||
|
||
if (! $ajax) header('Location:' . str_replace("//", "", $link) . '#end');
|
||
exit;
|
||
}
|
||
|
||
function commentPostEdit($comment_id)
|
||
{
|
||
global $AVE_DB;
|
||
|
||
// Инициализация данных
|
||
$comment_id = (int)$comment_id;
|
||
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||
$user_group = (int)(defined('UGROUP') ? UGROUP : 0);
|
||
$anon_key = $this->_getAnonKey();
|
||
|
||
if ($comment_id <= 0 || $user_group <= 0) exit('INVALID_ID');
|
||
|
||
// Получаем данные комментария и настройки модуля (JOIN)
|
||
$row = $AVE_DB->Query("
|
||
SELECT
|
||
msg.*,
|
||
cmnt.comment_max_chars,
|
||
cmnt.comment_need_approve,
|
||
cmnt.comment_user_groups,
|
||
cmnt.comment_edit_time,
|
||
cmnt.comment_allowed_extensions,
|
||
cmnt.comment_max_file_size,
|
||
cmnt.comment_max_files,
|
||
cmnt.comment_allow_files,
|
||
cmnt.comment_allow_files_anon
|
||
FROM " . PREFIX . "_module_comment_info AS msg
|
||
JOIN " . PREFIX . "_module_comments AS cmnt ON cmnt.Id = 1
|
||
WHERE msg.Id = '" . $comment_id . "'
|
||
LIMIT 1
|
||
")->FetchAssocArray();
|
||
|
||
if (!$row) exit('NOT_FOUND');
|
||
|
||
// Проверка прав
|
||
$is_admin = ($user_group == 1);
|
||
|
||
$is_author = ($user_id > 0 && $user_id == $row['comment_author_id']) ||
|
||
($row['comment_author_id'] == 0 && !empty($row['anon_key']) && $row['anon_key'] == $anon_key);
|
||
|
||
$time_limit = (isset($row['comment_edit_time'])) ? (int)$row['comment_edit_time'] : 60;
|
||
$time_passed = time() - (int)$row['comment_published'];
|
||
$is_time_ok = ($time_passed < $time_limit);
|
||
|
||
if (!$is_admin) {
|
||
if (!$is_author) exit('NOT_AUTHOR');
|
||
if (!$is_time_ok) {
|
||
echo "TIME_EXPIRED";
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// Обработка текста
|
||
// 1. Получаем текст
|
||
$comment_text = $_POST['text'] ?? '';
|
||
|
||
// 2. Убираем слеши
|
||
$comment_text = stripslashes($comment_text);
|
||
|
||
// 3. Декодируем сущности (чтобы не было &lt;)
|
||
$comment_text = html_entity_decode($comment_text, ENT_QUOTES, 'UTF-8');
|
||
|
||
// 4. УБИРАЕМ ДУБЛИРОВАНИЕ СТРОК:
|
||
// Сначала превращаем все варианты переносов (Windows \r\n, Mac \r) в единый \n
|
||
$comment_text = str_replace(array("\r\n", "\r"), "\n", $comment_text);
|
||
|
||
// Теперь заменяем <br> (если они есть) на \n, но так, чтобы не создавать лишних пустых строк
|
||
$comment_text = preg_replace('/<br\s*\/?>\n?/i', "\n", $comment_text);
|
||
|
||
// Удаляем лишние пробелы в концах строк (необязательно, но полезно)
|
||
$comment_text = implode("\n", array_map('trim', explode("\n", $comment_text)));
|
||
|
||
// 5. БЕЗОПАСНОСТЬ: Экранируем обратно для базы
|
||
$comment_text = htmlspecialchars($comment_text, ENT_QUOTES, 'UTF-8');
|
||
|
||
// 6. Ограничение по длине (как у тебя и было)
|
||
$max = ($row['comment_max_chars'] > 10) ? (int)$row['comment_max_chars'] : 1000;
|
||
if (mb_strlen($comment_text) > $max) {
|
||
$comment_text = mb_substr($comment_text, 0, $max) . '…';
|
||
}
|
||
$comment_text_cut = $comment_text;
|
||
|
||
|
||
// Работа с изображениями (МУЛЬТИЗАГРУЗКА И УДАЛЕНИЕ)
|
||
|
||
// ПРОВЕРКА ПРАВ НА ЗАГРУЗКУ (Защита от взлома через POST)
|
||
$allow_files = (int)($row['comment_allow_files'] ?? 0);
|
||
$allow_anon = (int)($row['comment_allow_files_anon'] ?? 0);
|
||
|
||
// Определяем, имеет ли право текущий пользователь загружать файлы
|
||
$can_upload = false;
|
||
if ($allow_files === 1) {
|
||
if ($user_group == 2) { // Если гость
|
||
if ($allow_anon === 1) $can_upload = true;
|
||
} else { // Если зарегистрирован
|
||
$can_upload = true;
|
||
}
|
||
}
|
||
|
||
// Если файлы пытаются загрузить, но прав нет — просто очищаем массив файлов
|
||
if (!$can_upload && isset($_FILES['comment_image'])) {
|
||
unset($_FILES['comment_image']);
|
||
}
|
||
|
||
$upload_dir = BASE_DIR . '/uploads/comments/';
|
||
// Получаем текущие файлы из БД в массив
|
||
$current_files = !empty($row['comment_file']) ? explode(',', $row['comment_file']) : [];
|
||
|
||
$processed_hashes = []; // Для защиты лога от дубликатов
|
||
|
||
// --- Обработка выборочного удаления ---
|
||
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;
|
||
|
||
// Удаляем физически
|
||
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;
|
||
});
|
||
}
|
||
}
|
||
|
||
// --- Загрузка новых файлов (с проверкой лимита) ---
|
||
if (isset($_FILES['comment_image']) && is_array($_FILES['comment_image']['name'])) {
|
||
|
||
// Считаем, сколько реально новых файлов пытаются загрузить
|
||
$new_files_to_upload_count = 0;
|
||
foreach ($_FILES['comment_image']['name'] as $k => $fname) {
|
||
if (!empty($fname) && $_FILES['comment_image']['error'][$k] == UPLOAD_ERR_OK) {
|
||
$new_files_to_upload_count++;
|
||
}
|
||
}
|
||
|
||
// Проверка лимита: (Оставшиеся старые + Новые) не должно быть больше MAX
|
||
$max_limit = (int)($row['comment_max_files'] ?? 5);
|
||
if ((count($current_files) + $new_files_to_upload_count) > $max_limit) {
|
||
echo "MAX_FILES_LIMIT_EXCEEDED";
|
||
exit;
|
||
}
|
||
|
||
$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_size = $_FILES['comment_image']['size'][$i];
|
||
$file_ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION));
|
||
|
||
// 1. Проверка MIME-типа и хэша
|
||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||
$mime = finfo_file($finfo, $tmp_name);
|
||
finfo_close($finfo);
|
||
|
||
$file_hash = md5_file($tmp_name);
|
||
if (in_array($file_hash, $processed_hashes)) continue;
|
||
|
||
// 2. Логика безопасности
|
||
$img_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
|
||
$is_image_ext = in_array($file_ext, $img_exts);
|
||
$is_allowed_ext = in_array($file_ext, $allowed_extensions);
|
||
$is_dangerous = in_array($file_ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi', 'html', 'js']);
|
||
|
||
$mime_is_valid = true;
|
||
if ($is_image_ext && strpos($mime, 'image/') !== 0) {
|
||
$mime_is_valid = false;
|
||
}
|
||
|
||
$is_dangerous_mime = in_array($mime, [
|
||
'text/php', 'text/x-php', 'application/x-php',
|
||
'application/x-httpd-php', 'application/x-executable',
|
||
'text/html', 'text/javascript'
|
||
]);
|
||
|
||
// 3. Условие загрузки
|
||
if ($is_allowed_ext && !$is_dangerous && !$is_dangerous_mime && $mime_is_valid && $file_size > 0 && $file_size <= ($max_kb * 1024))
|
||
{
|
||
if (!is_dir($upload_dir)) @mkdir($upload_dir, 0775, true);
|
||
|
||
$original_basename = pathinfo($fname, PATHINFO_FILENAME);
|
||
$clean_name = prepare_fname($original_basename);
|
||
if (empty($clean_name)) $clean_name = 'file';
|
||
|
||
$new_file_name = $clean_name . '_' . time() . '.' . $file_ext;
|
||
|
||
if (move_uploaded_file($tmp_name, $upload_dir . $new_file_name)) {
|
||
$current_files[] = $new_file_name;
|
||
$processed_hashes[] = $file_hash;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 4. ЛОГИРОВАНИЕ (с контекстом комментария)
|
||
$reason = "ОТКАЗ ПРИ РЕДАКТИРОВАНИИ КОММЕНТАРИЯ:";
|
||
if (!$is_allowed_ext) $reason .= " Запрещенное расширение($file_ext);";
|
||
if ($is_dangerous) $reason .= " Опасный формат файла;";
|
||
if ($is_dangerous_mime) $reason .= " Вредоносный тип контента($mime);";
|
||
if (!$mime_is_valid) $reason .= " Обнаружена подмена (Fake Image);";
|
||
if ($file_size > ($max_kb * 1024)) $reason .= " Файл слишком большой;";
|
||
|
||
$context = [
|
||
'COMMENT_ID' => $comment_id,
|
||
'AUTHOR_DB' => $row['comment_author_name'],
|
||
'CURRENT_UID' => $user_id
|
||
];
|
||
|
||
$this->_logUploadSecurity($reason, [
|
||
'name' => $fname,
|
||
'mime' => $mime,
|
||
'size' => $file_size
|
||
], $context);
|
||
|
||
$processed_hashes[] = $file_hash;
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// Собираем итоговую строку файлов для БД через запятую
|
||
$final_files_str = implode(',', array_unique(array_filter($current_files)));
|
||
|
||
//$user_rating = isset($_POST['user_rating']) ? (int)$_POST['user_rating'] : (int)$row['user_rating'];
|
||
|
||
// --- ПРОВЕРКА ПРАВ НА РЕДАКТИРОВАНИЕ РЕЙТИНГА ---
|
||
// Рейтинг может менять только Админ (группа 1) или Автор комментария
|
||
$can_edit_rating = ($is_admin || $is_author);
|
||
|
||
if ($can_edit_rating && isset($_POST['user_rating'])) {
|
||
$user_rating = (int)$_POST['user_rating'];
|
||
// Ограничиваем диапазон от 0 до 5
|
||
if ($user_rating < 0) $user_rating = 0;
|
||
if ($user_rating > 5) $user_rating = 5;
|
||
} else {
|
||
// Если прав нет или в запросе нет нового рейтинга — оставляем старый из БД
|
||
$user_rating = (int)$row['user_rating'];
|
||
}
|
||
|
||
// Обновление базы данных
|
||
$new_status = $is_admin ? (int)$row['comment_status'] : ($row['comment_need_approve'] == '1' ? 0 : 1);
|
||
|
||
// ПЕРЕКЛЮЧАЕМ РЕЖИМ ПЕРЕД ОБНОВЛЕНИЕМ:
|
||
$AVE_DB->Query("SET NAMES 'utf8mb4'");
|
||
|
||
$AVE_DB->Query("
|
||
UPDATE " . PREFIX . "_module_comment_info
|
||
SET
|
||
comment_changed = '" . time() . "',
|
||
comment_text = '" . addslashes($comment_text_cut) . "',
|
||
comment_status = '" . $new_status . "',
|
||
user_rating = '" . $user_rating . "',
|
||
comment_file = '" . addslashes($final_files_str) . "'
|
||
WHERE
|
||
Id = '" . $comment_id . "'
|
||
");
|
||
|
||
echo htmlspecialchars($comment_text_cut, ENT_QUOTES);
|
||
exit;
|
||
}
|
||
|
||
function commentPostDelete($comment_id)
|
||
{
|
||
global $AVE_DB;
|
||
|
||
if (ob_get_level()) ob_end_clean();
|
||
|
||
$comment_id = (int)$comment_id;
|
||
if ($comment_id <= 0) die('Ошибка: Неверный ID');
|
||
|
||
// Читаем тип удаления из запроса (для админки)
|
||
$delete_type = $_REQUEST['admin_action_type'] ?? 'auto';
|
||
|
||
$this->_commentSettingsGet();
|
||
|
||
$comment_data = $AVE_DB->Query("
|
||
SELECT comment_author_id, anon_key, comment_published, comment_file
|
||
FROM " . PREFIX . "_module_comment_info
|
||
WHERE Id = '" . $comment_id . "'
|
||
")->FetchAssocArray();
|
||
|
||
if (!$comment_data) die('Ошибка: Комментарий не найден');
|
||
|
||
$current_user_id = (int)($_SESSION['user_id'] ?? 0);
|
||
$user_group = (int)($_SESSION['user_group'] ?? (defined('UGROUP') ? UGROUP : 0));
|
||
$anon_key = $this->_getAnonKey();
|
||
|
||
$can_delete = false;
|
||
if ($user_group === 1) {
|
||
$can_delete = true;
|
||
} else {
|
||
$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);
|
||
|
||
$is_time_ok = (time() - (int)$comment_data['comment_published'] < (int)$this->conf_edit_time);
|
||
if ($is_author && $is_time_ok) $can_delete = true;
|
||
}
|
||
|
||
if (!$can_delete) {
|
||
header('HTTP/1.1 403 Forbidden');
|
||
die('Доступ запрещен');
|
||
}
|
||
|
||
$has_children = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info WHERE parent_id = '" . $comment_id . "'")->GetCell();
|
||
$upload_dir = BASE_DIR . '/uploads/comments/';
|
||
|
||
// Проверка на Ajax
|
||
$is_ajax = (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
|
||
|
||
// --- ЛОГИКА МЯГКОГО УДАЛЕНИЯ ---
|
||
if ($has_children > 0 && $delete_type !== 'full') {
|
||
// 1. Физически удаляем файлы, если они были
|
||
if (!empty($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);
|
||
}
|
||
}
|
||
|
||
$status_marker = ($user_group === 1) ? '__DEL_BY_ADM__' : '__DEL_BY_AUT__';
|
||
|
||
$AVE_DB->Query("
|
||
UPDATE " . PREFIX . "_module_comment_info
|
||
SET comment_text = '" . $status_marker . "',
|
||
comment_author_name = '__DELETED__',
|
||
comment_author_id = '0',
|
||
comment_author_email = '',
|
||
comment_author_ip = '0.0.0.0',
|
||
comment_file = '',
|
||
anon_key = '',
|
||
user_rating = '0',
|
||
rating_sum = '0',
|
||
rating_count = '0',
|
||
comment_status = '1',
|
||
comment_changed = '" . time() . "'
|
||
WHERE Id = '" . $comment_id . "'
|
||
");
|
||
|
||
echo $is_ajax ? "OK_SOFT" : "OK";
|
||
}
|
||
else {
|
||
// --- ПОЛНОЕ УДАЛЕНИЕ ---
|
||
$ids_to_delete = [$comment_id];
|
||
|
||
if ($user_group == 1) {
|
||
$all_child_ids = [];
|
||
$parent_ids = [$comment_id];
|
||
|
||
while (!empty($parent_ids)) {
|
||
$ids_string = implode(',', array_map('intval', $parent_ids));
|
||
$res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id IN ($ids_string)");
|
||
|
||
$parent_ids = [];
|
||
while ($row = $res->FetchAssocArray()) {
|
||
$all_child_ids[] = (int)$row['Id'];
|
||
$parent_ids[] = (int)$row['Id'];
|
||
}
|
||
}
|
||
$ids_to_delete = array_merge($ids_to_delete, $all_child_ids);
|
||
}
|
||
|
||
$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'])) {
|
||
$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";
|
||
}
|
||
die();
|
||
}
|
||
|
||
/**
|
||
* Метод для обработки голосования за комментарий
|
||
* Защита: ID для залогиненных, Пара (Ключ + IP) для анонимов.
|
||
* Добавлена проверка глобальной настройки прав для анонимов.
|
||
*/
|
||
function commentVote()
|
||
{
|
||
global $AVE_DB;
|
||
|
||
// Принимаем данные
|
||
$comment_id = (int)($_POST['comment_id'] ?? 0);
|
||
$vote_value = (int)($_POST['vote'] ?? 0);
|
||
$ajax = (isset($_POST['ajax']) && $_POST['ajax'] == 1);
|
||
|
||
// IP пользователя
|
||
$user_ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||
|
||
// Базовая валидация
|
||
if ($comment_id <= 0 || $vote_value < 1 || $vote_value > 5) {
|
||
if ($ajax) {
|
||
if (ob_get_length()) ob_end_clean();
|
||
echo 'error';
|
||
exit;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Идентификация текущего голосующего
|
||
$user_id = empty($_SESSION['user_id']) ? 0 : (int)$_SESSION['user_id'];
|
||
$anon_key = $this->_getAnonKey();
|
||
|
||
// ПРОВЕРКА ПРАВ ГОЛОСОВАНИЯ
|
||
// Получаем настройки модуля, чтобы узнать, разрешено ли анонимам голосовать
|
||
$settings = $this->_commentSettingsGet();
|
||
if (empty($user_id) && empty($settings['comment_rating_anon_vote'])) {
|
||
if ($ajax) {
|
||
if (ob_get_length()) ob_end_clean();
|
||
echo 'forbidden_anon'; // Специальный статус для JS
|
||
exit;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Получаем данные о комментарии, за который голосуют
|
||
$comment_row = $AVE_DB->Query("
|
||
SELECT comment_author_id, anon_key, comment_author_ip
|
||
FROM " . PREFIX . "_module_comment_info
|
||
WHERE Id = '" . $comment_id . "'
|
||
")->FetchRow();
|
||
|
||
if (!$comment_row) {
|
||
if ($ajax) {
|
||
if (ob_get_length()) ob_end_clean();
|
||
echo 'error';
|
||
exit;
|
||
}
|
||
return;
|
||
}
|
||
|
||
$c_author_id = (int)$comment_row->comment_author_id;
|
||
|
||
// ПРОВЕРКА АВТОРСТВА (Запрет голосовать за свой же комментарий)
|
||
$is_author = false;
|
||
|
||
if ($user_id > 0) {
|
||
// Если залогинен — проверяем только по ID
|
||
if ($user_id === $c_author_id) {
|
||
$is_author = true;
|
||
}
|
||
} else {
|
||
// Если аноним — проверяем парой (Ключ + IP)
|
||
// И только если автор комментария тоже был анонимом
|
||
if ($c_author_id === 0 &&
|
||
$anon_key === $comment_row->anon_key &&
|
||
$user_ip === $comment_row->comment_author_ip) {
|
||
$is_author = true;
|
||
}
|
||
}
|
||
|
||
if ($is_author) {
|
||
if ($ajax) {
|
||
if (ob_get_length()) ob_end_clean();
|
||
echo 'own_comment';
|
||
exit;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ПРОВЕРКА ПОВТОРНОГО ГОЛОСОВАНИЯ
|
||
// Ищем в истории, голосовал ли уже этот посетитель
|
||
if ($user_id > 0) {
|
||
// Для авторизованных поиск по ID
|
||
$sql_check = "SELECT id FROM " . PREFIX . "_module_comment_votes
|
||
WHERE comment_id = '" . $comment_id . "'
|
||
AND user_id = '" . $user_id . "'";
|
||
} else {
|
||
// Для анонимов поиск по связке Ключ + IP
|
||
$sql_check = "SELECT id FROM " . PREFIX . "_module_comment_votes
|
||
WHERE comment_id = '" . $comment_id . "'
|
||
AND anon_key = '" . $anon_key . "'
|
||
AND remote_addr = '" . $user_ip . "'";
|
||
}
|
||
|
||
if ($AVE_DB->Query($sql_check)->GetCell()) {
|
||
if ($ajax) {
|
||
if (ob_get_length()) ob_end_clean();
|
||
echo 'already_voted';
|
||
exit;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ЗАПИСЬ ГОЛОСА В ЛОГ
|
||
$AVE_DB->Query("
|
||
INSERT INTO " . PREFIX . "_module_comment_votes
|
||
(comment_id, user_id, anon_key, remote_addr, vote_value, date_voted)
|
||
VALUES
|
||
('" . $comment_id . "',
|
||
'" . $user_id . "',
|
||
'" . $AVE_DB->escape($anon_key) . "',
|
||
'" . $AVE_DB->escape($user_ip) . "',
|
||
'" . $vote_value . "',
|
||
'" . time() . "')
|
||
");
|
||
|
||
$AVE_DB->Query("
|
||
UPDATE " . PREFIX . "_module_comment_info
|
||
SET rating_sum = rating_sum + " . $vote_value . ",
|
||
rating_count = rating_count + 1
|
||
WHERE Id = '" . $comment_id . "'
|
||
");
|
||
|
||
// Чистый ответ для JS
|
||
if ($ajax) {
|
||
if (ob_get_length()) ob_end_clean();
|
||
echo 'success';
|
||
exit;
|
||
}
|
||
}
|
||
|
||
function commentAdminDelete($comment_id)
|
||
{
|
||
global $AVE_DB;
|
||
|
||
// Собираем ID в массив (поддерживаем и одиночное, и массовое удаление)
|
||
if (is_array($comment_id)) {
|
||
$ids = array_map('intval', $comment_id);
|
||
} else {
|
||
$ids = [(int)$comment_id];
|
||
}
|
||
|
||
if (empty($ids)) return;
|
||
|
||
// Путь к папке с файлами
|
||
$upload_dir = $_SERVER['DOCUMENT_ROOT'] . "/uploads/comments/";
|
||
// Читаем тип удаления из запроса
|
||
$delete_type = $_REQUEST['admin_action_type'] ?? 'auto';
|
||
|
||
foreach ($ids as $id) {
|
||
if ($id <= 0) continue;
|
||
|
||
// --- БЛОК УДАЛЕНИЯ ФАЙЛОВ (НАЧАЛО) ---
|
||
// Получаем имена файлов перед любым действием в БД
|
||
$comment_files = $AVE_DB->Query("SELECT comment_file FROM " . PREFIX . "_module_comment_info WHERE Id = '$id' LIMIT 1")->GetCell();
|
||
|
||
if (!empty($comment_files)) {
|
||
$files_to_delete = explode(',', $comment_files);
|
||
foreach ($files_to_delete as $filename) {
|
||
$filename = trim($filename);
|
||
if (empty($filename)) continue;
|
||
|
||
$full_path = $upload_dir . $filename;
|
||
$thumb_path = $upload_dir . "thumbs/" . $filename; // Удаляем и миниатюру, если есть
|
||
|
||
if (file_exists($full_path)) @unlink($full_path);
|
||
if (file_exists($thumb_path)) @unlink($thumb_path);
|
||
}
|
||
}
|
||
// --- БЛОК УДАЛЕНИЯ ФАЙЛОВ (КОНЕЦ) ---
|
||
|
||
// Проверяем наличие детей
|
||
$has_children = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info WHERE parent_id = '$id'")->GetCell();
|
||
|
||
// Если тип 'soft' ИЛИ (тип 'auto' и есть дети) — делаем мягкое удаление
|
||
if (($delete_type == 'soft') || ($delete_type == 'auto' && $has_children > 0)) {
|
||
$AVE_DB->Query("
|
||
UPDATE " . PREFIX . "_module_comment_info
|
||
SET comment_text = '__DEL_BY_ADM__',
|
||
comment_author_name = '__DELETED__',
|
||
comment_author_id = '0',
|
||
comment_file = '',
|
||
comment_status = '1',
|
||
comment_changed = '" . time() . "'
|
||
WHERE Id = '$id'
|
||
");
|
||
}
|
||
// Если детей нет или принудительно выбран 'full' — удаляем физически
|
||
else {
|
||
$all_tree_ids = [$id];
|
||
$parents = [$id];
|
||
while (!empty($parents)) {
|
||
$p_str = implode(',', $parents);
|
||
$res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id IN ($p_str)");
|
||
$parents = [];
|
||
if ($res) {
|
||
while ($r = $res->FetchAssocArray()) {
|
||
$all_tree_ids[] = (int)$r['Id'];
|
||
$parents[] = (int)$r['Id'];
|
||
}
|
||
}
|
||
}
|
||
$final_ids_str = implode(',', array_unique($all_tree_ids));
|
||
$AVE_DB->Query("DELETE FROM " . PREFIX . "_module_comment_info WHERE Id IN ($final_ids_str)");
|
||
}
|
||
}
|
||
|
||
// Редирект с сохранением фильтров
|
||
$session_id = SESSION ?? '';
|
||
$s_query = $_REQUEST['search_query'] ?? '';
|
||
$s_order = $_REQUEST['sort_order'] ?? 'new';
|
||
$f_type = $_REQUEST['filter_type'] ?? '';
|
||
$v_mode = $_REQUEST['view_mode'] ?? 'flat';
|
||
$p_page = $_REQUEST['per_page'] ?? '15';
|
||
$d_from = $_REQUEST['date_from'] ?? '';
|
||
$d_to = $_REQUEST['date_to'] ?? '';
|
||
|
||
header("Location: index.php?do=modules&action=modedit&mod=comment&moduleaction=1" .
|
||
"&cp=" . $session_id .
|
||
"&search_query=" . urlencode($s_query) .
|
||
"&sort_order=" . $s_order .
|
||
"&filter_type=" . $f_type .
|
||
"&view_mode=" . $v_mode .
|
||
"&per_page=" . $p_page .
|
||
"&date_from=" . $d_from .
|
||
"&date_to=" . $d_to);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Метод, предназначенный для вывода детальной информации об авторе комментария
|
||
*
|
||
* @param string $tpl_dir - путь к шаблонам модуля
|
||
*/
|
||
function commentPostInfoShow($tpl_dir)
|
||
{
|
||
global $AVE_DB, $AVE_Template;
|
||
|
||
$comment_id = (int)($_REQUEST['Id'] ?? 0);
|
||
|
||
// 1. Получаем данные комментария и настройки заголовков
|
||
$row = $AVE_DB->Query("
|
||
SELECT c.*, m.comment_name_f1, m.comment_name_f2
|
||
FROM " . PREFIX . "_module_comment_info AS c
|
||
LEFT JOIN " . PREFIX . "_module_comments AS m ON 1=1
|
||
WHERE c.Id = '$comment_id'
|
||
LIMIT 1
|
||
")->FetchAssocArray();
|
||
|
||
if (!$row) exit('Данные не найдены');
|
||
|
||
$author_id = (int)($row['comment_author_id'] ?? 0);
|
||
$a_key = $row['anon_key'] ?? '';
|
||
|
||
// --- ЛОГИКА ДАТ ---
|
||
if ($author_id > 0) {
|
||
$user_data = $AVE_DB->Query("SELECT reg_time, last_visit FROM " . PREFIX . "_users WHERE Id = '$author_id' LIMIT 1")->FetchAssocArray();
|
||
$row['date_label'] = 'Дата регистрации';
|
||
$row['date_value'] = $user_data['reg_time'] ?? 0;
|
||
$row['last_visit'] = $user_data['last_visit'] ?? 0;
|
||
} else {
|
||
$first_time = ($a_key != '') ? $AVE_DB->Query("SELECT MIN(comment_published) FROM " . PREFIX . "_module_comment_info WHERE anon_key = '" . addslashes($a_key) . "'")->GetCell() : 0;
|
||
$row['date_label'] = 'Дата первого комментария';
|
||
$row['date_value'] = $first_time;
|
||
$row['last_visit'] = 0;
|
||
}
|
||
|
||
// --- ПОДСЧЕТ КОММЕНТАРИЕВ И СУММАРНОГО РЕЙТИНГА ---
|
||
if ($author_id > 0) {
|
||
$where = "comment_author_id = '$author_id'";
|
||
} elseif ($a_key != '') {
|
||
$where = "anon_key = '" . addslashes($a_key) . "'";
|
||
} else {
|
||
$where = "Id = '$comment_id'"; // Крайний случай
|
||
}
|
||
|
||
// Считаем всё одним запросом: количество, сумму звезд и сумму голосов
|
||
$stats = $AVE_DB->Query("
|
||
SELECT
|
||
COUNT(*) as total_cnt,
|
||
SUM(rating_sum) as sum_stars,
|
||
SUM(rating_count) as sum_votes
|
||
FROM " . PREFIX . "_module_comment_info
|
||
WHERE $where
|
||
")->FetchAssocArray();
|
||
|
||
$row['num'] = $stats['total_cnt'] ?? 1;
|
||
|
||
// Вычисляем средний рейтинг (от 1 до 5)
|
||
$avg_rating = 0;
|
||
if (!empty($stats['sum_votes'])) {
|
||
$avg_rating = round($stats['sum_stars'] / $stats['sum_votes']);
|
||
}
|
||
$row['avg_rating'] = $avg_rating;
|
||
$row['total_votes'] = $stats['sum_votes'];
|
||
|
||
// --- ОБРАБОТКА ДОП. ПОЛЕЙ (F1, F2) ---
|
||
$custom_fields = [];
|
||
$fields_to_check = [
|
||
['val' => $row['comment_author_website'], 'title' => $row['comment_name_f1'], 'default' => 'Личный сайт'],
|
||
['val' => $row['comment_author_city'], 'title' => $row['comment_name_f2'], 'default' => 'Город']
|
||
];
|
||
foreach ($fields_to_check as $f) {
|
||
$val = trim($f['val'] ?? '');
|
||
if ($val != '') {
|
||
$title = (!empty($f['title'])) ? $f['title'] : $f['default'];
|
||
|
||
// Проверяем, является ли значение ссылкой
|
||
if (preg_match('/^(http|https|www\.)/i', $val)) {
|
||
// Если ввели с www, но без http/https — добавим https
|
||
$href = $val;
|
||
if (stripos($val, 'http') !== 0) {
|
||
$href = 'https://' . $val;
|
||
}
|
||
|
||
// Для текста ссылки (то, что видит глаз) убираем протоколы, чтобы было красиво
|
||
$display_name = str_replace(['http://', 'https://'], '', $val);
|
||
|
||
$value = '<a target="_blank" href="' . htmlspecialchars($href) . '">' . htmlspecialchars($display_name) . '</a>';
|
||
} else {
|
||
$value = htmlspecialchars($val);
|
||
}
|
||
$custom_fields[] = ['title' => $title, 'value' => $value];
|
||
}
|
||
}
|
||
|
||
$AVE_Template->assign('c', $row);
|
||
$AVE_Template->assign('custom_fields', $custom_fields);
|
||
|
||
header('Content-Type: text/html; charset=utf-8');
|
||
$AVE_Template->display($tpl_dir . $this->_postinfo_tpl);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Метод, предназначенный для управления запретом или разрешением отвечать на комментарии
|
||
*
|
||
* @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;
|
||
|
||
// Определяем цифровой статус: lock -> 0, unlock -> 1
|
||
$status_numeric = ($comment_status == 'lock') ? 0 : 1;
|
||
|
||
if ($status_numeric == 0) {
|
||
// --- Скрываем всю ветку ---
|
||
$all_ids = [$comment_id];
|
||
$stack = [$comment_id];
|
||
while (!empty($stack)) {
|
||
$curr_id = array_shift($stack);
|
||
$res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id = '$curr_id'");
|
||
while ($res && $row = $res->FetchAssocArray()) {
|
||
$all_ids[] = (int)$row['Id'];
|
||
$stack[] = (int)$row['Id'];
|
||
}
|
||
}
|
||
$ids_string = implode(',', $all_ids);
|
||
$AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '0' WHERE Id IN ($ids_string)");
|
||
echo "OK_LOCKED";
|
||
}
|
||
else {
|
||
// --- Открываем ТОЛЬКО один коммент ---
|
||
|
||
// Обновляем только если (родителя нет) ИЛИ (у родителя статус не 0)
|
||
$AVE_DB->Query("
|
||
UPDATE " . PREFIX . "_module_comment_info AS child
|
||
LEFT JOIN " . PREFIX . "_module_comment_info AS parent ON child.parent_id = parent.Id
|
||
SET child.comment_status = '1'
|
||
WHERE child.Id = '$comment_id'
|
||
AND (child.parent_id = 0 OR parent.comment_status != '0')
|
||
");
|
||
|
||
// Чтобы JS не тупил, всегда отвечаем OK (так как в базе либо уже 1, либо стала 1)
|
||
echo "OK_UNLOCKED";
|
||
}
|
||
|
||
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;
|
||
|
||
// ПЕРЕКЛЮЧАЕМ РЕЖИМ СРАЗУ ДЛЯ ВСЕЙ АДМИНКИ МОДУЛЯ
|
||
$AVE_DB->Query("SET NAMES 'utf8mb4'");
|
||
|
||
$session_id = SESSION ?? '';
|
||
|
||
// --- ОБРАБОТКА (Иконки и Кнопка "Применить") ---
|
||
$action = $_REQUEST['admin_action'] ?? '';
|
||
$items = [];
|
||
|
||
if (!empty($_REQUEST['ids'])) {
|
||
$items = explode(',', $_REQUEST['ids']);
|
||
} elseif (!empty($_REQUEST['id'])) {
|
||
$items = (is_array($_REQUEST['id'])) ? $_REQUEST['id'] : [$_REQUEST['id']];
|
||
}
|
||
|
||
// Проверяем наличие действия И (наличие массива галочек ИЛИ наличие одиночного ID в ссылке)
|
||
if (!empty($action) && (!empty($items) || !empty($_REQUEST['id']))) {
|
||
|
||
// Формируем единый массив $ids для обработки
|
||
if (!empty($items)) {
|
||
$ids = array_map('intval', $items);
|
||
} else {
|
||
// Если пришел одиночный ID (может быть как числом, так и массивом из одного элемента)
|
||
$single_id = $_REQUEST['id'];
|
||
$ids = is_array($single_id) ? array_map('intval', $single_id) : [(int)$single_id];
|
||
}
|
||
|
||
// 1. Для СКРЫТИЯ собираем "паровоз" (всех детей на любую глубину)
|
||
$all_related_for_hiding = $ids;
|
||
$current_p = $ids;
|
||
while (!empty($current_p)) {
|
||
$p_str = implode(',', $current_p);
|
||
$res = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE parent_id IN ($p_str)");
|
||
$found = [];
|
||
if ($res) {
|
||
while ($r = $res->FetchAssocArray()) { $found[] = (int)$r['Id']; }
|
||
}
|
||
if (!empty($found)) {
|
||
$all_related_for_hiding = array_merge($all_related_for_hiding, $found);
|
||
$current_p = $found;
|
||
} else { $current_p = []; }
|
||
}
|
||
$final_hide_list = implode(',', array_unique($all_related_for_hiding));
|
||
|
||
switch ($action) {
|
||
case 'approve':
|
||
case 'set_status_1':
|
||
// ПУБЛИКАЦИЯ: Работаем СТРОГО с теми ID, на которых кликнули или поставили галки
|
||
$active_db = [];
|
||
$res_act = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE comment_status = '1'");
|
||
if ($res_act) {
|
||
while ($row = $res_act->FetchAssocArray()) { $active_db[] = (int)$row['Id']; }
|
||
}
|
||
|
||
$to_publish = [];
|
||
foreach ($ids as $curr_id) {
|
||
$p_id = (int)$AVE_DB->Query("SELECT parent_id FROM " . PREFIX . "_module_comment_info WHERE Id = $curr_id")->GetCell();
|
||
|
||
// Разрешаем включить, только если родитель УЖЕ активен в базе
|
||
if ($p_id == 0 || in_array($p_id, $active_db)) {
|
||
$to_publish[] = $curr_id;
|
||
}
|
||
}
|
||
|
||
if (!empty($to_publish)) {
|
||
$pub_str = implode(',', $to_publish);
|
||
$AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '1' WHERE Id IN ($pub_str)");
|
||
}
|
||
break;
|
||
|
||
case 'unapprove':
|
||
case 'set_status_0':
|
||
// СКРЫТИЕ: Используем полный список (родители + все дети)
|
||
if (!empty($final_hide_list)) {
|
||
$AVE_DB->Query("UPDATE " . PREFIX . "_module_comment_info SET comment_status = '0' WHERE Id IN ($final_hide_list)");
|
||
}
|
||
break;
|
||
}
|
||
|
||
// 1. Сначала вытаскиваем текущие настройки из запроса
|
||
$s_query = $_GET['search_query'] ?? '';
|
||
$s_order = $_GET['sort_order'] ?? 'new';
|
||
$f_type = $_GET['filter_type'] ?? '';
|
||
$v_mode = $_GET['view_mode'] ?? 'flat';
|
||
$p_page = $_GET['per_page'] ?? '15';
|
||
$d_from = $_GET['date_from'] ?? '';
|
||
$d_to = $_GET['date_to'] ?? '';
|
||
|
||
// 2. Формируем строку возврата (редирект), чтобы настройки НЕ СЛЕТЕЛИ
|
||
header("Location: index.php?do=modules&action=modedit&mod=comment&moduleaction=1" .
|
||
"&cp=" . $session_id .
|
||
"&search_query=" . urlencode($s_query) .
|
||
"&sort_order=" . $s_order .
|
||
"&filter_type=" . $f_type .
|
||
"&view_mode=" . $v_mode .
|
||
"&per_page=" . $p_page .
|
||
"&date_from=" . $d_from .
|
||
"&date_to=" . $d_to);
|
||
exit;
|
||
}
|
||
|
||
// --- ПАРАМЕТРЫ ФИЛЬТРАЦИИ ---
|
||
$search = $_GET['search_query'] ?? '';
|
||
$sort = $_GET['sort_order'] ?? 'new';
|
||
$filter = $_GET['filter_type'] ?? '';
|
||
$view_mode = $_GET['view_mode'] ?? 'flat';
|
||
$per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 15;
|
||
$date_from = $_GET['date_from'] ?? '';
|
||
$date_to = $_GET['date_to'] ?? '';
|
||
|
||
$where = ["1=1"];
|
||
|
||
// Поиск
|
||
if ($search) {
|
||
$s = $AVE_DB->Escape($search);
|
||
$where[] = "(cmnt.comment_author_name LIKE '%$s%' OR cmnt.comment_author_ip LIKE '%$s%' OR cmnt.comment_text LIKE '%$s%')";
|
||
}
|
||
|
||
// Даты
|
||
if ($date_from) $where[] = "cmnt.comment_published >= " . strtotime($date_from);
|
||
if ($date_to) $where[] = "cmnt.comment_published <= " . (strtotime($date_to) + 86399);
|
||
|
||
// Спец. фильтры
|
||
if ($filter == 'with_files') $where[] = "cmnt.comment_file != ''";
|
||
if ($filter == 'hidden') $where[] = "cmnt.comment_status = '0'";
|
||
|
||
$where_sql = implode(" AND ", $where);
|
||
|
||
// Сортировка
|
||
switch ($sort) {
|
||
case 'old': $order_sql = "cmnt.comment_published ASC"; break;
|
||
case 'popular': $order_sql = "cmnt.rating_sum DESC, cmnt.rating_count DESC"; break;
|
||
case 'discussed': $order_sql = "(SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info WHERE parent_id = cmnt.Id) DESC"; break;
|
||
case 'user_rating': $order_sql = "cmnt.user_rating DESC"; break;
|
||
default: $order_sql = "cmnt.comment_published DESC"; break;
|
||
}
|
||
|
||
// --- ПАГИНАЦИЯ ---
|
||
$num = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info AS cmnt WHERE $where_sql")->GetCell();
|
||
$limit = $per_page;
|
||
$seiten = ceil($num / $limit);
|
||
$page = get_current_page();
|
||
$start = ($page * $limit) - $limit;
|
||
|
||
$sql = $AVE_DB->Query("
|
||
SELECT cmnt.*, doc.document_title, doc.document_alias
|
||
FROM " . PREFIX . "_module_comment_info AS cmnt
|
||
LEFT JOIN " . PREFIX . "_documents AS doc ON doc.Id = cmnt.document_id
|
||
WHERE $where_sql
|
||
ORDER BY $order_sql
|
||
LIMIT " . (int)$start . "," . (int)$limit
|
||
);
|
||
|
||
$all_items = array();
|
||
$format = "%d %B %Y, %H:%M";
|
||
|
||
// --- ПЕРЕД ЦИКЛОМ: Получаем список всех скрытых ID в системе ---
|
||
$locked_ids = [];
|
||
$res_locked = $AVE_DB->Query("SELECT Id FROM " . PREFIX . "_module_comment_info WHERE comment_status = '0'");
|
||
if ($res_locked) {
|
||
while ($rl = $res_locked->FetchAssocArray()) { $locked_ids[] = (int)$rl['Id']; }
|
||
}
|
||
|
||
while ($row = $sql->FetchAssocArray()) {
|
||
$name = !empty($row['comment_author_name']) ? stripslashes($row['comment_author_name']) : 'Guest';
|
||
if (isset($row['comment_author_id']) && $row['comment_author_id'] > 0) {
|
||
$row['avatar'] = getAvatar($row['comment_author_id'], 48);
|
||
} else { $row['avatar'] = ''; }
|
||
|
||
if (empty($row['avatar']) || strpos($row['avatar'], 'user.png') !== false) {
|
||
$row['avatar'] = '';
|
||
$row['first_letter'] = mb_strtoupper(mb_substr(trim($name), 0, 1, 'UTF-8'));
|
||
$row['avatar_color_index'] = (abs(crc32($name)) % 12) + 1;
|
||
}
|
||
|
||
// ОБРАБОТКА ФАЙЛОВ
|
||
$row['images'] = [];
|
||
$row['files'] = [];
|
||
if (!empty($row['comment_file'])) {
|
||
$img_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||
$all_files = explode(',', $row['comment_file']);
|
||
foreach ($all_files as $f_name) {
|
||
$f_name = trim($f_name);
|
||
if (!$f_name) continue;
|
||
$ext = strtolower(pathinfo($f_name, PATHINFO_EXTENSION));
|
||
$clean_name = preg_replace('/_[0-9]+(?=\.[a-z0-9]+$)/i', '', $f_name);
|
||
$file_data = ['orig_name' => $f_name, 'clean_name' => $clean_name, 'ext' => $ext];
|
||
if (in_array($ext, $img_exts)) {
|
||
$row['images'][] = $file_data;
|
||
} else {
|
||
$row['files'][] = $file_data;
|
||
}
|
||
}
|
||
}
|
||
|
||
$row['CId'] = $row['Id'];
|
||
|
||
// ДОБАВЛЯЕМ ПРОВЕРКУ РОДИТЕЛЯ (для линейного вида):
|
||
// Если parent_id этого комментария есть в списке скрытых ($locked_ids)
|
||
$row['parent_locked'] = (in_array((int)$row['parent_id'], $locked_ids)) ? 1 : 0;
|
||
|
||
$row['comment_text'] = stripslashes($row['comment_text']);
|
||
$ts_pub = (int)$row['comment_published'];
|
||
$row['date_pub'] = ($ts_pub > 0) ? pretty_date(ave_date_format($format, $ts_pub)) : '—';
|
||
$ts_changed = (int)$row['comment_changed'];
|
||
$row['date_edit'] = ($ts_changed > 0 && $ts_changed > $ts_pub) ? pretty_date(ave_date_format($format, $ts_changed)) : '';
|
||
|
||
$row['user_rating'] = (int)($row['user_rating'] ?? 0);
|
||
$row['rating_sum'] = (int)($row['rating_sum'] ?? 0);
|
||
$row['r_count'] = (int)($row['rating_count'] ?? 0);
|
||
$row['star_public'] = ($row['r_count'] > 0) ? round($row['rating_sum'] / $row['r_count']) : 0;
|
||
|
||
$doc_id = (int)$row['document_id'];
|
||
$alias = (!empty($row['document_alias']) && $row['document_alias'] != '/') ? $row['document_alias'] : '';
|
||
$raw_url = "index.php?id=" . $doc_id . "&doc=" . $alias . "/";
|
||
|
||
$final_url = function_exists('rewrite_link') ? rewrite_link($raw_url) : $alias;
|
||
$final_url = preg_replace('/(?<!:)\/\//', '/', str_ireplace(['"//"', '///'], ['/', '/'], $final_url));
|
||
$final_url = rtrim($final_url, '/');
|
||
|
||
if (empty($final_url)) {
|
||
$final_url = '/';
|
||
}
|
||
$row['document_link'] = $final_url;
|
||
|
||
$all_items[$row['Id']] = $row;
|
||
}
|
||
|
||
// --- ПОСТРОЕНИЕ ДЕРЕВА ---
|
||
$child_map = [];
|
||
foreach ($all_items as $id => $item) {
|
||
$p_id = (int)$item['parent_id'];
|
||
if ($p_id > 0) $child_map[$p_id][] = $id;
|
||
}
|
||
|
||
// --- Сортируем детей внутри карты веток ---
|
||
// Делаем это только если вид НЕ линейный
|
||
if ($view_mode != 'flat' && !empty($child_map)) {
|
||
foreach ($child_map as $p_id => &$children) {
|
||
usort($children, function($a, $b) use ($all_items) {
|
||
// Сравниваем по дате публикации из основного массива данных
|
||
$timeA = (int)$all_items[$a]['comment_published'];
|
||
$timeB = (int)$all_items[$b]['comment_published'];
|
||
return $timeA <=> $timeB;
|
||
});
|
||
}
|
||
unset($children);
|
||
}
|
||
|
||
foreach ($all_items as $id => &$item_for_check) {
|
||
$item_for_check['has_children'] = isset($child_map[$id]) ? 1 : 0;
|
||
}
|
||
unset($item_for_check);
|
||
|
||
$docs = [];
|
||
$processed = [];
|
||
|
||
// Если выбран линейный вид — просто выводим список без иерархии
|
||
if ($view_mode == 'flat') {
|
||
foreach ($all_items as $item) {
|
||
$item['depth_level'] = 0;
|
||
// берем parent_locked, который вычислили выше
|
||
$docs[] = $item;
|
||
}
|
||
}
|
||
// Если вид древовидный
|
||
else {
|
||
// функция с наследованием блокировки parent_locked
|
||
$buildTree = function($parent_id, $level, $is_parent_hidden) use (&$buildTree, &$docs, &$processed, &$all_items, &$child_map) {
|
||
if (isset($child_map[$parent_id])) {
|
||
foreach ($child_map[$parent_id] as $child_id) {
|
||
if (!in_array($child_id, $processed)) {
|
||
$item = $all_items[$child_id];
|
||
$item['depth_level'] = $level;
|
||
|
||
// Блокируем ребенка, если скрыт его родитель или кто-то выше по дереву
|
||
$item['parent_locked'] = ($is_parent_hidden || $all_items[$parent_id]['comment_status'] == '0') ? 1 : 0;
|
||
|
||
$docs[] = $item;
|
||
$processed[] = $child_id;
|
||
$buildTree($child_id, $level + 1, $item['parent_locked']);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Собираем дерево, начиная с корней
|
||
foreach ($all_items as $id => $item) {
|
||
if ($item['parent_id'] == 0 && !in_array($id, $processed)) {
|
||
$item['depth_level'] = 0;
|
||
$item['parent_locked'] = 0;
|
||
$docs[] = $item;
|
||
$processed[] = $id;
|
||
$buildTree($id, 1, false);
|
||
}
|
||
}
|
||
|
||
// Добираем то, что не попало в иерархию (битые связи)
|
||
foreach ($all_items as $id => $item) {
|
||
if (!in_array($id, $processed)) {
|
||
$item['depth_level'] = 0;
|
||
$item['parent_locked'] = 0;
|
||
$docs[] = $item;
|
||
}
|
||
}
|
||
}
|
||
// --- КОНЕЦ ПОСТРОЕНИЯ ДЕРЕВА ---
|
||
|
||
|
||
$comment_rating_type = $AVE_DB->Query("
|
||
SELECT comment_rating_type
|
||
FROM " . PREFIX . "_module_comments
|
||
LIMIT 1
|
||
")->GetCell();
|
||
$AVE_Template->assign('comment_rating_type', (int)$comment_rating_type);
|
||
|
||
|
||
// Собираем все текущие GET-параметры, кроме страницы
|
||
$current_params = $_GET;
|
||
unset($current_params['page']);
|
||
$query_string = http_build_query($current_params);
|
||
|
||
// --- ОБНОВЛЕННЫЙ БЛОК ПРОВЕРКИ ЛОГОВ ---
|
||
// Получаем настройки, чтобы проверить, включено ли логирование вообще
|
||
$settings = $this->_commentSettingsGet();
|
||
$log_enabled = !empty($settings['comment_log_dangerous']); // true, если 1
|
||
|
||
$log_path = BASE_DIR . '/modules/comment/logs/security_upload.log';
|
||
$has_new_logs = false;
|
||
|
||
// Проверяем наличие новых записей только если файл существует
|
||
if (file_exists($log_path)) {
|
||
$last_mod = @filemtime($log_path);
|
||
$last_view = isset($_SESSION['last_log_view_time']) ? (int)$_SESSION['last_log_view_time'] : 0;
|
||
|
||
if ($last_mod > $last_view) {
|
||
$has_new_logs = true;
|
||
}
|
||
}
|
||
|
||
// Передаем в шаблон флаг включения логирования и наличие новых логов
|
||
$AVE_Template->assign('log_enabled', $log_enabled); // Передаем состояние настройки
|
||
$AVE_Template->assign('has_new_logs', $has_new_logs);
|
||
$AVE_Template->assign('sess', $session_id);
|
||
// ---------------------------------------
|
||
|
||
$AVE_Template->assign([
|
||
'docs' => $docs,
|
||
'page_nav' => ($num > $limit) ? get_pagination($seiten, 'page', ' <a class="pnav" href="index.php?' . $query_string . '&page={s}">{t}</a> ') : '',
|
||
'sess' => $session_id
|
||
]);
|
||
|
||
$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;
|
||
|
||
// ПЕРЕКЛЮЧАЕМ КОДИРОВКУ
|
||
$AVE_DB->Query("SET NAMES 'utf8mb4'");
|
||
|
||
$post_sub = $_POST['sub'] ?? '';
|
||
$request_id = (int)($_REQUEST['Id'] ?? 0);
|
||
$is_ajax = isset($_REQUEST['ajax']);
|
||
|
||
// Получаем данные и настройки одним запросом
|
||
$row = $AVE_DB->Query("
|
||
SELECT
|
||
msg.*,
|
||
cmnt.comment_allowed_extensions,
|
||
cmnt.comment_max_file_size,
|
||
cmnt.comment_max_files,
|
||
cmnt.comment_allow_files,
|
||
cmnt.comment_show_f1,
|
||
cmnt.comment_show_f2,
|
||
cmnt.comment_name_f1,
|
||
cmnt.comment_name_f2,
|
||
cmnt.comment_show_user_rating
|
||
FROM " . PREFIX . "_module_comment_info AS msg
|
||
JOIN " . PREFIX . "_module_comments AS cmnt ON cmnt.Id = 1
|
||
WHERE msg.Id = '" . (int)$request_id . "'
|
||
LIMIT 1
|
||
")->FetchAssocArray();
|
||
|
||
if ($post_sub == 'send' && $row)
|
||
{
|
||
$upload_dir = BASE_DIR . '/uploads/comments/';
|
||
// Получаем массив текущих файлов (разбиваем по запятой)
|
||
$current_files = !empty($row['comment_file']) ? explode(',', $row['comment_file']) : [];
|
||
|
||
// Удаление файлов ---
|
||
if (!empty($_POST['delete_files'])) {
|
||
$files_to_remove = (array)$_POST['delete_files'];
|
||
foreach ($files_to_remove as $rem_file) {
|
||
$rem_file = basename(trim($rem_file)); // Очистка имени
|
||
if (empty($rem_file)) continue;
|
||
|
||
if (file_exists($upload_dir . $rem_file)) {
|
||
@unlink($upload_dir . $rem_file);
|
||
}
|
||
$current_files = array_filter($current_files, fn($v) => trim($v) !== $rem_file);
|
||
}
|
||
}
|
||
|
||
// Загрузка новых файлов ---
|
||
if ($row['comment_allow_files'] == 1 && isset($_FILES['comment_image']) && is_array($_FILES['comment_image']['name'])) {
|
||
|
||
if (!is_dir($upload_dir)) {
|
||
@mkdir($upload_dir, 0775, true);
|
||
@file_put_contents($upload_dir . 'index.php', "<?php\nheader('Location:/');\nexit;");
|
||
}
|
||
|
||
$max_limit = (int)($row['comment_max_files'] ?? 5);
|
||
$allowed_ext_str = $row['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif,webp';
|
||
$allowed_extensions = array_map(fn($e) => trim(strtolower($e)), explode(',', $allowed_ext_str));
|
||
$max_file_size_bytes = (int)($row['comment_max_file_size'] ?? 2048) * 1024;
|
||
|
||
$processed_hashes = [];
|
||
|
||
foreach ($_FILES['comment_image']['name'] as $k => $fname) {
|
||
if ($_FILES['comment_image']['error'][$k] == UPLOAD_ERR_OK) {
|
||
// проверяем лимит на каждой итерации
|
||
if (count($current_files) >= $max_limit) break;
|
||
|
||
$tmp_name = $_FILES['comment_image']['tmp_name'][$k];
|
||
$file_size = $_FILES['comment_image']['size'][$k];
|
||
$file_hash = md5_file($tmp_name);
|
||
|
||
if (in_array($file_hash, $processed_hashes)) continue;
|
||
|
||
$ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION));
|
||
$is_allowed_ext = in_array($ext, $allowed_extensions);
|
||
$is_dangerous = in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi', 'html', 'js']);
|
||
|
||
if ($is_allowed_ext && !$is_dangerous && $file_size > 0 && $file_size <= $max_file_size_bytes) {
|
||
$clean_name = function_exists('prepare_fname') ? prepare_fname(pathinfo($fname, PATHINFO_FILENAME)) : 'file';
|
||
if (empty($clean_name)) $clean_name = 'file';
|
||
|
||
$new_name = $clean_name . '_' . time() . '.' . $ext;
|
||
|
||
if (move_uploaded_file($tmp_name, $upload_dir . $new_name)) {
|
||
$current_files[] = $new_name;
|
||
$processed_hashes[] = $file_hash;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Финальная строка файлов для базы
|
||
$final_files_str = implode(',', array_unique(array_filter($current_files)));
|
||
|
||
// --- Рейтинг ---
|
||
$new_rating = ($this->_edit_avtor_rating == 1 && isset($_POST['user_rating']))
|
||
? (int)$_POST['user_rating']
|
||
: (int)($row['user_rating'] ?? 0);
|
||
|
||
if ($new_rating < 0) $new_rating = 0;
|
||
if ($new_rating > 5) $new_rating = 5;
|
||
|
||
// Обновление БД
|
||
$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'] ?? '')) . "',
|
||
user_rating = '" . $new_rating . "',
|
||
comment_file = '" . addslashes($final_files_str) . "',
|
||
comment_changed = '" . time() . "'
|
||
WHERE Id = '" . (int)$request_id . "'
|
||
");
|
||
|
||
if ($is_ajax) {
|
||
header('Content-Type: application/json');
|
||
echo json_encode([
|
||
'status' => 'success',
|
||
'theme' => 'success',
|
||
'header' => 'Обновлено',
|
||
'message' => 'Данные успешно сохранены'
|
||
]);
|
||
exit;
|
||
}
|
||
exit;
|
||
}
|
||
|
||
if (!$row) {
|
||
$AVE_Template->assign('editfalse', 1);
|
||
} else {
|
||
$row['file_list'] = !empty($row['comment_file']) ? explode(',', $row['comment_file']) : [];
|
||
$AVE_Template->assign('row', $row);
|
||
$AVE_Template->assign('edit_rating_enabled', $this->_edit_avtor_rating);
|
||
$AVE_Template->assign('comment_max_chars', (int)$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, $sess;
|
||
|
||
$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_allow_self_answer = $_POST['comment_allow_self_answer'] ?? 0;
|
||
|
||
// Порядок сортировки
|
||
$post_sort_order = $_POST['comment_sort_order'] ?? 'ASC';
|
||
|
||
$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;
|
||
$post_ajax_replies_limit = $_POST['comment_ajax_replies_limit'] ?? 5;
|
||
|
||
// Настройки рейтинга и файлов
|
||
$post_allow_files = $_POST['comment_allow_files'] ?? 0;
|
||
$post_allow_files_anon = $_POST['comment_allow_files_anon'] ?? 0;
|
||
|
||
/* Настройки расширений и максимального размера файла */
|
||
$post_allowed_extensions = $_POST['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif';
|
||
$post_max_file_size = $_POST['comment_max_file_size'] ?? 2048;
|
||
$post_max_files = $_POST['comment_max_files'] ?? 5;
|
||
|
||
// --- НАСТРОЙКА лога загрузки опасных файлов ---
|
||
$post_log_dangerous = $_POST['comment_log_dangerous'] ?? 0;
|
||
|
||
$post_rating_type = $_POST['comment_rating_type'] ?? 0;
|
||
$post_show_user_rating = $_POST['comment_show_user_rating'] ?? 0;
|
||
|
||
/* Настройка показа авторской оценки в ответах */
|
||
$post_show_user_rating_replies = $_POST['comment_show_user_rating_replies'] ?? 0;
|
||
|
||
$post_rating_anon_vote = $_POST['comment_rating_anon_vote'] ?? 0;
|
||
$post_rating_anon_set = $_POST['comment_rating_anon_set'] ?? 0;
|
||
|
||
// настройки времени и куки
|
||
$post_edit_time = $_POST['comment_edit_time'] ?? 60;
|
||
$post_cookie_life = $_POST['comment_cookie_life'] ?? 30;
|
||
|
||
// Дополнительные поля
|
||
$post_show_f1 = $_POST['comment_show_f1'] ?? 0;
|
||
$post_req_f1 = $_POST['comment_req_f1'] ?? 0;
|
||
$post_name_f1 = $_POST['comment_name_f1'] ?? '';
|
||
|
||
$post_show_f2 = $_POST['comment_show_f2'] ?? 0;
|
||
$post_req_f2 = $_POST['comment_req_f2'] ?? 0;
|
||
$post_name_f2 = $_POST['comment_name_f2'] ?? '';
|
||
|
||
// Обработка сохранения
|
||
if ($request_sub == 'save' || $request_sub == 'apply')
|
||
{
|
||
$max_chars = (empty($post_max_chars) || $post_max_chars < 50) ? 50 : $post_max_chars;
|
||
|
||
// Валидация сортировки (ASC, DESC или RATING)
|
||
$allowed_sorts = ['ASC', 'DESC', 'RATING', 'USER_RATING'];
|
||
$sort_order = in_array(strtoupper($post_sort_order), $allowed_sorts) ? strtoupper($post_sort_order) : 'ASC';
|
||
|
||
// Подготовка имен полей и настроек файлов
|
||
$clean_name_f1 = htmlspecialchars(stripslashes($post_name_f1), ENT_QUOTES);
|
||
$clean_name_f2 = htmlspecialchars(stripslashes($post_name_f2), ENT_QUOTES);
|
||
|
||
// Очищаем строку расширений от пробелов и лишних запятых
|
||
$clean_extensions = implode(',', array_filter(array_map('trim', explode(',', $post_allowed_extensions))));
|
||
|
||
$AVE_DB->Query("
|
||
UPDATE " . PREFIX . "_module_comments
|
||
SET
|
||
comment_max_chars = '" . (int)$max_chars . "',
|
||
comment_user_groups = '" . addslashes(implode(',', $post_user_groups)) . "',
|
||
comment_user_groups_read = '" . addslashes(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 . "',
|
||
comment_ajax_replies_limit = '" . (int)$post_ajax_replies_limit . "',
|
||
comment_allow_self_answer = '" . (int)$post_allow_self_answer . "',
|
||
comment_sort_order = '" . $sort_order . "',
|
||
comment_allow_files = '" . (int)$post_allow_files . "',
|
||
comment_allow_files_anon = '" . (int)$post_allow_files_anon . "',
|
||
comment_allowed_extensions = '" . addslashes($clean_extensions) . "',
|
||
comment_max_file_size = '" . (int)$post_max_file_size . "',
|
||
comment_max_files = '" . (int)$post_max_files . "',
|
||
comment_log_dangerous = '" . (int)$post_log_dangerous . "',
|
||
comment_rating_type = '" . (int)$post_rating_type . "',
|
||
comment_show_user_rating = '" . (int)$post_show_user_rating . "',
|
||
comment_show_user_rating_replies = '" . (int)$post_show_user_rating_replies . "',
|
||
comment_rating_anon_vote = '" . (int)$post_rating_anon_vote . "',
|
||
comment_rating_anon_set = '" . (int)$post_rating_anon_set . "',
|
||
comment_edit_time = '" . (int)$post_edit_time . "',
|
||
comment_cookie_life = '" . (int)$post_cookie_life . "',
|
||
comment_show_f1 = '" . (int)$post_show_f1 . "',
|
||
comment_req_f1 = '" . (int)$post_req_f1 . "',
|
||
comment_name_f1 = '" . addslashes($clean_name_f1) . "',
|
||
comment_show_f2 = '" . (int)$post_show_f2 . "',
|
||
comment_req_f2 = '" . (int)$post_req_f2 . "',
|
||
comment_name_f2 = '" . addslashes($clean_name_f2) . "'
|
||
WHERE
|
||
Id = 1
|
||
");
|
||
|
||
if ($request_sub == 'apply') {
|
||
@ob_clean();
|
||
echo "success";
|
||
exit;
|
||
}
|
||
|
||
if ($request_sub == 'save') {
|
||
header("Location: index.php?do=modules&cp=" . $sess);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// Получаем данные для отображения в шаблоне
|
||
$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));
|
||
}
|
||
|
||
|
||
/**
|
||
* Метод для получения последних X комментариев
|
||
*/
|
||
function getLatestComments($limit = 10, $chars = 150)
|
||
{
|
||
global $AVE_DB;
|
||
|
||
// 1. Принудительно подключаем CSS
|
||
$GLOBALS['user_header']['comment_css'] = '<link rel="stylesheet" href="' . ABS_PATH . 'modules/comment/css/mod_comment_styles.css" type="text/css" />';
|
||
|
||
$sql = $AVE_DB->Query("
|
||
SELECT
|
||
comm.*,
|
||
doc.document_title,
|
||
doc.document_alias
|
||
FROM " . PREFIX . "_module_comment_info AS comm
|
||
LEFT JOIN " . PREFIX . "_documents AS doc ON doc.Id = comm.document_id
|
||
WHERE comm.comment_status = '1'
|
||
ORDER BY comm.comment_published DESC
|
||
LIMIT " . (int)$limit
|
||
);
|
||
|
||
$items = array();
|
||
if ($sql && $sql->NumRows() > 0) {
|
||
while ($res = $sql->FetchAssocArray()) {
|
||
$row = array_change_key_case($res, CASE_LOWER);
|
||
|
||
// Обработка текста комментария
|
||
$row['comment_text'] = mb_strimwidth(strip_tags($row['comment_text'] ?? ''), 0, (int)$chars, "...");
|
||
$row['date'] = ave_date_format(get_settings('date_format'), $row['comment_published']);
|
||
|
||
// Логика аватара (системный или буквенный)
|
||
if (isset($row['comment_author_id']) && $row['comment_author_id'] > 0) {
|
||
$row['avatar'] = function_exists('getAvatar') ? getAvatar($row['comment_author_id'], 48) : '';
|
||
} else {
|
||
$row['avatar'] = '';
|
||
}
|
||
|
||
if (empty($row['avatar']) || strpos($row['avatar'], 'user.png') !== false) {
|
||
$row['avatar'] = '';
|
||
$name = !empty($row['comment_author_name']) ? stripslashes($row['comment_author_name']) : 'Guest';
|
||
$row['first_letter'] = mb_strtoupper(mb_substr(trim($name), 0, 1, 'UTF-8'));
|
||
$row['avatar_color_index'] = (abs(crc32($name)) % 12) + 1;
|
||
}
|
||
|
||
// Авторские звезды
|
||
$user_rating = (int)($row['user_rating'] ?? 0);
|
||
$stars_html = '';
|
||
if ($user_rating > 0) {
|
||
for ($i = 1; $i <= 5; $i++) {
|
||
if ($i <= $user_rating) {
|
||
$stars_html .= '<i class="bi bi-star-fill text-warning me-1" style="font-size: 0.8rem;"></i>';
|
||
} else {
|
||
$stars_html .= '<i class="bi bi-star text-muted me-1" style="font-size: 0.8rem; opacity: 0.5;"></i>';
|
||
}
|
||
}
|
||
}
|
||
$row['stars'] = $stars_html;
|
||
|
||
// Формирование ссылки
|
||
$doc_id = (int)$row['document_id'];
|
||
$alias = (!empty($row['document_alias']) && $row['document_alias'] != '/') ? $row['document_alias'] : '';
|
||
$raw_url = "index.php?id=" . $doc_id . "&doc=" . $alias . "/";
|
||
|
||
$final_url = function_exists('rewrite_link') ? rewrite_link($raw_url) : $alias;
|
||
|
||
// Чистим слеши
|
||
$final_url = preg_replace('/(?<!:)\/\//', '/', str_ireplace(['"//"', '///'], ['/', '/'], $final_url));
|
||
$final_url = rtrim($final_url, '/');
|
||
|
||
// ФИКС ГЛАВНОЙ: если ссылка пустая после rtrim, значит это корень сайта
|
||
if (empty($final_url)) {
|
||
$final_url = '/';
|
||
}
|
||
|
||
// Собираем финальный URL с якорем
|
||
$row['link'] = $final_url . '#comment_wrapper_' . ($row['id'] ?? 0);
|
||
|
||
// Дополнительная проверка корректности начала ссылки
|
||
if (isset($row['link'][0]) && $row['link'][0] == '#') {
|
||
$row['link'] = '/' . $row['link'];
|
||
}
|
||
|
||
$row['document_title'] = !empty($row['document_title']) ? $row['document_title'] : 'Документ #' . $doc_id;
|
||
|
||
$items[] = $row;
|
||
}
|
||
}
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* Отображение логов безопасности (security_upload.log)
|
||
*/
|
||
public function commentAdminLogsShow($tpl_dir)
|
||
{
|
||
global $AVE_Template, $AVE_DB;
|
||
$log_file = BASE_DIR . '/modules/comment/logs/security_upload.log';
|
||
$logs = [];
|
||
|
||
if (file_exists($log_file)) {
|
||
$file_content = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
if ($file_content) {
|
||
foreach ($file_content as $line) {
|
||
preg_match('/^\[(.*?)\]/', $line, $date_match);
|
||
|
||
$get_val = function($key) use ($line) {
|
||
if (preg_match('/' . $key . ':\s*(.*?)\s*($|\|)/', $line, $m)) {
|
||
return trim($m[1]);
|
||
}
|
||
return '';
|
||
};
|
||
|
||
$uid = (int)$get_val('UID');
|
||
$comm_id = (int)$get_val('COMMENT_ID');
|
||
$email = $get_val('EMAIL');
|
||
$full_status = $get_val('STATUS');
|
||
|
||
// Короткое название для колонки "Действие"
|
||
$short_action = (strpos($full_status, 'СОЗДАНИИ') !== false) ? 'Создание' : 'Правка';
|
||
|
||
// Текст ошибки без префикса
|
||
$error_reason = trim(preg_replace('/^.*?:/', '', $full_status));
|
||
|
||
if (empty($email) && $comm_id > 0) {
|
||
$sql = "SELECT comment_author_email FROM " . PREFIX . "_module_comment_info WHERE Id = '" . $comm_id . "' LIMIT 1";
|
||
$result = $AVE_DB->Query($sql);
|
||
if ($result) {
|
||
$c_row = $result->FetchAssocArray();
|
||
if (!empty($c_row['comment_author_email'])) {
|
||
$email = $c_row['comment_author_email'];
|
||
}
|
||
}
|
||
}
|
||
|
||
$logs[] = [
|
||
'date' => $date_match[1] ?? '',
|
||
'ip' => $get_val('IP'),
|
||
'user_id' => $uid,
|
||
'comm_id' => $comm_id ?: '',
|
||
'author' => $get_val('AUTHOR') ?: $get_val('AUTHOR_DB'),
|
||
'email' => $email,
|
||
'status' => $short_action, // ВЕРНУЛ КЛЮЧ STATUS (теперь это "Создание" или "Правка")
|
||
'reason' => $error_reason, // Детали ошибки
|
||
'file' => $get_val('FILE'),
|
||
'mime' => $get_val('MIME'),
|
||
'size' => $get_val('SIZE')
|
||
];
|
||
}
|
||
}
|
||
$logs = array_reverse($logs);
|
||
}
|
||
|
||
$AVE_Template->assign(['logs' => $logs, 'sess' => SESSION]);
|
||
$AVE_Template->display($tpl_dir . 'view_logs.tpl');
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Очистка файла security_upload.log
|
||
*/
|
||
public function commentAdminClearLogs()
|
||
{
|
||
$log_file = BASE_DIR . '/modules/comment/logs/security_upload.log';
|
||
if (file_exists($log_file)) {
|
||
@unlink($log_file);
|
||
}
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['status' => 'success']);
|
||
exit;
|
||
}
|
||
|
||
}
|
||
|
||
?>
|