| @copyright 2007-2026 (c) AVE.cms | @link https://ave-cms.ru | @version 3.3 */ namespace App\Frontend\Debug; defined('BASEPATH') || die('Direct access to this location is not allowed.'); use App\Common\Auth; use App\Common\DebugChannel; use App\Common\HookCatalog; use App\Common\PublicConfiguration; use App\Common\PublicModuleRuntime; use App\Common\Registry; use App\Frontend\PublicPageContext; use App\Frontend\PublicProfiler; use App\Helpers\Hooks; use DB; /** Protected, dependency-free profiler injected into public HTML responses. */ class PublicDebugToolbar { public static function boot() { if (!self::allowed()) { return; } PublicLayoutInspector::boot(); Hooks::enableTrace(true); if (class_exists('App\\Common\\Db\\Core\\QueryDebug')) { \App\Common\Db\Core\QueryLogger::enableCallerTrace(true); \App\Common\Db\Core\QueryDebug::init(true); PublicDebugCollector::boot(); } } public static function inject($html, $core = null) { $html = (string) $html; $allowed = self::allowed(); $decorate = $allowed && !defined('ONLYCONTENT') && !self::isAjax() && stripos($html, '') !== false; $html = PublicLayoutInspector::finalize($html, $decorate); if (!$decorate) { return $html; } $panel = self::render($core); return preg_replace('/<\/body>/i', $panel . '', $html, 1); } protected static function allowed() { $config = PublicConfiguration::all(); $debug = isset($config['debug']) && is_array($config['debug']) ? $config['debug'] : array(); if (empty($debug['enabled'])) { return false; } $groups = isset($debug['groups']) && is_array($debug['groups']) ? array_map('intval', $debug['groups']) : array(1); $user = Auth::publicUser(); if (is_array($user) && in_array((int) $user['group'], $groups, true)) { return true; } return Auth::systemUserCan('view_public_debug'); } protected static function render($core) { $queries = self::queries(); $timeline = PublicProfiler::timeline(); $document = is_object($core) && isset($core->curentdoc) ? $core->curentdoc : null; $elapsed = defined('START_MICROTIME') ? microtime(true) - self::timestamp(START_MICROTIME) : 0.0; $status = (int) (http_response_code() ?: 200); $method = isset($_SERVER['REQUEST_METHOD']) ? (string) $_SERVER['REQUEST_METHOD'] : 'GET'; $uri = self::safeUri(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'); $systemUser = Auth::systemUser(); $publicUser = Auth::publicUser(); $queryTime = 0.0; foreach ($queries as $query) { $queryTime += (float) $query['time']; } $overview = self::cards(array( array('Время ответа', number_format($elapsed * 1000, 1, ',', ' ') . ' ms', self::metricTone($elapsed * 1000, 300, 800)), array('Пиковая память', self::bytes(memory_get_peak_usage(true)), 'blue'), array('SQL-запросы', count($queries) . ' · ' . number_format($queryTime * 1000, 1, ',', ' ') . ' ms', self::metricTone($queryTime * 1000, 80, 250)), array('HTTP-статус', (string) $status, $status >= 500 ? 'red' : ($status >= 400 ? 'amber' : 'green')), )); $overview .= self::definitionList(array( 'Метод' => $method, 'Адрес' => $uri, 'PHP runtime' => PHP_VERSION, 'Системный пользователь' => self::systemUserLabel($systemUser), 'Публичный пользователь' => self::publicUserLabel($publicUser), 'Public-модули' => implode(', ', array_keys(PublicModuleRuntime::loaded())) ?: '—', 'Данные документа' => Registry::get('document_snapshot_source', 'не запрашивались') === 'snapshot' ? 'JSON-снимок' : (Registry::get('document_snapshot_source', '') === 'database' ? 'БД + пересборка' : 'не запрашивались'), 'Отложенная страница' => PublicPageContext::hasDeferredPage() ? 'Да' : 'Нет', )); $overview = self::sectionHead('Сводка запроса', $method . ' · ' . $uri, self::statusBadge($status)) . $overview; $documentHtml = self::documentPanel($document); $debugEntries = DebugChannel::all(); $debugHtml = self::sectionHead('Данные приложения', 'Структурированные значения из Debug::panel()', self::countBadge(count($debugEntries), $debugEntries ? 'violet' : 'slate')) . self::debugEntries($debugEntries); $sqlHtml = self::sectionHead('SQL-запросы', number_format($queryTime * 1000, 2, ',', ' ') . ' ms суммарно', self::countBadge(count($queries))) . self::queryTable($queries); $timelineHtml = self::sectionHead('Timeline', 'Этапы формирования текущего ответа', self::countBadge(self::leafCount($timeline))) . self::timeline($timeline); $cacheItems = method_exists('DB', 'getDebugInfo') ? DB::getDebugInfo() : array(); $cacheItems = is_array($cacheItems) ? $cacheItems : array(); $cacheHtml = self::sectionHead('Кеш', 'События кеширования текущего запроса', self::countBadge(self::leafCount($cacheItems))) . self::dataView($cacheItems); $httpData = self::masked(array( 'GET' => $_GET, 'POST' => $_POST, 'COOKIE' => $_COOKIE, 'HEADERS' => self::headers(), )); $httpHtml = self::sectionHead('HTTP', $method . ' · ' . $uri, self::countBadge(self::leafCount($httpData))) . self::httpGroups($httpData); $sessionData = self::masked(isset($_SESSION) && is_array($_SESSION) ? $_SESSION : array()); $sessionHtml = self::sectionHead('Контексты авторизации', 'Публичный профиль и роль панели управления связаны в единую учётную запись', self::countBadge(($systemUser ? 1 : 0) + ($publicUser ? 1 : 0), $systemUser ? 'green' : 'slate')) . self::identityPanel($systemUser, $publicUser) . self::sectionHead('PHP Session', session_id() !== '' ? 'ID ' . self::shortId(session_id()) : 'Сессия не запущена', self::countBadge(self::leafCount($sessionData))) . self::dataView($sessionData); $errors = PublicDebugCollector::errors(); $errorsHtml = self::sectionHead('Ошибки PHP', $errors ? 'Зафиксированы в текущем запросе' : 'Текущий запрос выполнен без предупреждений', self::countBadge(count($errors), $errors ? 'red' : 'green')) . self::errorList($errors); $hookTrace = Hooks::trace(); $hookEntries = isset($hookTrace['entries']) && is_array($hookTrace['entries']) ? $hookTrace['entries'] : array(); $hooksHtml = self::sectionHead('Хуки', 'События и обработчики текущего публичного запроса', self::countBadge(count($hookEntries), $hookEntries ? 'blue' : 'slate')) . self::hookList($hookEntries, isset($hookTrace['dropped']) ? (int) $hookTrace['dropped'] : 0); $files = array_map(function ($file) { return str_replace(BASEPATH . '/', '', $file); }, get_included_files()); sort($files, SORT_NATURAL | SORT_FLAG_CASE); $filesHtml = self::sectionHead('Подключённые файлы', 'PHP-файлы текущего запроса', self::countBadge(count($files))) . self::fileList($files); $inspectorEntries = PublicLayoutInspector::entries(); $inspectorHtml = self::sectionHead( 'Состав страницы', 'Управляемые элементы в порядке фактического вывода', self::countBadge(count($inspectorEntries), $inspectorEntries ? 'blue' : 'slate') ) . self::inspectorPanel($inspectorEntries); $tabs = array( 'overview' => array('Обзор', null), 'document' => array('Документ', $document && isset($document->Id) ? (int) $document->Id : 0), 'debug' => array('Данные', count($debugEntries)), 'timeline' => array('Timeline', self::leafCount($timeline)), 'sql' => array('SQL', count($queries)), 'cache' => array('Кеш', self::leafCount($cacheItems)), 'http' => array('HTTP', self::leafCount($httpData)), 'session' => array('Session', self::leafCount($sessionData)), 'errors' => array('Ошибки', count($errors)), 'hooks' => array('Хуки', count($hookEntries)), 'elements' => array('Элементы', count($inspectorEntries)), 'files' => array('Файлы', count($files)), ); $contents = array('overview' => $overview, 'document' => $documentHtml, 'debug' => $debugHtml, 'timeline' => $timelineHtml, 'sql' => $sqlHtml, 'cache' => $cacheHtml, 'http' => $httpHtml, 'session' => $sessionHtml, 'errors' => $errorsHtml, 'hooks' => $hooksHtml, 'elements' => $inspectorHtml, 'files' => $filesHtml); $nav = ''; $sections = ''; foreach ($tabs as $id => $tab) { $active = $id === 'overview'; $nav .= ''; $sections .= '
' . $contents[$id] . '
'; } $inspectorJson = json_encode($inspectorEntries, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); if ($inspectorJson === false) { $inspectorJson = '[]'; } return '
' . self::assets(); } protected static function queries() { $result = array(); if (method_exists('DB', 'getQueries')) { foreach ((array) DB::getQueries() as $item) { $item = (array) $item; $result[] = array('source' => 'Native', 'query' => isset($item['query']) ? $item['query'] : '', 'time' => self::number(isset($item['time']) ? $item['time'] : 0), 'caller' => self::caller(isset($item['caller']) ? $item['caller'] : array())); } } return $result; } protected static function queryTable(array $queries) { if (!$queries) { return self::emptyState('SQL-запросы не собраны', 'В текущем запросе обращения к базе данных не зафиксированы.'); } $html = self::filterBar('sql', 'Найти SQL или файл вызова', count($queries)); $html .= '
'; foreach ($queries as $index => $query) { $milliseconds = (float) $query['time'] * 1000; $tone = self::metricTone($milliseconds, 15, 50); $filter = self::lower($query['query'] . ' ' . $query['caller'] . ' ' . $query['source']); $html .= '
' . '#' . ($index + 1) . '' . '' . self::e($query['source']) . '' . '' . self::e($query['caller'] ?: 'Источник не определён') . '' . '' . number_format($milliseconds, 2, ',', ' ') . ' ms' . '
' . self::e(self::mask($query['query'])) . '
'; } return $html . '
'; } protected static function hookList(array $entries, $dropped) { if (!$entries) { return self::emptyState('Хуки не вызывались', 'Текущий маршрут не прошел через зарегистрированные lifecycle-точки.'); } $html = self::filterBar('hooks', 'Найти событие или обработчик', count($entries)); if ((int) $dropped > 0) { $html .= '
Не показано записей: ' . (int) $dropped . '. Ограничение защищает от переполнения отладчика.
'; } $html .= '
'; foreach ($entries as $index => $entry) { $name = isset($entry['name']) ? (string) $entry['name'] : ''; $handler = isset($entry['handler']) && $entry['handler'] !== '' ? (string) $entry['handler'] : 'Нет подписчиков'; $status = isset($entry['status']) ? (string) $entry['status'] : 'ok'; $duration = isset($entry['duration']) ? (float) $entry['duration'] * 1000 : 0.0; $definition = HookCatalog::get($name); $description = is_array($definition) && isset($definition['description']) ? (string) $definition['description'] : ''; $tone = $status === 'error' ? 'red' : ($status === 'unhandled' ? 'slate' : self::metricTone($duration, 5, 20)); $filter = self::lower($name . ' ' . $handler . ' ' . $description . ' ' . $status); $priority = isset($entry['priority']) && $entry['priority'] !== null ? 'P' . (int) $entry['priority'] : '—'; $details = array( 'Описание' => $description !== '' ? $description : 'Событие не описано в каталоге', 'Тип' => isset($entry['kind']) ? (string) $entry['kind'] : 'action', 'Приоритет' => $priority, 'Глубина' => isset($entry['depth']) ? (int) $entry['depth'] : 0, 'Состояние' => $status, ); if (!empty($entry['exception'])) { $details['Ошибка'] = (string) $entry['exception']; } $html .= '
' . '#' . ($index + 1) . '' . '' . self::e($priority) . '' . '' . self::e($name) . '' . '' . number_format($duration, 2, ',', ' ') . ' ms' . '' . self::definitionList(array_merge(array('Обработчик' => $handler), $details)) . '
'; } return $html . '
'; } protected static function cards(array $cards) { $html = '
'; foreach ($cards as $card) { $tone = isset($card[2]) ? preg_replace('/[^a-z]/', '', (string) $card[2]) : 'blue'; $html .= '
' . self::e($card[0]) . '' . self::e($card[1]) . '
'; } return $html . '
'; } protected static function definitionList(array $items) { $html = '
'; foreach ($items as $key => $value) { $html .= '
' . self::e($key) . '
' . self::e($value) . '
'; } return $html . '
'; } protected static function tree($value) { return self::dataView($value); } protected static function errorList(array $errors) { if (!$errors) { return self::emptyState('Ошибок нет', 'PHP-предупреждения и ошибки в текущем запросе не зафиксированы.', 'success'); } $html = '
'; foreach ($errors as $error) { $type = isset($error['type']) ? (string) $error['type'] : 'PHP'; $tone = stripos($type, 'notice') !== false || stripos($type, 'deprecated') !== false ? 'amber' : 'red'; $html .= '
' . self::e($type) . '' . (isset($error['code']) ? 'Код ' . (int) $error['code'] . '' : '') . '
' . '

