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