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
@@ -0,0 +1,72 @@
<?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\Controller;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
/**
* A class representing error messages shown on a page, rendered via fluid.
* Classic Example: "No pages are found on rootlevel"
*/
#[Autoconfigure(public: true)]
readonly class ErrorPageController
{
public function __construct(
protected ViewFactoryInterface $viewFactory,
protected RequestId $requestId,
protected Typo3Information $typo3Information,
protected ContentSecurityPolicy\PolicyRegistry $policyRegistry,
) {}
/**
* Renders the view and returns the content.
*/
public function errorAction(string $title, string $message, int $errorCode = 0, ?int $httpStatusCode = null): string
{
$viewFactoryData = new ViewFactoryData(
templateRootPaths: ['EXT:core/Resources/Private/Templates'],
);
$view = $this->viewFactory->create($viewFactoryData);
$view->assignMultiple([
'message' => $message,
'title' => $title,
'httpStatusCode' => $httpStatusCode,
'errorCodeUrlPrefix' => Typo3Information::URL_EXCEPTION,
'donationUrl' => Typo3Information::URL_DONATE,
'errorCode' => $errorCode,
'requestId' => GeneralUtility::makeInstance(RequestId::class),
'copyrightYear' => $this->typo3Information->getCopyrightYear(),
]);
$this->policyRegistry->appendMutationCollection(
new ContentSecurityPolicy\MutationCollection(
new ContentSecurityPolicy\Mutation(
ContentSecurityPolicy\MutationMode::Extend,
ContentSecurityPolicy\Directive::StyleSrcElem,
ContentSecurityPolicy\SourceKeyword::nonceProxy
)
)
);
return $view->render('ErrorPage/Error');
}
}
+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\Core\Controller;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\Event\ModifyFileDumpEvent;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
#[Autoconfigure(public: true)]
readonly class FileDumpController
{
public function __construct(
protected EventDispatcherInterface $eventDispatcher,
protected ResourceFactory $resourceFactory,
protected ResponseFactoryInterface $responseFactory,
protected HashService $hashService,
private FileNameValidator $fileNameValidator,
private ProcessedFileRepository $processedFileRepository,
) {}
/**
* Main method to dump a file
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws \UnexpectedValueException
*/
public function dumpAction(ServerRequestInterface $request): ResponseInterface
{
$parameters = $this->buildParametersFromRequest($request);
if (!$this->isTokenValid($parameters, $request)) {
return $this->responseFactory->createResponse(403);
}
$file = $this->createFileObjectByParameters($parameters);
if ($file === null) {
return $this->responseFactory->createResponse(404);
}
// Allow some other process to do some security/access checks.
// Event Listeners should return a 403 response if access is rejected
$event = new ModifyFileDumpEvent($file, $request);
$event = $this->eventDispatcher->dispatch($event);
if ($event->isPropagationStopped()) {
return $this->applyContentSecurityPolicy($event->getFile(), $event->getResponse());
}
$file = $event->getFile();
$processingInstructions = [];
// Apply cropping, if possible
if (!empty($parameters['cv'])) {
$cropVariant = $parameters['cv'];
$cropString = $file instanceof FileReference ? $file->getProperty('crop') : '';
$cropArea = CropVariantCollection::create((string)$cropString)->getCropArea($cropVariant);
$processingInstructions = array_merge(
$processingInstructions,
[
'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($file),
]
);
}
// Apply width/height, if given
if (!empty($parameters['s'])) {
$size = GeneralUtility::trimExplode(':', $parameters['s']);
$processingInstructions = array_merge(
$processingInstructions,
[
'width' => $size[0] ?? null,
'height' => $size[1] ?? null,
'minWidth' => $size[2] ? (int)$size[2] : null,
'minHeight' => $size[3] ? (int)$size[3] : null,
'maxWidth' => $size[4] ? (int)$size[4] : null,
'maxHeight' => $size[5] ? (int)$size[5] : null,
]
);
}
if (!empty($processingInstructions) && !($file instanceof ProcessedFile)) {
if (is_callable([$file, 'getOriginalFile'])) {
// Get the original file from the file reference
$file = $file->getOriginalFile();
}
$file = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);
}
return $this->applyContentSecurityPolicy(
$file,
$file->getStorage()->streamFile(
$file,
(bool)($parameters['dl'] ?? false),
$parameters['fn'] ?? null
)
);
}
protected function buildParametersFromRequest(ServerRequestInterface $request): array
{
$parameters = ['eID' => 'dumpFile'];
$queryParams = $request->getQueryParams();
// Identifier of what to process. f, r or p
// Only needed while hash_equals
$t = (string)($queryParams['t'] ?? '');
if ($t) {
$parameters['t'] = $t;
}
// sys_file
$f = (string)($queryParams['f'] ?? '');
if ($f) {
$parameters['f'] = (int)$f;
}
// sys_file_reference
$r = (string)($queryParams['r'] ?? '');
if ($r) {
$parameters['r'] = (int)$r;
}
// Processed file
$p = (string)($queryParams['p'] ?? '');
if ($p) {
$parameters['p'] = (int)$p;
}
// File's width and height in this order: w:h:minW:minH:maxW:maxH
$s = (string)($queryParams['s'] ?? '');
if ($s) {
$parameters['s'] = $s;
}
// File's crop variant
$cv = (string)($queryParams['cv'] ?? '');
if ($cv) {
$parameters['cv'] = $cv;
}
// As download
$dl = (string)($queryParams['dl'] ?? '');
if ($dl) {
$parameters['dl'] = (int)$dl;
}
// Alternative file name
$fn = (string)($queryParams['fn'] ?? '');
if ($fn) {
$parameters['fn'] = $fn;
}
return $parameters;
}
protected function isTokenValid(array $parameters, ServerRequestInterface $request): bool
{
return hash_equals(
$this->hashService->hmac(implode('|', $parameters), 'resourceStorageDumpFile', HashAlgo::SHA3_256),
$request->getQueryParams()['token'] ?? ''
);
}
/**
* @return File|FileReference|ProcessedFile|null
*/
protected function createFileObjectByParameters(array $parameters)
{
$file = null;
if (isset($parameters['f'])) {
try {
$file = $this->resourceFactory->getFileObject($parameters['f']);
if ($file->isDeleted() || $file->isMissing() || !$this->isFileValid($file)) {
$file = null;
}
} catch (\Exception $e) {
$file = null;
}
} elseif (isset($parameters['r'])) {
try {
$file = $this->resourceFactory->getFileReferenceObject($parameters['r']);
if ($file->isMissing() || !$this->isFileValid($file->getOriginalFile())) {
$file = null;
}
} catch (\Exception $e) {
$file = null;
}
} elseif (isset($parameters['p'])) {
try {
$file = $this->processedFileRepository->findByUid((int)$parameters['p']);
if ($file->isDeleted() || !$this->isFileValid($file->getOriginalFile())) {
$file = null;
}
} catch (\Exception) {
$file = null;
}
}
return $file;
}
protected function isFileValid(FileInterface $file): bool
{
return $file->getStorage()->getDriverType() !== 'Local'
|| $this->fileNameValidator->isValid(basename($file->getIdentifier()));
}
/**
* Applies hard-coded content-security-policy (CSP) for file to be dumped.
*/
protected function applyContentSecurityPolicy(ResourceInterface $file, ResponseInterface $response): ResponseInterface
{
$extension = PathUtility::pathinfo($file->getName(), PATHINFO_EXTENSION);
// same as in `typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/resources-root-htaccess`
if ($extension === 'pdf' || $response->getHeaderLine('content-type') === 'application/pdf') {
$policy = "default-src 'self' 'unsafe-inline'; script-src 'none'; object-src 'self'; plugin-types application/pdf;";
} elseif ($extension === 'svg' || $response->getHeaderLine('content-type') === 'image/svg+xml') {
$policy = "default-src 'self'; script-src 'none'; style-src 'unsafe-inline'; object-src 'none';";
} else {
$policy = "default-src 'self'; script-src 'none'; style-src 'none'; object-src 'none';";
}
return $response->withAddedHeader('content-security-policy', $policy);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Imaging\IconState;
/**
* Controller for icon handling
*
* @internal
*/
#[AsController]
readonly class IconController
{
public function __construct(
private IconFactory $iconFactory
) {}
public function getIcon(ServerRequestInterface $request): ResponseInterface
{
$parsedBody = $request->getParsedBody();
$queryParams = $request->getQueryParams();
$requestedIcon = json_decode($parsedBody['icon'] ?? $queryParams['icon'], true);
[$identifier, $size, $overlayIdentifier, $iconState, $alternativeMarkupIdentifier] = $requestedIcon;
if (empty($overlayIdentifier)) {
$overlayIdentifier = null;
}
$iconState = IconState::tryFrom($iconState);
$icon = $this->iconFactory->getIcon($identifier, IconSize::from($size), $overlayIdentifier, $iconState);
return new HtmlResponse($icon->render($alternativeMarkupIdentifier));
}
}
@@ -0,0 +1,96 @@
<?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\Controller;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal Only to be used within TYPO3. Might change in the future.
*/
#[Autoconfigure(public: true)]
readonly class PasswordGeneratorController
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
private StreamFactoryInterface $streamFactory,
private LoggerInterface $logger,
) {}
public function generate(ServerRequestInterface $request): ResponseInterface
{
$passwordPolicy = $request->getParsedBody()['passwordPolicy'] ?? null;
try {
if (is_string($passwordPolicy) && $passwordPolicy !== 'null') {
$generator = $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'][$passwordPolicy]['generator'] ?? null;
if (empty($generator['className'])
|| !is_string($generator['className'])
|| !class_exists($generator['className'])
|| !isset($generator['options'])
|| !is_array($generator['options'])
) {
throw new \LogicException(
'The TYPO3_CONF_VARS.SYS.passwordPolicies.' . $passwordPolicy . '.generator configuration is misconfigured.'
. ' Please ensure that the sub key \'className\' is set, and the sub key \'options\' is an array of required option values.',
1770142937
);
}
$passwordGeneratorClassName = $generator['className'];
$passwordGeneratorOptions = $generator['options'];
$passwordGenerator = GeneralUtility::makeInstance($passwordGeneratorClassName);
if (!$passwordGenerator instanceof PasswordGeneratorInterface) {
throw new \LogicException('Class ' . $passwordGeneratorClassName . ' does not implement PasswordGeneratorInterface', 1770142966);
}
$password = $passwordGenerator->generate($passwordGeneratorOptions);
return $this->createResponse([
'success' => true,
'password' => $password,
]);
}
} catch (\LogicException $exception) {
$this->logger->error('Password generation failed', ['exception' => $exception]);
}
return $this->createResponse([
'success' => false,
]);
}
protected function createResponse(array $data): ResponseInterface
{
return $this->responseFactory->createResponse()
->withHeader('Content-Type', 'application/json; charset=utf-8')
->withBody($this->streamFactory->createStream((string)json_encode($data)));
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}