TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Error\Http\StatusException;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* An abstract exception handler
*
* This file is a backport from TYPO3 Flow
*/
abstract class AbstractExceptionHandler implements ExceptionHandlerInterface, SingletonInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
public const CONTEXT_WEB = 'WEB';
public const CONTEXT_CLI = 'CLI';
protected const IGNORED_EXCEPTION_CODES = [
1396795884, // Current host header value does not match the configured trusted hosts pattern
1616175867, // Backend login request is rate limited
1616175847, // Frontend login request is rate limited
1436717275, // Request with unsupported HTTP method
1699604555, // Outdated __trustedProperties format in extbase property mapping
1517949792, // The IP address of your client does not match the list of allowed IP addresses
1517949793, // Backend access by browser is locked for maintenance
1517949794, // Backend and Install Tool are locked for maintenance
1436717270, // Client sends a header with an invalid name
1436717269, // Client sends a header with an invalid value
];
public const IGNORED_HMAC_EXCEPTION_CODES = [
1581862822, // Failed HMAC validation due to modified __trustedProperties in extbase property mapping
1581862823, // Failed HMAC validation due to modified form state in ext:forms
1320830018, // Failed HMAC validation due to modified HMAC string in Extbase HashService
1320830276, // Failed HMAC validation due to too short HMAC string in Extbase HashService
1704454157, // Failed HMAC validation due to modified HMAC string in Core HashService
1704454152, // Failed HMAC validation due to too short HMAC string in Core HashService
];
protected bool $logExceptionStackTrace = false;
/**
* Displays the given exception
*
* @param \Throwable $exception The throwable object.
*
* @throws \Exception
*/
public function handleException(\Throwable $exception)
{
switch (PHP_SAPI) {
case 'cli':
$this->echoExceptionCLI($exception);
break;
default:
$this->echoExceptionWeb($exception);
}
}
/**
* Writes exception to different logs
*
* @param \Throwable $exception The throwable object.
* @param string $mode The context where the exception was thrown.
* Either self::CONTEXT_WEB or self::CONTEXT_CLI.
*/
protected function writeLogEntries(\Throwable $exception, string $mode): void
{
// Do not write any logs for some messages to avoid filling up tables or files with illegal requests
$ignoredCodes = array_merge(self::IGNORED_EXCEPTION_CODES, self::IGNORED_HMAC_EXCEPTION_CODES);
if (in_array($exception->getCode(), $ignoredCodes, true)) {
return;
}
// PSR-3 logging framework.
try {
if ($this->logger) {
// 'FE' if in FrontendApplication, else 'BE' (also in CLI without request object)
// @todo: We could reconsider this construct with PHP 8.5: It might be possible to register
// the exception handler early during bootstrap. Then, later, when a request is available,
// get it, and reconfigure exception handler to its final state. This would avoid the runtime
// dependency to request including the funny PHP_SAPI fork in handleException().
$applicationMode = ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
? 'FE'
: 'BE';
$requestUrl = $this->anonymizeToken(NormalizedParams::createFromServerParams($_SERVER)->getRequestUrl());
$this->logger->critical('Core: Exception handler ({mode}: {application_mode}): {exception_class}, code #{exception_code}, file {file}, line {line}: {message}', [
'mode' => $mode,
'application_mode' => $applicationMode,
'exception_class' => get_class($exception),
'exception_code' => $exception->getCode(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'message' => $exception->getMessage(),
'request_url' => $requestUrl,
'exception' => $this->logExceptionStackTrace ? $exception : null,
]);
}
} catch (\Exception $exception) {
// A nested exception here was probably caused by a database failure, which means there's little
// else that can be done other than moving on and letting the system hard-fail.
}
// Legacy logger. Remove this section eventually.
$filePathAndName = $exception->getFile();
$exceptionCodeNumber = $exception->getCode() > 0 ? '#' . $exception->getCode() . ': ' : '';
$logTitle = 'Core: Exception handler (' . $mode . ')';
$logMessage = 'Uncaught TYPO3 Exception: ' . $exceptionCodeNumber . $exception->getMessage() . ' | '
. get_class($exception) . ' thrown in file ' . $filePathAndName . ' in line ' . $exception->getLine();
if ($mode === self::CONTEXT_WEB) {
$logMessage .= '. Requested URL: ' . $this->anonymizeToken(NormalizedParams::createFromServerParams($_SERVER)->getRequestUrl());
}
// When database credentials are wrong, the exception is probably
// caused by this. Therefore we cannot do any database operation,
// otherwise this will lead into recurring exceptions.
try {
// Write error message to sys_log table
$this->writeLog($logTitle . ': ' . $logMessage);
} catch (\Throwable $exception) {
}
}
/**
* Writes an exception in the sys_log table
*
* @param string $logMessage Default text that follows the message.
*/
protected function writeLog(string $logMessage)
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('sys_log');
if (!$connection->isConnected()) {
return;
}
$userId = 0;
$workspace = 0;
$data = [];
$backendUser = $this->getBackendUser();
if ($backendUser !== null) {
if (isset($backendUser->user['uid'])) {
$userId = $backendUser->user['uid'];
}
$workspace = $backendUser->workspace;
if ($backUserId = $backendUser->getOriginalUserIdWhenInSwitchUserMode()) {
$data['originalUser'] = $backUserId;
}
}
$connection->insert(
'sys_log',
[
'userid' => $userId,
'type' => SystemLogType::ERROR,
'channel' => SystemLogType::toChannel(SystemLogType::ERROR),
'action' => SystemLogGenericAction::UNDEFINED,
'error' => SystemLogErrorClassification::SYSTEM_ERROR,
'level' => SystemLogType::toLevel(SystemLogType::ERROR),
'details' => str_replace('%', '%%', $logMessage),
'log_data' => empty($data) ? '' : json_encode($data),
'IP' => NormalizedParams::createFromServerParams($_SERVER)->getRemoteAddress(),
'tstamp' => $GLOBALS['EXEC_TIME'],
'workspace' => $workspace,
]
);
}
/**
* Sends the HTTP Status 500 code, if $exception is *not* a
* TYPO3\CMS\Core\Error\Http\StatusException and headers are not sent, yet.
*
* @param \Throwable $exception The throwable object.
*/
protected function sendStatusHeaders(\Throwable $exception)
{
$headers = $exception instanceof StatusException
? $exception->getStatusHeaders()
: [HttpUtility::HTTP_STATUS_500];
if (!headers_sent()) {
foreach ($headers as $header) {
header($header);
}
}
}
/**
* Derives the numeric HTTP status code from the exception.
*
* Mirrors the logic of {@see sendStatusHeaders()}: returns the status code
* from the HTTP status line of a StatusException, or 500 for any other exception.
*/
protected function getHttpStatusCodeFromException(\Throwable $exception): int
{
if (!($exception instanceof StatusException)) {
return 500;
}
foreach ($exception->getStatusHeaders() ?? [] as $header) {
if (preg_match('/^HTTP\/[\d.]+\s+(\d{3})/', $header, $matches)) {
return (int)$matches[1];
}
}
return 500;
}
protected function getBackendUser(): ?BackendUserAuthentication
{
return $GLOBALS['BE_USER'] ?? null;
}
/**
* Replaces the generated token with a generic equivalent
*/
protected function anonymizeToken(string $requestedUrl): string
{
$pattern = '/(?:(?<=[tT]oken=)|(?<=[tT]oken%3D))[0-9a-fA-F]{40}/';
return preg_replace($pattern, '--AnonymizedToken--', $requestedUrl);
}
}
+668
View File
@@ -0,0 +1,668 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Information\Typo3Information;
/**
* A basic but solid exception handler which catches everything which
* falls through the other exception handlers and provides useful debugging
* information.
*/
class DebugExceptionHandler extends AbstractExceptionHandler
{
protected bool $logExceptionStackTrace = true;
/**
* Constructs this exception handler - registers itself as the default exception handler.
*/
public function __construct()
{
set_exception_handler($this->handleException(...));
}
/**
* Formats and echoes the exception as XHTML.
*
* @param \Throwable $exception The throwable object.
*/
public function echoExceptionWeb(\Throwable $exception)
{
$this->sendStatusHeaders($exception);
$this->writeLogEntries($exception, self::CONTEXT_WEB);
$content = $this->getContent($exception);
$css = $this->getStylesheet();
$js = $this->getJavascript();
echo <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>TYPO3 Exception</title>
<meta name="robots" content="noindex,nofollow" />
<style>$css</style>
<script>$js</script>
</head>
<body>
$content
</body>
</html>
HTML;
}
/**
* Formats and echoes the exception for the command line
*
* @param \Throwable $exception The throwable object.
*/
public function echoExceptionCLI(\Throwable $exception)
{
$filePathAndName = $exception->getFile();
$exceptionCodeNumber = $exception->getCode() > 0 ? '#' . $exception->getCode() . ': ' : '';
$this->writeLogEntries($exception, self::CONTEXT_CLI);
echo LF . 'Uncaught TYPO3 Exception ' . $exceptionCodeNumber . $exception->getMessage() . LF;
echo 'thrown in file ' . $filePathAndName . LF;
echo 'in line ' . $exception->getLine() . LF . LF;
die(1);
}
/**
* Generates the HTML for the error output.
*/
protected function getContent(\Throwable $throwable): string
{
$content = '';
// exceptions can be chained
// for easier debugging, all exceptions are displayed to the developer
$throwables = $this->getAllThrowables($throwable);
$count = count($throwables);
foreach ($throwables as $position => $e) {
$content .= $this->getSingleThrowableContent($e, $position + 1, $count);
}
$exceptionInfo = '';
if ($throwable->getCode() > 0) {
$documentationLink = Typo3Information::URL_EXCEPTION . 'debug/' . $throwable->getCode();
$exceptionInfo = <<<INFO
<div class="container">
<div class="callout">
<div class="callout-content">
<div class="callout-title">Get help in the TYPO3 Documentation</div>
<div class="callout-body">
<p>
If you need help solving this exception, you can have a look at the TYPO3 Documentation.
There you can find solutions provided by the TYPO3 community.
Once you have found a solution to the problem, help others by contributing to the
documentation page.
</p>
<p>
<a href="$documentationLink" target="_blank" rel="noreferrer">Find a solution for this exception in the TYPO3 Documentation.</a>
<span id="stacktrace-action-buttons"></span>
</p>
</div>
</div>
</div>
</div>
INFO;
}
$typo3Logo = $this->getTypo3LogoAsSvg();
try {
// This outside dependency class is always loaded before the exception handler is setup.
// So it is safe to access without affecting the output of this handler.
$projectPath = Environment::getProjectPath() . DIRECTORY_SEPARATOR;
} catch (\Throwable) {
// just in case something goes wrong.
$projectPath = '';
}
$projectPathEscaped = $this->escapeHtml($projectPath);
return <<<HTML
<div class="exception-page" data-project-path="$projectPathEscaped">
<div class="exception-summary">
<div class="container">
<div class="exception-message-wrapper">
<div class="exception-illustration hidden-xs-down">$typo3Logo</div>
<h1 class="exception-message break-long-words">
Whoops, looks like something went wrong.
<span id="stacktrace-action-buttons"></span>
</h1>
</div>
</div>
</div>
$exceptionInfo
<div class="container">
$content
</div>
</div>
HTML;
}
/**
* Renders the HTML for a single throwable.
*/
protected function getSingleThrowableContent(\Throwable $throwable, int $index, int $total): string
{
$exceptionTitle = get_class($throwable);
$exceptionCode = $throwable->getCode() ? '#' . $throwable->getCode() . ' ' : '';
$exceptionMessage = $this->escapeHtml($throwable->getMessage());
// The trace does not contain the step where the exception is thrown.
// To display it as well it is added manually to the trace.
$trace = $throwable->getTrace();
array_unshift($trace, [
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'args' => [],
]);
$backtraceCode = $this->getBacktraceCode($trace);
return <<<HTML
<div class="trace">
<div class="trace-head">
<h3 class="trace-class">
<span class="text-variant">({$index}/{$total})</span>
<span class="exception-title">{$exceptionCode}{$exceptionTitle}</span>
</h3>
<p class="trace-message break-long-words">{$exceptionMessage}</p>
</div>
<div class="trace-body">
{$backtraceCode}
</div>
</div>
HTML;
}
/**
* Generates the stylesheet needed to display the error page.
*/
protected function getStylesheet(): string
{
return <<<STYLESHEET
html {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
}
.exception-page {
background-color: #eaeaea;
color: #212121;
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";
font-weight: 400;
height: 100dvh;
line-height: 1.5;
overflow-x: hidden;
overflow-y: scroll;
text-align: left;
top: 0;
}
.panel-collapse .exception-page {
height: 100%;
}
.exception-page a {
color: #ff8700;
text-decoration: underline;
}
.exception-page a:hover {
text-decoration: none;
}
.exception-page abbr[title] {
border-bottom: none;
cursor: help;
text-decoration: none;
}
.exception-page code,
.exception-page kbd,
.exception-page pre,
.exception-page samp {
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
font-size: 1em;
}
.exception-page pre {
background-color: #ffffff;
overflow-x: auto;
border: 1px solid rgba(0,0,0,0.125);
}
.exception-page pre span {
display: block;
line-height: 1.3em;
}
.exception-page pre span:before {
display: inline-block;
content: attr(data-line);
border-right: 1px solid #b9b9b9;
margin-right: 0.5em;
padding-right: 0.5em;
background-color: #f4f4f4;
width: 4em;
text-align: right;
color: #515151;
}
.exception-page pre span.highlight {
background-color: #cce5ff;
}
.exception-page .break-long-words {
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
.exception-page .callout {
padding: 1.5rem;
background-color: #fff;
margin-bottom: 2em;
box-shadow: 0 2px 1px rgba(0,0,0,.15);
border-left: 3px solid #8c8c8c;
}
.exception-page .callout-title {
margin: 0;
}
.exception-page .callout-body p:last-child {
margin-bottom: 0;
}
.exception-page .container {
max-width: 1140px;
margin: 0 auto;
padding: 0 30px;
}
.panel-collapse .exception-page .container {
width: 100%;
}
.exception-page .exception-illustration {
width: 3em;
height: 3em;
float: left;
margin-right: 1rem;
}
.exception-page .exception-illustration svg {
width: 100%;
}
.exception-page .exception-illustration svg path {
fill: #ff8700;
}
.exception-page .exception-summary {
background: #000000;
color: #fff;
padding: 1.5rem 0;
margin-bottom: 2rem;
}
.exception-page .exception-summary h1 {
margin: 0;
}
.exception-page .text-variant {
opacity: 0.5;
}
.exception-page .trace {
background-color: #fff;
margin-bottom: 2rem;
box-shadow: 0 2px 1px rgba(0,0,0,.15);
}
.exception-page .trace-arguments {
color: #8c8c8c;
}
.exception-page .trace-hint {
margin: 0 0 0.5rem 0;
text-align: center;
}
.exception-page .trace-body {
}
.exception-page .trace-call {
margin-bottom: 1rem;
}
.exception-page .trace-class {
margin: 0;
}
.exception-page .trace-file pre {
margin-top: 1.5rem;
margin-bottom: 0;
}
.exception-page .trace-head {
color: #721c24;
background-color: #f8d7da;
padding: 1.5rem;
}
.exception-page .trace-file-path {
word-break: break-all;
}
.exception-page .trace-message {
margin-bottom: 0;
}
.exception-page .trace-step {
padding: 1.5rem;
border-bottom: 1px solid #b9b9b9;
}
.exception-page .trace-step > *:first-child {
margin-top: 0;
}
.exception-page .trace-step > *:last-child {
margin-bottom: 0;
}
.exception-page .trace-step:nth-child(even)
{
background-color: #fafafa;
}
.exception-page .trace-step:last-child {
border-bottom: none;
}
.exception-page .copy-button {
cursor: pointer;
border: 0.1rem solid transparent;
background-color: transparent;
padding: 0;
margin-left: 1rem;
}
.exception-page .copy-button:hover {
border: 0.1rem solid #b9b9b9;
}
.exception-page #stacktrace-action-buttons {
display: inline-flex;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.exception-page .stacktrace-action-button {
cursor: pointer;
padding: 0.5rem;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0,0,0,0);
box-sizing: border-box;
background-color: color(srgb 0.97 0.97 0.97);
border: 1px solid color(srgb 0.75 0.75 0.75);
border-radius: .75em;
color: color(srgb 0.1 0.1 0.1);
display: inline-flex;
font-weight: 400;
gap: .35em;
justify-content: center;
outline-offset: 0;
text-decoration: none;
--typo3-transition-color: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,opacity .15s ease-in-out;
transition: var(--typo3-transition-color);
user-select: none;
vertical-align: middle;
white-space: nowrap;
margin-bottom: 0;
margin-top: 0;
}
pre.plaintextFallback {
margin: 2rem auto;
border: 1px solid black;
max-height: 250px;
font-size: 0.8em;
padding: 1rem;
}
STYLESHEET;
}
/**
* Returns JavaScript functionality. Loaded from a TypeScript build.
* It is loaded inline, to not need to load additional URIs or build routes to assets.
* Also, it does not use ES6 module loading to be light-weight and dependency free.
*/
protected function getJavascript(): string
{
return file_get_contents(__DIR__ . '/../../Resources/Public/JavaScript/utility/debug-exception-handler-service.js');
}
/**
* Renders the backtrace as HTML.
*/
protected function getBacktraceCode(array $trace): string
{
$content = '';
foreach ($trace as $step) {
$content .= '<div class="trace-step">';
$args = $this->flattenArgs($step['args'] ?? []);
if (isset($step['function'])) {
$content .= '<div class="trace-call">' . sprintf(
'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>)',
$step['class'] ?? '',
$step['type'] ?? '',
$step['function'],
$this->formatArgs($args)
) . '</div>';
}
if (isset($step['file']) && isset($step['line'])) {
$content .= $this->getCodeSnippet($step['file'], $step['line']);
}
$content .= '</div>';
}
return $content;
}
/**
* Returns a code snippet from the specified file.
*
* @param string $filePathAndName Absolute path and file name of the PHP file
* @param int $lineNumber Line number defining the center of the code snippet
* @return string The code snippet
*/
protected function getCodeSnippet(string $filePathAndName, int $lineNumber): string
{
$showLinesAround = 4;
$content = '<div class="trace-file">';
$content .= '<div class="trace-file-head">' . $this->formatPath($filePathAndName, $lineNumber) . '</div>';
if (@file_exists($filePathAndName)) {
$phpFile = @file($filePathAndName);
if (is_array($phpFile)) {
$startLine = $lineNumber > $showLinesAround ? $lineNumber - $showLinesAround : 1;
$phpFileCount = count($phpFile);
$endLine = $lineNumber < $phpFileCount - $showLinesAround ? $lineNumber + $showLinesAround + 1 : $phpFileCount + 1;
if ($endLine > $startLine) {
$content .= '<div class="trace-file-content">';
$content .= '<pre>';
for ($line = $startLine; $line < $endLine; $line++) {
$codeLine = str_replace("\t", ' ', $phpFile[$line - 1]);
$spanClass = '';
if ($line === $lineNumber) {
$spanClass = 'highlight';
}
$content .= '<span class="' . $spanClass . '" data-line="' . $line . '">' . $this->escapeHtml($codeLine) . '</span>';
}
$content .= '</pre>';
$content .= '</div>';
}
}
}
$content .= '</div>';
return $content;
}
/**
* Formats a path adding a line number.
*
* @param string $path The full path of the file.
* @param int $line The line number.
*/
protected function formatPath(string $path, int $line): string
{
// "data-lineno" is evaluated by debug-exception-handler-service.js
return sprintf(
'<span class="block trace-file-path">in <strong data-lineno="%s">%s</strong>%s</span>',
$line > 0 ? $line : 1,
$this->escapeHtml($path),
$line > 0 ? ' line ' . $line : ''
);
}
/**
* Formats the arguments of a method call.
*
* @param array $args The flattened args of method/function call
*/
protected function formatArgs(array $args): string
{
$result = [];
foreach ($args as $key => $item) {
if ($item[0] === 'object') {
$formattedValue = sprintf('<em>object</em>(%s)', $item[1]);
} elseif ($item[0] === 'array') {
$formattedValue = sprintf('<em>array</em>(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
} elseif ($item[0] === 'null') {
$formattedValue = '<em>null</em>';
} elseif ($item[0] === 'boolean') {
$formattedValue = '<em>' . strtolower(var_export($item[1], true)) . '</em>';
} elseif ($item[0] === 'resource') {
$formattedValue = '<em>resource</em>';
} else {
$formattedValue = str_replace("\n", '', $this->escapeHtml(var_export($item[1], true)));
}
$result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue);
}
return implode(', ', $result);
}
protected function flattenArgs(array $args, int $level = 0, int &$count = 0): array
{
$result = [];
foreach ($args as $key => $value) {
if (++$count > 1e4) {
return ['array', '*SKIPPED over 10000 entries*'];
}
if ($value instanceof \__PHP_Incomplete_Class) {
// is_object() returns false on PHP<=7.1
$result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)];
} elseif (is_object($value)) {
$result[$key] = ['object', get_class($value)];
} elseif (is_array($value)) {
if ($level > 10) {
$result[$key] = ['array', '*DEEP NESTED ARRAY*'];
} else {
$result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)];
}
} elseif ($value === null) {
$result[$key] = ['null', null];
} elseif (is_bool($value)) {
$result[$key] = ['boolean', $value];
} elseif (is_int($value)) {
$result[$key] = ['integer', $value];
} elseif (is_float($value)) {
$result[$key] = ['float', $value];
} elseif (is_resource($value)) {
$result[$key] = ['resource', get_resource_type($value)];
} else {
$result[$key] = ['string', (string)$value];
}
}
return $result;
}
protected function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value): string
{
$array = new \ArrayObject($value);
return $array['__PHP_Incomplete_Class_Name'];
}
protected function escapeHtml(string $str): string
{
return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE);
}
protected function getTypo3LogoAsSvg(): string
{
return <<<SVG
<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>
SVG;
}
protected function getAllThrowables(\Throwable $throwable): array
{
$all = [$throwable];
while ($throwable = $throwable->getPrevious()) {
$all[] = $throwable;
}
return $all;
}
}
+290
View File
@@ -0,0 +1,290 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Global error handler for TYPO3
*
* This file is a backport from TYPO3 Flow
*/
class ErrorHandler implements ErrorHandlerInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
protected const ERROR_LEVEL_LABELS = [
E_WARNING => 'PHP Warning',
E_NOTICE => 'PHP Notice',
E_USER_ERROR => 'PHP User Error',
E_USER_WARNING => 'PHP User Warning',
E_USER_NOTICE => 'PHP User Notice',
E_RECOVERABLE_ERROR => 'PHP Catchable Fatal Error',
E_USER_DEPRECATED => 'TYPO3 Deprecation Notice',
E_DEPRECATED => 'PHP Runtime Deprecation Notice',
// @todo: Remove 2048 (deprecated E_STRICT) in v14, as this value is no longer used by PHP itself
// and only kept here here because possible custom PHP extensions may still use it.
// See https://wiki.php.net/rfc/deprecations_php_8_4#remove_e_strict_error_level_and_deprecate_e_strict_constant
2048 /* deprecated E_STRICT */ => 'PHP Runtime Notice',
];
/**
* Error levels which should result in an exception thrown.
*/
protected int $exceptionalErrors = 0;
/**
* Error levels which should be handled.
*/
protected int $errorHandlerErrors = 0;
/**
* Whether to write a flash message in case of an error
*/
protected bool $debugMode = false;
/**
* Registers this class as default error handler
*
* @param int $errorHandlerErrors The integer representing the E_* error level which should be
*/
public function __construct($errorHandlerErrors)
{
$excludedErrors = E_COMPILE_WARNING | E_COMPILE_ERROR | E_CORE_WARNING | E_CORE_ERROR | E_PARSE | E_ERROR;
// reduces error types to those a custom error handler can process
$this->errorHandlerErrors = (int)$errorHandlerErrors & ~$excludedErrors;
}
/**
* Defines which error levels should result in an exception thrown.
*
* @param int $exceptionalErrors The integer representing the E_* error level to handle as exceptions
*/
public function setExceptionalErrors($exceptionalErrors)
{
$exceptionalErrors = (int)$exceptionalErrors;
// We always disallow E_USER_DEPRECATED to generate exceptions as this may cause
// bad user experience specifically during upgrades.
$this->exceptionalErrors = $exceptionalErrors & ~E_USER_DEPRECATED;
}
/**
* @param bool $debugMode
*/
public function setDebugMode($debugMode)
{
$this->debugMode = (bool)$debugMode;
}
public function registerErrorHandler()
{
set_error_handler([$this, 'handleError']);
}
/**
* Handles an error.
* If the error is registered as exceptionalError it will by converted into an exception, to be handled
* by the configured exceptionhandler. Additionally the error message is written to the configured logs.
* If application is backend, the error message is also added to the flashMessageQueue, in frontend the
* error message is displayed in the admin panel (as TsLog message).
*
* @param int $errorLevel The error level - one of the E_* constants
* @param string $errorMessage The error message
* @param string $errorFile Name of the file the error occurred in
* @param int $errorLine Line number where the error occurred
* @return bool
* @throws Exception with the data passed to this method if the error is registered as exceptionalError
*/
public function handleError($errorLevel, $errorMessage, $errorFile, $errorLine)
{
// Filter all errors, that should not be reported/ handled from current error reporting
$reportingLevel = $this->errorHandlerErrors & error_reporting();
// Since symfony does this:
// @trigger_error('...', E_USER_DEPRECATED), and we DO want to log these,
// we always enforce deprecation messages to be handled, even when they are silenced
$reportingLevel |= E_USER_DEPRECATED;
$shouldHandleError = (bool)($reportingLevel & $errorLevel);
if (!$shouldHandleError) {
return self::ERROR_HANDLED;
}
$message = self::ERROR_LEVEL_LABELS[$errorLevel] . ': ' . $errorMessage . ' in ' . $errorFile . ' line ' . $errorLine;
if ($errorLevel & $this->exceptionalErrors) {
throw new Exception($message, 1476107295);
}
$message = $this->getFormattedLogMessage($message);
if ($errorLevel === E_USER_DEPRECATED || $errorLevel === E_DEPRECATED) {
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger('TYPO3.CMS.deprecations');
$logger->notice($message);
return self::ERROR_HANDLED;
}
switch ($errorLevel) {
case E_USER_ERROR:
case E_RECOVERABLE_ERROR:
$logLevel = LogLevel::ERROR;
break;
case E_USER_WARNING:
case E_WARNING:
$logLevel = LogLevel::WARNING;
break;
default:
$logLevel = LogLevel::NOTICE;
}
if ($this->logger) {
$this->logger->log($logLevel, $message);
}
try {
// Write error message to TSlog (admin panel)
$this->getTimeTracker()->setTSlogMessage($message, $logLevel);
} catch (\Throwable $e) {
// Silently catch in case an error occurs before the DI container is in place
}
// Write error message to sys_log table (ext: belog, Tools->Log)
if ($errorLevel & ($GLOBALS['TYPO3_CONF_VARS']['SYS']['belogErrorReporting'] ?? 0)) {
// Silently catch in case an error occurs before a database connection exists.
try {
$this->writeLog($message, $logLevel);
} catch (\Exception $e) {
}
}
if ($logLevel === LogLevel::ERROR) {
// Let the internal handler continue. This will stop the script
return self::PROPAGATE_ERROR;
}
if ($this->debugMode) {
$this->createAndEnqueueFlashMessage($message, $errorLevel);
}
// Don't execute PHP internal error handler
return self::ERROR_HANDLED;
}
protected function createAndEnqueueFlashMessage(string $message, int $errorLevel): void
{
switch ($errorLevel) {
case E_USER_WARNING:
case E_WARNING:
$flashMessageSeverity = ContextualFeedbackSeverity::WARNING;
break;
default:
$flashMessageSeverity = ContextualFeedbackSeverity::NOTICE;
}
$flashMessage = new FlashMessage(
$message,
self::ERROR_LEVEL_LABELS[$errorLevel],
$flashMessageSeverity
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
/**
* Writes an error in the sys_log table
*
* @param string $logMessage Default text that follows the message (in english!).
* @param string $logLevel The error level, see LogLevel::* constants
*/
protected function writeLog($logMessage, string $logLevel)
{
// Avoid ConnectionPool usage prior boot completion (see #96291).
if (!GeneralUtility::getContainer()->get('boot.state')->complete) {
if ($this->logger) {
// Log via debug(), the original message has already been logged with the original serverity in handleError().
// This log entry is targeted for users that try to debug why a log record is missing in sys_log
// while it has been logged to the logging framework.
$this->logger->debug(
'An error could not be logged to database as it appeared during early bootstrap (TCA or ext_localconf.php loading).',
['original_message' => $logMessage, 'original_loglevel' => $logLevel]
);
}
return;
}
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('sys_log');
if ($connection->isConnected()) {
$userId = 0;
$workspace = 0;
$data = [];
$backendUser = $this->getBackendUser();
if (is_object($backendUser)) {
if (isset($backendUser->user['uid'])) {
$userId = $backendUser->user['uid'];
}
$workspace = $backendUser->workspace;
if ($backUserId = $backendUser->getOriginalUserIdWhenInSwitchUserMode()) {
$data['originalUser'] = $backUserId;
}
}
$connection->insert(
'sys_log',
[
'userid' => $userId,
'type' => SystemLogType::ERROR,
'channel' => SystemLogType::toChannel(SystemLogType::ERROR),
'action' => SystemLogGenericAction::UNDEFINED,
'error' => SystemLogErrorClassification::SYSTEM_ERROR,
'level' => $logLevel,
'details' => str_replace('%', '%%', $logMessage),
'log_data' => empty($data) ? '' : json_encode($data),
'IP' => NormalizedParams::createFromServerParams($_SERVER)->getRemoteAddress(),
'tstamp' => $GLOBALS['EXEC_TIME'],
'workspace' => $workspace,
]
);
}
}
protected function getFormattedLogMessage(string $message): string
{
// String 'FE' if in FrontendApplication, else 'BE' (also in CLI without request object)
$applicationType = ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() ? 'FE' : 'BE';
$logPrefix = 'Core: Error handler (' . $applicationType . ')';
return $logPrefix . ': ' . $message;
}
protected function getTimeTracker(): TimeTracker
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
protected function getBackendUser(): ?BackendUserAuthentication
{
return $GLOBALS['BE_USER'] ?? null;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
/**
* Error handler interface for TYPO3
*
* This file is a backport from TYPO3 Flow
*/
interface ErrorHandlerInterface
{
// Constants to make the implications of the PHP error handling API a bit more obvious.
public const ERROR_HANDLED = true;
public const PROPAGATE_ERROR = false;
/**
* Registers this class as default error handler
*
* If dependencies need to be added using injector methods, the error handler may
* also be registered later on, within the optional registerErrorHandler() method.
*
* @param int $errorHandlerErrors The integer representing the E_* error level which should be
*/
public function __construct($errorHandlerErrors);
/**
* Defines which error levels should result in an exception thrown.
*
* @param int $exceptionalErrors The integer representing the E_* error level to handle as exceptions
*/
public function setExceptionalErrors($exceptionalErrors);
/**
* Handles an error.
* If the error is registered as exceptionalError it will by converted into an exception, to be handled
* by the configured exceptionhandler. Additionally the error message is written to the configured logs.
* If application is backend, the error message is also added to the flashMessageQueue, in frontend the
* error message is displayed in the admin panel (as TsLog message).
*
* @param int $errorLevel The error level - one of the E_* constants
* @param string $errorMessage The error message
* @param string $errorFile Name of the file the error occurred in
* @param int $errorLine Line number where the error occurred
* @return bool
* @throws \TYPO3\CMS\Core\Error\Exception with the data passed to this method if the error is registered as exceptionalError
*/
public function handleError($errorLevel, $errorMessage, $errorFile, $errorLine);
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
/**
* An exception which represents a PHP error.
*/
class Exception extends \TYPO3\CMS\Core\Exception {}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
/**
* Contract for an exception handler
*
* This file is a backport from TYPO3 Flow
*/
interface ExceptionHandlerInterface
{
/**
* Constructs this exception handler - registers itself as the default exception handler.
*/
public function __construct();
/**
* Handles the given exception
*
* @param \Throwable $exception The throwable object.
*/
public function handleException(\Throwable $exception);
/**
* Formats and echoes the exception as XHTML.
*
* @param \Throwable $exception The throwable object.
*/
public function echoExceptionWeb(\Throwable $exception);
/**
* Formats and echoes the exception for the command line
*
* @param \Throwable $exception The throwable object.
*/
public function echoExceptionCLI(\Throwable $exception);
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
/**
* HTTP Client Error Exception (Error 4xx)
*/
abstract class AbstractClientErrorException extends StatusException {}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
/**
* HTTP Server Error Exception (Error 5xx)
*/
abstract class AbstractServerErrorException extends StatusException {}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Exception for Error 400 - Bad Request
*/
class BadRequestException extends AbstractClientErrorException
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders = [HttpUtility::HTTP_STATUS_400];
/**
* @var string Title of the message
*/
protected $title = 'Bad Request (400)';
/**
* @var string Error Message
*/
protected $message = 'The request cannot be fulfilled due to bad syntax.';
/**
* Constructor for this Status Exception
*
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($message = null, $code = 0)
{
if (!empty($message)) {
$this->message = $message;
}
parent::__construct($this->statusHeaders, $this->message, $this->title, $code);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Exception for Error 403 - Forbidden
*/
class ForbiddenException extends AbstractClientErrorException
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders = [HttpUtility::HTTP_STATUS_403];
/**
* @var string Title of the message
*/
protected $title = 'Forbidden (403)';
/**
* @var string Error Message
*/
protected $message = 'You are not allowed to access this page.';
/**
* Constructor for this Status Exception
*
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($message = null, $code = 0)
{
if (!empty($message)) {
$this->message = $message;
}
parent::__construct($this->statusHeaders, $this->message, $this->title, $code);
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Exception for Error 500 - Internal Server Error
*/
class InternalServerErrorException extends AbstractServerErrorException
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders = [HttpUtility::HTTP_STATUS_500];
/**
* @var string Title of the message
*/
protected $title = 'Internal Server Error (500)';
/**
* @var string Error Message
*/
protected $message = 'This page is currently not available due to server errors.';
/**
* Constructor for this Status Exception
*
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($message = null, $code = 0)
{
if (!empty($message)) {
$this->message = $message;
}
parent::__construct($this->statusHeaders, $this->message, $this->title, $code);
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
/**
* Excepts if typolink could not be resolved for link pages.
*/
class LinkedPageNotResolvableException extends PageNotFoundException {}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Exception for Error 404 - Page Not Found
*/
class PageNotFoundException extends AbstractClientErrorException
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders = [HttpUtility::HTTP_STATUS_404];
/**
* @var string Title of the message
*/
protected $title = 'Page Not Found (404)';
/**
* @var string Error Message
*/
protected $message = 'The page you tried to access was not found.';
/**
* Constructor for this Status Exception
*
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($message = null, $code = 0)
{
if (!empty($message)) {
$this->message = $message;
}
parent::__construct($this->statusHeaders, $this->message, $this->title, $code);
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Exception for Error 503 - Service Unavailable
*/
class ServiceUnavailableException extends AbstractServerErrorException
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders = [HttpUtility::HTTP_STATUS_503];
/**
* @var string Title of the message
*/
protected $title = 'Service Unavailable (503)';
/**
* @var string Error Message
*/
protected $message = 'This page is currently not available.';
/**
* Constructor for this Status Exception
*
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($message = null, $code = 0)
{
if (!empty($message)) {
$this->message = $message;
}
parent::__construct($this->statusHeaders, $this->message, $this->title, $code);
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
/**
* Exception when a shortcut target page could not be resolved
*/
class ShortcutTargetPageNotFoundException extends PageNotFoundException {}
+85
View File
@@ -0,0 +1,85 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Error\Exception;
/**
* HTTP Status Exception
*
* @todo: Rename to AbstractStatusException and declare abstract
*/
class StatusException extends Exception
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders;
/**
* @var string Title of the message
*/
protected $title = 'Oops, an error occurred!';
/**
* Constructor for this Status Exception
*
* @param string|array $statusHeaders HTTP Status header line(s)
* @param string $title Title of the error message
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($statusHeaders, $message, $title = '', $code = 0)
{
if (is_array($statusHeaders)) {
$this->statusHeaders = $statusHeaders;
} else {
$this->statusHeaders = [$statusHeaders];
}
$this->title = $title ?: $this->title;
parent::__construct($message, $code);
}
/**
* Setter for the title.
*
* @param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Getter for the title.
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Getter for the Status Header.
*
* @return array
*/
public function getStatusHeaders()
{
return $this->statusHeaders;
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\Http;
use TYPO3\CMS\Core\Utility\HttpUtility;
/**
* Exception for Error 401 - Unauthorized
*/
class UnauthorizedException extends AbstractClientErrorException
{
/**
* @var array HTTP Status Header lines
*/
protected $statusHeaders = [HttpUtility::HTTP_STATUS_401];
/**
* @var string Title of the message
*/
protected $title = 'Unauthorized (401)';
/**
* @var string Error Message
*/
protected $message = 'Accessing this page requires authorization.';
/**
* Constructor for this Status Exception
*
* @param string $message Error Message
* @param int $code Exception Code
*/
public function __construct($message = null, $code = 0)
{
if (!empty($message)) {
$this->message = $message;
}
parent::__construct($this->statusHeaders, $this->message, $this->title, $code);
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
/**
* An error handler that renders a fluid template.
* This is typically configured via the "Sites configuration" module in the backend.
*/
class FluidPageErrorHandler implements PageErrorHandlerInterface
{
/**
* @todo: Change this "API" to not pollute __construct() anymore
*/
public function __construct(
protected int $statusCode,
protected array $configuration
) {}
public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
$configuration = $this->configuration;
$templateRootPaths = null;
if (is_string($configuration['errorFluidTemplatesRootPath'] ?? false) && $configuration['errorFluidTemplatesRootPath'] !== '') {
$templateRootPaths = [$configuration['errorFluidTemplatesRootPath']];
}
$layoutRootPaths = null;
if (is_string($configuration['errorFluidLayoutsRootPath'] ?? false) && $configuration['errorFluidLayoutsRootPath'] !== '') {
$layoutRootPaths = [$configuration['errorFluidLayoutsRootPath']];
}
$partialRootPaths = null;
if (is_string($configuration['errorFluidPartialsRootPath'] ?? false) && $configuration['errorFluidPartialsRootPath'] !== '') {
$partialRootPaths = [$configuration['errorFluidPartialsRootPath']];
}
$templatePathAndFilename = null;
if (is_string($configuration['errorFluidTemplate'] ?? false) && $configuration['errorFluidTemplate'] !== '') {
$templatePathAndFilename = GeneralUtility::getFileAbsFileName($configuration['errorFluidTemplate']);
}
if ($templatePathAndFilename === null || !is_file($templatePathAndFilename)) {
throw new \RuntimeException('FluidPageErrorHandler: Configured Fluid template file not found.', 1764510148);
}
$viewFactoryDate = new ViewFactoryData(
templateRootPaths: $templateRootPaths,
partialRootPaths: $partialRootPaths,
layoutRootPaths: $layoutRootPaths,
templatePathAndFilename: $templatePathAndFilename,
request: $request,
);
$viewFactory = GeneralUtility::makeInstance(ViewFactoryInterface::class);
$view = $viewFactory->create($viewFactoryDate);
$view->assignMultiple([
'request' => $request,
'message' => $message,
'reasons' => $reasons,
]);
return new HtmlResponse($view->render(), $this->statusCode);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use TYPO3\CMS\Core\Error\Exception;
/**
* Is typically used, when a site configuration has a page-error handler configured but this does not implement
* the PageErrorHandlerInterface
*/
class InvalidPageErrorHandlerException extends Exception {}
@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\Client\GuzzleClientFactory;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Http\Application;
/**
* Renders the content of a page to be displayed (also in relation to language etc)
* This is typically configured via the "Sites configuration" module in the backend.
*/
class PageContentErrorHandler implements PageErrorHandlerInterface
{
protected int $statusCode;
protected array $errorHandlerConfiguration;
protected int $pageUid = 0;
protected Application $application;
protected ResponseFactoryInterface $responseFactory;
protected SiteFinder $siteFinder;
protected LinkService $link;
protected RequestFactoryInterface $requestFactory;
protected GuzzleClientFactory $guzzleClientFactory;
/**
* PageContentErrorHandler constructor.
* @throws \InvalidArgumentException
*/
public function __construct(int $statusCode, array $configuration)
{
$this->statusCode = $statusCode;
if (empty($configuration['errorContentSource'])) {
throw new \InvalidArgumentException('PageContentErrorHandler needs to have a proper link set.', 1522826413);
}
$this->errorHandlerConfiguration = $configuration;
// @todo Convert this to DI once this class can be injected properly.
$container = GeneralUtility::getContainer();
$this->application = $container->get(Application::class);
$this->responseFactory = $container->get(ResponseFactoryInterface::class);
$this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
$this->link = $container->get(LinkService::class);
$this->requestFactory = $container->get(RequestFactoryInterface::class);
$this->guzzleClientFactory = $container->get(GuzzleClientFactory::class);
}
public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
try {
$urlParams = $this->link->resolve($this->errorHandlerConfiguration['errorContentSource']);
$urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0);
$urlType = $urlParams['type'] ?? LinkService::TYPE_UNKNOWN;
$resolvedUrl = $this->resolveUrl($request, $urlParams);
// avoid denial-of-service amplification scenario
if ($resolvedUrl === (string)$request->getUri()) {
return new HtmlResponse(
'The error page could not be resolved, as the error page itself is not accessible',
$this->statusCode
);
}
// External URL most likely pointing to additional hosts or pages not contained in the current instance,
// and using internal sub requests would never receive a valid page. Send an external request instead.
if ($urlType === LinkService::TYPE_URL) {
return $this->sendExternalRequest($resolvedUrl, $request);
}
// Create a sub-request and do not take any special query parameters into account
$subRequest = $request->withQueryParams([])->withUri(new Uri($resolvedUrl))->withMethod('GET');
$subResponse = $this->sendSubRequest($subRequest, $urlParams['pageuid'], $request);
if ($subResponse->getStatusCode() >= 300) {
throw new \RuntimeException(sprintf('Error handler could not fetch error page "%s", status code: %s', $resolvedUrl, $subResponse->getStatusCode()), 1544172839);
}
$response = $this->responseFactory->createResponse($this->statusCode)
->withHeader('content-type', $subResponse->getHeader('content-type'))
->withBody($subResponse->getBody());
foreach (['Content-Security-Policy', 'Content-Security-Policy-Report-Only'] as $header) {
if ($subResponse->hasHeader($header)) {
$response = $response->withHeader($header, $subResponse->getHeader($header));
}
}
return $response;
} catch (InvalidRouteArgumentsException|SiteNotFoundException $e) {
return new HtmlResponse('Invalid error handler configuration: ' . $this->errorHandlerConfiguration['errorContentSource']);
}
}
/**
* Sends an in-process subrequest.
*
* The $pageId is used to ensure the correct site is accessed.
*/
protected function sendSubRequest(ServerRequestInterface $request, int $pageId, ServerRequestInterface $originalRequest): ResponseInterface
{
$site = $request->getAttribute('site');
if (!$site instanceof Site) {
$site = $this->siteFinder->getSiteByPageId($pageId);
$request = $request->withAttribute('site', $site);
}
$request = $request->withAttribute('originalRequest', $originalRequest);
return $this->application->handle($request);
}
/**
* Sends an external request to fetch the error page from a remote resource.
*
* A custom header is added and checked to mitigate request loops, which
* indicates additional configuration error in the error handler config.
*/
protected function sendExternalRequest(string $url, ServerRequestInterface $originalRequest): ResponseInterface
{
if ($originalRequest->hasHeader('Requested-By')
&& in_array('TYPO3 Error Handler', $originalRequest->getHeader('Requested-By'), true)
) {
// If the header is set here, it is a recursive call within the same instance where an
// outer error handler called a page that results in another error handler call. To break
// the loop, we except here.
return new HtmlResponse(
'The error page could not be resolved, the error page itself is not accessible',
$this->statusCode
);
}
try {
$request = $this->requestFactory->createRequest('GET', $url)
->withHeader('Content-Type', 'text/html')
->withHeader('Requested-By', 'TYPO3 Error Handler');
$response = $this->guzzleClientFactory->getClient()->send($request);
// In case global guzzle configuration has been changed to not throw an exception
// for error status codes, the response status code is checked here.
if ($response->getStatusCode() >= 300) {
return new HtmlResponse(
'The error page could not be resolved, as the error page itself is not accessible',
$this->statusCode
);
}
return $this->responseFactory
->createResponse($this->statusCode)
->withHeader('Content-Type', $response->getHeader('Content-Type'))
->withBody($response->getBody());
} catch (GuzzleException) {
return new HtmlResponse(
'The error page could not be resolved, the error page itself is not accessible',
$this->statusCode
);
}
}
/**
* Resolve the URL (currently only page and external URL are supported)
*/
protected function resolveUrl(ServerRequestInterface $request, array $urlParams): string
{
if (!in_array($urlParams['type'], ['page', 'url'])) {
throw new \InvalidArgumentException('PageContentErrorHandler can only handle TYPO3 URLs of types "page" or "url"', 1522826609);
}
if ($urlParams['type'] === 'url') {
return $urlParams['url'];
}
// Get the site related to the configured error page
$site = $this->siteFinder->getSiteByPageId($urlParams['pageuid']);
$requestLanguage = $request->getAttribute('language');
// Try to get the current request language from the site that was found above
if ($requestLanguage instanceof SiteLanguage && $requestLanguage->isEnabled()) {
try {
$language = $site->getLanguageById($requestLanguage->getLanguageId());
} catch (\InvalidArgumentException $e) {
$language = $site->getDefaultLanguage();
}
} else {
$language = $site->getDefaultLanguage();
}
// Requested language or default language is disabled in current site => Fetch first "enabled" language
if (!$language->isEnabled()) {
$enabledLanguages = $site->getLanguages();
if ($enabledLanguages === []) {
throw new \RuntimeException(
'Site ' . $site->getIdentifier() . ' does not define any enabled language.',
1674487171
);
}
$language = reset($enabledLanguages);
}
// Build Url
$uri = $site->getRouter()->generateUri(
(int)$urlParams['pageuid'],
['_language' => $language]
);
// Fallback to the current URL if the site is not having a proper scheme and host
$currentUri = $request->getUri();
if (empty($uri->getScheme())) {
$uri = $uri->withScheme($currentUri->getScheme());
}
if (empty($uri->getUserInfo())) {
$uri = $uri->withUserInfo($currentUri->getUserInfo());
}
if (empty($uri->getHost())) {
$uri = $uri->withHost($currentUri->getHost());
}
if ($uri->getPort() === null) {
$uri = $uri->withPort($currentUri->getPort());
}
return (string)$uri;
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Page error handler interface, used to jump in for Frontend-related calls
*
* Needs to be implemented by all custom PHP-related Page Error Handlers.
*/
interface PageErrorHandlerInterface
{
/**
* @param array<string, mixed> $reasons
*/
public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface;
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use TYPO3\CMS\Core\Error\Exception;
/**
* Is typically used, when a site configuration has no page-error handler configured
* for a specific HTTP Status type that is requested.
*/
class PageErrorHandlerNotConfiguredException extends Exception {}
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error\PageErrorHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Controller\ErrorPageController;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
/**
* An error handler that redirects to a configured page, where the login process is handled. Passes a configurable
* url parameter (`return_url` or `redirect_url`) to the target page.
*/
class RedirectLoginErrorHandler implements PageErrorHandlerInterface
{
protected int $loginRedirectPid = 0;
protected int $statusCode = 0;
protected string $loginRedirectParameter = '';
protected Context $context;
protected LinkService $linkService;
public function __construct(int $statusCode, array $configuration)
{
$this->statusCode = $statusCode;
$this->context = GeneralUtility::makeInstance(Context::class);
$this->linkService = GeneralUtility::makeInstance(LinkService::class);
$urlParams = $this->linkService->resolve($configuration['loginRedirectTarget'] ?? '');
$this->loginRedirectPid = (int)($urlParams['pageuid'] ?? 0);
$this->loginRedirectParameter = $configuration['loginRedirectParameter'] ?? 'return_url';
}
public function handlePageError(
ServerRequestInterface $request,
string $message,
array $reasons = []
): ResponseInterface {
$this->checkHandlerConfiguration();
if ($this->shouldHandleRequest($reasons)) {
return $this->handleLoginRedirect($request);
}
// Show general error message with a 403 HTTP statuscode
return $this->getGenericAccessDeniedResponse($message);
}
private function getGenericAccessDeniedResponse(string $reason): ResponseInterface
{
$content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
'Page Not Found',
'The page did not exist or was inaccessible.' . ($reason ? ' Reason: ' . $reason : ''),
0,
$this->statusCode,
);
return new HtmlResponse($content, $this->statusCode);
}
private function handleLoginRedirect(ServerRequestInterface $request): ResponseInterface
{
if ($this->isLoggedIn()) {
return $this->getGenericAccessDeniedResponse(
'The requested page was not accessible with the provided credentials'
);
}
/** @var Site $site */
$site = $request->getAttribute('site');
$language = $request->getAttribute('language');
$loginUrl = $site->getRouter()->generateUri(
$this->loginRedirectPid,
[
'_language' => $language,
$this->loginRedirectParameter => (string)$request->getUri(),
]
);
return new RedirectResponse($loginUrl);
}
private function shouldHandleRequest(array $reasons): bool
{
if (!isset($reasons['code'])) {
return false;
}
$accessDeniedReasons = [
PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED,
PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED,
];
$isAccessDenied = in_array($reasons['code'], $accessDeniedReasons, true);
return $isAccessDenied || $this->isSimulatedBackendGroup();
}
private function isLoggedIn(): bool
{
return $this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn') || $this->isSimulatedBackendGroup();
}
protected function isSimulatedBackendGroup(): bool
{
// look for special "any group"
return $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn')
&& $this->context->getPropertyFromAspect('frontend.user', 'groupIds')[1] === -2;
}
private function checkHandlerConfiguration(): void
{
if ($this->loginRedirectPid === 0) {
throw new \RuntimeException('No loginRedirectTarget configured for LoginRedirect errorhandler', 1700813537);
}
if ($this->statusCode !== 403) {
throw new \RuntimeException('Invalid HTTP statuscode ' . $this->statusCode . ' for LoginRedirect errorhandler', 1700813545);
}
}
}
@@ -0,0 +1,139 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Error;
use TYPO3\CMS\Core\Controller\ErrorPageController;
use TYPO3\CMS\Core\Error\Http\AbstractClientErrorException;
use TYPO3\CMS\Core\Error\Http\StatusException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* An exception handler which catches any exception and
* renders an error page without backtrace (Web) or a slim
* message on CLI.
*/
class ProductionExceptionHandler extends AbstractExceptionHandler
{
/**
* Default title for error messages
*
* @var string
*/
protected $defaultTitle = 'Oops, an error occurred!';
/**
* Default message for error messages
*
* @var string
*/
protected $defaultMessage = '';
/**
* Constructs this exception handler - registers itself as the default exception handler.
*/
public function __construct()
{
set_exception_handler($this->handleException(...));
}
/**
* Echoes an exception for the web.
*
* @param \Throwable $exception The throwable object.
*/
public function echoExceptionWeb(\Throwable $exception)
{
$this->sendStatusHeaders($exception);
$this->writeLogEntries($exception, self::CONTEXT_WEB);
echo GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
$this->getTitle($exception),
$this->getMessage($exception),
$this->discloseExceptionInformation($exception) ? $exception->getCode() : 0,
$this->getHttpStatusCodeFromException($exception)
);
}
/**
* Echoes an exception for the command line.
*
* @param \Throwable $exception The throwable object.
*/
public function echoExceptionCLI(\Throwable $exception)
{
$filePathAndName = $exception->getFile();
$exceptionCodeNumber = $exception->getCode() > 0 ? '#' . $exception->getCode() . ': ' : '';
$this->writeLogEntries($exception, self::CONTEXT_CLI);
echo LF . 'Uncaught TYPO3 Exception ' . $exceptionCodeNumber . $exception->getMessage() . LF;
echo 'thrown in file ' . $filePathAndName . LF;
echo 'in line ' . $exception->getLine() . LF . LF;
die(1);
}
/**
* Determines, whether Exception details should be outputted
*
* @param \Throwable $exception The throwable object.
* @return bool
*/
protected function discloseExceptionInformation(\Throwable $exception)
{
// Allow message to be shown in production mode if the exception is about
// trusted host configuration. By doing so we do not disclose
// any valuable information to an attacker but avoid confusions among TYPO3 admins
// in production context.
if ($exception->getCode() === 1396795884) {
return true;
}
// Show client error messages 40x in every case
if ($exception instanceof AbstractClientErrorException) {
return true;
}
// Only show errors if a BE user is authenticated
$backendUser = $this->getBackendUser();
if ($backendUser === null) {
return false;
}
return ($backendUser->user['uid'] ?? 0) > 0;
}
/**
* Returns the title for the error message
*
* @param \Throwable $exception The throwable object.
* @return string
*/
protected function getTitle(\Throwable $exception)
{
if ($this->discloseExceptionInformation($exception) && $exception instanceof StatusException && $exception->getTitle() !== '') {
return $exception->getTitle();
}
return $this->defaultTitle;
}
/**
* Returns the message for the error message
*
* @param \Throwable $exception The throwable object.
* @return string
*/
protected function getMessage(\Throwable $exception)
{
if ($this->discloseExceptionInformation($exception)) {
return $exception->getMessage();
}
return $this->defaultMessage;
}
}