Skip to content
Snippets Groups Projects
Commit 3b8dc9ab authored by Stefan Galinski's avatar Stefan Galinski :video_game:
Browse files

[FEATURE] File uploads foo finally works... Thanks for nothing TYPO3

parent ad297cdf
No related branches found
No related tags found
No related merge requests found
......@@ -26,6 +26,7 @@ namespace SGalinski\SgJobs\Controller;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use SGalinski\SgJobs\Domain\Model\Job;
use SGalinski\SgJobs\Domain\Model\JobApplication;
use SGalinski\SgJobs\Property\TypeConverter\UploadedFileReferenceConverter;
use SGalinski\SgJobs\Service\FrontendFilterService;
......@@ -98,12 +99,11 @@ class JoblistController extends ActionController {
/**
* Renders the application form with an optional job
*
* @param JobApplication $applyData
* @param string $error
* @param string $uid
* @throws \InvalidArgumentException
* @param JobApplication|NULL $applyData
* @param string|NULL $error
* @param string|NULL $folderName
*/
public function applyFormAction(JobApplication $applyData = NULL, $error = NULL, $uid = NULL) {
public function applyFormAction(JobApplication $applyData = NULL, $error = NULL, $folderName = NULL) {
// $uploadedFiles = $this->getExistingApplicationFiles($uniqueFolderName);
// if (\count($uploadedFiles['coverLetter']) <= 0 && \count($uploadedFiles['cv']) <= 0
// && \count($uploadedFiles['certificate']) <= 0
......@@ -117,8 +117,15 @@ class JoblistController extends ActionController {
$this->view->assign('internalError', $error);
}
if ($folderName === NULL) {
$folderName = md5(uniqid('sgjobs-', TRUE));
}
$this->view->assign('folderName', $folderName);
$jobId = $this->request->getArguments()['uid'];
$jobData = NULL;
if (!empty($jobId)) {
/** @var Job $jobData */
$jobData = $this->jobRepository->findByUid($jobId);
$this->view->assign('job', $jobData);
}
......@@ -133,6 +140,15 @@ class JoblistController extends ActionController {
$this->view->assign('allowedMimeTypes', $allowedMimeTypes);
$allowedFileExtensions = $this->settings['allowedFileExtensions'];
$this->view->assign('allowedFileExtensions', $allowedFileExtensions);
if ($applyData === NULL) {
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
$applyData = $this->objectManager->get(JobApplication::class);
if ($jobData) {
$applyData->setJobId($jobData->getJobId());
}
}
$this->view->assign('applyData', $applyData);
}
......@@ -140,25 +156,10 @@ class JoblistController extends ActionController {
* Pre-apply action setup, configures model-property mapping and handles file upload
*
* @return void
* @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException
* @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException
* @throws \TYPO3\CMS\Core\Resource\Exception\ExistingTargetFolderException
* @throws \InvalidArgumentException
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
* @throws \Exception
*/
protected function initializeApplyAction() {
if ($this->request->hasArgument('folderName')) {
$uniqueFolderName = $this->request->getArgument('folderName');
} else {
$uniqueFolderName = uniqid('sgjobs-', TRUE);
$resourceFactory = $this->objectManager->get(ResourceFactory::class);
$storage = $resourceFactory->getStorageObject(1);
$storage->createFolder('/JobApplication/' . $uniqueFolderName);
}
$this->request->setArgument('folderName', $uniqueFolderName);
$uniqueFolderName = $this->request->getArgument('folderName');
$propertyMappingConfiguration = $this->arguments->getArgument('applyData')->getPropertyMappingConfiguration();
$typeConverter1 = $this->objectManager->get(UploadedFileReferenceConverter::class);
......@@ -210,6 +211,8 @@ class JoblistController extends ActionController {
public function applyAction(JobApplication $applyData) {
try {
$applyData->setPid($GLOBALS['TSFE']->id);
// @TODO repository is missing / OR UPDATE!!!
// $this->jobApplicationRepository->add($applyData);
$folderName = $this->request->getArgument('folderName');
$this->submitApplicationFiles($applyData, $folderName);
......@@ -232,7 +235,7 @@ class JoblistController extends ActionController {
$this->redirectToUri($uri);
} catch (\Exception $exception) {
$this->forward('applyForm', NULL, NULL, ['error' => $exception->getMessage()]);
$this->forward('applyForm', NULL, NULL, ['applyData' => $applyData, 'error' => $exception->getMessage()]);
}
}
......
......@@ -29,6 +29,7 @@ namespace SGalinski\SgJobs\Property\TypeConverter;
use TYPO3\CMS\Core\Resource\DuplicationBehavior;
use TYPO3\CMS\Core\Resource\File as FalFile;
use TYPO3\CMS\Core\Resource\FileReference as FalFileReference;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
......@@ -276,40 +277,22 @@ class UploadedFileReferenceConverter implements TypeConverterInterface {
* @param array $convertedChildProperties
* @param PropertyMappingConfigurationInterface $configuration
* @return null|\TYPO3\CMS\Core\Resource\FileInterface|FileReference|Error
* @throws \TYPO3\CMS\Extbase\Security\Exception\InvalidHashException
* @throws \TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException
* @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
* @api
* @throws \TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException
*/
public function convertFrom(
$source, $targetType, array $convertedChildProperties = [],
PropertyMappingConfigurationInterface $configuration = NULL
) {
if (!isset($source['error']) || $source['error'] === \UPLOAD_ERR_NO_FILE) {
if (isset($source['submittedFile']['resourcePointer'])) {
try {
$resourcePointer = $this->hashService->validateAndStripHmac(
$source['submittedFile']['resourcePointer']
);
if (strpos($resourcePointer, 'file:') === 0) {
$fileUid = substr($resourcePointer, 5);
return $this->createFileReferenceFromFalFileObject(
$this->resourceFactory->getFileObject($fileUid)
);
}
return $this->createFileReferenceFromFalFileReferenceObject(
$this->resourceFactory->getFileReferenceObject($resourcePointer), $resourcePointer
);
} catch (\InvalidArgumentException $e) {
// Nothing to do. No file is uploaded and resource pointer is invalid. Discard!
}
}
if ($source['name'] === '' && \is_array($source['submittedFile'])) {
$source = $source['submittedFile'];
$source['wasUploaded'] = TRUE;
}
if ($source['error'] === \UPLOAD_ERR_NO_FILE) {
return NULL;
}
if ($source['error'] !== \UPLOAD_ERR_OK) {
if ($source['error'] !== '' && (int) $source['error'] !== \UPLOAD_ERR_OK) {
switch ($source['error']) {
case \UPLOAD_ERR_INI_SIZE:
case \UPLOAD_ERR_FORM_SIZE:
......@@ -346,49 +329,70 @@ class UploadedFileReferenceConverter implements TypeConverterInterface {
*
* @param array $uploadInfo
* @return \SGalinski\SgJobs\Domain\Model\FileReference
* @throws \InvalidArgumentException
* @throws TypeConverterException
* @throws \TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException
* @throws \TYPO3\CMS\Extbase\Security\Exception\InvalidHashException
* @throws \Exception
* @throws \TYPO3\CMS\Core\Resource\Exception\ExistingTargetFolderException
* @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException
* @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException
*/
protected function importUploadedResource(array $uploadInfo): FileReference {
if (!GeneralUtility::verifyFilenameAgainstDenyPattern($uploadInfo['name'])) {
throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1399312430);
}
$allowedFileExtensions = $this->getAllowedFileExtensions();
$filePathInfo = PathUtility::pathinfo($uploadInfo['name']);
if ($allowedFileExtensions !== NULL) {
if (!GeneralUtility::inList($allowedFileExtensions, strtolower($filePathInfo['extension']))) {
throw new TypeConverterException('File extension is not allowed!', 1399312430);
$fileName = $this->getTargetUploadFileName();
$finalFileName = $fileName . '.' . strtolower($filePathInfo['extension']);
if (!$uploadInfo['wasUploaded']) {
if (!GeneralUtility::verifyFilenameAgainstDenyPattern($uploadInfo['name'])) {
throw new TypeConverterException(
'Uploading files with PHP file extensions is not allowed!', 1399312430
);
}
}
$uploadFolderId = $this->getUploadFolder();
$conflictMode = $this->getUploadConflictMode();
$uploadFolder = $this->resourceFactory->retrieveFileOrFolderObject($uploadFolderId);
$uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode);
$allowedFileExtensions = $this->getAllowedFileExtensions();
if ($allowedFileExtensions !== NULL) {
if (!GeneralUtility::inList($allowedFileExtensions, strtolower($filePathInfo['extension']))) {
throw new TypeConverterException('File extension is not allowed!', 1399312430);
}
}
$fileName = $this->getTargetUploadFileName();
if ($fileName !== '') {
$uploadedFile->rename($fileName . '.' . $filePathInfo['extension']);
}
$uploadFolderId = $this->getUploadFolder();
$resourceFactory = $this->objectManager->get(ResourceFactory::class);
$storage = $resourceFactory->getStorageObject(1);
$folderWithoutStorage = preg_replace('/^1:/', '', $uploadFolderId);
if (!$storage->hasFolder($folderWithoutStorage)) {
$storage->createFolder($folderWithoutStorage);
}
$uploadFolder = $this->resourceFactory->retrieveFileOrFolderObject($uploadFolderId);
$uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $this->getUploadConflictMode());
if ($fileName !== '') {
$uploadedFile->rename($finalFileName, $this->getUploadConflictMode());
}
$resourcePointer = isset($uploadInfo['submittedFile']['resourcePointer']) &&
strpos($uploadInfo['submittedFile']['resourcePointer'], 'file:') === FALSE
? $this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer'])
: NULL;
} else {
$uploadFolderId = $this->getUploadFolder();
// Security protection to not allow manipulations outside of this specific folder
if (strpos($uploadFolderId, '/JobApplication/') !== FALSE) {
try {
$uploadedFile = $this->resourceFactory->retrieveFileOrFolderObject(
$uploadFolderId . '/' . $finalFileName
);
} catch (\Exception $exception) {
// nope
}
$fileReferenceModel = $this->createFileReferenceFromFalFileObject($uploadedFile, $resourcePointer);
} else {
throw new \Exception('Not allowed!');
}
}
return $fileReferenceModel;
return $this->createFileReferenceFromFalFileObject($uploadedFile);
}
/**
* @param FalFile $file
* @param int $resourcePointer
* @return \SGalinski\SgJobs\Domain\Model\FileReference
*/
protected function createFileReferenceFromFalFileObject(FalFile $file, $resourcePointer = NULL
protected function createFileReferenceFromFalFileObject(FalFile $file
): \SGalinski\SgJobs\Domain\Model\FileReference {
$fileReference = $this->resourceFactory->createFileReferenceObject(
[
......@@ -398,28 +402,18 @@ class UploadedFileReferenceConverter implements TypeConverterInterface {
'crop' => NULL,
]
);
return $this->createFileReferenceFromFalFileReferenceObject($fileReference, $resourcePointer);
return $this->createFileReferenceFromFalFileReferenceObject($fileReference);
}
/**
* @param FalFileReference $falFileReference
* @param int $resourcePointer
* @return \SGalinski\SgJobs\Domain\Model\FileReference
*/
protected function createFileReferenceFromFalFileReferenceObject(
FalFileReference $falFileReference, $resourcePointer = NULL
protected function createFileReferenceFromFalFileReferenceObject(FalFileReference $falFileReference
): \SGalinski\SgJobs\Domain\Model\FileReference {
if ($resourcePointer === NULL) {
/** @var \SGalinski\SgJobs\Domain\Model\FileReference $fileReference */
$fileReference = $this->objectManager->get(FileReference::class);
} else {
$fileReference = $this->persistenceManager->getObjectByIdentifier(
$resourcePointer, FileReference::class, FALSE
);
}
/** @var \SGalinski\SgJobs\Domain\Model\FileReference $fileReference */
$fileReference = $this->objectManager->get(FileReference::class);
$fileReference->setOriginalResource($falFileReference);
return $fileReference;
}
}
......@@ -28,7 +28,7 @@ namespace SGalinski\SgJobs\ViewHelpers\Form;
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
/** @noinspection LongInheritanceChainInspection */
/**
* Class UploadViewHelper
......@@ -46,6 +46,7 @@ class UploadViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\UploadViewHelpe
*/
protected $propertyMapper;
/** @noinspection PhpDocMissingThrowsInspection */
/**
* Render the upload field including possible resource pointer
*
......@@ -53,32 +54,19 @@ class UploadViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\UploadViewHelpe
* @api
* @param string $resourceName
* @throws \InvalidArgumentException
* @throws \TYPO3\CMS\Extbase\Property\Exception
*/
public function render($resourceName = 'resource'): string {
$output = '';
$resource = $this->getUploadedResource();
if ($resource !== NULL) {
$resourcePointerIdAttribute = '';
if ($this->hasArgument('id')) {
$resourcePointerIdAttribute = ' id="' . htmlspecialchars($this->arguments['id']) . '-file-reference"';
}
$resourcePointerValue = $resource->getUid();
if ($resourcePointerValue === NULL) {
// Newly created file reference which is not persisted yet.
// Use the file UID instead, but prefix it with "file:" to communicate this to the type converter
$resourcePointerValue = 'file:' . $resource->getOriginalResource()->getOriginalFile()->getUid();
}
$output .= '<input type="hidden" name="' . $this->getName() .
'[submittedFile][resourcePointer]" value="' .
htmlspecialchars(
$this->hashService->appendHmac((string) $resourcePointerValue)
) . '"' . $resourcePointerIdAttribute . ' />';
if (\is_array($resource) && $resource['name'] === '' && isset($resource['submittedFile'])) {
$resource = $resource['submittedFile'];
}
if ($resource !== NULL) {
/** @noinspection PhpUnhandledExceptionInspection */
$this->templateVariableContainer->add($resourceName, $resource);
$output .= $this->renderChildren();
$this->templateVariableContainer->remove('resource');
}
$output .= parent::render();
......@@ -89,19 +77,14 @@ class UploadViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\UploadViewHelpe
* Return a previously uploaded resource.
* Return NULL if errors occurred during property mapping for this property.
*
* @return FileReference|NULL
* @throws \TYPO3\CMS\Extbase\Property\Exception
* @return array
*/
protected function getUploadedResource() {
if ($this->getMappingResultsForProperty()->hasErrors()) {
return NULL;
}
$resource = $this->getValueAttribute();
if ($resource instanceof FileReference) {
return $resource;
}
return $this->propertyMapper->convert($resource, FileReference::class);
$this->respectSubmittedDataValue = TRUE;
return $this->getValueAttribute();
}
}
......@@ -14,7 +14,7 @@
<f:if condition="{job}">
<p>
<f:form.hidden value="{job.jobId}" property="jobId" />
<f:form.hidden value="{applyData.jobId}" property="jobId" />
<label for="apply-title"><f:translate key="frontend.apply.title" /></label>
<span id="apply-title">{job.title}</span>
</p>
......@@ -106,7 +106,7 @@
<p>
<label for="apply-nationality"><f:translate key="frontend.apply.nationality" /></label>
<f:form.select property="nationality" id="apply-nationality" options="{countries}" optionLabelField="{f:if(condition: '{sysLanguageUid} == 0', then: 'shortNameDe', else: 'shortNameEn')}" optionValueField="{f:if(condition: '{sysLanguageUid} == 0', then: 'shortNameDe', else: 'shortNameEn')}"/>
<f:form.select property="nationality" id="apply-nationality" options="{countries}" optionLabelField="{f:if(condition: '{sysLanguageUid} == 0', then: 'shortNameDe', else: 'shortNameEn')}" optionValueField="{f:if(condition: '{sysLanguageUid} == 0', then: 'shortNameDe', else: 'shortNameEn')}" />
<f:form.validationResults for="applyData.nationality">
<f:for each="{validationResults.errors}" as="error">
<div class="sg-jobs-validation-error">
......@@ -176,51 +176,65 @@
</f:form.validationResults>
</p>
<p>
<div>
<label for="apply-cover-letter">
<f:translate key="frontend.apply.cover_letter" />
(<f:translate key="frontend.apply.allowed_file_extensions" /> {allowedFileExtensions})
</label>
<h:form.upload property="coverLetter" resourceName="coverLetter" id="apply-cover-letter" additionalAttributes="{accept: '{allowedMimeTypes}'}" />
<f:if condition="{coverLetter}">
<div class="sg-jobs-uploaded-file">
<f:image image="{coverLetter}" alt="" width="50"/>
</div>
<f:if condition="{coverLetter.name}">
<p>
Aktuell: {coverLetter.name}
<f:comment><!-- Important, due to a fluid cache issue with the fluid syntax--></f:comment>
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][name]" value="{coverLetter.name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][type]" value="{coverLetter.type}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][tmp_name]" value="{coverLetter.tmp_name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][error]" value="0" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][size]" value="{coverLetter.size}" />
</p>
</f:if>
<f:form.validationResults for="applyData.coverLetter">
<f:for each="{validationResults.errors}" as="error">
<div class="sg-jobs-validation-error">
<p class="sg-jobs-validation-error">
{error.message}
</div>
</p>
</f:for>
</f:form.validationResults>
</p>
</div>
<p>
<div>
<label for="apply-cv">
<f:translate key="frontend.apply.cv" />
(<f:translate key="frontend.apply.allowed_file_extensions" /> {allowedFileExtensions})
</label>
<h:form.upload property="cv" resourceName="cv" id="apply-cv" additionalAttributes="{accept: '{allowedMimeTypes}'}" />
<f:if condition="{cv}">
<div class="sg-jobs-uploaded-file">
<f:image image="{cv}" alt="" width="50"/>
</div>
<f:if condition="{cv.name}">
<p>
Aktuell: {cv.name}
<f:comment><!-- Important, due to a fluid cache issue with the fluid syntax--></f:comment>
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][name]" value="{cv.name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][type]" value="{cv.type}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][tmp_name]" value="{cv.tmp_name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][error]" value="0" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][size]" value="{cv.size}" />
</p>
</f:if>
<f:form.validationResults for="applyData.cv">
<f:for each="{validationResults.errors}" as="error">
<div class="sg-jobs-validation-error">
<p class="sg-jobs-validation-error">
{error.message}
</div>
</p>
</f:for>
</f:form.validationResults>
</p>
</div>
<p>
<div>
<label for="apply-certificate">
<f:translate key="frontend.apply.certificate" />
(<f:translate key="frontend.apply.allowed_file_extensions" /> {allowedFileExtensions})
......@@ -228,19 +242,26 @@
<h:form.upload property="certificate" resourceName="certificate" id="apply-certificate" additionalAttributes="{accept: '{allowedMimeTypes}'}" />
<f:if condition="{certificate.name}">
<div class="sg-jobs-uploaded-file">
<f:image image="{certificate}" alt="" width="50"/>
</div>
<p>
Aktuell: {certificate.name}
<f:comment><!-- Important, due to a fluid cache issue with the fluid syntax--></f:comment>
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][name]" value="{certificate.name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][type]" value="{certificate.type}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][tmp_name]" value="{certificate.tmp_name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][error]" value="0" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][size]" value="{certificate.size}" />
</p>
</f:if>
<f:form.validationResults for="applyData.certificate">
<f:for each="{validationResults.errors}" as="error">
<div class="sg-jobs-validation-error">
<p class="sg-jobs-validation-error">
{error.message}
</div>
</p>
</f:for>
</f:form.validationResults>
</p>
</div>
<p>
<label for="apply-message"><f:translate key="frontend.apply.message" /></label>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment