_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'] = ''; // --- ОПРЕДЕЛЯЕМ ТЕКУЩЕГО ПОЛЬЗОВАТЕЛЯ --- $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 = '{t} '; $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('/(? $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 = ""; @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. ПРЕВРАЩАЕМ теги в текст (экранируем), а не вырезаем их // Теперь 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); // Теперь заменяем
(если они есть) на \n, но так, чтобы не создавать лишних пустых строк $comment_text = preg_replace('/\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 = '' . htmlspecialchars($display_name) . ''; } 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('/(? $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', ' {t} ') : '', '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', " 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'] = ''; $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 .= ''; } else { $stars_html .= ''; } } } $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('/(? 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; } } ?>