diff --git a/Classes/Controller/JoblistController.php b/Classes/Controller/JoblistController.php index 6e40d54ed6e452209976e354492ff7b38f81c192..b3508d012bce30789b4322a0b546cd762aa9b402 100644 --- a/Classes/Controller/JoblistController.php +++ b/Classes/Controller/JoblistController.php @@ -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()]); } } diff --git a/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php b/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php index 8014d12dabbc4cccb58ca918c86c8c1248acc3ae..fe99a5be30db44bc9c53b65a55e4531c50fc4df1 100644 --- a/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php +++ b/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php @@ -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; } } diff --git a/Classes/ViewHelpers/Form/UploadViewHelper.php b/Classes/ViewHelpers/Form/UploadViewHelper.php index 72b5d1b264a0a28c5562ee7ededf6228b2d7753b..6d2d1068f1743a3ecd635eb309db139a7a920009 100644 --- a/Classes/ViewHelpers/Form/UploadViewHelper.php +++ b/Classes/ViewHelpers/Form/UploadViewHelper.php @@ -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(); } } diff --git a/Resources/Private/Templates/Joblist/ApplyForm.html b/Resources/Private/Templates/Joblist/ApplyForm.html index 50da0e48af962cdca7f2808b2701abf584db0244..098f06f549af8d07722713d4fe997e1b9862ba67 100644 --- a/Resources/Private/Templates/Joblist/ApplyForm.html +++ b/Resources/Private/Templates/Joblist/ApplyForm.html @@ -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>