' . self::e(isset($error['message']) ? $error['message'] : '') . '

' . '' . self::e((isset($error['file']) ? $error['file'] : '') . ':' . (isset($error['line']) ? $error['line'] : 0)) . '
'; } return $html . '
'; } protected static function debugEntries(array $entries) { if (!$entries) { return self::emptyState('Записей нет', 'В текущем запросе Debug::panel() не вызывался.'); } $html = '
'; foreach ($entries as $index => $entry) { $source = (isset($entry['file']) ? $entry['file'] : '') . (isset($entry['line']) ? ':' . (int) $entry['line'] : ''); $html .= '
' . ($index + 1) . '
' . self::e(isset($entry['label']) ? $entry['label'] : 'Данные') . '' . '' . self::e($source) . '
' . self::e(isset($entry['type']) ? $entry['type'] : 'value') . '
' . self::dataView(self::masked(isset($entry['value']) ? $entry['value'] : null)) . '
'; } return $html . '
'; } protected static function documentPanel($document) { if (!is_object($document)) { return self::sectionHead('Документ', 'Маршрут не связан с документом', self::countBadge(0)) . self::emptyState('Документ не определён', 'Ответ сформирован системным маршрутом, модулем или страницей ошибки.'); } $id = self::objectValue($document, array('Id', 'id'), 0); $rubricId = self::objectValue($document, array('rubric_id', 'rubricId'), 0); $templateId = self::objectValue($document, array('document_template_id', 'template_id', 'rubric_template_id'), 0); $status = (int) self::objectValue($document, array('document_status', 'status'), 0); $title = self::objectValue($document, array('document_title', 'title'), 'Без названия'); $html = self::sectionHead('Документ', (string) $title, self::countBadge((int) $id, 'blue')); $html .= self::cards(array( array('ID документа', $id ?: '—', 'blue'), array('Рубрика', $rubricId ? '#' . $rubricId : '—', 'violet'), array('Шаблон', $templateId ? '#' . $templateId : '—', 'amber'), array('Состояние', $status === 1 ? 'Опубликован' : 'Не опубликован', $status === 1 ? 'green' : 'red'), )); $html .= self::definitionList(array( 'Заголовок' => $title, 'Алиас' => self::objectValue($document, array('document_alias', 'alias'), '—'), 'Рубрика' => self::objectValue($document, array('rubric_title'), $rubricId ? '#' . $rubricId : '—'), 'Опубликован' => self::objectValue($document, array('document_published'), '—'), 'Изменён' => self::objectValue($document, array('document_changed'), '—'), 'Кеш документа' => self::objectValue($document, array('document_cache'), '—'), )); return $html; } protected static function objectValue($object, array $keys, $fallback = null) { foreach ($keys as $key) { if (is_object($object) && isset($object->{$key})) { return $object->{$key}; } } return $fallback; } protected static function sectionHead($title, $meta = '', $badge = '') { return '

