2 declare(strict_types
= 1);
3 namespace TYPO3\CMS\Core\Error
;
6 * This file is part of the TYPO3 CMS project.
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.
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
15 * The TYPO3 project - inspiring people to share!
19 * A basic but solid exception handler which catches everything which
20 * falls through the other exception handlers and provides useful debugging
23 class DebugExceptionHandler
extends AbstractExceptionHandler
26 * Constructs this exception handler - registers itself as the default exception handler.
28 public function __construct()
30 set_exception_handler([$this, 'handleException']);
34 * Formats and echoes the exception as XHTML.
36 * @param \Throwable $exception The throwable object.
38 public function echoExceptionWeb(\Throwable
$exception)
40 $this->sendStatusHeaders($exception);
41 $this->writeLogEntries($exception, self
::CONTEXT_WEB
);
43 $content = $this->getContent($exception);
44 $css = $this->getStylesheet();
50 <meta charset="UTF-8" />
51 <title>TYPO3 Exception</title>
52 <meta name="robots" content="noindex,nofollow" />
63 * Formats and echoes the exception for the command line
65 * @param \Throwable $exception The throwable object.
67 public function echoExceptionCLI(\Throwable
$exception)
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
;
79 * Generates the HTML for the error output.
81 * @param \Throwable $throwable
84 protected function getContent(\Throwable
$throwable): string
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);
97 if ($throwable->getCode() > 0) {
98 $wikiLink = 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 Wiki</h4>
103 <div class="callout-body">
105 If you need help solving this exception, you can have a look at the TYPO3 Wiki.
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 wiki page.
110 <a href="$wikiLink" target="_blank">Find a solution for this exception in the TYPO3 wiki.</a>
118 $typo3Logo = $this->getTypo3LogoAsSvg();
121 <div class="exception-page">
122 <div class="exception-summary">
123 <div class="container">
124 <div class="exception-message-wrapper">
125 <div class="exception-illustration hidden-xs-down">$typo3Logo</div>
126 <h1 class="exception-message break-long-words">Whoops, looks like something went wrong.</h1>
133 <div class="container">
141 * Renders the HTML for a single throwable.
143 * @param \Throwable $throwable
148 protected function getSingleThrowableContent(\Throwable
$throwable, int $index, int $total): string
150 $exceptionTitle = get_class($throwable);
151 $exceptionCode = $throwable->getCode() ?
'#' . $throwable->getCode() . ' ' : '';
152 $exceptionMessage = $this->escapeHtml($throwable->getMessage());
154 // The trace does not contain the step where the exception is thrown.
155 // To display it as well it is added manually to the trace.
156 $trace = $throwable->getTrace();
157 array_unshift($trace, [
158 'file' => $throwable->getFile(),
159 'line' => $throwable->getLine(),
163 $backtraceCode = $this->getBacktraceCode($trace);
167 <div class="trace-head">
168 <h3 class="trace-class">
169 <span class="text-muted">({$index}/{$total})</span>
170 <span class="exception-title">{$exceptionCode}{$exceptionTitle}</span>
172 <p class="trace-message break-long-words">{$exceptionMessage}</p>
174 <div class="trace-body">
182 * Generates the stylesheet needed to display the error page.
186 protected function getStylesheet(): string
190 -webkit-text-size-adjust: 100%;
191 -ms-text-size-adjust: 100%;
192 -ms-overflow-style: scrollbar;
193 -webkit-tap-highlight-color: transparent;
201 background-color: #eaeaea;
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";
218 text-decoration: underline;
221 .exception-page a:hover {
222 text-decoration: none;
225 .exception-page abbr[title] {
228 text-decoration: none;
231 .exception-page code,
234 .exception-page samp {
235 font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
239 .exception-page pre {
240 background-color: #ffffff;
242 border: 1px solid rgba(0,0,0,0.125);
245 .exception-page pre span {
250 .exception-page pre span:before {
251 display: inline-block;
252 content: attr(data-line);
253 border-right: 1px solid #b9b9b9;
255 padding-right: 0.5em;
256 background-color: #f4f4f4;
262 .exception-page pre span.highlight {
263 background-color: #cce5ff;
266 .exception-page .break-long-words {
267 -ms-word-break: break-all;
268 word-break: break-all;
269 word-break: break-word;
270 -webkit-hyphens: auto;
275 .exception-page .callout {
277 background-color: #fff;
279 box-shadow: 0 2px 1px rgba(0,0,0,.15);
280 border-left: 3px solid #8c8c8c;
283 .exception-page .callout-title {
287 .exception-page .callout-body p:last-child {
291 .exception-page .container {
297 .exception-page .exception-illustration {
304 .exception-page .exception-illustration svg {
308 .exception-page .exception-illustration svg path {
312 .exception-page .exception-summary {
319 .exception-page .exception-summary h1 {
323 .exception-page .text-muted {
327 .exception-page .trace {
328 background-color: #fff;
330 box-shadow: 0 2px 1px rgba(0,0,0,.15);
333 .exception-page .trace-arguments {
337 .exception-page .trace-body {
340 .exception-page .trace-call {
344 .exception-page .trace-class {
348 .exception-page .trace-file pre {
353 .exception-page .trace-head {
355 background-color: #f8d7da;
359 .exception-page .trace-message {
363 .exception-page .trace-step {
365 border-bottom: 1px solid #b9b9b9;
368 .exception-page .trace-step > *:first-child {
372 .exception-page .trace-step > *:last-child {
376 .exception-page .trace-step:nth-child(even)
378 background-color: #fafafa;
381 .exception-page .trace-step:last-child {
388 * Renders the backtrace as HTML.
390 * @param array $trace
393 protected function getBacktraceCode(array $trace): string
397 foreach ($trace as $index => $step) {
398 $content .= '<div class="trace-step">';
399 $args = $this->flattenArgs($step['args'] ??
[]);
401 if (isset($step['function'])) {
402 $content .= '<div class="trace-call">' . sprintf(
403 '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>)',
404 $step['class'] ??
'',
407 $this->formatArgs($args)
411 if (isset($step['file']) && isset($step['line'])) {
412 $content .= $this->getCodeSnippet($step['file'], $step['line']);
415 $content .= '</div>';
422 * Returns a code snippet from the specified file.
424 * @param string $filePathAndName Absolute path and file name of the PHP file
425 * @param int $lineNumber Line number defining the center of the code snippet
426 * @return string The code snippet
428 protected function getCodeSnippet(string $filePathAndName, int $lineNumber): string
430 $showLinesAround = 4;
432 $content = '<div class="trace-file">';
433 $content .= '<div class="trace-file-head">' . $this->formatPath($filePathAndName, $lineNumber) . '</div>';
435 if (@file_exists
($filePathAndName)) {
436 $phpFile = @file
($filePathAndName);
437 if (is_array($phpFile)) {
438 $startLine = $lineNumber > $showLinesAround ?
$lineNumber - $showLinesAround : 1;
439 $phpFileCount = count($phpFile);
440 $endLine = $lineNumber < $phpFileCount - $showLinesAround ?
$lineNumber +
$showLinesAround +
1 : $phpFileCount +
1;
441 if ($endLine > $startLine) {
442 $content .= '<div class="trace-file-content">';
445 for ($line = $startLine; $line < $endLine; $line++
) {
446 $codeLine = str_replace(TAB
, ' ', $phpFile[$line - 1]);
448 if ($line === $lineNumber) {
449 $spanClass = 'highlight';
452 $content .= '<span class="' . $spanClass . '" data-line="' . $line . '">' . $this->escapeHtml($codeLine) . '</span>';
455 $content .= '</pre>';
456 $content .= '</div>';
461 $content .= '</div>';
467 * Formats a path adding a line number.
469 * @param string $path The full path of the file.
470 * @param int $line The line number.
473 protected function formatPath(string $path, int $line): string
476 '<span class="block trace-file-path">in <strong>%s</strong>%s</span>',
477 $this->escapeHtml($path),
478 0 < $line ?
' line ' . $line : ''
483 * Formats the arguments of a method call.
485 * @param array $args The flattened args of method/function call
488 protected function formatArgs(array $args): string
491 foreach ($args as $key => $item) {
492 if ('object' === $item[0]) {
493 $formattedValue = sprintf('<em>object</em>(%s)', $item[1]);
494 } elseif ('array' === $item[0]) {
495 $formattedValue = sprintf('<em>array</em>(%s)', is_array($item[1]) ?
$this->formatArgs($item[1]) : $item[1]);
496 } elseif ('null' === $item[0]) {
497 $formattedValue = '<em>null</em>';
498 } elseif ('boolean' === $item[0]) {
499 $formattedValue = '<em>' . strtolower(var_export($item[1], true)) . '</em>';
500 } elseif ('resource' === $item[0]) {
501 $formattedValue = '<em>resource</em>';
503 $formattedValue = str_replace("\n", '', $this->escapeHtml(var_export($item[1], true)));
506 $result[] = \
is_int($key) ?
$formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue);
509 return implode(', ', $result);
512 protected function flattenArgs(array $args, int $level = 0, int &$count = 0): array
515 foreach ($args as $key => $value) {
516 if (++
$count > 1e4
) {
517 return ['array', '*SKIPPED over 10000 entries*'];
519 if ($value instanceof \__PHP_Incomplete_Class
) {
520 // is_object() returns false on PHP<=7.1
521 $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)];
522 } elseif (is_object($value)) {
523 $result[$key] = ['object', get_class($value)];
524 } elseif (is_array($value)) {
526 $result[$key] = ['array', '*DEEP NESTED ARRAY*'];
528 $result[$key] = ['array', $this->flattenArgs($value, $level +
1, $count)];
530 } elseif (null === $value) {
531 $result[$key] = ['null', null];
532 } elseif (is_bool($value)) {
533 $result[$key] = ['boolean', $value];
534 } elseif (is_int($value)) {
535 $result[$key] = ['integer', $value];
536 } elseif (is_float($value)) {
537 $result[$key] = ['float', $value];
538 } elseif (is_resource($value)) {
539 $result[$key] = ['resource', get_resource_type($value)];
541 $result[$key] = ['string', (string)$value];
548 protected function getClassNameFromIncomplete(\__PHP_Incomplete_Class
$value): string
550 $array = new \
ArrayObject($value);
552 return $array['__PHP_Incomplete_Class_Name'];
555 protected function escapeHtml(string $str): string
557 return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE
);
560 protected function getTypo3LogoAsSvg(): string
563 <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>
567 protected function getAllThrowables(\Throwable
$throwable): array
571 while ($throwable = $throwable->getPrevious()) {