From ca07015b284d64db4231d57ff2a6d5dece9f8e6f Mon Sep 17 00:00:00 2001
From: Ralf Zimmermann <ralf.zimmermann@tritum.de>
Date: Tue, 6 Oct 2015 10:34:01 +0200
Subject: [PATCH] [TASK] EXT:form - Optimize file upload/ handling of files

Determine the file mime type with \TYPO3\CMS\Core\Type\File\FileInfo
before a validation or other operations with the files are made.

Resolves: #69956
Releases: master
Change-Id: Iac0381b9847b82dfa7bc7a78f970c91ce51d4272
Reviewed-on: http://review.typo3.org/43836
Tested-by: Bjoern Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Bjoern Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
---
 .../ArrayToValidationElementConverter.php     | 51 ++++++++++++++-
 .../Validator/FileAllowedTypesValidator.php   | 43 ++-----------
 .../Validator/ValidationElementValidator.php  | 64 +++++++++++++++++--
 .../Hooks/HandleIncomingFormValues.php        | 50 ++++-----------
 .../Classes/PostProcess/MailPostProcessor.php |  2 +-
 .../form/Classes/Utility/SessionUtility.php   |  1 -
 .../Confirmation/FlatElements/Upload.html     |  2 +-
 .../Mail/Html/FlatElements/Upload.html        |  2 +-
 .../Mail/Plain/FlatElements/Upload.html       |  2 +-
 .../Confirmation/FlatElements/Upload.html     |  2 +-
 .../Mail/Html/FlatElements/Upload.html        |  2 +-
 .../Mail/Plain/FlatElements/Upload.html       |  2 +-
 12 files changed, 135 insertions(+), 88 deletions(-)

