247 lines
10 KiB
PHP
247 lines
10 KiB
PHP
<?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);
|
|
}
|
|
}
|