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