TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:27 +02:00
commit 44c503b2d1
233 changed files with 31556 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
<?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\Frontend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Controller\ErrorPageController;
use TYPO3\CMS\Core\Error\Http\InternalServerErrorException;
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerNotConfiguredException;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Middleware\ContentSecurityPolicyHeaders;
/**
* This controller provides actions for common HTTP error scenarios (404, 403, 500, 503) and supports custom error
* handling through site-specific error handlers. If no custom error handler is configured, it falls back to
* rendering a standard TYPO3 error page with appropriate status code and message.
*/
#[Autoconfigure(public: true)]
readonly class ErrorController
{
public function __construct(
private ContentSecurityPolicyHeaders $contentSecurityPolicyHeaders,
) {}
/**
* Used for creating a 500 response ("Internal Server Error"), usually due to some misconfiguration.
* If a page unavailable handler is configured, a RedirectResponse could be returned as well.
*
* @throws InternalServerErrorException
*/
public function internalErrorAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
if ($this->isRequestFromDevIp($request)) {
throw new InternalServerErrorException($message, 1607585445);
}
$errorHandler = $this->getErrorHandlerFromSite($request, 500);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
$response = $this->handleError(
$request,
500,
'Internal Server Error',
'An error occurred while processing your request. Please try again later.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
}
/**
* Used for creating a 503 response ("Service Unavailable"), to be used for maintenance mode
* or when the server is overloaded, a RedirectResponse could be returned as well.
*
* @throws ServiceUnavailableException
*/
public function unavailableAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
if ($this->isRequestFromDevIp($request)) {
throw new ServiceUnavailableException($message, 1518472181);
}
$errorHandler = $this->getErrorHandlerFromSite($request, 503);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
$response = $this->handleError(
$request,
503,
'Service Unavailable',
'The application is currently down for maintenance. Please check back shortly.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
}
/**
* Used for creating a 404 response ("Page Not Found"), but if configured, a RedirectResponse could be returned
* as well.
*
* @throws PageNotFoundException
*/
public function pageNotFoundAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
$errorHandler = $this->getErrorHandlerFromSite($request, 404);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
try {
$response = $this->handleError(
$request,
404,
'Page Not Found',
'The page did not exist or was inaccessible.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
} catch (\RuntimeException) {
throw new PageNotFoundException($message, 1518472189);
}
}
/**
* Used for creating a 403 response ("Access denied"), but if configured, a RedirectResponse could be returned
* as well.
*
* @throws PageNotFoundException
*/
public function accessDeniedAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
{
$errorHandler = $this->getErrorHandlerFromSite($request, 403);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
try {
$response = $this->handleError(
$request,
403,
'Access Denied',
'You do not have the necessary permissions to access this resource.',
$message
);
return $this->contentSecurityPolicyHeaders->applyToResponse($request, $response);
} catch (\RuntimeException) {
throw new PageNotFoundException($message, 1518472195);
}
}
/**
* Used for creating an error with a custom status code, but if configured, a RedirectResponse could be
* returned as well.
*
* @param array<string, mixed> $reasons An array of reasons for evaluation in a possible resolved the error handler
*
* @throws PageNotFoundException
*/
public function customErrorAction(
ServerRequestInterface $request,
int $statusCode,
string $title,
string $message,
string $technicalReason = '',
array $reasons = [],
int $errorCode = 0
): ResponseInterface {
$errorHandler = $this->getErrorHandlerFromSite($request, $statusCode);
if ($errorHandler !== null) {
return $errorHandler->handlePageError($request, $message, $reasons);
}
try {
return $this->handleError($request, $statusCode, $title, $message, $technicalReason, $errorCode);
} catch (\RuntimeException) {
throw new PageNotFoundException($message, 1770466857);
}
}
/**
* Checks whether the devIPMask matches the current visitor's IP address.
*
* @return bool False if the server error handler should be used.
*/
protected function isRequestFromDevIp(ServerRequestInterface $request): bool
{
$normalizedParams = $request->getAttribute('normalizedParams');
return GeneralUtility::cmpIP($normalizedParams->getRemoteAddress(), $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']);
}
/**
* Checks if a site is configured, and an error handler is configured for this specific status code.
*/
protected function getErrorHandlerFromSite(ServerRequestInterface $request, int $statusCode): ?PageErrorHandlerInterface
{
$site = $request->getAttribute('site');
if ($site instanceof Site) {
try {
return $site->getErrorHandler($statusCode);
} catch (PageErrorHandlerNotConfiguredException $e) {
// No error handler found, so fallback back to the generic TYPO3 error handler.
}
}
return null;
}
/**
* Handles the error by creating a response object. Acts as a fallback when no error handler is configured.
*/
protected function handleError(
ServerRequestInterface $request,
int $statusCode,
string $title,
string $message,
string $technicalReason = '',
int $errorCode = 0
): ResponseInterface {
if (str_contains($request->getHeaderLine('Accept'), 'application/json')) {
return new JsonResponse(['reason' => $technicalReason], $statusCode);
}
$content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
$title,
$message . ($technicalReason ? ' Reason: ' . $technicalReason : ''),
$errorCode,
$statusCode
);
return new HtmlResponse($content, $statusCode);
}
}
+245
View File
@@ -0,0 +1,245 @@
<?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\Frontend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Exception;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* eID-Script "tx_cms_showpic"
*
* Shows a picture from FAL in enlarged format in a separate window.
* Picture file and settings is supplied by GET-parameters:
*
* - file = fileUid or Combined Identifier
* - encoded in a parameter Array (with weird format - see ContentObjectRenderer about ll. 1500)
* - width, height = usual width an height, m/c supported
* - frame
* - bodyTag
* - title
*
* @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
#[Autoconfigure(public: true)]
class ShowImageController
{
protected const ALLOWED_PARAMETER_NAMES = ['width', 'height', 'crop', 'bodyTag', 'title'];
/**
* @var \Psr\Http\Message\ServerRequestInterface
*/
protected $request;
/**
* @var File|Folder|null
*/
protected $file;
/**
* @var string
*/
protected $width;
/**
* @var int
*/
protected $height;
/**
* @var string
*/
protected $crop;
/**
* @var int|null
*/
protected $frame;
/**
* @var string
*/
protected $bodyTag = '<body>';
/**
* @var string
*/
protected $title = 'Image';
/**
* @var string
*/
protected $content = <<<EOF
<!DOCTYPE html>
<html>
<head>
<title>###TITLE###</title>
<meta name="robots" content="noindex,follow" />
</head>
###BODY###
###IMAGE###
</body>
</html>
EOF;
public function __construct(
protected readonly Features $features,
private readonly FileNameValidator $fileNameValidator,
private readonly ResourceFactory $resourceFactory,
) {}
/**
* Init function, setting the input vars in the global space.
*
* @throws \InvalidArgumentException
* @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
*/
public function initialize()
{
$fileUid = $this->request->getQueryParams()['file'] ?? null;
$parametersArray = $this->request->getQueryParams()['parameters'] ?? null;
// If no file-param or parameters are given, we must exit
if (!$fileUid || !isset($parametersArray) || !is_array($parametersArray)) {
throw new \InvalidArgumentException('No valid fileUid given', 1476048455);
}
// rebuild the parameter array and check if the HMAC is correct
$parametersEncoded = implode('', $parametersArray);
/* For backwards compatibility the HMAC is transported within the md5 param */
$hmacParameter = $this->request->getQueryParams()['md5'] ?? null;
$hashService = GeneralUtility::makeInstance(HashService::class);
$hmac = $hashService->hmac(implode('|', [$fileUid, $parametersEncoded]), 'tx_cms_showpic', HashAlgo::SHA3_256);
if (!is_string($hmacParameter) || !hash_equals($hmac, $hmacParameter)) {
throw new \InvalidArgumentException('hash does not match', 1476048456);
}
// decode the parameters Array - `bodyTag` contains HTML if set and would lead
// to a false-positive XSS-detection, that's why parameters are base64-encoded
$parameters = json_decode(base64_decode($parametersEncoded), true) ?? [];
foreach ($parameters as $parameterName => $parameterValue) {
if (in_array($parameterName, static::ALLOWED_PARAMETER_NAMES, true)) {
$this->{$parameterName} = $parameterValue;
}
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
$this->file = $this->resourceFactory->getFileObject((int)$fileUid);
} else {
$this->file = $this->resourceFactory->retrieveFileOrFolderObject($fileUid);
}
if (!($this->file instanceof FileInterface && $this->isFileValid($this->file))) {
throw new Exception('File processing for local storage is denied', 1594043425);
}
if ($this->features->isFeatureEnabled('security.frontend.allowInsecureFrameOptionInShowImageController')) {
$frameValue = $this->request->getQueryParams()['frame'] ?? null;
if ($frameValue !== null && MathUtility::canBeInterpretedAsInteger($frameValue)) {
$this->frame = (int)$frameValue;
}
}
}
/**
* Main function which creates the image if needed and outputs the HTML code for the page displaying the image.
* Accumulates the content in $this->content
*/
public function main()
{
$processedImage = $this->processImage();
$imageAttributes = [
'src' => $processedImage->getPublicUrl() ?? '',
'alt' => $this->file->getProperty('alternative') ?: $this->title,
'title' => $this->file->getProperty('title') ?: $this->title,
'width' => (string)$processedImage->getProperty('width'),
'height' => (string)$processedImage->getProperty('height'),
];
$markerArray = [
'###TITLE###' => htmlspecialchars($this->file->getProperty('title') ?: $this->title),
'###IMAGE###' => sprintf('<img %s>', GeneralUtility::implodeAttributes($imageAttributes, true)),
'###BODY###' => $this->bodyTag,
];
$this->content = str_replace(array_keys($markerArray), array_values($markerArray), $this->content);
}
/**
* Does the actual image processing
*
* @return \TYPO3\CMS\Core\Resource\ProcessedFile
*/
protected function processImage()
{
$max = str_contains($this->width . $this->height, 'm') ? 'm' : '';
$this->height = MathUtility::forceIntegerInRange($this->height, 0);
$this->width = MathUtility::forceIntegerInRange((int)$this->width, 0) . $max;
$processingConfiguration = [
'width' => $this->width,
'height' => $this->height,
'frame' => $this->frame,
'crop' => $this->crop,
];
return $this->file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
}
/**
* Fetches the content and builds a content file out of it
*
* @param ServerRequestInterface $request the current request object
* @return ResponseInterface the modified response
*/
public function processRequest(ServerRequestInterface $request): ResponseInterface
{
$this->request = $request;
try {
$this->initialize();
$this->main();
$response = new Response();
$response->getBody()->write($this->content);
return $response;
} catch (\InvalidArgumentException $e) {
// add a 410 "gone" if invalid parameters given
return (new Response())->withStatus(410);
} catch (Exception $e) {
return (new Response())->withStatus(404);
}
}
protected function isFileValid(FileInterface $file): bool
{
return $file->getStorage()->getDriverType() !== 'Local'
|| $this->fileNameValidator->isValid(basename($file->getIdentifier()));
}
}