nativegallery/app/Models/Comment.php
2025-05-26 12:17:26 +03:00

270 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use \App\Services\{DB, Date, Auth, Emoji, Word};
use \App\Models\{User, Photo, Vote};
class Comment
{
public $commentid;
public $c;
public $class;
function __construct($user_id)
{
$this->c = $user_id;
}
public function class($class)
{
$this->class = $class;
}
public function content($table)
{
$content = json_decode($this->c['content'], true);
return $content[$table];
}
private function processContent($rawText)
{
// 1. Обработка упоминаний и смайлов
$withTags = Emoji::parseSmileys(Word::processMentions($rawText));
// 2. Селективное экранирование
$safeContent = $this->selectiveHtmlEscape($withTags);
// 3. Обрезка контента
return $this->truncateContent($safeContent, 200);
}
private function selectiveHtmlEscape(string $html): string
{
// 0. Если текст не UTF8, конвертируем из CP1251
if (!mb_check_encoding($html, 'UTF-8')) {
$html = mb_convert_encoding($html, 'UTF-8', 'CP1251');
}
// 1. Разбиваем на «теги» и «текст», сохраняя теги
$parts = preg_split('/(<[^>]+>)/u', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as &$part) {
// 2. Тег — пропускаем
if (preg_match('/^<[^>]+>$/u', $part)) {
continue;
}
// 3. Текст — сначала декодируем все сущности, потом экранируем спецсимволы
// ENT_QUOTES|ENT_HTML5 и false у double_encode гарантируют корректную работу с &nbsp; etc.
$decoded = html_entity_decode($part, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$part = htmlspecialchars($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
}
unset($part);
// 4. Собираем обратно
return implode('', $parts);
}
private function truncateContent(string $html, int $maxLength): string
{
$dom = new \DOMDocument('1.0', 'UTF-8');
libxml_use_internal_errors(true);
$wrapped = '<?xml encoding="UTF-8"><div>' . $html . '</div>';
$dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
$xpath = new \DOMXPath($dom);
$node = $xpath->query('//div')->item(0);
$this->truncateNode($node, $maxLength);
return $dom->saveHTML($node);
}
private function truncateNode(\DOMNode $node, &$remaining)
{
if ($remaining <= 0) return;
foreach ($node->childNodes as $child) {
if ($child instanceof \DOMText) {
$text = $child->nodeValue;
$visible = mb_substr($text, 0, $remaining);
$hidden = mb_substr($text, $remaining);
if ($remaining < mb_strlen($text)) {
$child->nodeValue = $visible;
$remaining = 0;
// Создаём элемент для скрытой части
$hiddenNode = $child->ownerDocument->createElement('span');
$hiddenNode->setAttribute('class', 'hidden-text');
$hiddenTextNode = $child->ownerDocument->createTextNode($hidden);
$hiddenNode->appendChild($hiddenTextNode);
// Вставляем hiddenNode после текущего текстового узла
$parent = $child->parentNode;
if ($parent) {
if ($child->nextSibling) {
$parent->insertBefore($hiddenNode, $child->nextSibling);
} else {
$parent->appendChild($hiddenNode);
}
}
// Создаём кнопку "показать больше"
$button = $child->ownerDocument->createElement('a');
$buttonText = $child->ownerDocument->createTextNode('показать больше');
$button->appendChild($buttonText);
$button->setAttribute('class', 'toggle-message');
if ($parent) {
$parent->appendChild($button);
}
break;
}
$remaining -= mb_strlen($text);
} else {
$this->truncateNode($child, $remaining);
}
}
}
public function i()
{
$user = new User($this->c['user_id']);
$content = json_decode($this->c['content'], true);
$photo = new \App\Models\Photo($this->c['photo_id']);
$pinc = 'Закрепить';
echo '<div class="' . $this->class . ' comment" wid="' . $this->c['id'] . '">';
if ($photo->i('pinnedcomment_id') === $this->c['id']) {
echo '<i style="margin-bottom: 1px;">Комментарий закреплён</i><br>';
$pinc = 'Открепить';
}
echo '
<div style="float:right; text-align:right" class="sm">
<span class="message_date">' . Date::zmdate($this->c['posted_at']) . '</span><br>
<a href="#' . $this->c['id'] . '" class="cmLink dot">Ссылка</a>
';
echo '
</div>
<a name="2681468"></a><a name="last"></a>
<div><img src="' . $user->i('photourl') . '" width="32" style="border-radius: 3px; margin-right: 5px;"><b><a href="/author/' . $this->c['user_id'] . '/" class="message_author">' . htmlspecialchars($user->i('username')) . '</a></b> &middot;
<span class="flag">';
if (json_decode($user->i('content'), true)['aboutrid']['value'] != null) {
echo '<img src="/static/img/flags/' . json_decode($user->i('content'), true)['aboutrid']['value'] . '.gif">';
}
if (json_decode($user->i('content'), true)['aboutlive']['value'] != null) {
echo ' ' . htmlspecialchars(json_decode($user->i('content'), true)['aboutlive']['value']);
}
if ($content['edited'] === 'true') {
echo '<br>(отредактировано)';
}
if ($user->i('admin') === 1) {
$admintype = ' · Администратор сервера';
} else if ($user->i('admin') === 2) {
$admintype = ' · Фотомодератор';
}
if ((int)Vote::countcommrates($this->c['id'], -1) >= 1) {
$commclass = 'pro';
$symb = '+';
} else if ((int)Vote::countcommrates($this->c['id'], -1) < 0) {
$commclass = 'con';
$symb = '';
} else if ((int)Vote::countcommrates($this->c['id'], -1) === 0) {
$commclass = '';
}
echo '</span></div>
<div class="rank">Фото: ' . Photo::fetchAll($this->c['user_id']) . ' ' . $admintype . '</div>'; ?>
<div class="message-text">
<?php
// Правильный порядок:
$processedText = $this->processContent($this->c['body']);
// Шаг 4: Вывод без дополнительного экранирования
echo '<div class="message-text">' . $processedText . '</div>';
// ========== Вспомогательные методы ==========
?>
</div> <?php
if ($content['filetype'] === 'img') {
echo '<div class="message-text"><img src="' . $content['src'] . '" width="250"></div>';
}
if ($content['filetype'] === 'video') {
echo '<div class="message-text"><video controls src="' . $content['src'] . '" width="250"></div>';
}
echo '
<div class="comment-votes-block">
';
echo '<style>
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
padding: 12px 16px;
z-index: 1;
}
.dropdown:hover .dropdown-content {
display: block;
}
</style>';
if ($this->c['user_id'] === Auth::userid() || $photo->i('user_id') === Auth::userid()) {
echo '
<div class="dropdown">
<a style="color: #000" class="compl" href="#">...</a>
<div class="dropdown-content">';
?>
<a href="#" onclick="pinComment(<?= $this->c['id'] ?>); return false;"><?= $pinc ?></a><br>
<?php
if ($this->c['user_id'] === Auth::userid()) { ?>
<a style="margin-bottom: 10px;" href="#" onclick="createModal(<?= $this->c['id'] ?>, 'EDIT_COMMENT', '<?= htmlspecialchars($this->c['body']) ?>', 'modaledit<?= $this->c['id'] ?>'); return false;">Редактировать</a><br>
<a href="#" onclick="createModal(<?= $this->c['id'] ?>, 'DELETE_COMMENT', '', 'modaldel<?= $this->c['id'] ?>'); return false;">Удалить</a>
<?php }
echo '
</div>
</div>
';
}
echo '
<div class="wvote" wid="' . $this->c['id'] . '">
<a href="#" vote="1" class="w-btn s2"><span>+</span></a>
<div class="w-rating ' . $commclass . ' active">' . $symb . Vote::countcommrates($this->c['id'], -1) . '</div>
<div class="w-rating-ext">
<div><span class="pro">+' . Vote::countcommrates($this->c['id'], 1) . '</span> / <span class="con">' . Vote::countcommrates($this->c['id'], 0) . '</span></div>
</div>
<a href="#" vote="0" class="w-btn s5"><span></span></a>
</div>
</div>
</div>';
}
}