TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user