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,55 @@
<?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\Resource\Service;
use TYPO3\CMS\Core\Resource\FileInterface;
/**
* Resources can contain configurations: For example to define image dimensions or
* image masking. Resource configurations form part of the object identity and are
* used to create an object signature that itself is used to cache objects but NOT
* to reconstruct them. To prevent objects to be reconstructed they MUST NOT be
* serialized. This is why an object signature is obtained by serializing its array
* rather than serializing it directly. But attention...as well the objects array
* MUST NOT contain resource objects which could be the case when a configuration
* defines image masking. Here this service comes into play: it serializes any
* object configuration, as well those containing resources.
*/
class ConfigurationService
{
public function serialize(array $configuration): string
{
return serialize($this->makeSerializable($configuration));
}
/**
* Recursively substitute file objects with their array representation.
*/
protected function makeSerializable(array $configuration): array
{
return array_map(function (mixed $value): mixed {
if (is_array($value)) {
return $this->makeSerializable($value);
}
if ($value instanceof FileInterface) {
return $value->toArray();
}
return $value;
}, $configuration);
}
}
@@ -0,0 +1,83 @@
<?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\Resource\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileType;
use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
use TYPO3\CMS\Core\Resource\Index\ExtractorRegistry;
/**
* Service class to extract metadata
*/
#[Autoconfigure(public: true)]
readonly class ExtractorService
{
public function __construct(
private ExtractorRegistry $extractorRegistry,
) {}
public function extractMetaData(File $fileObject): array
{
$newMetaData = $extractedMetaData = [];
// Loop through available extractors and fetch metadata for the given file.
$extractionServices = $this->extractorRegistry->getExtractorsWithDriverSupport($fileObject->getStorage()->getDriverType());
foreach ($extractionServices as $extractorService) {
if ($this->isFileTypeSupportedByExtractor($fileObject, $extractorService)
&& $extractorService->canProcess($fileObject)
) {
$metaDataFromExtractor = $extractorService->extractMetaData($fileObject, $extractedMetaData);
if (!empty($metaDataFromExtractor)) {
$extractedMetaData[] = $metaDataFromExtractor;
$newMetaData[$extractorService->getPriority()][] = $metaDataFromExtractor;
}
}
}
// Sort metadata by priority so that merging happens in order of precedence.
ksort($newMetaData);
// Merge the collected metadata.
$metaData = [[]];
foreach ($newMetaData as $dataFromExtractors) {
foreach ($dataFromExtractors as $data) {
$metaData[] = $data;
}
}
return array_filter(array_merge(...$metaData));
}
/**
* Check whether the extractor service supports this file according to file type restrictions.
*/
private function isFileTypeSupportedByExtractor(File $file, ExtractorInterface $extractor): bool
{
$supportedFileTypes = $extractor->getFileTypeRestrictions();
if ($supportedFileTypes === []) {
return true;
}
foreach ($supportedFileTypes as $supportedFileType) {
if (is_int($supportedFileType)) {
$supportedFileType = FileType::tryFrom($supportedFileType);
}
if ($supportedFileType->value === $file->getType()) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,97 @@
<?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\Resource\Service;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
use TYPO3\CMS\Core\Resource\Event\AfterFileProcessingEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
use TYPO3\CMS\Core\Resource\Processing\ProcessorInterface;
use TYPO3\CMS\Core\Resource\Processing\ProcessorRegistry;
use TYPO3\CMS\Core\Resource\Processing\TaskInterface;
use TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry;
/**
* This is a general service for creating Processed Files a.k.a. processing a File object with a given configuration.
*
* This is how it works:
* -> File->process(string $taskType, array $configuration)
* -> ResourceStorage->processFile(File $file, $taskType, array $configuration)
* -> FileProcessingService->processFile(File $file, $taskType, array $configuration)
*
* This class then transforms the information of a Task through a Processor into a ProcessedFile object.
* For this, the DB is checked if there is a ProcessedFile which has been processed or does not need
* to be processed. If processing is required, a valid Processor is searched for to process the
* Task object (which is created from the TaskTypeRegistry when needed for processing).
*/
#[Autoconfigure(public: true)]
readonly class FileProcessingService
{
public function __construct(
protected EventDispatcherInterface $eventDispatcher,
protected ProcessedFileRepository $processedFileRepository,
protected ProcessorRegistry $processorRegistry,
protected TaskTypeRegistry $taskTypeRegistry,
) {}
public function processFile(File|FileReference $fileObject, string $taskType, DriverInterface $driver, array $configuration): ProcessedFile
{
// Processing always works on the original file
$originalFile = $fileObject instanceof FileReference ? $fileObject->getOriginalFile() : $fileObject;
// Find an entry in the DB or create a new ProcessedFile which can then be added (see ->add below)
$processedFile = $this->processedFileRepository->findOneByOriginalFileAndTaskTypeAndConfiguration($originalFile, $taskType, $configuration);
// Make sure to work with the sanitized configuration from now on!
$configuration = $processedFile->getProcessingConfiguration();
// Pre-process the file
$event = $this->eventDispatcher->dispatch(
new BeforeFileProcessingEvent($driver, $processedFile, $fileObject, $taskType, $configuration)
);
$processedFile = $event->getProcessedFile();
$task = $this->taskTypeRegistry->getTaskForType($taskType, $processedFile, $configuration);
// Only handle the file if it is not processed yet
// (maybe modified or already processed by an event)
// or (in case of preview images) already in the DB/in the processing folder
if ($task->fileNeedsProcessing()) {
$this->getProcessorByTask($task)->processTask($task);
if ($task->isExecuted() && $task->isSuccessful() && $processedFile->isProcessed()) {
$this->processedFileRepository->add($processedFile, $task);
}
}
// Post-process (enrich) the file
$event = $this->eventDispatcher->dispatch(
new AfterFileProcessingEvent($driver, $processedFile, $fileObject, $taskType, $configuration)
);
return $event->getProcessedFile();
}
protected function getProcessorByTask(TaskInterface $task): ProcessorInterface
{
return $this->processorRegistry->getProcessorByTask($task);
}
}
@@ -0,0 +1,81 @@
<?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\Resource\Service;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\FileProcessingAspect;
use TYPO3\CMS\Core\Locking\ResourceMutex;
use TYPO3\CMS\Core\Resource\Exception\FileAlreadyProcessedException;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
/**
* Disables deferred processing and actually processes a preprocessed processed file
*/
readonly class ImageProcessingService
{
public function __construct(
private ProcessedFileRepository $processedFileRepository,
private Context $context,
private ResourceMutex $locker,
) {}
public function process(int $processedFileId): ProcessedFile
{
/** @var ProcessedFile $processedFile */
$processedFile = $this->processedFileRepository->findByUid($processedFileId);
try {
$this->validateProcessedFile($processedFile);
$hadToWaitForLock = $this->locker->acquireLock(self::class, (string)$processedFileId);
if ($hadToWaitForLock) {
// Fetch the processed file again, as it might have been processed by
// another process while waiting for the lock
/** @var ProcessedFile $processedFile */
$processedFile = $this->processedFileRepository->findByUid($processedFileId);
$this->validateProcessedFile($processedFile);
}
$this->context->setAspect('fileProcessing', new FileProcessingAspect(false));
$processedFile = $processedFile->getOriginalFile()->process(
$processedFile->getTaskIdentifier(),
$processedFile->getProcessingConfiguration()
);
$this->validateProcessedFile($processedFile);
} catch (FileAlreadyProcessedException $e) {
$processedFile = $e->getProcessedFile();
} finally {
$this->locker->releaseLock(self::class);
}
return $processedFile;
}
/**
* Check whether a processed file was already processed
*
* @throws FileAlreadyProcessedException
*/
private function validateProcessedFile(ProcessedFile $processedFile): void
{
if ($processedFile->isProcessed()) {
throw new FileAlreadyProcessedException($processedFile, 1599395651);
}
}
}
@@ -0,0 +1,196 @@
<?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\Resource\Service;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Localization\LabelBag;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Type\File\FileInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Validation\ResultException;
use TYPO3\CMS\Core\Validation\ResultMessage;
/**
* This service is invoked by ResourceStorage when modifying files, validating the following:
* + only explicitly allowed file-extensions are allowed:
* see `TYPO3_CONF_VARS` settings for `textfile_ext`, `mediafile_ext` and `miscfile_ext`
* + only files having valid file-extension to mime-type items are allowed:
* e.g. denies using `image.exe` with `image/png`
*
* @phpstan-type ExceptionItem array{storage: ResourceStorage, resource: string|FileInterface, targetFileName: string}
* @phpstan-type ExceptionItemCollection array<string, ExceptionItem>
* @internal
*/
final class ResourceConsistencyService
{
/**
* Exception items, which shall not be validated.
* These are usually set by internal components (e.g. `ext:impexp`).
*
* @var ExceptionItemCollection
*/
private array $exceptionItems = [];
public function __construct(
private readonly Random $random,
private readonly Features $features,
private readonly MimeTypeDetector $mimeTypeDetector,
) {}
public function addExceptionItem(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName): void
{
$identifier = $this->random->generateRandomHexString(40);
$this->exceptionItems[$identifier] = $this->createExceptionItem($storage, $resource, $targetFileName);
}
public function removeException(string $identifier): void
{
unset($this->exceptionItems[$identifier]);
}
/**
* @param FileInterface|string $resource holding the contents
* @param string $targetFileName (optional) target file name to be used as the identifier
* @throws ResultException
*/
public function validate(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName = ''): void
{
if (!$this->shallValidate($storage, $resource, $targetFileName)) {
return;
}
if ($targetFileName !== '') {
$fileExtension = pathinfo($targetFileName, PATHINFO_EXTENSION);
}
if ($resource instanceof FileInterface) {
$mimeType = $resource->getMimeType();
$fileSize = $resource->getSize();
$fileExtension ??= $resource->getExtension();
} else {
$fileInfo = new FileInfo($resource);
$mimeType = (string)$fileInfo->getMimeType($targetFileName);
$fileSize = $fileInfo->isReadable() ? $fileInfo->getSize() : 0;
$fileExtension ??= $fileInfo->getExtension();
}
$isEmptyFile = $fileSize === 0;
$messages = [];
// skip mime-type checks for empty files
if (!$isEmptyFile && !$this->areFileExtensionAndMimeTypeConsistent($fileExtension, $mimeType)) {
$expectedTypes = $this->mimeTypeDetector->getMimeTypesForFileExtension($fileExtension);
if ($expectedTypes === []) {
$listOfExpectedTypes = 'N/A';
} else {
$listOfExpectedTypes = implode(', ', $expectedTypes);
}
$arguments = [$mimeType, $fileExtension, $listOfExpectedTypes];
$messages[] = new ResultMessage(
sprintf('Mime-type "%s" not allowed for file extension "%s" (expected: %s).', ...$arguments),
new LabelBag(
'LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:FileUtility.MimeTypeNotAllowedForFileExtensionWithExpectation',
...$arguments
)
);
}
if (!$this->isFileExtensionAllowed($fileExtension)) {
$arguments = [$fileExtension];
$messages[] = new ResultMessage(
sprintf('File extension "%s" is not in the list of allowed values.', ...$arguments),
new LabelBag(
'LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:FileUtility.FileExtensionIsNotAllowed',
...$arguments
)
);
}
if ($messages !== []) {
throw new ResultException('Resource consistency check failed', 1747230949, ...$messages);
}
}
private function areFileExtensionAndMimeTypeConsistent(string $fileExtension, string $mimeType): bool
{
if (!$this->features->isFeatureEnabled('security.system.enforceFileExtensionMimeTypeConsistency')) {
return true;
}
$fileExtension = mb_strtolower($fileExtension);
$assumedMimesTypeOfFileExtension = $this->mimeTypeDetector->getMimeTypesForFileExtension($fileExtension);
// pass, in case no assumed mime-type was found (e.g., for individual file extension)
return $assumedMimesTypeOfFileExtension === []
|| ($mimeType !== '' && in_array($mimeType, $assumedMimesTypeOfFileExtension, true));
}
private function isFileExtensionAllowed(string $fileExtension): bool
{
if (!$this->features->isFeatureEnabled('security.system.enforceAllowedFileExtensions')) {
return true;
}
$fileExtension = mb_strtolower($fileExtension);
return in_array($fileExtension, $this->getAllowedFileExtensions(), true);
}
private function getAllowedFileExtensions(): array
{
$allowedFileExtensions = GeneralUtility::trimExplode(
',',
$GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] . ','
. $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] . ','
. $GLOBALS['TYPO3_CONF_VARS']['SYS']['miscfile_ext'],
true
);
return array_map(mb_strtolower(...), $allowedFileExtensions);
}
private function shallValidate(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName): bool
{
$needle = $this->createExceptionItem($storage, $resource, $targetFileName);
$exceptionItems = array_filter(
$this->exceptionItems,
fn(array $exception): bool => $this->exceptionItemsMatch($exception, $needle),
);
if ($exceptionItems === []) {
return true;
}
foreach (array_keys($exceptionItems) as $identifier) {
$this->removeException($identifier);
}
return false;
}
/**
* @return ExceptionItem
*/
private function createExceptionItem(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName): array
{
return [
'storage' => $storage,
'resource' => $resource,
'targetFileName' => $targetFileName,
];
}
private function exceptionItemsMatch(array $left, array $right): bool
{
foreach ($right as $key => $value) {
if ($value !== ($left[$key] ?? null)) {
return false;
}
}
return true;
}
}