' . self::e($title) . '

' . ($meta !== '' ? '

' . self::e($meta) . '

' : '') . '
' . $badge . '
'; } protected static function countBadge($count, $tone = 'slate') { $tone = preg_replace('/[^a-z]/', '', (string) $tone); return '' . (int) $count . ''; } protected static function statusBadge($status) { $status = (int) $status; $tone = $status >= 500 ? 'red' : ($status >= 400 ? 'amber' : ($status >= 300 ? 'violet' : 'green')); return 'HTTP ' . $status . ''; } protected static function metricTone($value, $warning, $danger) { if ((float) $value >= (float) $danger) { return 'red'; } if ((float) $value >= (float) $warning) { return 'amber'; } return 'green'; } protected static function timeline(array $timeline) { $rows = array(); self::flattenTimeline($timeline, array(), $rows); if (!$rows) { return self::emptyState('Timeline пуст', 'Этапы выполнения текущего запроса не зафиксированы.'); } $max = 0.0; foreach ($rows as $row) { if ($row['seconds'] !== null) { $max = max($max, $row['seconds']); } } $html = '
'; foreach ($rows as $row) { $width = $row['seconds'] !== null && $max > 0 ? max(2, min(100, ($row['seconds'] / $max) * 100)) : 0; $tone = $row['seconds'] !== null ? self::metricTone($row['seconds'] * 1000, 15, 50) : 'blue'; $html .= '
' . self::e(implode(' › ', $row['path'])) . '' . '
' . ($width > 0 ? '' : '') . '
' . '' . self::e($row['label']) . '
'; } return $html . '
'; } protected static function flattenTimeline($value, array $path, array &$rows) { if (is_array($value)) { foreach ($value as $key => $child) { $childPath = $path; $childPath[] = (string) $key; self::flattenTimeline($child, $childPath, $rows); } return; } $seconds = null; $label = self::scalarLabel($value); if (is_numeric($value)) { $seconds = (float) $value; $label = number_format($seconds * 1000, 2, ',', ' ') . ' ms'; } elseif (is_string($value) && preg_match('/^([0-9\s,.]+)\s*sec$/i', trim($value), $match)) { $seconds = self::number(str_replace(' ', '', $match[1])); $label = number_format($seconds * 1000, 2, ',', ' ') . ' ms'; } $rows[] = array('path' => $path, 'seconds' => $seconds, 'label' => $label); } protected static function leafCount($value) { if (!is_array($value)) { return $value === null ? 0 : 1; } $count = 0; foreach ($value as $child) { $count += self::leafCount($child); } return $count; } protected static function dataView($value, $depth = 0) { if ($depth === 0 && is_array($value) && !$value) { return self::emptyState('Данные не собраны', 'Для текущего запроса значения отсутствуют.'); } if ($depth >= 7) { return '
Вложенность ограничена
'; } if (!is_array($value)) { return '
' . self::scalarValue($value) . '
'; } $html = '
'; foreach ($value as $key => $child) { $label = is_int($key) ? '#' . $key : (string) $key; if (is_array($child)) { $html .= '
' . self::e($label) . '' . '' . count($child) . ' ' . self::plural(count($child), 'элемент', 'элемента', 'элементов') . '' . self::dataView($child, $depth + 1) . '
'; } else { $html .= '
' . self::e($label) . '
' . self::scalarValue($child) . '
'; } } return $html . '
'; } protected static function scalarValue($value) { if ($value === null) { return 'null'; } if (is_bool($value)) { return '' . ($value ? 'true' : 'false') . ''; } if (is_int($value) || is_float($value)) { return '' . self::e($value) . ''; } if ($value === '') { return 'пустая строка'; } return '' . self::e($value) . ''; } protected static function identityPanel($systemUser, $publicUser) { $system = is_array($systemUser) ? array( 'ID' => isset($systemUser['id']) ? (int) $systemUser['id'] : 0, 'Имя' => isset($systemUser['name']) ? (string) $systemUser['name'] : '', 'Email' => isset($systemUser['email']) ? (string) $systemUser['email'] : '', 'Роль' => isset($systemUser['role']) ? (string) $systemUser['role'] : '', ) : array(); $public = is_array($publicUser) ? array( 'ID' => isset($publicUser['Id']) ? (int) $publicUser['Id'] : (isset($publicUser['id']) ? (int) $publicUser['id'] : 0), 'Имя' => self::publicUserLabel($publicUser), 'Email' => isset($publicUser['email']) ? (string) $publicUser['email'] : '', 'Группа' => isset($publicUser['group']) ? (int) $publicUser['group'] : 0, ) : array(); return '
' . '
Система' . ($system ? 'Авторизован' : 'Не авторизован') . '
' . ($system ? self::dataView($system) : self::emptyState('Системного входа нет', 'Общий auth_token не найден.')) . '
' . '
Публичный сайт' . ($public ? 'Авторизован' : 'Гостевая сессия') . '
' . ($public ? self::dataView($public) : self::emptyState('Публичный пользователь не вошел', 'Legacy-поля Anonymous относятся только к гостевой сессии сайта.')) . '
' . '
'; } protected static function systemUserLabel($user) { if (!is_array($user)) { return 'Не авторизован'; } $name = trim((string) (isset($user['name']) ? $user['name'] : '')); if ($name === '') { $name = isset($user['email']) ? (string) $user['email'] : 'Системный пользователь'; } return $name . ' (#' . (isset($user['id']) ? (int) $user['id'] : 0) . ')'; } protected static function publicUserLabel($user) { if (!is_array($user)) { return 'Гость'; } $parts = array(); foreach (array('firstname', 'lastname', 'name') as $key) { if (!empty($user[$key])) { $parts[] = trim((string) $user[$key]); } } $name = trim(implode(' ', array_unique($parts))); if ($name === '') { $name = isset($user['email']) ? (string) $user['email'] : 'Публичный пользователь'; } $id = isset($user['Id']) ? (int) $user['Id'] : (isset($user['id']) ? (int) $user['id'] : 0); return $name . ' (#' . $id . ')'; } protected static function scalarLabel($value) { if (is_bool($value)) { return $value ? 'Да' : 'Нет'; } if ($value === null) { return 'null'; } if (is_array($value)) { return count($value) . ' элементов'; } return (string) $value; } protected static function httpGroups(array $groups) { $html = '
'; foreach ($groups as $name => $values) { $count = self::leafCount($values); $tone = $name === 'POST' ? 'violet' : ($name === 'COOKIE' ? 'amber' : ($name === 'HEADERS' ? 'green' : 'blue')); $html .= '
' . self::e($name) . '' . self::countBadge($count, $tone) . '
' . self::dataView(is_array($values) ? $values : array($values)) . '
'; } return $html . '
'; } protected static function fileList(array $files) { if (!$files) { return self::emptyState('Файлы не собраны', 'Список подключённых PHP-файлов пуст.'); } $html = self::filterBar('files', 'Найти подключённый файл', count($files)); $html .= '
'; foreach ($files as $index => $file) { $directory = dirname($file); $name = basename($file); $html .= '
' . ($index + 1) . '
' . self::e($name) . '' . '' . self::e($directory === '.' ? '/' : $directory . '/') . '
'; } return $html . '
'; } protected static function inspectorPanel(array $entries) { if (!$entries) { return self::emptyState('Элементы не найдены', 'Страница не содержит поддерживаемых границ вывода.'); } $available = count(array_filter($entries, function ($entry) { return !empty($entry['has_boundary']); })); $html = '
Доступно областей: ' . (int) $available . '
'; $html .= self::filterBar('elements', 'Тип, название, ID, alias или тег', count($entries)); $html .= '
'; foreach ($entries as $entry) { $type = isset($entry['type']) ? (string) $entry['type'] : 'component'; $token = isset($entry['token']) ? (int) $entry['token'] : 0; $title = isset($entry['title']) && $entry['title'] !== '' ? (string) $entry['title'] : self::inspectorLabel($type); $identity = isset($entry['entity_id']) && $entry['entity_id'] !== '' ? '#' . $entry['entity_id'] : ''; $alias = isset($entry['alias']) && $entry['alias'] !== '' ? (string) $entry['alias'] : ''; $tag = isset($entry['tag']) ? (string) $entry['tag'] : ''; $search = self::lower(self::inspectorLabel($type) . ' ' . $title . ' ' . $identity . ' ' . $alias . ' ' . $tag); $depth = isset($entry['depth']) ? max(0, min(8, (int) $entry['depth'])) : 0; $html .= '
'; $html .= '
' . self::e(self::inspectorLabel($type)) . ($identity !== '' ? ' · ' . self::e($identity) : '') . '' . self::e($title) . ''; if ($alias !== '' || $tag !== '') { $html .= '' . self::e($tag !== '' ? $tag : $alias) . ''; } $html .= '
'; if (!empty($entry['has_boundary'])) { $html .= ''; } if ($tag !== '') { $html .= ''; } if (!empty($entry['edit_url'])) { $html .= 'Редактировать'; } $html .= '
'; } return $html . '
'; } protected static function inspectorLabel($type) { $labels = array( 'document' => 'Документ', 'block' => 'Блок', 'request' => 'Запрос', 'navigation' => 'Навигация', 'module' => 'Модуль', ); return isset($labels[$type]) ? $labels[$type] : 'Компонент'; } protected static function filterBar($id, $placeholder, $count) { return '
' . (int) $count . '
'; } protected static function emptyState($title, $text, $tone = 'neutral') { return '
' . self::e($title) . '' . self::e($text) . '
'; } protected static function shortId($value) { $value = (string) $value; if (strlen($value) <= 16) { return $value; } return substr($value, 0, 8) . '…' . substr($value, -6); } protected static function lower($value) { return function_exists('mb_strtolower') ? mb_strtolower((string) $value, 'UTF-8') : strtolower((string) $value); } protected static function plural($count, $one, $few, $many) { $count = abs((int) $count) % 100; $last = $count % 10; if ($count > 10 && $count < 20) { return $many; } if ($last > 1 && $last < 5) { return $few; } if ($last === 1) { return $one; } return $many; } protected static function headers() { $headers = array(); foreach ($_SERVER as $key => $value) { if (strpos($key, 'HTTP_') !== 0 && !in_array($key, array('CONTENT_TYPE', 'CONTENT_LENGTH'), true)) { continue; } $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', preg_replace('/^HTTP_/', '', $key))))); $headers[$name] = $value; } return $headers; } protected static function masked($value, $key = '') { if (is_array($value)) { $result = array(); foreach ($value as $itemKey => $itemValue) { $result[$itemKey] = self::masked($itemValue, (string) $itemKey); } return $result; } if (preg_match('/pass|password|passwd|secret|token|csrf|auth|cookie|api.?key|session|sessid|phpsessid/i', $key)) { return '***'; } return is_scalar($value) || $value === null ? $value : gettype($value); } protected static function safeUri($uri) { $uri = (string) $uri; $parts = parse_url($uri); if (!is_array($parts) || empty($parts['query'])) { return $uri; } parse_str($parts['query'], $query); $query = self::masked($query); $path = isset($parts['path']) ? $parts['path'] : '/'; return $path . ($query ? '?' . http_build_query($query) : ''); } protected static function caller($caller) { if (!is_array($caller) || !$caller) { return ''; } for ($index = count($caller) - 1; $index >= 0; $index--) { $frame = $caller[$index]; if (!is_array($frame) || empty($frame['call_file'])) { continue; } $file = str_replace('\\', '/', (string) $frame['call_file']); if (strpos($file, '/system/App/Common/Db/') !== false) { continue; } $file = str_replace(BASEPATH . '/', '', $file); return $file . (isset($frame['call_line']) ? ':' . $frame['call_line'] : ''); } return ''; } protected static function mask($value) { return preg_replace('/((?:password|passwd|secret|token|api[_-]?key)\s*=\s*)([\'\"][^\'\"]*[\'\"]|[^\s,;]+)/i', '$1***', (string) $value); } protected static function number($value) { return (float) str_replace(',', '.', (string) $value); } protected static function timestamp($value) { $parts = explode(' ', trim((string) $value), 2); return count($parts) === 2 ? (float) $parts[1] + (float) $parts[0] : (float) $value; } protected static function bytes($bytes) { return number_format((float) $bytes / 1048576, 1, ',', ' ') . ' MB'; } protected static function isAjax() { return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower((string) $_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; } protected static function e($value) { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } protected static function assets() { return <<<'HTML' HTML; } }