в админку добавлено управление логом

This commit is contained in:
2026-01-27 14:08:53 +05:00
parent bfda704956
commit 52d718d874
9 changed files with 381 additions and 10 deletions

View File

@@ -53,7 +53,15 @@
* Шаблоны (файлы .tpl) отвечающие за вывод в публичную часть:
* comments_tree.tpl - выводит на страницу форму "Написать комментарий"
* comments_tree_sub.tpl - выводит на страницу кооментарий (ветку комментариев)
* last_comments.tpl - выводит на страницу виджет "Последние комментарии"
* last_comments.tpl - выводит на страницу виджет "Последние комментарии"
* Лог загрузки опасных файлов:
* Вы можете вкл/выкл запись лога загрузки опасных файлов. При включенном режиме в Админ панели в списке комментариев появится ссылка "Просмотреть логи безопасности", при новых записях будет включен оранжевый индикатор и текст ссылки будет изменен на "Есть новые записи в логах!". Основные типы записи в лог:
* Обнаружена подмена (Fake Image)
* Вредоносный тип контента (MIME)
* Опасный формат файла
* Файл слишком большой: Превышение лимита
* Пустой файл: Попытка загрузки файла нулевого размера.
* Запрещенное расширение: Файл не прошел по списку разрешенных расширений.
* Система тегов:
* Тег [mod_comment] (Без параметров), - Это основной системный тег для вывода полноценного функционала на странице документа, выводит форму создания нового комментария + сами комментарии.
* Тег [mod_comment:X] (Один параметр) - Выводит виджет последних комментариев, где X - количество выводимых комментариев, количество символов текста комментария равно 150 по умолчанию.

View File

@@ -241,6 +241,13 @@ function _commentSettingsGet($param = '')
*/
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);
@@ -1901,6 +1908,30 @@ $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> ') : '',
@@ -2096,6 +2127,9 @@ function commentAdminSettingsEdit($tpl_dir)
$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;
@@ -2152,7 +2186,8 @@ function commentAdminSettingsEdit($tpl_dir)
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_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 . "',
@@ -2284,6 +2319,87 @@ function getLatestComments($limit = 10, $chars = 150)
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;
}
}
?>

View File

@@ -3,7 +3,7 @@ if (!defined('BASE_DIR')) exit;
$module = array(
'ModuleSysName' => 'comment',
'ModuleVersion' => '3.33',
'ModuleVersion' => '3.34',
'ModuleAutor' => 'Repellent',
'ModuleCopyright' => '&copy; 2025-' . date('Y') . ' ave4cms.ru',
'ModuleStatus' => 1,

View File

@@ -193,6 +193,31 @@ COMMENT_FILES_ALLOWED_EXT = "Допустимые расширения:"
COMMENT_FILES_MAX_SIZE = "Макс. размер файла (Кб):"
COMMENT_FILES_MAX_COUNT = "Макс. кол-во файлов:"
COMMENT_FILES_MAX_COUNT_TEXT = "шт. на один комментарий"
COMMENT_FILES_LOG_TITEL = "Включить лог загрузки опасных файлов"
COMMENT_FILES_LOG_TITEL_A = "Вести запись попыток"
COMMENT_FILES_LOG_TITEL_B = "Журнал безопасности загрузок"
COMMENT_FILES_LOG_TITEL_C = "Есть новые записи в логах!"
COMMENT_FILES_LOG_TITEL_D = "Просмотреть логи безопасности"
COMMENT_FILES_LOG_MODAL_A = "Дата / Время"
COMMENT_FILES_LOG_MODAL_B = "IP адрес"
COMMENT_FILES_LOG_MODAL_C = "ID Авт."
COMMENT_FILES_LOG_MODAL_D = "ID Ком."
COMMENT_FILES_LOG_MODAL_E = "Автор"
COMMENT_FILES_LOG_MODAL_F = "Email адрес"
COMMENT_FILES_LOG_MODAL_G = "Действие"
COMMENT_FILES_LOG_MODAL_H = "Причина блокировки загрузки файла"
COMMENT_FILES_LOG_MODAL_I = "не указан"
COMMENT_FILES_LOG_MODAL_J = "Файл:"
COMMENT_FILES_LOG_MODAL_K = "Журнал пуст. Подозрительных действий не зафиксировано."
COMMENT_FILES_LOG_MODAL_L = "Очистить лог"
COMMENT_FILES_LOG_MODAL_M = "Закрыть окно"
COMMENT_FILES_LOG_MODAL_N = "Вы уверены, что хотите полностью очистить файл лога безопасности?"
COMMENT_FILES_LOG_MODAL_O = "Подтверждение очистки"
COMMENT_FILES_LOG_MODAL_P = "Журнал успешно очищен."
COMMENT_FILES_LOG_MODAL_R = "Просмотреть логи безопасности"
COMMENT_FILES_LOG_MODAL_S = "Ошибка"
COMMENT_PAGE_NAV_COUNT = "Кол-во комментариев на странице (родителей)"

View File

@@ -171,6 +171,20 @@ if (defined('ACP') && !empty($_REQUEST['moduleaction']))
$comment_id = $_REQUEST['id'] ?? $_REQUEST['Id'];
$comment->commentAdminDelete($comment_id);
break;
case 'view_logs':
$comment->commentAdminLogsShow($tpl_dir); // Называем его так
break;
case 'clear_logs':
$comment->commentAdminClearLogs();
break;
case 'mark_log_read':
if (isset($_SESSION['user_group']) && $_SESSION['user_group'] == 1) {
$_SESSION['last_log_view_time'] = time();
echo json_encode(['status' => 'ok']);
}
exit;
}
}

View File

@@ -4,6 +4,7 @@
/**
* AVE.cms - Модуль Комментарии
* Исправлено: Поддержка Emoji (utf8mb4)
* Добавлено: Управление логом опасных файлов
*/
$module_sql_install = array();
@@ -48,6 +49,7 @@
`comment_ajax_replies_limit` tinyint(3) NOT NULL DEFAULT '5',
`comment_allow_self_answer` tinyint(1) NOT NULL DEFAULT '0',
`comment_sort_order` varchar(20) NOT NULL DEFAULT 'ASC',
`comment_log_dangerous` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci PACK_KEYS=0 AUTO_INCREMENT=1;
";
@@ -98,7 +100,11 @@
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci PACK_KEYS=0 AUTO_INCREMENT=1;
";
$module_sql_install[] = "INSERT INTO `%%PRFX%%_module_comments` VALUES (1, 1000, '1,3', '1,2,3,4', '0', '1', '1' , '0', '', 1, 0, '', 1, 0, '', 0, 'jpg,jpeg,png,gif,webp', 2048, 5, 0, 1, 0, 0, 0, 0, 60, 30, 5, 0, 'ASC');";
// Добавлена 0 в конце (для comment_log_dangerous)
$module_sql_install[] = "INSERT INTO `%%PRFX%%_module_comments` VALUES (1, 1000, '1,3', '1,2,3,4', '0', '1', '1' , '0', '', 1, 0, '', 1, 0, '', 0, 'jpg,jpeg,png,gif,webp', 2048, 5, 0, 1, 0, 0, 0, 0, 60, 30, 5, 0, 'ASC', 0);";
// Если нужно добавить поле в уже существующую таблицу при обновлении
$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD `comment_log_dangerous` tinyint(1) NOT NULL DEFAULT '0';";
$module_sql_update[] = "
UPDATE `%%PRFX%%_module`

View File

