‪TYPO3CMS  ‪main
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 
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 
150  protected function ‪getSingleThrowableContent(\Throwable $throwable, int $index, int $total): string
151  {
152  $exceptionTitle = get_class($throwable);
153  $exceptionCode = $throwable->getCode() ? '#' . $throwable->getCode() . ' ' : '';
154  $exceptionMessage = $this->‪escapeHtml($throwable->getMessage());
155 
156  // The trace does not contain the step where the exception is thrown.
157  // To display it as well it is added manually to the trace.
158  $trace = $throwable->getTrace();
159  array_unshift($trace, [
160  'file' => $throwable->getFile(),
161  'line' => $throwable->getLine(),
162  'args' => [],
163  ]);
164 
165  $backtraceCode = $this->‪getBacktraceCode($trace);
166 
167  return <<<HTML
168  <div class="trace">
169  <div class="trace-head">
170  <h3 class="trace-class">
171  <span class="text-body-secondary">({$index}/{$total})</span>
172  <span class="exception-title">{$exceptionCode}{$exceptionTitle}</span>
173  </h3>
174  <p class="trace-message break-long-words">{$exceptionMessage}</p>
175  </div>
176  <div class="trace-body">
177  {$backtraceCode}
178  </div>
179  </div>
180 HTML;
181  }
182 
186  protected function ‪getStylesheet(): string
187  {
188  return <<<STYLESHEET
189  html {
190  -webkit-text-size-adjust: 100%;
191  -ms-text-size-adjust: 100%;
192  -ms-overflow-style: scrollbar;
193  -webkit-tap-highlight-color: transparent;
194  }
195 
196  body {
197  margin: 0;
198  }
199 
200  .exception-page {
201  background-color: #eaeaea;
202  color: #212121;
203  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";
204  font-weight: 400;
205  height: 100vh;
206  line-height: 1.5;
207  overflow-x: hidden;
208  overflow-y: scroll;
209  text-align: left;
210  top: 0;
211  }
212 
213  .panel-collapse .exception-page {
214  height: 100%;
215  }
216 
217  .exception-page a {
218  color: #ff8700;
219  text-decoration: underline;
220  }
221 
222  .exception-page a:hover {
223  text-decoration: none;
224  }
225 
226  .exception-page abbr[title] {
227  border-bottom: none;
228  cursor: help;
229  text-decoration: none;
230  }
231 
232  .exception-page code,
233  .exception-page kbd,
234  .exception-page pre,
235  .exception-page samp {
236  font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
237  font-size: 1em;
238  }
239 
240  .exception-page pre {
241  background-color: #ffffff;
242  overflow-x: auto;
243  border: 1px solid rgba(0,0,0,0.125);
244  }
245 
246  .exception-page pre span {
247  display: block;
248  line-height: 1.3em;
249  }
250 
251  .exception-page pre span:before {
252  display: inline-block;
253  content: attr(data-line);
254  border-right: 1px solid #b9b9b9;
255  margin-right: 0.5em;
256  padding-right: 0.5em;
257  background-color: #f4f4f4;
258  width: 4em;
259  text-align: right;
260  color: #515151;
261  }
262 
263  .exception-page pre span.highlight {
264  background-color: #cce5ff;
265  }
266 
267  .exception-page .break-long-words {
268  -ms-word-break: break-all;
269  word-break: break-all;
270  word-break: break-word;
271  -webkit-hyphens: auto;
272  -moz-hyphens: auto;
273  hyphens: auto;
274  }
275 
276  .exception-page .callout {
277  padding: 1.5rem;
278  background-color: #fff;
279  margin-bottom: 2em;
280  box-shadow: 0 2px 1px rgba(0,0,0,.15);
281  border-left: 3px solid #8c8c8c;
282  }
283 
284  .exception-page .callout-title {
285  margin: 0;
286  }
287 
288  .exception-page .callout-body p:last-child {
289  margin-bottom: 0;
290  }
291 
292  .exception-page .container {
293  max-width: 1140px;
294  margin: 0 auto;
295  padding: 0 30px;
296  }
297 
298  .panel-collapse .exception-page .container {
299  width: 100%;
300  }
301 
302  .exception-page .exception-illustration {
303  width: 3em;
304  height: 3em;
305  float: left;
306  margin-right: 1rem;
307  }
308 
309  .exception-page .exception-illustration svg {
310  width: 100%;
311  }
312 
313  .exception-page .exception-illustration svg path {
314  fill: #ff8700;
315  }
316 
317  .exception-page .exception-summary {
318  background: #000000;
319  color: #fff;
320  padding: 1.5rem 0;
321  margin-bottom: 2rem;
322  }
323 
324  .exception-page .exception-summary h1 {
325  margin: 0;
326  }
327 
328  .exception-page .text-body-secondary {
329  opacity: 0.5;
330  }
331 
332  .exception-page .trace {
333  background-color: #fff;
334  margin-bottom: 2rem;
335  box-shadow: 0 2px 1px rgba(0,0,0,.15);
336  }
337 
338  .exception-page .trace-arguments {
339  color: #8c8c8c;
340  }
341 
342  .exception-page .trace-body {
343  }
344 
345  .exception-page .trace-call {
346  margin-bottom: 1rem;
347  }
348 
349  .exception-page .trace-class {
350  margin: 0;
351  }
352 
353  .exception-page .trace-file pre {
354  margin-top: 1.5rem;
355  margin-bottom: 0;
356  }
357 
358  .exception-page .trace-head {
359  color: #721c24;
360  background-color: #f8d7da;
361  padding: 1.5rem;
362  }
363 
364  .exception-page .trace-file-path {
365  word-break: break-all;
366  }
367 
368  .exception-page .trace-message {
369  margin-bottom: 0;
370  }
371 
372  .exception-page .trace-step {
373  padding: 1.5rem;
374  border-bottom: 1px solid #b9b9b9;
375  }
376 
377  .exception-page .trace-step > *:first-child {
378  margin-top: 0;
379  }
380 
381  .exception-page .trace-step > *:last-child {
382  margin-bottom: 0;
383  }
384 
385  .exception-page .trace-step:nth-child(even)
386  {
387  background-color: #fafafa;
388  }
389 
390  .exception-page .trace-step:last-child {
391  border-bottom: none;
392  }
393 STYLESHEET;
394  }
395 
399  protected function ‪getBacktraceCode(array $trace): string
400  {
401  $content = '';
402 
403  foreach ($trace as $index => $step) {
404  $content .= '<div class="trace-step">';
405  ‪$args = $this->‪flattenArgs($step['args'] ?? []);
406 
407  if (isset($step['function'])) {
408  $content .= '<div class="trace-call">' . sprintf(
409  '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>)',
410  $step['class'] ?? '',
411  $step['type'] ?? '',
412  $step['function'],
413  $this->‪formatArgs(‪$args)
414  ) . '</div>';
415  }
416 
417  if (isset($step['file']) && isset($step['line'])) {
418  $content .= $this->‪getCodeSnippet($step['file'], $step['line']);
419  }
420 
421  $content .= '</div>';
422  }
423 
424  return $content;
425  }
426 
434  protected function ‪getCodeSnippet(string $filePathAndName, int $lineNumber): string
435  {
436  $showLinesAround = 4;
437 
438  $content = '<div class="trace-file">';
439  $content .= '<div class="trace-file-head">' . $this->‪formatPath($filePathAndName, $lineNumber) . '</div>';
440 
441  if (@file_exists($filePathAndName)) {
442  $phpFile = @file($filePathAndName);
443  if (is_array($phpFile)) {
444  $startLine = $lineNumber > $showLinesAround ? $lineNumber - $showLinesAround : 1;
445  $phpFileCount = count($phpFile);
446  $endLine = $lineNumber < $phpFileCount - $showLinesAround ? $lineNumber + $showLinesAround + 1 : $phpFileCount + 1;
447  if ($endLine > $startLine) {
448  $content .= '<div class="trace-file-content">';
449  $content .= '<pre>';
450 
451  for ($line = $startLine; $line < $endLine; $line++) {
452  $codeLine = str_replace("\t", ' ', $phpFile[$line - 1]);
453  $spanClass = '';
454  if ($line === $lineNumber) {
455  $spanClass = 'highlight';
456  }
457 
458  $content .= '<span class="' . $spanClass . '" data-line="' . $line . '">' . $this->‪escapeHtml($codeLine) . '</span>';
459  }
460 
461  $content .= '</pre>';
462  $content .= '</div>';
463  }
464  }
465  }
466 
467  $content .= '</div>';
468 
469  return $content;
470  }
471 
478  protected function ‪formatPath(string $path, int $line): string
479  {
480  return sprintf(
481  '<span class="block trace-file-path">in <strong>%s</strong>%s</span>',
482  $this->‪escapeHtml($path),
483  $line > 0 ? ' line ' . $line : ''
484  );
485  }
486 
492  protected function ‪formatArgs(array ‪$args): string
493  {
494  $result = [];
495  foreach (‪$args as $key => $item) {
496  if ($item[0] === 'object') {
497  $formattedValue = sprintf('<em>object</em>(%s)', $item[1]);
498  } elseif ($item[0] === 'array') {
499  $formattedValue = sprintf('<em>array</em>(%s)', is_array($item[1]) ? $this->‪formatArgs($item[1]) : $item[1]);
500  } elseif ($item[0] === 'null') {
501  $formattedValue = '<em>null</em>';
502  } elseif ($item[0] === 'boolean') {
503  $formattedValue = '<em>' . strtolower(var_export($item[1], true)) . '</em>';
504  } elseif ($item[0] === 'resource') {
505  $formattedValue = '<em>resource</em>';
506  } else {
507  $formattedValue = str_replace("\n", '', $this->‪escapeHtml(var_export($item[1], true)));
508  }
509 
510  $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->‪escapeHtml($key), $formattedValue);
511  }
512 
513  return implode(', ', $result);
514  }
515 
516  protected function ‪flattenArgs(array ‪$args, int $level = 0, int &$count = 0): array
517  {
518  $result = [];
519  foreach (‪$args as $key => $value) {
520  if (++$count > 1e4) {
521  return ['array', '*SKIPPED over 10000 entries*'];
522  }
523  if ($value instanceof \__PHP_Incomplete_Class) {
524  // is_object() returns false on PHP<=7.1
525  $result[$key] = ['incomplete-object', $this->‪getClassNameFromIncomplete($value)];
526  } elseif (is_object($value)) {
527  $result[$key] = ['object', get_class($value)];
528  } elseif (is_array($value)) {
529  if ($level > 10) {
530  $result[$key] = ['array', '*DEEP NESTED ARRAY*'];
531  } else {
532  $result[$key] = ['array', $this->‪flattenArgs($value, $level + 1, $count)];
533  }
534  } elseif ($value === null) {
535  $result[$key] = ['null', null];
536  } elseif (is_bool($value)) {
537  $result[$key] = ['boolean', $value];
538  } elseif (is_int($value)) {
539  $result[$key] = ['integer', $value];
540  } elseif (is_float($value)) {
541  $result[$key] = ['float', $value];
542  } elseif (is_resource($value)) {
543  $result[$key] = ['resource', get_resource_type($value)];
544  } else {
545  $result[$key] = ['string', (string)$value];
546  }
547  }
548 
549  return $result;
550  }
551 
552  protected function ‪getClassNameFromIncomplete(\__PHP_Incomplete_Class $value): string
553  {
554  $array = new \ArrayObject($value);
555 
556  return $array['__PHP_Incomplete_Class_Name'];
557  }
558 
559  protected function ‪escapeHtml(string $str): string
560  {
561  return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE);
562  }
563 
564  protected function ‪getTypo3LogoAsSvg(): string
565  {
566  return <<<SVG
567 <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>
568 SVG;
569  }
570 
571  protected function ‪getAllThrowables(\Throwable $throwable): array
572  {
573  $all = [$throwable];
574 
575  while ($throwable = $throwable->getPrevious()) {
576  $all[] = $throwable;
577  }
578 
579  return $all;
580  }
581 }
‪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:516
‪TYPO3\CMS\Core\Error\AbstractExceptionHandler\writeLogEntries
‪writeLogEntries(\Throwable $exception, string $mode)
Definition: AbstractExceptionHandler.php:87
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getStylesheet
‪getStylesheet()
Definition: DebugExceptionHandler.php:186
‪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\AbstractExceptionHandler
Definition: AbstractExceptionHandler.php:37
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getClassNameFromIncomplete
‪getClassNameFromIncomplete(\__PHP_Incomplete_Class $value)
Definition: DebugExceptionHandler.php:552
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getTypo3LogoAsSvg
‪getTypo3LogoAsSvg()
Definition: DebugExceptionHandler.php:564
‪TYPO3\CMS\Core\Error\AbstractExceptionHandler\sendStatusHeaders
‪sendStatusHeaders(\Throwable $exception)
Definition: AbstractExceptionHandler.php:192
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getContent
‪getContent(\Throwable $throwable)
Definition: DebugExceptionHandler.php:90
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\formatPath
‪formatPath(string $path, int $line)
Definition: DebugExceptionHandler.php:478
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getBacktraceCode
‪getBacktraceCode(array $trace)
Definition: DebugExceptionHandler.php:399
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getAllThrowables
‪getAllThrowables(\Throwable $throwable)
Definition: DebugExceptionHandler.php:571
‪TYPO3\CMS\Core\Error\Exception
Definition: Exception.php:21
‪$args
‪$args
Definition: validateRstFiles.php:258
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getCodeSnippet
‪string getCodeSnippet(string $filePathAndName, int $lineNumber)
Definition: DebugExceptionHandler.php:434
‪TYPO3\CMS\Core\Error
Definition: AbstractExceptionHandler.php:16
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\escapeHtml
‪escapeHtml(string $str)
Definition: DebugExceptionHandler.php:559
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\formatArgs
‪formatArgs(array $args)
Definition: DebugExceptionHandler.php:492
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\getSingleThrowableContent
‪getSingleThrowableContent(\Throwable $throwable, int $index, int $total)
Definition: DebugExceptionHandler.php:150
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\__construct
‪__construct()
Definition: DebugExceptionHandler.php:34
‪TYPO3\CMS\Core\Error\DebugExceptionHandler\echoExceptionWeb
‪echoExceptionWeb(\Throwable $exception)
Definition: DebugExceptionHandler.php:47