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

'; + var charsLeft = '

'+COMMENT_CHARS_LEFT+'

'; + var buttonSave = ' '; + var buttonReset = ''; + $(this).after('
'+COMMENT_EDIT_TITLE + +''+textarea+buttonSave+buttonReset+charsLeft+'
').remove(); + $('.saveButton').click(function(){saveChanges(this,false,cid);}); + $('.cancelButton').click(function(){saveChanges(this,revert,cid);}); + $('#ta_'+cid).limit(MAX_CHARS,'#charsLeft_'+cid); + }) + .attr('title',COMMENT_EDIT_LINK) + .mouseover(function(){$(this).addClass('editable');}) + .mouseout(function(){$(this).removeClass('editable');}); + $('#in_message').limit(MAX_CHARS,'#charsLeft_new'); + }; + + // ИСПРАВЛЕННАЯ ФУНКЦИЯ saveChanges + function saveChanges(obj,cancel,cid){ + if (!cancel){ + var t = $(obj).parent().children().children().val(); + + $.ajax({ + url: aveabspath+'index.php?ajax=1', + type: 'POST', + data: { + module: 'comment', + action: 'edit', + Id: cid, + text: t + }, + success: function(txt){ + $(obj).parent().parent().parent().after('
'+txt+'
').remove(); + var now = new Date(); + var date = now.toLocaleString(); + $('#'+cid).find('.mod_comment_changed').html(' ('+COMMENT_TEXT_CHANGED+' '+date+')'); + setClickable(); + }, + error: function(xhr, status, error) { + console.error("AJAX Error (Edit):", status, error); + alert("Ошибка при редактировании комментария. Пожалуйста, проверьте консоль."); + } + }); + } + else { + $(obj).parent().parent().parent().after('
'+cancel+'
').remove(); + setClickable(); + } + }; + + function displayNewComment(data){ + if (data=='wrong_securecode'){ + $('#captcha').after('
'+COMMENT_WRONG_CODE+'
'); + $('#securecode').focus(); + } + else { + $('#end'+$('#parent_id').val()).before(data); + $('#parent_id').val(''); + $('#mod_comment_new form')[0].reset(); + // --- Перемещаем форму обратно в конец. УБРАНО .hide() для сохранения видимости. + $('#mod_comment_new').insertAfter('#end'); + setClickable(); + } + getCaptha(); + }; + + $(document).ready(function(){ + + setClickable(); + + // Делегирование событий + $(document).on('click', '#captcha img', function(){getCaptha();}); + + $(document).on('click', '.mod_comment_answer', function(){cAction(this,'answer');}); + + $(document).on('click', '.mod_comment_edit', function(){ + var cid = $(this).parents('.mod_comment_box').attr('id'); + var commentTextBlock = $('#'+cid).find('.mod_comment_text'); + var revert = commentTextBlock.html(); + + var textarea = '

'; + var charsLeft = '

'+COMMENT_CHARS_LEFT+'

