‪TYPO3CMS  10.4
DebugExceptionHandler.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 namespace ‪TYPO3\CMS\Core\Error;
19 
21 
28 {
29  protected ‪$logExceptionStackTrace = true;
30 
34  public function ‪__construct()
35  {
36  set_exception_handler([$this, 'handleException']);
37  }
38 
44  public function ‪echoExceptionWeb(\Throwable $exception)
45  {
46  $this->‪sendStatusHeaders($exception);
47  $this->‪writeLogEntries($exception, self::CONTEXT_WEB);
48 
49  $content = $this->‪getContent($exception);
50  $css = $this->‪getStylesheet();
51 
52  echo <<<HTML
53 <!DOCTYPE html>
54 <html>
55  <head>
56  <meta charset="UTF-8" />
57  <title>‪TYPO3 ‪Exception</title>
58  <meta name="robots" content="noindex,nofollow" />
59  <style>$css</style>
60  </head>
61  <body>
62  $content
63  </body>
64 </html>
65 HTML;
66  }
67 
73  public function ‪echoExceptionCLI(\Throwable $exception)
74  {
75  $filePathAndName = $exception->getFile();
76  $exceptionCodeNumber = $exception->getCode() > 0 ? '#' . $exception->getCode() . ': ' : '';
77  $this->‪writeLogEntries($exception, self::CONTEXT_CLI);
78  echo LF . 'Uncaught TYPO3 Exception ' . $exceptionCodeNumber . $exception->getMessage() . LF;
79  echo 'thrown in file ' . $filePathAndName . LF;
80  echo 'in line ' . $exception->getLine() . LF . LF;
81  die(1);
82  }
83 
90  protected function ‪getContent(\Throwable $throwable): string
91  {
92  $content = '';
93 
94  // exceptions can be chained
95  // for easier debugging, all exceptions are displayed to the developer
96  $throwables = $this->‪getAllThrowables($throwable);
97  $count = count($throwables);
98  foreach ($throwables as $position => $e) {
99  $content .= $this->‪getSingleThrowableContent($e, $position + 1, $count);
100  }
101 
102  $exceptionInfo = '';
103  if ($throwable->getCode() > 0) {
104  $documentationLink = ‪Typo3Information::URL_EXCEPTION . 'debug/' . $throwable->getCode();
105  $exceptionInfo = <<<INFO
106  <div class="container">
107  <div class="callout">
108  <h4 class="callout-title">Get help in the ‪TYPO3 Documentation</h4>
109  <div class="callout-body">
110  <p>
111  If you need help solving this exception, you can have a look at the ‪TYPO3 Documentation.
112  There you can find solutions provided by the ‪TYPO3 community.
113  Once you have found a solution to the problem, help others by contributing to the
114  documentation page.
115  </p>
116  <p>
117  <a href="$documentationLink" target="_blank" rel="noreferrer">Find a solution for this exception in the ‪TYPO3 Documentation.</a>
118  </p>
119  </div>
120  </div>
121  </div>
122 INFO;
123  }
124 
125  $typo3Logo = $this->‪getTypo3LogoAsSvg();
126 
127  return <<<HTML
128  <div class="exception-page">
129  <div class="exception-summary">
130  <div class="container">
131  <div class="exception-message-wrapper">
132  <div class="exception-illustration hidden-xs-down">$typo3Logo</div>
133  <h1 class="exception-message break-long-words">Whoops, looks like something went wrong.</h1>
134  </div>
135  </div>
136  </div>
137 
138  $exceptionInfo
139 
140  <div class="container">
141  $content
142  </div>
143  </div>
144 HTML;
145  }
146 
155  protected function ‪getSingleThrowableContent(\Throwable $throwable, int $index, int $total): string
156  {
157  $exceptionTitle = get_class($throwable);
158  $exceptionCode = $throwable->getCode() ? '#' . $throwable->getCode() . ' ' : '';
159  $exceptionMessage = $this->‪escapeHtml($throwable->getMessage());
160 
161  // The trace does not contain the step where the exception is thrown.
162  // To display it as well it is added manually to the trace.
163  $trace = $throwable->getTrace();
164  array_unshift($trace, [
165  'file' => $throwable->getFile(),
166  'line' => $throwable->getLine(),
167  'args' => [],
168  ]);
169 
170  $backtraceCode = $this->‪getBacktraceCode($trace);
171 
172  return <<<HTML
173  <div class="trace">
174  <div class="trace-head">
175  <h3 class="trace-class">
176  <span class="text-muted">({$index}/{$total})</span>
177  <span class="exception-title">{$exceptionCode}{$exceptionTitle}</span>
178  </h3>
179  <p class="trace-message break-long-words">{$exceptionMessage}</p>
180  </div>
181  <div class="trace-body">
182  {$backtraceCode}
183  </div>
184  </div>
185 HTML;
186  }
187 
193  protected function ‪getStylesheet(): string
194  {
195  return <<<STYLESHEET
196  html {
197  -webkit-text-size-adjust: 100%;
198  -ms-text-size-adjust: 100%;
199  -ms-overflow-style: scrollbar;
200  -webkit-tap-highlight-color: transparent;
201  }
202 
203  body {
204  margin: 0;
205  }
206 
207  .exception-page {
208  background-color: #eaeaea;
209  color: #212121;
210  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
211  font-weight: 400;
212  height: 100vh;
213  line-height: 1.5;
214  overflow-x: hidden;
215  overflow-y: scroll;
216  text-align: left;
217  top: 0;
218  }
219 
220  .panel-collapse .exception-page {
221  height: 100%;
222  }
223 
224  .exception-page a {
225  color: #ff8700;
226  text-decoration: underline;
227  }
228 
229  .exception-page a:hover {
230  text-decoration: none;
231  }
232 
233  .exception-page abbr[title] {
234  border-bottom: none;
235  cursor: help;
236  text-decoration: none;
237  }
238 
239  .exception-page code,
240  .exception-page kbd,
241  .exception-page pre,
242  .exception-page samp {
243  font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
244  font-size: 1em;
245  }
246 
247  .exception-page pre {
248  background-color: #ffffff;
249  overflow-x: auto;
250  border: 1px solid rgba(0,0,0,0.125);
251  }
252 
253  .exception-page pre span {
254  display: block;
255  line-height: 1.3em;
256  }
257 
258  .exception-page pre span:before {
259  display: inline-block;
260  content: attr(data-line);
261  border-right: 1px solid #b9b9b9;
262  margin-right: 0.5em;
263  padding-right: 0.5em;
264  background-color: #f4f4f4;
265  width: 4em;
266  text-align: right;
267  color: #515151;
268  }
269 
270  .exception-page pre span.highlight {
271  background-color: #cce5ff;
272  }
273 
274  .exception-page .break-long-words {
275  -ms-word-break: break-all;
276  word-break: break-all;
277  word-break: break-word;
278  -webkit-hyphens: auto;
279  -moz-hyphens: auto;
280  hyphens: auto;
281  }
282 
283  .exception-page .callout {
284  padding: 1.5rem;
285  background-color: #fff;
286  margin-bottom: 2em;
287  box-shadow: 0 2px 1px rgba(0,0,0,.15);
288  border-left: 3px solid #8c8c8c;
289  }
290 
291  .exception-page .callout-title {
292  margin: 0;
293  }
294 
295  .exception-page .callout-body p:last-child {
296  margin-bottom: 0;
297  }
298 
299  .exception-page .container {
300  max-width: 1140px;
301  margin: 0 auto;
302  padding: 0 30px;
303  }
304 
305  .panel-collapse .exception-page .container {
306  width: 100%;
307  }
308 
309  .exception-page .exception-illustration {
310  width: 3em;
311  height: 3em;
312  float: left;
313  margin-right: 1rem;
314  }
315 
316  .exception-page .exception-illustration svg {
317  width: 100%;
318  }
319 
320  .exception-page .exception-illustration svg path {
321  fill: #ff8700;
322  }
323 
324  .exception-page .exception-summary {
325  background: #000000;
326  color: #fff;
327  padding: 1.5rem 0;
328  margin-bottom: 2rem;
329  }
330 
331  .exception-page .exception-summary h1 {
332  margin: 0;
333  }
334 
335  .exception-page .text-muted {
336  opacity: 0.5;
337  }
338 
339  .exception-page .trace {
340  background-color: #fff;
341  margin-bottom: 2rem;
342  box-shadow: 0 2px 1px rgba(0,0,0,.15);
343  }
344 
345  .exception-page .trace-arguments {
346  color: #8c8c8c;
347  }
348 
349  .exception-page .trace-body {
350  }
351 
352  .exception-page .trace-call {
353  margin-bottom: 1rem;
354  }
355 
356  .exception-page .trace-class {
357  margin: 0;
358  }
359 
360  .exception-page .trace-file pre {
361  margin-top: 1.5rem;
362  margin-bottom: 0;
363  }
364 
365  .exception-page .trace-head {
366  color: #721c24;
367  background-color: #f8d7da;
368  padding: 1.5rem;
369  }
370 
371  .exception-page .trace-file-path {
372  word-break: break-all;
373  }
374 
375  .exception-page .trace-message {
376  margin-bottom: 0;
377  }
378 
379  .exception-page .trace-step {
380  padding: 1.5rem;
381  border-bottom: 1px solid #b9b9b9;
382  }
383 
384  .exception-page .trace-step > *:first-child {
385  margin-top: 0;
386  }
387 
388  .exception-page .trace-step > *:last-child {
389  margin-bottom: 0;
390  }
391 
392  .exception-page .trace-step:nth-child(even)
393  {
394  background-color: #fafafa;
395  }
396 
397  .exception-page .trace-step:last-child {
398  border-bottom: none;
399  }
400 STYLESHEET;
401  }
402 
409  protected function ‪getBacktraceCode(array $trace): string
410  {
411  $content = '';
412 
413  foreach ($trace as $index => $step) {
414  $content .= '<div class="trace-step">';
415  ‪$args = $this->‪flattenArgs($step['args'] ?? []);
416 
417  if (isset($step['function'])) {
418  $content .= '<div class="trace-call">' . sprintf(
419  'at <span class="trace-class">%s</span><span class="trace-type">%s</span><span class="trace-method">%s</span>(<span class="trace-arguments">%s</span>)',
420  $step['class'] ?? '',
421  $step['type'],
422  $step['function'],
423  $this->‪formatArgs(‪$args)
424  ) . '</div>';
425  }
426 
427  if (isset($step['file']) && isset($step['line'])) {
428  $content .= $this->‪getCodeSnippet($step['file'], $step['line']);
429  }
430 
431  $content .= '</div>';
432  }
433 
434  return $content;
435  }
436 
444  protected function ‪getCodeSnippet(string $filePathAndName, int $lineNumber): string
445  {
446  $showLinesAround = 4;
447 
448  $content = '<div class="trace-file">';
449  $content .= '<div class="trace-file-head">' . $this->‪formatPath($filePathAndName, $lineNumber) . '</div>';
450 
451  if (@file_exists($filePathAndName)) {
452  $phpFile = @file($filePathAndName);
453  if (is_array($phpFile)) {
454  $startLine = $lineNumber > $showLinesAround ? $lineNumber - $showLinesAround : 1;
455  $phpFileCount = count($phpFile);
456  $endLine = $lineNumber < $phpFileCount - $showLinesAround ? $lineNumber + $showLinesAround + 1 : $phpFileCount + 1;
457  if ($endLine > $startLine) {
458  $content .= '<div class="trace-file-content">';
459  $content .= '<pre>';
460 
461  for ($line = $startLine; $line < $endLine; $line++) {
462  $codeLine = str_replace("\t", ' ', $phpFile[$line - 1]);
463  $spanClass = '';
464  if ($line === $lineNumber) {
465  $spanClass = 'highlight';
466  }
467 
468  $content .= '<span class="' . $spanClass . '" data-line="' . $line . '">' . $this->‪escapeHtml($codeLine) . '</span>';
469  }
470 
471  $content .= '</pre>';
472  $content .= '</div>';
473  }
474  }
475  }
476 
477  $content .= '</div>';
478 
479  return $content;
480  }
481 
489  protected function ‪formatPath(string $path, int $line): string
490  {
491  return sprintf(
492  '<span class="block trace-file-path">in <strong>%s</strong>%s</span>',
493  $this->‪escapeHtml($path),
494  0 < $line ? ' line ' . $line : ''
495  );
496  }
497 
504  protected function ‪formatArgs(array ‪$args): string
505  {
506  $result = [];
507  foreach (‪$args as $key => $item) {
508  if ('object' === $item[0]) {
509  $formattedValue = sprintf('<em>object</em>(%s)', $item[1]);
510  } elseif ('array' === $item[0]) {
511  $formattedValue = sprintf('<em>array</em>(%s)', is_array($item[1]) ? $this->‪formatArgs($item[1]) : $item[1]);
512  } elseif ('null' === $item[0]) {
513  $formattedValue = '<em>null</em>';
514  } elseif ('boolean' === $item[0]) {
515  $formattedValue = '<em>' . strtolower(var_export($item[1], true)) . '</em>';
516  } elseif ('resource' === $item[0]) {
517  $formattedValue = '<em>resource</em>';
518  } else {
519  $formattedValue = str_replace("\n", '', $this->‪escapeHtml(var_export($item[1], true)));
520  }
521 
522  $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->‪escapeHtml($key), $formattedValue);
523  }
524 
525  return implode(', ', $result);
526  }
527 
528  protected function ‪flattenArgs(array ‪$args, int $level = 0, int &$count = 0): array
529  {
530  $result = [];
531  foreach (‪$args as $key => $value) {
532  if (++$count > 1e4) {
533  return ['array', '*SKIPPED over 10000 entries*'];
534  }
535  if ($value instanceof \__PHP_Incomplete_Class) {
536  // is_object() returns false on PHP<=7.1
537  $result[$key] = ['incomplete-object', $this->‪getClassNameFromIncomplete($value)];
538  } elseif (is_object($value)) {
539  $result[$key] = ['object', get_class($value)];
540  } elseif (is_array($value)) {
541  if ($level > 10) {
542  $result[$key] = ['array', '*DEEP NESTED ARRAY*'];
543  } else {
544  $result[$key] = ['array', $this->‪flattenArgs($value, $level + 1, $count)];
545  }
546  } elseif (null === $value) {
547  $result[$key] = ['null', null];
548  } elseif (is_bool($value)) {
549  $result[$key] = ['boolean', $value];
550  } elseif (is_int($value)) {
551  $result[$key] = ['integer', $value];
552  } elseif (is_float($value)) {
553  $result[$key] = ['float', $value];
554  } elseif (is_resource($value)) {
555  $result[$key] = ['resource', get_resource_type($value)];
556  } else {
557  $result[$key] = ['string', (string)$value];
558  }
559  }
560 
561  return $result;
562  }
563 
564  protected function ‪getClassNameFromIncomplete(\__PHP_Incomplete_Class $value): string
565  {
566  $array = new \ArrayObject($value);
567 
568  return $array['__PHP_Incomplete_Class_Name'];
569  }
570 
571  protected function ‪escapeHtml(string $str): string
572  {
573  return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE);
574  }
575 
576  protected function ‪getTypo3LogoAsSvg(): string
577  {
578  return <<<SVG
579 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M11.1 10.3c-.2 0-.3.1-.5.1C9 10.4 6.8 5 6.8 3.2c0-.7.2-.9.4-1.1-2 .2-4.2.9-4.9 1.8-.2.2-.3.6-.3 1 0 2.8 3 9.2 5.1 9.2 1 0 2.6-1.6 4-3.8m-1-8.4c1.9 0 3.9.3 3.9 1.4 0 2.2-1.4 4.9-2.1 4.9C10.6 8.3 9 4.7 9 2.9c0-.8.3-1 1.1-1"></path></svg>
580 SVG;
581  }
582 
583  protected function ‪getAllThrowables(\Throwable $throwable): array
584  {
585  $all = [$throwable];
586 
587  while ($throwable = $throwable->getPrevious()) {
588  $all[] = $throwable;
589  }
590 
591  return $all;
592  }
593 }
‪TYPO3\CMS\Core\Error\DebugExceptionHandler
Definition: DebugExceptionHandler.php:28
‪TYPO3\CMS\Core\Information\Typo3Information
Definition: Typo3Information.php:26
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\flattenArgs
‪flattenArgs(array $args, int $level=0, int &$count=0)
Definition: DebugExceptionHandler.php:528
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\formatPath
‪string formatPath(string $path, int $line)
Definition: DebugExceptionHandler.php:489
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\echoExceptionCLI
‪echoExceptionCLI(\Throwable $exception)
Definition: DebugExceptionHandler.php:73
‪TYPO3
‪TYPO3\CMS\Core\Information\Typo3Information\URL_EXCEPTION
‪const URL_EXCEPTION
Definition: Typo3Information.php:29
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getBacktraceCode
‪string getBacktraceCode(array $trace)
Definition: DebugExceptionHandler.php:409
‪TYPO3\CMS\Core\Error\AbstractExceptionHandler
Definition: AbstractExceptionHandler.php:34
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getClassNameFromIncomplete
‪getClassNameFromIncomplete(\__PHP_Incomplete_Class $value)
Definition: DebugExceptionHandler.php:564
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getContent
‪string getContent(\Throwable $throwable)
Definition: DebugExceptionHandler.php:90
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getTypo3LogoAsSvg
‪getTypo3LogoAsSvg()
Definition: DebugExceptionHandler.php:576
‪TYPO3\CMS\Core\Error\AbstractExceptionHandler\sendStatusHeaders
‪sendStatusHeaders(\Throwable $exception)
Definition: AbstractExceptionHandler.php:154
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getStylesheet
‪string getStylesheet()
Definition: DebugExceptionHandler.php:193
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getAllThrowables
‪getAllThrowables(\Throwable $throwable)
Definition: DebugExceptionHandler.php:583
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\$logExceptionStackTrace
‪$logExceptionStackTrace
Definition: DebugExceptionHandler.php:29
‪TYPO3\CMS\Core\Error\Exception
Definition: Exception.php:22
‪$args
‪$args
Definition: validateRstFiles.php:214
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getCodeSnippet
‪string getCodeSnippet(string $filePathAndName, int $lineNumber)
Definition: DebugExceptionHandler.php:444
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\formatArgs
‪string formatArgs(array $args)
Definition: DebugExceptionHandler.php:504
‪TYPO3\CMS\Core\Error
Definition: AbstractExceptionHandler.php:16
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\escapeHtml
‪escapeHtml(string $str)
Definition: DebugExceptionHandler.php:571
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getSingleThrowableContent
‪string getSingleThrowableContent(\Throwable $throwable, int $index, int $total)
Definition: DebugExceptionHandler.php:155
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\__construct
‪__construct()
Definition: DebugExceptionHandler.php:34
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\echoExceptionWeb
‪echoExceptionWeb(\Throwable $exception)
Definition: DebugExceptionHandler.php:44
‪TYPO3\CMS\Core\Error\AbstractExceptionHandler\writeLogEntries
‪writeLogEntries(\Throwable $exception, $context)
Definition: AbstractExceptionHandler.php:72