diff --git a/typo3/sysext/form/Classes/Domain/Property/TypeConverter/ArrayToValidationElementConverter.php b/typo3/sysext/form/Classes/Domain/Property/TypeConverter/ArrayToValidationElementConverter.php
index 09d8974858b9..f4b55ce4c227 100644
--- a/typo3/sysext/form/Classes/Domain/Property/TypeConverter/ArrayToValidationElementConverter.php
+++ b/typo3/sysext/form/Classes/Domain/Property/TypeConverter/ArrayToValidationElementConverter.php
@@ -63,8 +63,57 @@ class ArrayToValidationElementConverter extends AbstractTypeConverter {
 	public function convertFrom($source, $targetType, array $convertedChildProperties = array(), PropertyMappingConfigurationInterface $configuration = NULL) {
 		/** @var ValidationElement $validationElement */
 		$validationElement = GeneralUtility::makeInstance(ValidationElement::class);
-
 		if (is_array($source)) {
+			/**
+			 * Find uploaded files.
+			 *
+			 * Extbase has already mapped the $_FILES data into the request
+			 * @see TYPO3\CMS\Extbase\Mvc\Web\Request::build()
+			 * If a $_FILES array is found in the request data ($source),
+			 * set the file mime type with
+			 * \TYPO3\CMS\Core\Type\File\FileInfo
+			 * and write the data back into $source.
+			 */
+			foreach ($source as $propertyName => $value) {
+				if (is_array($value)) {
+					$uploadedFiles = array();
+					if (
+						isset($value['name'])
+						&& isset($value['type'])
+						&& isset($value['tmp_name'])
+						&& isset($value['size'])
+					) {
+						// if single file upload - cast to array
+						$uploadedFiles[] = $value;
+					} elseif (
+						isset($value[0]['name'])
+						&& isset($value[0]['type'])
+						&& isset($value[0]['tmp_name'])
+						&& isset($value[0]['size'])
+					) {
+						// multi file upload
+						$uploadedFiles = $value;
+					}
+
+					if (!empty($uploadedFiles)) {
+						foreach ($uploadedFiles as $key => &$file) {
+							if (
+								$file['name'] === ''
+								&& $file['type'] === ''
+								&& $file['tmp_name'] === ''
+								&& $file['size'] === 0
+							) {
+								unset($uploadedFiles[$key]);
+								continue;
+							}
+							$fileInfo = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Type\File\FileInfo::class, $file['tmp_name']);
+							$file['type'] = $fileInfo->getMimeType();
+							$file['name'] = htmlspecialchars($file['name']);
+						}
+						$source[$propertyName] = $uploadedFiles;
+					}
+				}
+			}
 			$validationElement->setIncomingFields($source);
 		}
 
diff --git a/typo3/sysext/form/Classes/Domain/Validator/FileAllowedTypesValidator.php b/typo3/sysext/form/Classes/Domain/Validator/FileAllowedTypesValidator.php
index 039a2c87e694..c627fe131aae 100755
--- a/typo3/sysext/form/Classes/Domain/Validator/FileAllowedTypesValidator.php
+++ b/typo3/sysext/form/Classes/Domain/Validator/FileAllowedTypesValidator.php
@@ -35,48 +35,19 @@ class FileAllowedTypesValidator extends AbstractValidator {
 	const LOCALISATION_OBJECT_NAME = 'tx_form_system_validate_fileallowedtypes';
 
 	/**
-	 * Check if $value is valid. If it is not valid, needs to add an error
-	 * to result.
+	 * Check if the file mime type is allowed.
+	 *
+	 * The mime type is set in the propertymapper
+	 * @see TYPO3\CMS\Form\Domain\Property\TypeConverter::convertFrom
 	 *
 	 * @param mixed $value
 	 * @return void
 	 */
 	public function isValid($value) {
-		// @todo $value is never used, what's the process flow here?
-
 		$allowedTypes = strtolower($this->options['types']);
-		$this->options['types'] = GeneralUtility::trimExplode(', ', $allowedTypes);
-
-		if (isset($this->rawArgument[$this->options['element']]['name'])) {
-			$request = $this->rawArgument[$this->options['element']];
-			$this->checkFileType($request);
-		} else {
-				// multi upload
-			foreach ($this->rawArgument[$this->options['element']] as $file) {
-				if (
-					$file['name'] === ''
-					&& $file['type'] === ''
-					&& $file['tmp_name'] === ''
-					&& $file['size'] === 0
-				) {
-					continue;
-				}
-				$this->checkFileType($file);
-			}
-		}
-	}
-
-	/**
-	 * Check if $value is valid. If it is not valid, needs to add an error
-	 * to result.
-	 *
-	 * @param array $request
-	 * @return void
-	 */
-	public function checkFileType($request) {
-		// @todo Using $_FILES[...]['type] is probably insecure, since it's submitted by the client directly
-		$value = strtolower($request['type']);
-		if (!in_array($value, $this->options['types'])) {
+		$allowedMimeTypes = GeneralUtility::trimExplode(', ', $allowedTypes);
+		$fileMimeType = strtolower($value['type']);
+		if (!in_array($fileMimeType, $allowedMimeTypes, TRUE)) {
 			$this->addError(
 				$this->renderMessage(
 					$this->options['errorMessage'][0],
diff --git a/typo3/sysext/form/Classes/Domain/Validator/ValidationElementValidator.php b/typo3/sysext/form/Classes/Domain/Validator/ValidationElementValidator.php
index e363d496e8c8..4ecb95b77e58 100644
--- a/typo3/sysext/form/Classes/Domain/Validator/ValidationElementValidator.php
+++ b/typo3/sysext/form/Classes/Domain/Validator/ValidationElementValidator.php
@@ -27,6 +27,18 @@ class ValidationElementValidator extends \TYPO3\CMS\Extbase\Validation\Validator
 	 */
 	protected $propertyValidators = array();
 
+	/**
+	 * @var \TYPO3\CMS\Form\Utility\SessionUtility
+	 */
+	protected $sessionUtility;
+
+	/**
+	 * @param \TYPO3\CMS\Form\Utility\SessionUtility $sessionUtility
+	 */
+	public function injectSessionUtility(\TYPO3\CMS\Form\Utility\SessionUtility $sessionUtility) {
+		$this->sessionUtility = $sessionUtility;
+	}
+
 	/**
 	 * Checks if the given value is valid according to the validator, and returns
 	 * the Error Messages object which occurred.
@@ -58,7 +70,19 @@ class ValidationElementValidator extends \TYPO3\CMS\Extbase\Validation\Validator
 	 * @return mixed
 	 */
 	protected function getPropertyValue(\TYPO3\CMS\Form\Domain\Model\ValidationElement $validationElement, $propertyName) {
-		return $validationElement->getIncomingField($propertyName);
+		/**
+		 * If a confirmation page is set and a fileupload was done before
+		 * there is no incoming data if the process action is called.
+		 * The data is only in the session at this time.
+		 * This results in a negative validation (if a validation is set).
+		 * Therefore, look first in the session.
+		 */
+		if ($this->sessionUtility->getSessionData($propertyName)) {
+			$propertyValue = $this->sessionUtility->getSessionData($propertyName);
+		} else {
+			$propertyValue = $validationElement->getIncomingField($propertyName);
+		}
+		return $propertyValue;
 	}
 
 	/**
@@ -77,12 +101,38 @@ class ValidationElementValidator extends \TYPO3\CMS\Extbase\Validation\Validator
 			if ($validator instanceof ObjectValidatorInterface) {
 				$validator->setValidatedInstancesContainer($this->validatedInstancesContainer);
 			}
-			$currentResult = $validator->validate($value);
-			if ($currentResult->hasMessages()) {
-				if ($result == NULL) {
-					$result = $currentResult;
-				} else {
-					$result->merge($currentResult);
+
+			/**
+			 * File upload validation.
+			 *
+			 * If a $_FILES array is found in the request data,
+			 * iterate over all requested files and validate each
+			 * single file.
+			 */
+			if (
+				isset($value[0]['name'])
+				&& isset($value[0]['type'])
+				&& isset($value[0]['tmp_name'])
+				&& isset($value[0]['size'])
+			) {
+				foreach ($value as $file) {
+					$currentResult = $validator->validate($file);
+					if ($currentResult->hasMessages()) {
+						if ($result == NULL) {
+							$result = $currentResult;
+						} else {
+							$result->merge($currentResult);
+						}
+					}
+				}
+			} else {
+				$currentResult = $validator->validate($value);
+				if ($currentResult->hasMessages()) {
+					if ($result == NULL) {
+						$result = $currentResult;
+					} else {
+						$result->merge($currentResult);
+					}
 				}
 			}
 		}
diff --git a/typo3/sysext/form/Classes/Hooks/HandleIncomingFormValues.php b/typo3/sysext/form/Classes/Hooks/HandleIncomingFormValues.php
index 06a867f3e977..4a8fc75469b2 100644
--- a/typo3/sysext/form/Classes/Hooks/HandleIncomingFormValues.php
+++ b/typo3/sysext/form/Classes/Hooks/HandleIncomingFormValues.php
@@ -145,25 +145,19 @@ class HandleIncomingFormValues implements SingletonInterface {
 					&& $formBuilder->getValidationErrors()->forProperty($elementName)->hasErrors() !== TRUE
 				)
 			) {
-				$formPrefix = $formBuilder->getFormPrefix();
-				if (
-					isset($_FILES['tx_form_form']['tmp_name'][$formPrefix])
-					&& is_array($_FILES['tx_form_form']['tmp_name'][$formPrefix])
-				) {
-					foreach ($_FILES['tx_form_form']['tmp_name'][$formPrefix] as $fieldName => $uploadedFile) {
-						$uploadedFiles = array();
-						if (is_string($uploadedFile)) {
-							$uploadedFiles[] = $this->saveUploadedFile($formPrefix, $fieldName, -1, $uploadedFile);
-						} else {
-								// multi upload
-							foreach ($uploadedFile as $key => $file) {
-								$uploadedFiles[] = $this->saveUploadedFile($formPrefix, $fieldName, $key, $file);
-							}
+				$uploadedFiles = $formBuilder->getIncomingData()->getIncomingField($elementName);
+				if (is_array($uploadedFiles)) {
+					foreach ($uploadedFiles as $key => &$file) {
+						$tempFilename = $this->saveUploadedFile($file['tmp_name']);
+						if (!$tempFilename) {
+							unset($uploadedFiles[$key]);
+							continue;
 						}
-						$element->setAdditionalArgument('uploadedFiles', $uploadedFiles);
-						$this->setAttribute($element, 'value', '');
-						$this->sessionUtility->setSessionData($fieldName, $uploadedFiles);
+						$file['tempFilename'] = $tempFilename;
 					}
+					$element->setAdditionalArgument('uploadedFiles', $uploadedFiles);
+					$this->setAttribute($element, 'value', '');
+					$this->sessionUtility->setSessionData($elementName, $uploadedFiles);
 				}
 			}
 		}
@@ -172,33 +166,17 @@ class HandleIncomingFormValues implements SingletonInterface {
 	/**
 	 * Save a uploaded file
 	 *
-	 * @param string $formPrefix
-	 * @param string $fieldName
-	 * @param integer $key
 	 * @param string $uploadedFile
-	 * @return NULL|array
+	 * @return NULL|string
 	 */
-	public function saveUploadedFile($formPrefix, $fieldName, $key, $uploadedFile) {
+	public function saveUploadedFile($uploadedFile) {
 		if (is_uploaded_file($uploadedFile)) {
 			$tempFilename = GeneralUtility::upload_to_tempfile($uploadedFile);
 			if (TYPO3_OS === 'WIN') {
 				$tempFilename = GeneralUtility::fixWindowsFilePath($tempFilename);
 			}
 			if ($tempFilename !== '') {
-				if ($key == -1) {
-					$originalFilename = $_FILES['tx_form_form']['name'][$formPrefix][$fieldName];
-					$size = $_FILES['tx_form_form']['size'][$formPrefix][$fieldName];
-				} else {
-					$originalFilename = $_FILES['tx_form_form']['name'][$formPrefix][$fieldName][$key];
-					$size = $_FILES['tx_form_form']['size'][$formPrefix][$fieldName][$key];
-				}
-				$fileInfo = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Type\File\FileInfo::class, $tempFilename);
-				return array(
-						'tempFilename' => $tempFilename,
-						'originalFilename' => $originalFilename,
-						'type' => $fileInfo->getMimeType(),
-						'size' => (int)$size
-					);
+				return $tempFilename;
 			}
 		}
 		return NULL;
diff --git a/typo3/sysext/form/Classes/PostProcess/MailPostProcessor.php b/typo3/sysext/form/Classes/PostProcess/MailPostProcessor.php
index c704b4a271de..00a29509a270 100644
--- a/typo3/sysext/form/Classes/PostProcess/MailPostProcessor.php
+++ b/typo3/sysext/form/Classes/PostProcess/MailPostProcessor.php
@@ -425,7 +425,7 @@ class MailPostProcessor extends AbstractPostProcessor implements PostProcessorIn
 								is_file($file['tempFilename'])
 								&& GeneralUtility::isAllowedAbsPath($file['tempFilename'])
 							) {
-								$this->mailMessage->attach(\Swift_Attachment::fromPath($file['tempFilename'])->setFilename($file['originalFilename']));
+								$this->mailMessage->attach(\Swift_Attachment::fromPath($file['tempFilename'])->setFilename($file['name']));
 							}
 						}
 					}
diff --git a/typo3/sysext/form/Classes/Utility/SessionUtility.php b/typo3/sysext/form/Classes/Utility/SessionUtility.php
index e22dd96519a9..82ea3778b5f5 100644
--- a/typo3/sysext/form/Classes/Utility/SessionUtility.php
+++ b/typo3/sysext/form/Classes/Utility/SessionUtility.php
@@ -141,7 +141,6 @@ class SessionUtility implements SingletonInterface {
 			foreach ($sessionData as $fieldName => $values) {
 				if (is_array($values)) {
 					foreach ($values as $file) {
-
 						if (isset($file['tempFilename'])) {
 							GeneralUtility::unlink_tempfile($file['tempFilename']);
 						}
diff --git a/typo3/sysext/form/Resources/Private/Partials/Compatibility/Confirmation/FlatElements/Upload.html b/typo3/sysext/form/Resources/Private/Partials/Compatibility/Confirmation/FlatElements/Upload.html
index 1afa3ca04e5c..9837361c8b93 100644
--- a/typo3/sysext/form/Resources/Private/Partials/Compatibility/Confirmation/FlatElements/Upload.html
+++ b/typo3/sysext/form/Resources/Private/Partials/Compatibility/Confirmation/FlatElements/Upload.html
@@ -1,7 +1,7 @@
 <f:if condition="{model.showElement}">
 	<f:format.raw>{model.layout.elementOuterWrap.0}</f:format.raw>
 	<f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile">
-		{uploadedFile.originalFilename}<br />
+		{uploadedFile.name}<br />
 	</f:for>
 	<f:format.raw>{model.layout.elementOuterWrap.1}</f:format.raw>
 </f:if>
diff --git a/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Html/FlatElements/Upload.html b/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Html/FlatElements/Upload.html
index f9eec9cfaa4c..4e2934003840 100644
--- a/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Html/FlatElements/Upload.html
+++ b/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Html/FlatElements/Upload.html
@@ -1,5 +1,5 @@
 <f:if condition="{model.showElement}">
 	<f:format.raw>{model.layout.elementOuterWrap.0}</f:format.raw>
-		<f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile">{uploadedFile.originalFilename}<br /></f:for>
+		<f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile">{uploadedFile.name}<br /></f:for>
 	<f:format.raw>{model.layout.elementOuterWrap.1}</f:format.raw>
 </f:if>
diff --git a/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Plain/FlatElements/Upload.html b/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Plain/FlatElements/Upload.html
index 5a0a8219f736..bb9573121a7a 100644
--- a/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Plain/FlatElements/Upload.html
+++ b/typo3/sysext/form/Resources/Private/Partials/Compatibility/PostProcessor/Mail/Plain/FlatElements/Upload.html
@@ -1,2 +1,2 @@
-{namespace form=TYPO3\CMS\Form\ViewHelpers}<f:if condition="{model.showElement}"><form:plainMail labelContent="{model}" newLineAfterLabel="1" indent="4" /><f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile"><form:plainMail content="{uploadedFile.originalFilename}" />
+{namespace form=TYPO3\CMS\Form\ViewHelpers}<f:if condition="{model.showElement}"><form:plainMail labelContent="{model}" newLineAfterLabel="1" indent="4" /><f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile"><form:plainMail content="{uploadedFile.name}" />
 </f:for><form:plainMail indent="-4"/></f:if>
\ No newline at end of file
diff --git a/typo3/sysext/form/Resources/Private/Partials/Default/Confirmation/FlatElements/Upload.html b/typo3/sysext/form/Resources/Private/Partials/Default/Confirmation/FlatElements/Upload.html
index 50c5b5520046..0bd9d2c01c8a 100644
--- a/typo3/sysext/form/Resources/Private/Partials/Default/Confirmation/FlatElements/Upload.html
+++ b/typo3/sysext/form/Resources/Private/Partials/Default/Confirmation/FlatElements/Upload.html
@@ -2,7 +2,7 @@
 	<li class="csc-form-{model.elementCounter} csc-form-element csc-form-element-{model.elementTypeLowerCase}">
 		<label>{model.additionalArguments.label}</label>
 		<f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile">
-			{uploadedFile.originalFilename}<br />
+			{uploadedFile.name}<br />
 		</f:for>
 	</li>
 </f:if>
diff --git a/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Html/FlatElements/Upload.html b/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Html/FlatElements/Upload.html
index 042c67be9ba0..34082b12609c 100644
--- a/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Html/FlatElements/Upload.html
+++ b/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Html/FlatElements/Upload.html
@@ -4,7 +4,7 @@
     <em>{model.additionalArguments.label}</em>
   </td>
   <td>
-<f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile">{uploadedFile.originalFilename}<br /></f:for>
+<f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile">{uploadedFile.name}<br /></f:for>
   </td>
 </tr>
 </f:if>
diff --git a/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Plain/FlatElements/Upload.html b/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Plain/FlatElements/Upload.html
index 5a0a8219f736..bb9573121a7a 100644
--- a/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Plain/FlatElements/Upload.html
+++ b/typo3/sysext/form/Resources/Private/Partials/Default/PostProcessor/Mail/Plain/FlatElements/Upload.html
@@ -1,2 +1,2 @@
-{namespace form=TYPO3\CMS\Form\ViewHelpers}<f:if condition="{model.showElement}"><form:plainMail labelContent="{model}" newLineAfterLabel="1" indent="4" /><f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile"><form:plainMail content="{uploadedFile.originalFilename}" />
+{namespace form=TYPO3\CMS\Form\ViewHelpers}<f:if condition="{model.showElement}"><form:plainMail labelContent="{model}" newLineAfterLabel="1" indent="4" /><f:for each="{model.additionalArguments.uploadedFiles}" as="uploadedFile"><form:plainMail content="{uploadedFile.name}" />
 </f:for><form:plainMail indent="-4"/></f:if>
\ No newline at end of file
-- 
GitLab