From 9a8834505797889b01fd433fcbdb3050d2d1a035 Mon Sep 17 00:00:00 2001 From: themohooks <81331307+themohooks@users.noreply.github.com> Date: Sun, 25 May 2025 15:04:58 +0300 Subject: [PATCH] update models --- app/Models/Comment.php | 189 ++++++++++++++++++++++++++++++++++------- app/Models/User.php | 9 ++ 2 files changed, 167 insertions(+), 31 deletions(-) diff --git a/app/Models/Comment.php b/app/Models/Comment.php index 5a356da..40e9c96 100644 --- a/app/Models/Comment.php +++ b/app/Models/Comment.php @@ -2,7 +2,7 @@ namespace App\Models; -use \App\Services\{DB, Date, Auth}; +use \App\Services\{DB, Date, Auth, Emoji, Word}; use \App\Models\{User, Photo, Vote}; class Comment @@ -24,6 +24,123 @@ class Comment $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. Если текст не UTF‑8, конвертируем из 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 гарантируют корректную работу с   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 = '
' . $html . '
'; + + $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']); @@ -39,8 +156,6 @@ class Comment echo '
' . Date::zmdate($this->c['posted_at']) . '
- Цитировать - · Ссылка '; @@ -73,19 +188,31 @@ class Comment $commclass = ''; } echo '
-
Фото: ' . Photo::fetchAll($this->c['user_id']) . ' ' . $admintype . '
-
' . preg_replace("~(?:[\p{M}]{1})([\p{M}])+?~uis", "", htmlspecialchars($this->c['body'])) . '
- '; - if ($content['filetype'] === 'img') { - echo '
'; - } - if ($content['filetype'] === 'video') { - echo '
'; - } - echo ' +
Фото: ' . Photo::fetchAll($this->c['user_id']) . ' ' . $admintype . '
'; ?> +
+ processContent($this->c['body']); + + // Шаг 4: Вывод без дополнительного экранирования + echo '
' . $processedText . '
'; + + // ========== Вспомогательные методы ========== + + + ?> +
'; + } + if ($content['filetype'] === 'video') { + echo '
'; + } + echo '
'; - echo ''; - if ($this->c['user_id'] === Auth::userid() || $photo->i('user_id') === Auth::userid()) { - echo ' + if ($this->c['user_id'] === Auth::userid() || $photo->i('user_id') === Auth::userid()) { + echo '
'; - } -} + } + } diff --git a/app/Models/User.php b/app/Models/User.php index a0115d4..1d4b440 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -15,5 +15,14 @@ class User { $content = json_decode(self::i('content'), true); return $content[$table]; } + public function getPhotoUrl(): string + { + return $this->i('photourl'); + } + + public function getId(): int + { + return (int)$this->i('user_id'); + } } \ No newline at end of file