'; + var buttonSave = ' '; + var buttonReset = ''; + + commentTextBlock.after( + '
'+COMMENT_EDIT_TITLE + +''+textarea+buttonSave+buttonReset+charsLeft+'
' + ).remove(); + + $('.saveButton').click(function(){saveChanges(this,false,cid);}); + $('.cancelButton').click(function(){saveChanges(this,revert,cid);}); + $('#ta_'+cid).limit(MAX_CHARS,'#charsLeft_'+cid); + }); + + if (UGROUP == 1) { + $(document).on('click', '.mod_comment_delete', function(){ + if (confirm('Вы уверены, что хотите удалить комментарий?')) { + cAction(this, 'delete'); + } + }); + + $(document).on('click', '.mod_comment_lock', function(){cAction(this, 'lock');}); + $(document).on('click', '.mod_comment_unlock', function(){cAction(this, 'unlock');}); + + $(document).on('click', '#mod_comment_open', function(){cAction(this,'open');}); + $(document).on('click', '#mod_comment_close', function(){cAction(this,'close');}); + } + + // Отправка формы (AJAX) - ИСПРАВЛЕНИЕ ДВОЙНОЙ ОТПРАВКИ + $('#mod_comment_new form').on('submit', function(e){ + e.preventDefault(); + + var form = $(this); + if (!validate(null, form, null)) { + return false; + } + + var submitButton = form.find('input[type="submit"], button[type="submit"]').first(); + var originalButtonText = submitButton.val(); + + var formData = form.serialize(); + + $.ajax({ + url: aveabspath+'index.php?ajax=1', + type: 'POST', + data: formData, + dataType: 'html', + + // Отключаем кнопку перед отправкой + beforeSend: function() { + submitButton.prop('disabled', true).val('Отправка...'); + }, + + // Включаем кнопку обратно и обрабатываем ответ + success: function(data) { + displayNewComment(data); + submitButton.prop('disabled', false).val(originalButtonText); + }, + timeout: 3000, + error: function(xhr, status, error) { + console.error("AJAX Error (Creation):", status, error); + alert("Ошибка при отправке комментария. Пожалуйста, проверьте консоль."); + // Включаем кнопку обратно в случае ошибки + submitButton.prop('disabled', false).val(originalButtonText); + } + }); + return false; + }); + + // Сброс формы + $('#buttonReset').click(function(){ + $('#parent_id').val(''); + $('#mod_comment_new form')[0].reset(); + $('#mod_comment_new').insertAfter('#end'); + }); + }); + + })(jQuery); + } +})(); \ No newline at end of file diff --git a/lang/index.php b/lang/index.php new file mode 100644 index 0000000..5ca71c3 --- /dev/null +++ b/lang/index.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/lang/ru.txt b/lang/ru.txt new file mode 100644 index 0000000..0b482a7 --- /dev/null +++ b/lang/ru.txt @@ -0,0 +1,92 @@ +[name] +MODULE_NAME = "Комментарии" +MODULE_DESCRIPTION = "Данный модуль предназначен для организации системы комментариев для документов на сайте. Для того, чтобы использовать данный модуль, разместите системный тег [mod_comment] в нужном месте шаблона рубрики." + +[module] +COMMENT_EDIT_TITLE = "Редактирование комментария" +COMMENT_IS_CLOSED = "Редактирование не возможно, поскольку обсуждение закрыто." +COMMENT_CLOSE_BUTTON = "Закрыть окно" +COMMENT_EDIT_FALSE = "Извините, но вы не имеете прав для редактирования." +COMMENT_YOUR_NAME = "* Имя:" +COMMENT_YOUR_EMAIL = "* E-mail:" +COMMENT_YOUR_SITE = "Веб-сайт:" +COMMENT_YOUR_FROM = "Город:" +COMMENT_YOUR_TEXT = "* Комментарий:" +COMMENT_BUTTON_EDIT = "Сохранить изменения" +COMMENT_BUTTON_CANCEL = "Отменить" +COMMENT_BUTTON_RESET = "Очистить" +COMMENT_NEW_TITLE = "Написать комментарий" +COMMENT_NEW_CLOSED = "Извините, но Вы не можете добавить комментарий, поскольку обсуждение закрыто." +COMMENT_NEW_FALSE = "Извините, но у Вас не достаточно прав для добавления комментария." +COMMENT_READ_FALSE = "Извините, но у Вас не достаточно прав для просмотра комментариев." +COMMENT_CHARS_LEFT = "Количество оставшихся символов" +COMMENT_BUTTON_ADD = "Добавить комментарий" +COMMENT_INFO = "Информация о пользователе" +COMMENT_USER_NAME = "Имя пользователя:" +COMMENT_DATE_CREATE = "Опубликовано:" +COMMENT_USER_EMAIL = "E-mail пользователя:" +COMMENT_USER_SITE = "Личный сайт:" +COMMENT_USER_FROM = "Откуда:" +COMMENT_USER_COMMENTS = "Оставил комментариев:" +COMMENT_THANKYOU_TITLE = "Спасибо за комментарий" +COMMENT_THANKYOU_TEXT = "Ваш комментарий успешно добавлен." +COMMENT_SITE_TITLE = "Комментарии пользователей" +COMMENT_SITE_CLOSED = "(комментирование временно отключено)" +COMMENT_SITE_ADD = "Добавить комментарий" +COMMENT_LAST_COMMENT = "Последний комментарий" +COMMENT_SITE_CLOSE = "Запретить комментирование" +COMMENT_SITE_OPEN = "Разрешить комментирование" +COMMENT_USER_ADD = "Опубликовано:" +COMMENT_USER_TIME = "в" +COMMENT_ANSWER_LINK = "Ответить на этот комментарий" +COMMENT_EDIT_LINK = "Редактировать комментарий" +COMMENT_LOCK_LINK = "Заблокировать ответы" +COMMENT_DELETE_LINK = "Удалить комментарий" +COMMENT_UNLOCK_LINK = "Разрешить ответы" +COMMENT_TEXT_CHANGED = "Изменено:" +COMMENT_TEXT_ANSWER = "» Ответил(а):" +COMMENT_AFTER_MODER = "Ваш комментарий добавлен.
Прежде чем, он будет опубликован, он должен пройти проверку Администрацией сайта." +COMMENT_MESSAGE_ADMIN = "На Вашем сайте добавлен новый комментарий:%N%%N%%COMMENT% %N%%N%Для просмотра комментария, перейдите по ссылке:%N%%PAGE%" +COMMENT_SUBJECT_MAIL = "Добавлен новый комментарий" +COMMENT_CHARSET_LEFT = "осталось символов" +COMMENT_FORM_CODE = "Защитный код:" +COMMENT_FORM_CODE_ENTER = "Введите код:" +COMMENT_WRONG_CODE = "Указанный защитный код неверен." +COMMENT_DATE_TIME_FORMAT = "%d-%m-%Y %H:%M" +COMMENT_ERROR_AUTHOR = "Укажите Имя!" +COMMENT_ERROR_EMAIL = "Укажите eMail!" +COMMENT_ERROR_TEXT = "Напишите комментарий!" +COMMENT_ERROR_CAPTCHA = "Укажите защитный код!" + +[admin] +COMMENT_MODULE_NAME = "Комментарии" +COMMENT_MODULE_COMENTS = "Список комментариев" +COMMENT_MODULE_SETTINGS = "Настройки модуля" +COMMENT_TEXT_COMMENT = "Комментарий" +COMMENT_DATE_CREATE = "Дата создания" +COMMENT_DOC_TITLE = "Документ" +COMMENT_DATE_FORMAT = "%d/%m/%Y %H:%M" +COMMENT_ENABLE_COMMENT = "Разрешить комментарии?" +COMMENT_CHECK_ADMIN = "Опубликовывать только после проверки?" +COMMENT_FOR_GROUPS = "Группы пользователей, которым разрешено оставлять комментарии:" +COMMENT_MAX_CHARS = "Максимальное количество символов:" +COMMENT_BUTTON_SAVE = "Сохранить настройки" +COMMENT_EDIT = "Редактировать комментарий" +COMMENT_SPAMPROTECT = "Защита от спама" +COMMENT_USE_PAGE_NAV = "Использовать постраничную навигацию" +COMMENT_PAGE_NAV_COUNT = "Кол-во комментариев на странице" +COMMENT_EDIT_TITLE = "Редактирование комментария" +COMMENT_IS_CLOSED = "Редактирование не возможно, поскольку обсуждение закрыто." +COMMENT_CLOSE_BUTTON = "Закрыть окно" +COMMENT_EDIT_FALSE = "Извините, но вы не имеете прав для редактирования." +COMMENT_YOUR_NAME = "* Имя:" +COMMENT_YOUR_EMAIL = "* E-mail:" +COMMENT_YOUR_SITE = "Веб-сайт:" +COMMENT_YOUR_FROM = "Город:" +COMMENT_YOUR_TEXT = "* Комментарий:" +COMMENT_BUTTON_EDIT = "Сохранить изменения" +COMMENT_BUTTON_CANCEL = "Отменить" +COMMENT_CHARS_LEFT = "Количество оставшихся символов" +COMMENT_BUTTON_CANCEL = "Отменить" +COMMENT_BUTTON_RESET = "Очистить" +COMMENT_FOR_GROUPS_READ = "Группы пользователей, которым разрешен просмотр комментариев:" \ No newline at end of file diff --git a/module.php b/module.php new file mode 100644 index 0000000..0f0e8bf --- /dev/null +++ b/module.php @@ -0,0 +1,151 @@ +config_load($lang_file, 'module'); + + // Обращаемся к методу commentListShow() и отображаем список комментариев + $comment->commentListShow($tpl_dir); +} + +/** + * Следующий раздел описывает правила поведения модуля и его функциональные возможности + * только при работе в Публичной части сайта. + */ + + +// Определяем, что мы не находимся в Панели управления и в строке запроса происходит обращение именно к данному модулю +if (!defined('ACP') && isset($_REQUEST['module']) && $_REQUEST['module'] == 'comment' && isset($_REQUEST['action'])) +{ + // Подключаем основной класс и создаем объект + require_once(BASE_DIR . '/modules/comment/class/comment.php'); + $comment = new Comment; + + // Определяем директори, где хранятся файлы с шаблонами модуля и подключаем языковые переменные + $tpl_dir = BASE_DIR . '/modules/comment/templates/'; + $lang_file = BASE_DIR . '/modules/comment/lang/' . $_SESSION['user_language'] . '.txt'; + $AVE_Template->config_load($lang_file, 'module'); + + // Определяем, какой параметр пришел из строки запроса браузера + switch($_REQUEST['action']) + { + // Если form, тогда отображаем форму для добавления нового комментария + case 'form': + $comment->commentPostFormShow($tpl_dir); + break; + + // Если comment, тогда производим запись нового комментария в БД + case 'comment': + $comment->commentPostNew($tpl_dir); + break; + + // Если edit, тогда открываем форму для редактирования текста комментария + case 'edit': + $comment->commentPostEdit((int)$_REQUEST['Id']); + break; + + + // Если delete, тогда удаляем комментарий + case 'delete': + if (UGROUP==1) + { + $comment->commentPostDelete((int)$_REQUEST['Id']); + } + break; + + // Если postinfo, тогда отображаем окно с информацией об авторе комментария + case 'postinfo': + $comment->commentPostInfoShow($tpl_dir); + break; + + // Если lock или unlock, тогда запрещаем или разрешаем оставлять ответы для имеющихся комментариев + case 'lock': + case 'unlock': + if (UGROUP==1) + { + $comment->commentReplyStatusSet((int)$_REQUEST['Id'], $_REQUEST['action']); + } + break; + + + // Если open или close, тогда разрешаем или запрещаем полное комментирование документа + case 'open': + case 'close': + if (UGROUP==1) + { + $comment->commentStatusSet((int)$_REQUEST['docid'], $_REQUEST['action']); + } + break; + } +} + +/** + * Следующий раздел описывает правила поведения модуля и его функциональные возможности + * только при работе в Административной части сайта. + */ +if (defined('ACP') && !empty($_REQUEST['moduleaction'])) +{ + // Подключаем основной класс и создаем объект + require_once(BASE_DIR . '/modules/comment/class/comment.php'); + $comment = new Comment; + + // Определяем директори, где хранятся файлы с шаблонами модуля и подключаем языковые переменные + $tpl_dir = BASE_DIR . '/modules/comment/templates/'; + $lang_file = BASE_DIR . '/modules/comment/lang/' . $_SESSION['admin_language'] . '.txt'; + $AVE_Template->config_load($lang_file, 'admin'); + + + // Определяем, какой параметр пришел из строки запроса браузера + switch ($_REQUEST['moduleaction']) + { + + // Если 1, тогда отображаем список всех комментариев с постраничной навигацией + case '1': + $comment->commentAdminListShow($tpl_dir); + break; + + // Если admin_edit, тогда открываем форму для редактирования выбранного комментария + case 'admin_edit': + $comment->commentAdminPostEdit($tpl_dir); + break; + + // Если settings, тогда открываем страницу с настройками данного модуля + case 'settings': + // Подключаем файл класса для работы с пользователями, создаем объект и получаем список + // всех групп пользователей, имеющихся в системе. + require_once(BASE_DIR . '/class/class.user.php'); + $AVE_User = new AVE_User; + $AVE_Template->assign('groups', $AVE_User->userGroupListGet()); + + $comment->commentAdminSettingsEdit($tpl_dir); + break; + case 'admin_del': + $comment->commentAdminDelete((int)$_REQUEST['Id']); + break; + } +} + +?> \ No newline at end of file diff --git a/sql.php b/sql.php new file mode 100644 index 0000000..2209654 --- /dev/null +++ b/sql.php @@ -0,0 +1,117 @@ + \ No newline at end of file diff --git a/templates/admin_comments.tpl b/templates/admin_comments.tpl new file mode 100644 index 0000000..d8f7591 --- /dev/null +++ b/templates/admin_comments.tpl @@ -0,0 +1,89 @@ + + +
{#COMMENT_MODULE_NAME#}
+ +
+
+ {#COMMENT_MODULE_COMENTS#} +
+
+ + + + + +
+
+
{#COMMENT_MODULE_COMENTS#}
+ +
+ +{if isset($smarty.request.page)} + {assign var="current_page" value=$smarty.request.page|escape} +{else} + {assign var="current_page" value=1} +{/if} + + + + + + + + + + + + + + + + + + + {if $docs} + {foreach from=$docs item=doc} + + + + + + + + + {/foreach} + {else} + + + + {/if} + +
{#COMMENT_TEXT_COMMENT#} {#COMMENT_DATE_CREATE#} {#COMMENT_DOC_TITLE#} Действия
{if $doc.comment_status != "0"}{else}{/if}{$doc.comment_text|escape|truncate:'100'}{$doc.comment_published|date_format:$TIME_FORMAT|pretty_date}{$doc.document_title|escape} ({$doc.Comments})
+
    +
  • Сообщение:
    Нет комментариев.
  • +
+
+
+ +{if $page_nav} + +{/if} \ No newline at end of file diff --git a/templates/admin_edit.tpl b/templates/admin_edit.tpl new file mode 100644 index 0000000..bbb105c --- /dev/null +++ b/templates/admin_edit.tpl @@ -0,0 +1,112 @@ + + +
+ +
{#COMMENT_MODULE_NAME#}
+ +
+
+ {#COMMENT_EDIT_TITLE#} +
+
+ + + +
+ {if $closed == 1 && $smarty.const.UGROUP != 1} + + {#COMMENT_IS_CLOSED#} + +

+ {else} + {if $editfalse==1} + {#COMMENT_EDIT_FALSE#} + {else} +
+ + + {if $smarty.const.UGROUP == 1} + + + + + + + + + + {else} + + + {/if} + + + + + + + + + + + + + + + + + + + + + + + + + + +
{#COMMENT_YOUR_NAME#}
{#COMMENT_YOUR_EMAIL#}
{#COMMENT_YOUR_SITE#}
{#COMMENT_YOUR_FROM#}
{#COMMENT_YOUR_TEXT#} +
+  {#COMMENT_CHARS_LEFT#} +
+   + +
+ + +
+ {/if} + {/if} + +
\ No newline at end of file diff --git a/templates/admin_settings.tpl b/templates/admin_settings.tpl new file mode 100644 index 0000000..8e59e76 --- /dev/null +++ b/templates/admin_settings.tpl @@ -0,0 +1,105 @@ +
{#COMMENT_MODULE_NAME#}
+ +
+
+ {#COMMENT_MODULE_SETTINGS#} +
+
+ + + + +
+
+
{#COMMENT_MODULE_SETTINGS#}
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{#COMMENT_ENABLE_COMMENT#}
{#COMMENT_CHECK_ADMIN#} + +
{#COMMENT_SPAMPROTECT#} + +
{#COMMENT_USE_PAGE_NAV#} + +
{#COMMENT_PAGE_NAV_COUNT#} + +
{#COMMENT_FOR_GROUPS#} + +
{#COMMENT_FOR_GROUPS_READ#} + +
{#COMMENT_MAX_CHARS#}
+ +
+
\ No newline at end of file diff --git a/templates/comment_edit.tpl b/templates/comment_edit.tpl new file mode 100644 index 0000000..3590540 --- /dev/null +++ b/templates/comment_edit.tpl @@ -0,0 +1,71 @@ + + + + + +{#COMMENT_EDIT_TITLE#} + + + + + +

{#COMMENT_EDIT_TITLE#}

+ +
+ {if $closed == 1 && $smarty.const.UGROUP != 1} + {#COMMENT_IS_CLOSED#} +

 

+

+ {else} + {if $editfalse==1} + {#COMMENT_EDIT_FALSE#} + {else} +
+ {if $smarty.const.UGROUP==1} +
+ + +
+ +
+ + +
+ {else} + + + {/if} + +
+ + +
+ +
+ + +
+ +
+ + + {#COMMENT_CHARS_LEFT#} +
+ + + + + + + + +

+   + +

+
+ {/if} + {/if} +
+ + \ No newline at end of file diff --git a/templates/comment_form.tpl b/templates/comment_form.tpl new file mode 100644 index 0000000..1fdf808 --- /dev/null +++ b/templates/comment_form.tpl @@ -0,0 +1,84 @@ + + + + + +{#COMMENT_NEW_TITLE#} + + + + + + +

{#COMMENT_NEW_TITLE#}

+ +
+ {if $closed==1} + {#COMMENT_NEW_CLOSED#} +

 

+

+ {else} + {if !$cancomment} +

{#COMMENT_NEW_FALSE#}

+

 

+

+ {else} +
+ {if $smarty.session.user_name != ''} + + {else} +
+ + +
+
+ {/if} + + {if $smarty.session.user_email != ''} + + {else} +
+ + +
+
+ {/if} + +
+ + +
+
+ +
+ + +
+
+ +
+ + + {#COMMENT_CHARSET_LEFT#} +
+ + + + + + + + + + +

+   + +

+
+ {/if} + {/if} +
+ + + \ No newline at end of file diff --git a/templates/comment_info.tpl b/templates/comment_info.tpl new file mode 100644 index 0000000..86a5777 --- /dev/null +++ b/templates/comment_info.tpl @@ -0,0 +1,54 @@ + + + + + +{#COMMENT_INFO#} + + + + + +

{#COMMENT_INFO#}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{#COMMENT_USER_NAME#}{$c.comment_author_name|stripslashes|escape}
{#COMMENT_DATE_CREATE#}{$c.comment_published|date_format:$TIME_FORMAT|pretty_date}
{#COMMENT_USER_EMAIL#} + {assign var=comment_author_email value=$c.comment_author_email} + {mailto address="$comment_author_email" encode="javascript_charcode"} +
{#COMMENT_USER_SITE#}{$c.comment_author_website|default:'-'}
{#COMMENT_USER_FROM#}{$c.comment_author_city|stripslashes|escape|default:'-'}
{#COMMENT_USER_COMMENTS#}{$c.num|default:'-'}
+ +

+
+ + + \ No newline at end of file diff --git a/templates/comment_new.tpl b/templates/comment_new.tpl new file mode 100644 index 0000000..6a7d8d8 --- /dev/null +++ b/templates/comment_new.tpl @@ -0,0 +1,24 @@ + + + + + + + + +
+
+ {#COMMENT_USER_ADD#} {$comment_author_name} • {$smarty.now|date_format:$TIME_FORMAT|pretty_date}{if $smarty.const.UGROUP==1} • IP:{$comment_author_ip}{/if} +
+ + {if $smarty.const.UGROUP==1} +
+   + {if $comment_status!=1} +   + {else} +   + {/if} +
+ {/if} +
{$comment_text}
\ No newline at end of file diff --git a/templates/comment_thankyou.tpl b/templates/comment_thankyou.tpl new file mode 100644 index 0000000..4856327 --- /dev/null +++ b/templates/comment_thankyou.tpl @@ -0,0 +1,15 @@ + + + + +{#COMMENT_THANKYOU_TITLE#} + + + +

{#COMMENT_THANKYOU_TITLE#}

+
+

{$JsAfter}

+ +
+ + \ No newline at end of file diff --git a/templates/comments_show.tpl b/templates/comments_show.tpl new file mode 100644 index 0000000..cb448c8 --- /dev/null +++ b/templates/comments_show.tpl @@ -0,0 +1,122 @@ +{if $display_comments==1}
+ +
{#COMMENT_SITE_TITLE#}{if $closed==1} {#COMMENT_SITE_CLOSED#}{/if}
+ +{if $cancomment==1 && $closed!=1} + {#COMMENT_SITE_ADD#} |  +{/if} + +{#COMMENT_LAST_COMMENT#} + +{if $smarty.const.UGROUP == 1} +  |  + {if $closed==1} + {#COMMENT_SITE_OPEN#} + {else} + {#COMMENT_SITE_CLOSE#} + {/if} +{/if}
+
+ +{foreach from=$comments.0 item=c name=co} + {if $smarty.request.subaction=='showonly' && $smarty.request.comment_id==$c.Id} +
+ {/if} + + + + + + + + + +
+
+ {#COMMENT_USER_ADD#} {$c.comment_author_name}{*|stripslashes|escape:html*} • {$c.comment_published}{if $smarty.const.UGROUP==1} • IP:{$c.comment_author_ip}{/if} +
+ +
+ + {#COMMENT_ANSWER_LINK#} + + + {if $smarty.const.UGROUP==1 || $c.comment_author_id==$smarty.session.user_id} +   + {#COMMENT_EDIT_LINK#} + {/if} + + {if $smarty.const.UGROUP==1} +   + {#COMMENT_DELETE_LINK#} + + {if $c.comment_status!=1} +   + {#COMMENT_UNLOCK_LINK#} + + {else} +   + {#COMMENT_LOCK_LINK#} + + {/if} + {/if} +
+
+ {$c.comment_text} + {if $c.comment_changed > 1}
{#COMMENT_TEXT_CHANGED#} {$c.comment_changed}{/if} +
+ + {if $smarty.request.subaction=='showonly' && $smarty.request.comment_id==$c.Id} +
+ {/if} + + {foreach from=$comments[$c.Id] item=sc} +
+ {if $smarty.request.subaction=='showonly' && $smarty.request.comment_id==$sc.Id} +
+ {/if} + + + + + + + + + +
+
+ {#COMMENT_TEXT_ANSWER#} {$sc.comment_author_name}{*|stripslashes|escape:html*} ({$sc.comment_published}){if $smarty.const.UGROUP==1} IP:{$sc.comment_author_ip}{/if} +
+ +
+ {if $smarty.const.UGROUP==1 || $sc.comment_author_id==$smarty.session.user_id} + + {/if} + + {if $smarty.const.UGROUP==1} + + {if $sc.comment_status!=1} + + {else} + + {/if} + {else} +   + {/if} +
+
+ {$sc.comment_text} + {if $sc.comment_changed > 1}
{#COMMENT_TEXT_CHANGED#} {$sc.comment_changed}{/if} +
+ + {if $smarty.request.subaction=='showonly' && $smarty.request.comment_id==$sc.Id} +
+ {/if} +
+ {/foreach} +{/foreach} + +{if $smarty.foreach.co.last}{/if} + +{/if} \ No newline at end of file diff --git a/templates/comments_tree.tpl b/templates/comments_tree.tpl new file mode 100644 index 0000000..646db06 --- /dev/null +++ b/templates/comments_tree.tpl @@ -0,0 +1,193 @@ +{if $display_comments==1} + +{* ===================================================================== *} +{* НОВОЕ: ПРОВЕРКА ПРАВ НА ЧТЕНИЕ (самый высокий приоритет) *} +{* ===================================================================== *} +{if $no_read_permission == 1} + + +{else} + {* ===================================================================== *} + {* ВСЁ СУЩЕСТВУЮЩЕЕ СОДЕРЖИМОЕ БЛОКА КОММЕНТАРИЕВ (если права есть) *} + {* ===================================================================== *} + +

+ {#COMMENT_SITE_TITLE#} + {if $closed==1 && $smarty.const.UGROUP!=1} + {#COMMENT_SITE_CLOSED#} + {/if} +

+ +
+ {* Кнопка "Добавить комментарий" - ведет к якорю формы *} + + {#COMMENT_SITE_ADD#} + + + {if $smarty.const.UGROUP==1} + {* Кнопки управления комментированием для Администратора *} + {if $closed==1} + + {#COMMENT_SITE_OPEN#} + + {else} + + {#COMMENT_SITE_CLOSE#} + + {/if} + {/if} +
+ + {* ИСПРАВЛЕНИЕ 1: Добавлена проверка isset() для безопасного доступа к $comments[0] *} + {if isset($comments[0])} +
+ {include file="$subtpl" subcomments=$comments[0]} +
+ {/if} + + + + {* ===================================================================== *} + {* ФОРМА ДОБАВЛЕНИЯ КОММЕНТАРИЯ (MODAL/BLOCK) *} + {* ===================================================================== *} +
+ {if $closed==1 && $smarty.const.UGROUP!=1} +
{#COMMENT_NEW_CLOSED#}
+ {elseif $cancomment!=1 && $smarty.const.UGROUP!=1} +
{#COMMENT_NEW_FALSE#}
+ {else} +
+
+

{#COMMENT_NEW_TITLE#}

+
+ +
+
+ +
+ Форма добавления комментария + + {* Блок для имени и email (скрытые поля для авторизованных) *} + {if $smarty.session.user_group != '2'} + {* АВТОРИЗОВАННЫЙ ПОЛЬЗОВАТЕЛЬ *} + + {* ИСПРАВЛЕНИЕ: Добавлена проверка isset для user_email *} + {if isset($smarty.session.user_email) && $smarty.session.user_email != ''} + + {/if} + {else} + {* ГОСТЬ (требуется ввод имени) *} +
+
+ + +
+ + {* ГОСТЬ (email) *} +
+ + +
+
+ {/if} + + {* Сайт и Город *} +
+
+ + {* ИСПРАВЛЕНИЕ 2: Безопасное чтение comment_author_website через if/else *} + +
+ +
+ + {* ИСПРАВЛЕНИЕ 3: Безопасное чтение comment_author_city через if/else *} + +
+
+ + {* Текст комментария *} +
+ + +
+ + {* Оставшиеся символы *} +

+ {#COMMENT_CHARS_LEFT#} +

+ + {* CAPTCHA *} + {if $im} +
+
+ + + {#COMMENT_FORM_CODE#} + +
+
+ + +
+
+ {/if} + + {* Кнопки отправки *} +
+ + +
+ + {* Скрытые поля *} + + + + + + +
+
+
+
+ {/if} +
{* <-- Закрывает mt-5 для формы *} + + {if $page_nav} + + {/if} + + + + +{/if} {* <-- Закрывает if $no_read_permission *} + +{/if} \ No newline at end of file diff --git a/templates/comments_tree_sub.tpl b/templates/comments_tree_sub.tpl new file mode 100644 index 0000000..7154828 --- /dev/null +++ b/templates/comments_tree_sub.tpl @@ -0,0 +1,90 @@ +{foreach from=$subcomments item=c} + + {* Контейнер комментария: Используем .card для блока, и ms-4 для вложенности *} +
+ + {* Подсветка (Highlight) *} + {if isset($smarty.request.subaction) && $smarty.request.subaction=='showonly' && isset($smarty.request.comment_id) && $smarty.request.comment_id==$c.Id} +
+ {/if} + +
+ + {* Заголовок комментария: Используем card-header и d-flex для выравнивания метаданных и иконок *} +
+ + {* Информация об авторе и дате *} +
+ {* Автор (Иконка fa-user) *} + {#COMMENT_USER_ADD#} {$c.comment_author_name|stripslashes|escape} + + {* Дата публикации (Иконка fa-clock) *} + {$c.comment_published} + + {* IP-адрес (для Админа) *} + {if $smarty.const.UGROUP==1} + • IP:{$c.comment_author_ip} + {/if} + + {* Метка "Изменено" *} + {if isset($c.comment_changed) && $c.comment_changed > 1} ({#COMMENT_TEXT_CHANGED#} {$c.comment_changed}){/if} +
+ + {* Иконки действий - Используем p-2 me-3 для интервала и кликабельности *} +
+ + {* Ссылка "Ответить" (fa-reply) *} + {if $c.comment_author_id!=$smarty.session.user_id|default:'*' && (($cancomment==1 && $closed!=1) || $smarty.const.UGROUP==1)} + + + + {/if} + + {* ИКОНКА РЕДАКТИРОВАНИЯ (fa-pencil) *} + {if $smarty.const.UGROUP==1 || $c.comment_author_id==$smarty.session.user_id|default:'*'} + + + + {/if} + + {if $smarty.const.UGROUP==1} + {* Замок (Разблокировать/Заблокировать) *} + {if $c.comment_status!=1} + + + + {else} + + + + {/if} + + {* Корзина (Удалить) *} + + + + {/if} +
+
+ + {* Тело комментария: Используем card-body *} +
{$c.comment_text|escape}
+
+ + {* Закрываем подсветку *} + {if isset($smarty.request.subaction) && $smarty.request.subaction=='showonly' && isset($smarty.request.comment_id) && $smarty.request.comment_id==$c.Id} +
+ {/if} + + {* Точка вставки формы ответа *} + + + {* Рекурсивный вызов дочерних комментариев *} + {if isset($comments) && isset($comments[$c.Id])} +
+ {include file="$subtpl" subcomments=$comments[$c.Id] sub=1} +
+ {/if} + +
{* Закрывает mod_comment_comment *} +{/foreach} \ No newline at end of file diff --git a/templates/images/edit.gif b/templates/images/edit.gif new file mode 100644 index 0000000..1e024c1 Binary files /dev/null and b/templates/images/edit.gif differ diff --git a/templates/images/lock.gif b/templates/images/lock.gif new file mode 100644 index 0000000..7d2e8a9 Binary files /dev/null and b/templates/images/lock.gif differ diff --git a/templates/images/reply.gif b/templates/images/reply.gif new file mode 100644 index 0000000..16b6c61 Binary files /dev/null and b/templates/images/reply.gif differ diff --git a/templates/images/trash.gif b/templates/images/trash.gif new file mode 100644 index 0000000..f1b4afd Binary files /dev/null and b/templates/images/trash.gif differ diff --git a/templates/images/unlock.gif b/templates/images/unlock.gif new file mode 100644 index 0000000..edcaadd Binary files /dev/null and b/templates/images/unlock.gif differ diff --git a/templates/index.php b/templates/index.php new file mode 100644 index 0000000..4ca25aa --- /dev/null +++ b/templates/index.php @@ -0,0 +1,4 @@ + \ No newline at end of file