<?php namespace SGalinski\SgJobs\Controller; /*************************************************************** * Copyright notice * * (c) sgalinski Internet Services (https://www.sgalinski.de) * * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is * free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * The GNU General Public License can be found at * http://www.gnu.org/copyleft/gpl.html. * * This script is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ use SGalinski\ProjectBase\Domain\Repository\CountryRepository; use SGalinski\SgJobs\Domain\Model\Job; use SGalinski\SgJobs\Domain\Model\JobApplication; use SGalinski\SgJobs\Domain\Repository\CompanyRepository; use SGalinski\SgJobs\Domain\Repository\DepartmentRepository; use SGalinski\SgJobs\Domain\Repository\ExperienceLevelRepository; use SGalinski\SgJobs\Domain\Repository\JobApplicationRepository; use SGalinski\SgJobs\Domain\Repository\JobRepository; use SGalinski\SgJobs\Property\TypeConverter\UploadedFileReferenceConverter; use SGalinski\SgMail\Service\MailTemplateService; use SGalinski\SgSeo\Service\HeadTagService; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Http\ImmediateResponseException; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; /** * The joblist plugin controller */ class JoblistController extends ActionController { // the array key for the error message in the post array public const ERROR_KEY_IN_POST = 'error'; /** * @var CompanyRepository */ protected $companyRepository; /** * @var JobRepository */ protected $jobRepository; /** * @var JobApplicationRepository */ protected $jobApplicationRepository; /** * @var DepartmentRepository */ protected $departmentRepository; /** * @var ExperienceLevelRepository */ protected $experienceLevelRepository; /** * Inject the CompanyRepository * * @param CompanyRepository $companyRepository */ public function injectCompanyRepository(CompanyRepository $companyRepository): void { $this->companyRepository = $companyRepository; } /** * Inject the DepartmentRepository * * @param DepartmentRepository $departmentRepository */ public function injectDepartmentRepository(DepartmentRepository $departmentRepository): void { $this->departmentRepository = $departmentRepository; } /** * Inject the ExperienceLevelRepository * * @param ExperienceLevelRepository $experienceLevelRepository */ public function injectExperienceLevelRepository(ExperienceLevelRepository $experienceLevelRepository): void { $this->experienceLevelRepository = $experienceLevelRepository; } /** * Inject the JobApplicationRepository * * @param JobApplicationRepository $jobApplicationRepository */ public function injectJobApplicationRepository(JobApplicationRepository $jobApplicationRepository): void { $this->jobApplicationRepository = $jobApplicationRepository; } /** * Inject the JobRepository * * @param JobRepository $jobRepository */ public function injectJobRepository(JobRepository $jobRepository): void { $this->jobRepository = $jobRepository; } /** * Initialize the indexAction to set the currentPageBrowserPage parameter * * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException */ public function initializeIndexAction() { $currentPageBrowserPage = (int) GeneralUtility::_GP('tx_sgjobs_pagebrowser')['currentPage']; if ($currentPageBrowserPage > 0) { $this->request->setArgument('currentPageBrowserPage', $currentPageBrowserPage); } } /** * Show all job offers and options to manage them * * @param array $filters * @param int $jobId * @param int $currentPageBrowserPage * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException * @throws \TYPO3\CMS\Core\Error\Http\PageNotFoundException * @throws ImmediateResponseException * @throws \TYPO3\CMS\Core\Package\Exception */ public function indexAction(array $filters = [], int $jobId = NULL, int $currentPageBrowserPage = 0): ?\Psr\Http\Message\ResponseInterface { if ($filters) { $this->view->assign('selectedCountry', $filters['filterCountry']); $this->view->assign('selectedCompany', $filters['filterCompany']); $this->view->assign('selectedLocation', $filters['filterLocation']); $this->view->assign('selectedDepartment', $filters['filterDepartment']); $this->view->assign('selectedExperienceLevel', $filters['filterExperienceLevel']); $this->view->assign('selectedFunction', $filters['filterFunction']); $this->view->assign('selectedRemote', $filters['filterRemote']); } $storagePid = (int) $this->configurationManager->getConfiguration( ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK )['persistence']['storagePid']; $this->assignFilterValues($storagePid); $this->view->assign('recordPageId', $storagePid); $jobLimit = (int) $this->settings['jobLimit']; if ($jobId) { /** @var Job $job */ $job = $this->jobRepository->findByUid($jobId); if (!$job) { throw new \InvalidArgumentException('Given Job Id is invalid!'); } if (version_compare(ExtensionManagementUtility::getExtensionVersion('sg_seo'), '5.0.0', '>=')) { $headTagService = GeneralUtility::makeInstance( HeadTagService::class, TRUE, $job->getTitle(), $job->getDescription(), '&tx_sgjobs_jobapplication[jobId]=' . $jobId ); $headTagService->execute(); } else { $GLOBALS['TSFE']->page['titlebyextension'] = $job->getTitle(); $GLOBALS['TSFE']->page['description'] = \strip_tags(\substr($job->getDescription(), 0, 200)); $GLOBALS['TSFE']->page['extensionArgumentsForCanonicalAndHrefLang'] = '&tx_sgjobs_jobapplication[jobId]=' . $jobId; } $jobs = [$job]; $numberOfPages = 1; } else { // pagination logic $offset = 0; if ($currentPageBrowserPage && $jobLimit) { $offset = $currentPageBrowserPage * $jobLimit; } $frontendPluginSettings = $this->configurationManager->getConfiguration( ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK ); $frontendPluginSettings = $frontendPluginSettings['settings']; $isManualSortingAllowed = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_jobs']['allowManualSorting']; // get all jobs for the current page $ordering = (int) $frontendPluginSettings['orderBy']; $experienceLevel = (int) $frontendPluginSettings['filterByExperienceLevel']; if ($experienceLevel !== 0) { $filters['filterExperienceLevel'] = $experienceLevel; } $this->jobRepository->setAllowManualSorting((bool) $isManualSortingAllowed); $jobs = $this->jobRepository->findJobsByFilter($filters, $jobLimit, $offset, $ordering)->toArray(); // get all jobs for the current page $allJobs = $this->jobRepository->findJobsByFilter($filters)->toArray(); $numberOfPages = (int) ($jobLimit <= 0 ? 0 : \ceil(\count($allJobs) / $jobLimit)); if ($numberOfPages !== 0 && $currentPageBrowserPage >= $numberOfPages) { /** @var ErrorController $errorController */ $errorController = GeneralUtility::makeInstance(ErrorController::class); $response = $errorController->pageNotFoundAction( $GLOBALS['TYPO3_REQUEST'], 'The requested page does not exist', ['code' => PageAccessFailureReasons::PAGE_NOT_FOUND] ); throw new ImmediateResponseException($response); } } $this->view->assign('jobs', $jobs); $this->view->assign('limit', $jobLimit); $this->view->assign('numberOfPages', $numberOfPages); if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) { return NULL; } else { return $this->htmlResponse(); } } /** * Renders the application form with an optional job * * @param JobApplication|null $applyData * @param string $error * @param int $jobId * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException */ public function applyFormAction(JobApplication $applyData = NULL, string $error = '', int $jobId = NULL): ?\Psr\Http\Message\ResponseInterface { if ($error !== '') { $this->view->assign('internalError', $error); $this->request->setArgument('error', NULL); } if ($jobId === NULL && $this->settings['disallowUnsolicitedApplication']) { $uriBuilder = $this->uriBuilder; $uri = $uriBuilder ->setTargetPageUid($this->settings['offersPage']) ->build(); $this->redirectToUri($uri, 0, 301); } $folderName = NULL; try { $folderName = $this->request->getArgument('folderName'); } catch (\Exception $exception) { // this happens for the initial call, but works for any follow-up call as the form validation // throws you back to this one if something has failed } if ($folderName === NULL) { $folderName = \md5(\uniqid('sgjobs-', TRUE)); $this->request->setArgument('folderName', $folderName); } $this->view->assign('folderName', $folderName); $job = NULL; if ($jobId !== NULL) { /** @var Job $job */ $job = $this->jobRepository->findByUid($jobId); if ($job) { if (version_compare(ExtensionManagementUtility::getExtensionVersion('sg_seo'), '5.0.0', '>=')) { $headTagService = GeneralUtility::makeInstance( HeadTagService::class, FALSE, $job->getTitle(), $job->getDescription(), '&tx_sgjobs_jobapplication[jobId]=' . $jobId ); $headTagService->execute(); } else { $GLOBALS['TSFE']->page['titlebyextension'] = $job->getTitle(); $GLOBALS['TSFE']->page['description'] = \strip_tags(\substr($job->getDescription(), 0, 200)); $GLOBALS['TSFE']->page['extensionArgumentsForCanonicalAndHrefLang'] = '&tx_sgjobs_jobapplication[jobId]=' . $jobId; } } $this->view->assign('job', $job); } else { $storagePid = (int) $this->configurationManager->getConfiguration( ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK )['persistence']['storagePid']; $this->view->assign('companies', $this->companyRepository->getAllCompanies($storagePid)); } // display country options $context = GeneralUtility::makeInstance(Context::class); $sysLanguageUid = $context->getPropertyFromAspect('language', 'id'); $site = GeneralUtility::makeInstance(SiteFinder::class) ->getSiteByPageId($GLOBALS['TSFE']->id) ->getLanguageById($sysLanguageUid); $countries = []; if (ExtensionManagementUtility::isLoaded('project_base')) { $countryRepository = $this->objectManager->get(CountryRepository::class); $countries = $countryRepository->findAllOrderedByLanguage($site->getTwoLetterIsoCode()); } $this->view->assign('countries', $countries); $this->view->assign('sysLanguageUid', $sysLanguageUid); $allowedMimeTypes = $this->settings['allowedMimeTypes']; $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 ($job) { $applyData->setJobId($job->getJobId()); } } $this->view->assign('applyData', $applyData); $this->view->assign('maxFileSize', $this->settings['allowedMaxFileSize']); $this->view->assign('maxFileSizeMb', ((int) $this->settings['allowedMaxFileSize'] / 1000) . ' MByte'); $this->view->assign( 'maxFileSizeMessage', LocalizationUtility::translate('error.maxFileSizeMessage', 'sg_jobs') ); // This fixes a bug in the form ViewHelper that wants to serialize a Model with closures in it $arguments = $this->request->getArguments(); if ($arguments['applyData']) { $arguments['applyData'] = (string) $arguments['applyData']; $this->request->setArguments($arguments); } if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) { return NULL; } else { return $this->htmlResponse(); } } /** * Pre-apply action setup, configures model-property mapping and handles file upload * * @throws NoSuchArgumentException * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException */ protected function initializeApplyAction(): ?\Psr\Http\Message\ResponseInterface { try { $uniqueFolderName = $this->request->getArgument('folderName'); } catch (NoSuchArgumentException $exception) { $exceptionMessage = 'Some file could not be uploaded. Is it too large?'; if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) { $this->redirect('applyForm', NULL, NULL, ['error' => $exceptionMessage]); return NULL; } else { return $this->redirect('applyForm', NULL, NULL, ['error' => $exceptionMessage]); } } $propertyMappingConfiguration = $this->arguments->getArgument('applyData')->getPropertyMappingConfiguration(); $propertyMappingConfiguration->forProperty('job')->allowAllProperties(); foreach (['coverLetter', 'cv', 'certificate'] as $property) { $typeConverter = $this->objectManager->get(UploadedFileReferenceConverter::class); $typeConverter->setAllowedFileExtensions( $this->settings['allowedFileExtensions'] ?? $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] ); $typeConverter->setUploadFolder('1:/JobApplication/temp/' . $uniqueFolderName); $typeConverter->setTargetUploadFileName($property); $propertyMappingConfiguration->forProperty($property)->setTypeConverter($typeConverter); } if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) { return NULL; } else { return $this->htmlResponse(); } } /** * Moves the application files from temporary to permanent storage * * @param JobApplication $applicationData * @param string $folderName * @return void * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException */ protected function submitApplicationFiles(JobApplication $applicationData, $folderName): void { $resourceFactory = $this->objectManager->get(ResourceFactory::class); $newName = \date('Ymd-His') . '_' . $applicationData->getJobId() . '-' . $applicationData->getFirstName() . '-' . $applicationData->getLastName(); $storage = $resourceFactory->getStorageObject(1); $applicationFilePath = Environment::getPublicPath() . '/' . $storage->getConfiguration( )['basePath'] . 'JobApplication/' . $folderName . '/' . $newName . '.csv'; $this->writeApplicationFile($applicationData, $applicationFilePath); } /** * Saves the application send by the applyFormAction * * @param JobApplication $applyData * @throws NoSuchArgumentException * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException */ public function applyAction(JobApplication $applyData): ?\Psr\Http\Message\ResponseInterface { $folderName = $this->request->getArgument('folderName'); try { $applyData->setPid($GLOBALS['TSFE']->id); $job = $applyData->getJob(); // look for a configured default job, in case of unsolicited application if ((!$job || $applyData->getJobId() === NULL) && $applyData->getCompany() !== NULL) { $applyData->setJobId($applyData->getCompany()->getJobId()); } if ($applyData->_isNew()) { $this->jobApplicationRepository->add($applyData); } else { $this->jobApplicationRepository->update($applyData); } $this->moveTmpFolder($folderName); $this->submitApplicationFiles($applyData, $folderName); $mailService = $this->objectManager->get( MailTemplateService::class, 'application_mail', 'sg_jobs', $this->getApplicationMailMarkers($applyData) ); // get email from the job contact, fallback is TS settings $contact = NULL; if ($job !== NULL) { $contact = $job->getContact(); } if ($contact !== NULL) { $mailService->setToAddresses($contact->getEmail()); } else { $company = $applyData->getCompany(); if (($company !== NULL) && $company->getContact() !== NULL) { $mailService->setToAddresses($company->getContact()->getEmail()); } } $mailService->setMarkers( [ 'application' => $applyData, ] ); $mailService->setIgnoreMailQueue(TRUE); // add attachments for each file $coverLetter = $applyData->getCoverLetter(); if ($coverLetter) { $mailService->addFileResourceAttachment($coverLetter); } $cv = $applyData->getCv(); if ($cv) { $mailService->addFileResourceAttachment($cv); } $certificate = $applyData->getCertificate(); if ($certificate) { $mailService->addFileResourceAttachment($certificate); } $mailService->sendEmail(); $redirectPageUid = (int) $this->settings['redirectPage']; if ($redirectPageUid) { $contentObject = $this->configurationManager->getContentObject(); if ($contentObject) { $url = $contentObject->getTypoLink_URL($redirectPageUid); $this->redirectToUri($url); } } $this->redirect('applyForm'); } catch (\Exception $exception) { $this->deleteTmpFolder($folderName); $job = $applyData->getJob(); $jobId = $job !== NULL ? $job->getUid() : NULL; $this->request->setArgument('folderName', $folderName); $this->forward( 'applyForm', NULL, NULL, ['applyData' => $applyData, 'error' => $exception->getMessage(), 'jobId' => $jobId] ); } if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) { return NULL; } else { return $this->htmlResponse(); } } /** * Assign filter values * * @param int $rootPageId * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException */ protected function assignFilterValues($rootPageId): ?\Psr\Http\Message\ResponseInterface { $countries = $this->companyRepository->getAllCountries($rootPageId); $this->view->assign('countries', $countries); $cities = $this->companyRepository->getAllCities($rootPageId); $this->view->assign('cities', $cities); $companies = $this->companyRepository->getAllCompanyNames($rootPageId); $this->view->assign('companies', $companies); $departments = $this->departmentRepository->findAll(); $this->view->assign('departments', $departments); $experienceLevels = $this->experienceLevelRepository->findAll(); $this->view->assign('experienceLevels', $experienceLevels); if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0.0', '<')) { return NULL; } else { return $this->htmlResponse(); } } /** * Returns the application mail markers * * @param JobApplication $applyData * @return array */ protected function getApplicationMailMarkers(JobApplication $applyData): array { $location = ''; if ($applyData->getCompany() !== NULL) { $location = $applyData->getCompany()->getCity(); } return [ 'salutation' => $applyData->getGender(), 'location' => $location, 'firstname' => $applyData->getFirstName(), 'lastname' => $applyData->getLastName(), 'street' => $applyData->getStreet(), 'city' => $applyData->getCity(), 'country' => $applyData->getCountry(), 'phone' => $applyData->getPhone(), 'mobile' => $applyData->getMobile(), 'email' => $applyData->getEmail(), 'message' => $applyData->getFirstName() ]; } /** * Writes the application files * * @param JobApplication $data * @param string $filePath * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException */ protected function writeApplicationFile(JobApplication $data, $filePath): void { $coverLetter = ''; $coverLetterObject = $data->getCoverLetter(); if ($coverLetterObject) { $coverLetterObject = $coverLetterObject->getOriginalResource(); if ($coverLetterObject) { $coverLetter = $coverLetterObject->getPublicUrl(); } } $cv = ''; $cvObject = $data->getCv(); if ($cvObject) { $cvObject = $cvObject->getOriginalResource(); if ($cvObject) { $cv = $cvObject->getPublicUrl(); } } $certificate = ''; $certificateObject = $data->getCertificate(); if ($certificateObject) { $certificateObject = $certificateObject->getOriginalResource(); if ($certificateObject) { $certificate = $certificateObject->getPublicUrl(); } } $dataToInsertArr = [ $data->getJobId(), $data->getFirstName(), $data->getLastName(), $data->getGender(), $data->getCountry(), $data->getBirthDate(), $data->getEducation(), $data->getStreet(), $data->getZip(), $data->getCity(), $data->getNationality(), $data->getPhone(), $data->getEmail(), $coverLetter, $cv, $certificate, $data->getMessage() ]; try { GeneralUtility::mkdir_deep(\dirname($filePath)); $file = \fopen($filePath, 'wb+'); \fputcsv($file, $dataToInsertArr); \fclose($file); } catch (\RuntimeException $exception) { $this->redirect('applyForm', NULL, NULL, ['error' => $exception->getMessage()]); } } /** * Move the temp folder to its proper location * * @param string $folderName * @throws \TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException * @throws \TYPO3\CMS\Core\Resource\Exception\ExistingTargetFolderException * @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException * @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException */ protected function moveTmpFolder(string $folderName): void { // Move uploaded files & csv fo real folder and delete the tmp folder /** @var ResourceFactory $resourceFactory */ $resourceFactory = $this->objectManager->get(ResourceFactory::class); $storage = $resourceFactory->getStorageObject(1); $folder = $storage->getFolder('/JobApplication/'); if (!$storage->hasFolderInFolder($folderName, $folder)) { $newFolder = $storage->createFolder($folderName, $folder); } else { $newFolder = $storage->getFolder('/JobApplication/' . $folderName); } $tempFolder = $storage->getFolder('/JobApplication/temp/' . $folderName); $filesToMove = $storage->getFilesInFolder($tempFolder); foreach ($filesToMove as $fileToMove) { $storage->moveFile($fileToMove, $newFolder); } } /** * Delete uploaded files in tmp folder * * @param string $folderName */ protected function deleteTmpFolder($folderName): void { /** @var ResourceFactory $resourceFactory */ $resourceFactory = $this->objectManager->get(ResourceFactory::class); $storage = $resourceFactory->getStorageObject(1); try { $tempFolder = $storage->getFolder('/JobApplication/temp/' . $folderName); $storage->deleteFolder($tempFolder, TRUE); } catch (\Exception $exception) { // folder is already deleted for some reason } } /** * If for any reason something goes wrong, delete the tmp upload folder * * @return void * @throws NoSuchArgumentException */ public function errorAction() { if ($this->request->hasArgument('folderName')) { $folderName = $this->request->getArgument('folderName'); $this->deleteTmpFolder($folderName); } parent::errorAction(); } /** * Build Typo3 11 Response * @param string|NULL $html * @return \Psr\Http\Message\ResponseInterface */ protected function htmlResponse(string $html = null): \Psr\Http\Message\ResponseInterface { return $this->responseFactory->createResponse() ->withHeader('Content-Type', 'text/html; charset=utf-8') ->withBody($this->streamFactory->createStream($html ?? $this->view->render())); } }