getMethod() !== 'POST' || $arguments->count() === 0) { return; } /** @var Argument $argument */ foreach ($arguments as $argument) { if (!$argument->getValidator() || !class_exists($argument->getDataType()) ) { // Either argument has no validator (IgnoreValidation) or the datatype of the argument is not a class. continue; } $dataType = GeneralUtility::getClassName($argument->getDataType()); $classSchema = $this->reflectionService->getClassSchema($dataType); foreach ($classSchema->getProperties() as $property) { $this->addUploadConfigurationForProperty($argument, $property); } } } /** * Adds a new upload configuration for the given property to the given argument. */ private function addUploadConfigurationForProperty( Argument $argument, Property $property ): void { $primaryType = $property->getPrimaryType(); if (!$primaryType) { throw new \InvalidArgumentException( sprintf( 'There is no @var annotation or type declaration for file upload property "%s" in class "%s".', $property->getName(), $argument->getDataType() ), 1712309171 ); } $propertyTargetClassName = $primaryType->getClassName() ?? $primaryType->getBuiltinType(); if ($propertyTargetClassName !== FileReference::class && !TypeHandlingUtility::isSimpleType($propertyTargetClassName) ) { $primaryCollectionValueType = $property->getPrimaryCollectionValueType(); if ($propertyTargetClassName === ObjectStorage::class && $primaryCollectionValueType && $primaryType->isCollection() ) { $propertyTargetClassName = $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType(); } } // Skip unsupported classes for #[FileUpload] attribute or properties with empty FileUpload configuration if ($propertyTargetClassName !== FileReference::class || $property->getFileUpload() === null) { return; } $fileUploadConfiguration = $property->getFileUpload(); $configurationPropertyName = $property->getName(); $configuration = (new FileUploadConfiguration($configurationPropertyName)) ->initializeWithConfiguration($fileUploadConfiguration); $configuration->ensureValidConfiguration($propertyTargetClassName); $argument->getFileHandlingServiceConfiguration()->addFileUploadConfiguration($configuration); // If FileUpload is configured, the property mapping must be skipped $argument->getPropertyMappingConfiguration()->skipProperties($configurationPropertyName); } /** * Initializes file deletion configurations for properties of the given argument. */ public function initializeFileUploadDeletionConfigurationsFromRequest( RequestInterface $request, Arguments $arguments ): void { if ($request->getMethod() !== 'POST' || $arguments->count() === 0) { return; } $pluginNamespace = $this->extensionService->getPluginNamespace( $request->getControllerExtensionName(), $request->getPluginName() ); $fileDeletions = $request->getParsedBody()[$pluginNamespace][self::DELETE_IDENTIFIER] ?? []; // In case of validation errors, file deletions must not be processed if ($fileDeletions === [] || $this->hasMappingErrorOccurred($request)) { return; } /** @var Argument $argument */ foreach ($arguments as $argument) { if (isset($fileDeletions[$argument->getName()]) && is_array($fileDeletions[$argument->getName()])) { $this->addDeletionConfigurationsToArgument($argument, $fileDeletions[$argument->getName()]); } } } /** * Maps and persists (if required) uploaded files for the given argument. */ public function mapUploadedFilesToArgument(Argument $argument): void { foreach ($argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurations() as $configuration) { $this->mapUploadedFilesToArgumentForConfiguration($argument, $configuration); } } /** * Maps uploaded files to the argument for configuration. * * Maps uploaded files to the specified property of the argument, if property is allowed in current * PropertyMappingConfiguration */ private function mapUploadedFilesToArgumentForConfiguration( Argument $argument, FileUploadConfiguration $configuration ): void { $propertyName = $configuration->getPropertyName(); if ($this->shouldMapProperty($argument, $propertyName)) { $argumentValue = $argument->getValue(); $uploadedFiles = $argument->getUploadedFilesForProperty($propertyName); $this->mapUploadedFilesToArgumentForProperty( $argumentValue, $propertyName, $uploadedFiles, $configuration ); } } /** * Maps uploaded files to the specified property of the object, based on the provided configuration. */ private function mapUploadedFilesToArgumentForProperty( mixed $argumentValue, string $propertyName, array $uploadedFiles, FileUploadConfiguration $configuration ): void { if ($uploadedFiles === [] || !ObjectAccess::isPropertyGettable($argumentValue, $propertyName) || !ObjectAccess::isPropertySettable($argumentValue, $propertyName) ) { return; } $classSchema = $this->reflectionService->getClassSchema($argumentValue); $property = $classSchema->getProperty($propertyName); if (!$property->getPrimaryType()) { return; } $isObjectStorage = $property->isObjectStorageType(); $targetType = $isObjectStorage ? $property->getPrimaryCollectionValueType()->getClassName() : $property->getPrimaryType()->getClassName(); if ($targetType === FileReference::class) { $configuration->ensureValidConfiguration($targetType); $this->persistUploadedFilesAndMapAsFileReferencesToProperty( $argumentValue, $propertyName, $isObjectStorage, $configuration, $uploadedFiles ); } } /** * Moves PSR-7 uploaded files to the target storage defined in the given file upload configuration. * * For target property type FileReference, either a new FileReference object is created or a possible existing * FileReference object is reused and the uploaded file is set. * * For target property type ObjectStorage, new FileReference objects are created and attached * to the property. */ private function persistUploadedFilesAndMapAsFileReferencesToProperty( mixed $argumentValue, string $propertyName, bool $isObjectStorage, FileUploadConfiguration $configuration, array $uploadedFiles, ): void { $uploadFolder = $this->provideUploadFolder($configuration); $storage = $uploadFolder->getStorage(); if ($isObjectStorage) { /** @var ObjectStorage $currentPropertyValue */ $currentPropertyValue = ObjectAccess::getProperty($argumentValue, $propertyName); foreach ($uploadedFiles as $uploadedFile) { $targetFilename = $this->getTargetFilename($uploadedFile->getClientFilename(), $configuration); $this->skipResourceConsistencyCheckForUploads($storage, $uploadedFile, $targetFilename); $file = $storage->addUploadedFile($uploadedFile, $uploadFolder, $targetFilename, $configuration->getDuplicationBehavior()); $coreFileReference = $this->createCoreFileReference($file); $fileReference = $this->createExtbaseFileReference($coreFileReference); $currentPropertyValue->attach($fileReference); } } else { /** @var UploadedFile $uploadedFile */ $uploadedFile = $uploadedFiles[0]; $targetFilename = $this->getTargetFilename($uploadedFile->getClientFilename(), $configuration); $this->skipResourceConsistencyCheckForUploads($storage, $uploadedFile, $targetFilename); $file = $storage->addUploadedFile($uploadedFile, $uploadFolder, $targetFilename, $configuration->getDuplicationBehavior()); $coreFileReference = $this->createCoreFileReference($file); /** @var FileReference|null $currentPropertyValue */ $currentPropertyValue = ObjectAccess::getProperty($argumentValue, $propertyName); if ($currentPropertyValue) { $currentPropertyValue->setOriginalResource($coreFileReference); } else { $currentPropertyValue = $this->createExtbaseFileReference($coreFileReference); } } ObjectAccess::setProperty($argumentValue, $propertyName, $currentPropertyValue); } private function addDeletionConfigurationsToArgument(Argument $argument, array $fileDeletions): void { foreach ($fileDeletions as $signedDeletionData) { $deletionData = $this->hashService->validateAndStripHmac( $signedDeletionData, self::DELETE_IDENTIFIER ); $deletionData = json_decode($deletionData, true, 512, JSON_THROW_ON_ERROR); $fileReferenceUid = (int)$deletionData['fileReference']; $property = $deletionData['property']; $argumentValue = $argument->getValue(); $propertyValue = ObjectAccess::getPropertyPath($argumentValue, $property); if ($propertyValue instanceof FileReference) { if ($propertyValue->getUid() === $fileReferenceUid) { $argument->getFileHandlingServiceConfiguration() ->registerFileDeletion($property, $fileReferenceUid); } } elseif ($propertyValue instanceof ObjectStorage) { foreach ($propertyValue as $fileReference) { if ($fileReference instanceof FileReference && $fileReference->getUid() === $fileReferenceUid) { $argument->getFileHandlingServiceConfiguration() ->registerFileDeletion($property, $fileReferenceUid); } } } } } public function applyDeletionsToArgument(Argument $argument): void { $fileUploadDeletionConfigurations = $argument->getFileHandlingServiceConfiguration() ->getFileUploadDeletionConfigurations(); /** @var FileUploadDeletionConfiguration $fileUploadDeletionConfiguration */ foreach ($fileUploadDeletionConfigurations as $fileUploadDeletionConfiguration) { $property = $fileUploadDeletionConfiguration->getPropertyName(); foreach ($fileUploadDeletionConfiguration->getFileReferenceUids() as $fileReferenceUid) { $argumentValue = $argument->getValue(); $propertyValue = ObjectAccess::getPropertyPath($argumentValue, $property); if ($propertyValue instanceof FileReference) { if ($propertyValue->getUid() === $fileReferenceUid) { $propertyValue->getOriginalResource()->getOriginalFile()->delete(); $propertyValue->getOriginalResource()->delete(); ObjectAccess::setProperty($argumentValue, $property, null); } } elseif ($propertyValue instanceof ObjectStorage) { foreach ($propertyValue as $fileReference) { if ($fileReference instanceof FileReference && $fileReference->getUid() === $fileReferenceUid) { $propertyValue->detach($fileReference); $fileReference->getOriginalResource()->getOriginalFile()->delete(); $fileReference->getOriginalResource()->delete(); } } ObjectAccess::setProperty($argumentValue, $property, $propertyValue); } } } } /** * Checks if a property mapping error has occurred in given request. */ private function hasMappingErrorOccurred(RequestInterface $request): bool { /** @var ExtbaseRequestParameters $extbaseRequestParameters */ $extbaseRequestParameters = $request->getAttribute('extbase'); return $extbaseRequestParameters->getOriginalRequest() !== null; } /** * Determines whether a property should be mapped for the given argument and property name. */ private function shouldMapProperty(Argument $argument, string $propertyName): bool { if ($propertyName === '') { return false; } return $argument->getPropertyMappingConfiguration()->shouldMap($propertyName); } /** * Returns the target filename to use for the given client filename provided by the file upload. */ private function getTargetFilename(string $clientFilename, FileUploadConfiguration $configuration): string { $targetFilename = $clientFilename; if ($configuration->isAddRandomSuffix()) { $pathInfo = pathinfo($targetFilename); $name = $pathInfo['filename']; $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; $randomSuffix = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(16); $targetFilename = $name . '-' . $randomSuffix . $extension; } $event = new ModifyUploadedFileTargetFilenameEvent( targetFilename: $targetFilename, configuration: $configuration ); $this->eventDispatcher->dispatch($event); return $event->getTargetFilename(); } /** * Ensures that upload folder exists, creates it if it does not and if automatic folder creation is defined * @throws FolderDoesNotExistException */ private function provideUploadFolder(FileUploadConfiguration $configuration): Folder { $uploadFolderIdentifier = $configuration->getUploadFolder(); try { return $this->resourceFactory->getFolderObjectFromCombinedIdentifier($uploadFolderIdentifier); } catch (FolderDoesNotExistException $exception) { if (!$configuration->isCreateUploadFolderIfNotExist()) { throw $exception; } [$storageId, $storagePath] = explode(':', $uploadFolderIdentifier, 2); $storage = $this->storageRepository->getStorageObject((int)$storageId); if (!$storage->hasFolder($storagePath)) { $folder = $storage->createFolder($storagePath); } else { $folder = $storage->getFolder($storagePath); } return $folder; } } private function createCoreFileReference(FileInterface $file): CoreFileReference { if (!$file instanceof File) { throw new \RuntimeException('Given file must be a TYPO3\\CMS\\Core\\Resource.', 1712062607); } return $this->resourceFactory->createFileReferenceObject( [ 'uid_local' => $file->getUid(), 'uid_foreign' => StringUtility::getUniqueId('NEW_'), 'uid' => StringUtility::getUniqueId('NEW_'), ] ); } private function createExtbaseFileReference( CoreFileReference $falFileReference ): FileReference { $fileReference = GeneralUtility::makeInstance(FileReference::class); $fileReference->setOriginalResource($falFileReference); return $fileReference; } }