TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user