1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Core\ExceptionRenderer;
6:
7: use Atk4\Core\Exception;
8:
9: class Html extends RendererAbstract
10: {
11: #[\Override]
12: protected function processHeader(): void
13: {
14: $title = $this->getExceptionTitle();
15: $class = get_class($this->exception);
16:
17: $tokens = [
18: '{TITLE}' => $title,
19: '{CLASS}' => $class,
20: '{MESSAGE}' => $this->getExceptionMessage(),
21: '{CODE}' => $this->exception->getCode() ? ' [code: ' . $this->exception->getCode() . ']' : '',
22: ];
23:
24: $this->output .= $this->replaceTokens('
25: <div class="ui negative icon message">
26: <i class="warning sign icon"></i>
27: <div class="content">
28: <div class="header">{TITLE}</div>
29: {CLASS}{CODE}:
30: {MESSAGE}
31: </div>
32: </div>
33: ', $tokens);
34: }
35:
36: protected function encodeHtml(string $value): string
37: {
38: return htmlspecialchars($value, \ENT_HTML5 | \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
39: }
40:
41: #[\Override]
42: protected function processParams(): void
43: {
44: if (!$this->exception instanceof Exception) {
45: return;
46: }
47:
48: if (count($this->exception->getParams()) === 0) {
49: return;
50: }
51:
52: $text = '
53: <table class="ui very compact small selectable table top aligned">
54: <thead><tr><th colspan="2" class="ui inverted red table">Exception Parameters</th></tr></thead>
55: <tbody>{PARAMS}
56: </tbody>
57: </table>
58: ';
59:
60: $tokens = [
61: '{PARAMS}' => '',
62: ];
63: $textInner = '
64: <tr><td><b>{KEY}</b></td><td style="width: 100%;">{VAL}</td></tr>';
65: foreach ($this->exception->getParams() as $key => $val) {
66: $key = $this->encodeHtml($key);
67: $val = '<span style="white-space: pre-wrap;">' . preg_replace('~(?<=\n)( +)~', '$1$1', $this->encodeHtml(static::toSafeString($val, true))) . '</span>';
68:
69: $tokens['{PARAMS}'] .= $this->replaceTokens($textInner, [
70: '{KEY}' => $key,
71: '{VAL}' => $val,
72: ]);
73: }
74:
75: $this->output .= $this->replaceTokens($text, $tokens);
76: }
77:
78: #[\Override]
79: protected function processSolutions(): void
80: {
81: if (!$this->exception instanceof Exception) {
82: return;
83: }
84:
85: /** @var Exception $exception */
86: $exception = $this->exception;
87:
88: if (count($exception->getSolutions()) === 0) {
89: return;
90: }
91:
92: $text = '
93: <table class="ui very compact small selectable table top aligned">
94: <thead><tr><th colspan="2" class="ui inverted green table">Suggested solutions</th></tr></thead>
95: <tbody>{SOLUTIONS}
96: </tbody>
97: </table>
98: ';
99:
100: $tokens = [
101: '{SOLUTIONS}' => '',
102: ];
103: $textInner = '
104: <tr><td>{VAL}</td></tr>';
105: foreach ($exception->getSolutions() as $key => $val) {
106: $tokens['{SOLUTIONS}'] .= $this->replaceTokens($textInner, ['{VAL}' => $this->encodeHtml($val)]);
107: }
108:
109: $this->output .= $this->replaceTokens($text, $tokens);
110: }
111:
112: #[\Override]
113: protected function processStackTrace(): void
114: {
115: $this->output .= '
116: <table class="ui very compact small selectable table top aligned">
117: <thead><tr><th colspan="4">Stack Trace</th></tr></thead>
118: <thead><tr><th style="text-align: right">#</th><th>File</th><th>Object</th><th>Method</th></tr></thead>
119: <tbody>
120: ';
121:
122: $this->processStackTraceInternal();
123:
124: $this->output .= '
125: </tbody>
126: </table>
127: ';
128: }
129:
130: #[\Override]
131: protected function processStackTraceInternal(): void
132: {
133: $text = '
134: <tr class="{CSS_CLASS}">
135: <td style="text-align: right">{INDEX}</td>
136: <td>{FILE_LINE}</td>
137: <td>{OBJECT}</td>
138: <td>{FUNCTION}{FUNCTION_ARGS}</td>
139: </tr>
140: ';
141:
142: $inAtk = true;
143: $shortTrace = $this->getStackTrace(true);
144: $isShortened = end($shortTrace) && key($shortTrace) !== 0 && key($shortTrace) !== 'self';
145: foreach ($shortTrace as $index => $call) {
146: $call = $this->parseStackTraceFrame($call);
147:
148: $escapeFrame = false;
149: if ($inAtk && !preg_match('~atk4[/\\\\][^/\\\\]+[/\\\\]src[/\\\\]~', $call['file'])) {
150: $escapeFrame = true;
151: $inAtk = false;
152: }
153:
154: $tokens = [];
155: $tokens['{INDEX}'] = $index === 'self' ? '' : $index + 1;
156: $tokens['{FILE_LINE}'] = $call['file_rel'] !== '' ? $call['file_rel'] . ':' . $call['line'] : '';
157: $tokens['{OBJECT}'] = $call['object'] !== false ? $call['object_formatted'] : '-';
158: $tokens['{CLASS}'] = $call['class'] !== false ? $call['class_formatted'] . '::' : '';
159: $tokens['{CSS_CLASS}'] = $escapeFrame ? 'negative' : '';
160:
161: $tokens['{FUNCTION}'] = $call['function'];
162:
163: if ($index === 'self') {
164: $tokens['{FUNCTION_ARGS}'] = '';
165: } elseif (count($call['args']) === 0) {
166: $tokens['{FUNCTION_ARGS}'] = '()';
167: } else {
168: if ($escapeFrame) {
169: $tokens['{FUNCTION_ARGS}'] = '(<br>' . implode(',<br>', array_map(function ($arg) {
170: return $this->encodeHtml(static::toSafeString($arg, false, 1));
171: }, $call['args'])) . ')';
172: } else {
173: $tokens['{FUNCTION_ARGS}'] = '(...)';
174: }
175: }
176:
177: $this->output .= $this->replaceTokens($text, $tokens);
178: }
179:
180: if ($isShortened) {
181: $this->output .= '
182: <tr>
183: <td style="text-align: right">...</td>
184: <td></td>
185: <td></td>
186: <td></td>
187: </tr>
188: ';
189: }
190: }
191:
192: #[\Override]
193: protected function processPreviousException(): void
194: {
195: if (!$this->exception->getPrevious()) {
196: return;
197: }
198:
199: $this->output .= '
200: <div class="ui top attached segment">
201: <div class="ui top attached label">Caused by Previous Exception:</div>
202: </div>
203: ';
204:
205: $this->output .= (string) (new static($this->exception->getPrevious(), $this->adapter, $this->exception));
206: }
207: }
208: