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); } }