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. Если текст не 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']); $content = json_decode($this->c['content'], true); $photo = new \App\Models\Photo($this->c['photo_id']); $pinc = 'Закрепить'; echo '
'; if ($photo->i('pinnedcomment_id') === $this->c['id']) { echo 'Комментарий закреплён
'; $pinc = 'Открепить'; } echo '
' . Date::zmdate($this->c['posted_at']) . '
Ссылка '; echo '
' . htmlspecialchars($user->i('username')) . ' · '; if (json_decode($user->i('content'), true)['aboutrid']['value'] != null) { echo ''; } 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 '
(отредактировано)'; } 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 '
Фото: ' . 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 '
'; } }