@@ -229,7 +229,47 @@
.tfoot {
border-top: 1px solid #CBD5DD;
}
}
/* Индикатор логов безопасности */
.log-indicator-container {
display: flex;
align-items: center;
padding: 10px 15px;
background: #fdfdfd;
border: 1px dashed #cbd5dd;
border-radius: 4px;
margin-top: 15px;
}
.log-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.log-dot-gray { background-color: #bdc3c7; }
.log-dot-new {
background-color: #f39c12;
box-shadow: 0 0 8px #f39c12;
animation: log-blink 1.2s infinite;
}
@keyframes log-blink {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}
.log-link {
font-size: 13px;
font-weight: bold;
text-decoration: none;
color: #34495e;
}
.log-link:hover { color: #2980b9; }
</style>
@@ -551,15 +591,30 @@
</tbody>
</table>
{if !empty($docs)}
<div class="tfoot">
<div class="left" style="padding:15px;">
<div class="tfoot" style="display: flex; align-items: center; justify-content: flex-start;">
<div class="left" style="padding:15px; display: flex; align-items: center;">
<select id="mass_action_select">
<option value="">{#COMMENT_ACTION_SELECT#}</option>
<option value="set_status_1">{#COMMENT_ACTION_SELECT_PUB#}</option>
<option value="set_status_0">{#COMMENT_ACTION_HIDE#}</option>
<option value="delete">{#COMMENT_ACTION_DEL#}</option>
</select>
<input type="button" value="{#COMMENT_BUTTON_APPLY#}" class="blueBtn" onclick="runMassAction()" style="margin-left:10px;" />
<input type="button" value="{#COMMENT_BUTTON_APPLY#}" class="blueBtn" onclick="runMassAction()" style="margin-left:10px; margin-right: 20px;" />
{if $log_enabled}
<div class="log-indicator-container" style="margin-top: 0; padding: 0; background: none; border: none;">
<span class="log-dot {if $has_new_logs}log-dot-new{else}log-dot-gray{/if}"></span>
<a href="index.php?do=modules&action=modedit&mod=comment&moduleaction=view_logs&cp={$sess}&pop=1&onlycontent=1"
class="openDialog log-link"
data-dialog="security-logs"
data-width=""
data-height="700"
data-modal="true"
data-title="{#COMMENT_FILES_LOG_TITEL_B#}">
{if $has_new_logs}<strong>{#COMMENT_FILES_LOG_TITEL_C#}</strong>{else}{#COMMENT_FILES_LOG_TITEL_D#}{/if}
</a>
</div>
{/if}
</div>
</div>
{/if}

View File

@@ -277,8 +277,13 @@
<input name="comment_max_files" type="text" value="{$comment_max_files|default:'5'}" size="4" style="width: 50px;" />
<span style="color: #888; font-size: 11px; margin-left: 5px;">{#COMMENT_FILES_MAX_COUNT_TEXT#}</span>
</td>
<td></td>
<td></td>
<td>{#COMMENT_FILES_LOG_TITEL#}</td>
<td>
<label style="cursor: pointer; display: flex; align-items: center; font-weight: normal;">
<input name="comment_log_dangerous" type="checkbox" value="1" {if $comment_log_dangerous=='1'}checked{/if} style="margin-right: 10px;" />
&nbsp;{#COMMENT_FILES_LOG_TITEL_A#}
</label>
</td>
</tr>
<tr>

142
templates/view_logs.tpl Normal file
View File

@@ -0,0 +1,142 @@
<style>
{literal}
.log-table { font-size: 12px !important; font-family: Tahoma, Arial, sans-serif !important; table-layout: fixed; }
.log-table thead td { background: #f5f5f5; font-weight: bold; border-bottom: 1px solid #ccc; padding: 12px 5px !important; color: #333; text-align: center; }
.log-table tbody td { padding: 10px 5px !important; border-bottom: 1px solid #eee; vertical-align: middle; color: #444; text-align: center; word-wrap: break-word; }
.log-table tbody td.details-col { text-align: left; padding-left: 15px !important; }
.status-badge { background: #666; color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold; display: inline-block; }
.status-create { background: #458045; }
.status-edit { background: #4d7298; }
.reason-text { color: #d9534f; font-weight: bold; font-size: 11px; display: block; margin-bottom: 4px; line-height: 1.3; }
.file-box { font-size: 11px; color: #005580; background: #f0f4f7; padding: 4px 8px; border-radius: 2px; border: 1px solid #d0dbe4; display: inline-block; }
.no-data { color: #ccc; font-style: italic; }
{/literal}
</style>
<div class="widget first" style="margin:0; border:none;">
<div class="whead">
<h6><i class="icon-shield"></i> {#COMMENT_FILES_LOG_TITEL_B#}</h6>
<div class="clear"></div>
</div>
<div id="logs_container" style="max-height:550px; overflow-y:auto;">
{if $logs}
<table cellpadding="0" cellspacing="0" width="100%" class="tableStatic log-table">
<thead>
<tr>
<td width="130">{#COMMENT_FILES_LOG_MODAL_A#}</td>
<td width="95">{#COMMENT_FILES_LOG_MODAL_B#}</td>
<td width="60">{#COMMENT_FILES_LOG_MODAL_C#}</td>
<td width="60">{#COMMENT_FILES_LOG_MODAL_D#}</td>
<td width="120">{#COMMENT_FILES_LOG_MODAL_E#}</td>
<td width="150">{#COMMENT_FILES_LOG_MODAL_F#}</td>
<td width="100">{#COMMENT_FILES_LOG_MODAL_G#}</td>
<td>{#COMMENT_FILES_LOG_MODAL_H#}</td>
</tr>
</thead>
<tbody>
{foreach from=$logs item=log}
<tr>
<td>{$log.date}</td>
<td><strong>{$log.ip}</strong></td>
<td>{$log.user_id|default:"0"}</td>
<td>{$log.comm_id|default:"—"}</td>
<td><b style="color:#c00;">{$log.author|default:"—"}</b></td>
<td>{$log.email|default:"<span class='no-data'>{#COMMENT_FILES_LOG_MODAL_I#}</span>"}</td>
<td>
<span class="status-badge {if $log.status == 'Создание'}status-create{else}status-edit{/if}">
{$log.status}
</span>
</td>
<td class="details-col">
<span class="reason-text">{$log.reason}</span>
<div class="file-box">
<b>{#COMMENT_FILES_LOG_MODAL_J#}</b> {$log.file} <span style="color:#777; font-weight:normal;">({$log.mime})</span>
</div>
</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<div style="padding:60px; text-align:center; color:#999;">
<h4>{#COMMENT_FILES_LOG_MODAL_K#}</h4>
</div>
{/if}
</div>
<div class="formBar" style="padding:15px; border-top:1px solid #ddd; background:#f9f9f9;">
<div style="float:left;">
{if $logs}
<a href="javascript:void(0);" class="button redBtn" id="clear_log_btn"><span>{#COMMENT_FILES_LOG_MODAL_L#}</span></a>
{/if}
</div>
<div style="float:right;">
<a href="javascript:void(0);" class="button blueBtn CloseLogDialog"><span>Закрыть окно</span></a>
</div>
<div class="clear"></div>
</div>
</div>
<script type="text/javascript">
{literal}
$(document).ready(function(){
// Очистка лога через системный jConfirm
$('#clear_log_btn').off('click').on('click', function(e){
e.preventDefault();
jConfirm('Вы уверены, что хотите полностью очистить файл лога безопасности?', 'Подтверждение очистки', function(r) {
if(r) {
$.ajax({
url: 'index.php?do=modules&action=modedit&mod=comment&moduleaction=clear_logs&cp={/literal}{$sess}{literal}',
type: 'POST',
success: function(){
$('#logs_container').html('<div style="padding:60px; text-align:center;"><h4 style="color:#999;">Журнал успешно очищен.</h4></div>');
$('#clear_log_btn').fadeOut();
if(window.parent) {
$('.log-dot', window.parent.document).removeClass('log-dot-new').addClass('log-dot-gray');
$('.log-link strong', window.parent.document).text('Просмотреть логи безопасности');
}
},
error: function() {
jAlert('Произошла ошибка при очистке лога', 'Ошибка');
}
});
}
});
});
// Универсальное закрытие (для jQuery UI Dialog и Colorbox)
// Универсальное закрытие с "гашением" желтого кружка
$(".CloseLogDialog").on('click', function(e){
e.preventDefault();
// Сначала помечаем логи как прочитанные в сессии
$.ajax({
url: 'index.php?do=modules&action=modedit&mod=comment&moduleaction=mark_log_read&cp={/literal}{$sess}{literal}',
type: 'GET',
complete: function() {
// Когда сессия обновилась, закрываем окно
if($.isFunction($.colorbox)) {
$.colorbox.close();
} else {
var $dialog = $(".CloseLogDialog").closest('.ui-dialog-content');
if ($dialog.length) $dialog.dialog('close');
}
// Перезагружаем страницу админки, чтобы PHP убрал желтый цвет
// Если лог открыт в модальном окне, обновляем родителя (window.parent)
if (window.parent && window.parent.location !== window.location) {
window.parent.location.reload();
} else {
window.location.reload();
}
}
});
});
});
{/literal}
</script>