From 0bbda2a4d062b8f7d2438cb0c6b3b02bbb9694ac Mon Sep 17 00:00:00 2001
From: Matthias Adrowski <matthias.adrowski@sgalinski.de>
Date: Mon, 11 Jul 2022 11:09:55 +0200
Subject: [PATCH] [TASK] Make coverletter work using ajax+dropzone

---
 Classes/Controller/Ajax/UploadController.php  | 90 +++++++++++++++++++
 Classes/Controller/JoblistController.php      | 17 ++++
 .../UploadedFileReferenceConverter.php        |  2 +-
 Resources/Private/Language/locallang.xlf      | 15 ++++
 .../Private/Templates/Joblist/ApplyForm.html  | 19 ++--
 Resources/Public/JavaScript/Form/upload.js    | 80 +++++++++++++++++
 Resources/Public/JavaScript/sgJobs.js         | 15 ++++
 ext_localconf.php                             | 12 ++-
 package-lock.json                             | 26 ++++++
 package.json                                  |  7 ++
 10 files changed, 275 insertions(+), 8 deletions(-)
 create mode 100644 Classes/Controller/Ajax/UploadController.php
 create mode 100644 Resources/Public/JavaScript/Form/upload.js
 create mode 100644 Resources/Public/JavaScript/sgJobs.js
 create mode 100644 package-lock.json
 create mode 100644 package.json

diff --git a/Classes/Controller/Ajax/UploadController.php b/Classes/Controller/Ajax/UploadController.php
new file mode 100644
index 00000000..5f06436f
--- /dev/null
+++ b/Classes/Controller/Ajax/UploadController.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace SGalinski\SgJobs\Controller\Ajax;
+
+use SGalinski\SgAjax\Controller\Ajax\AbstractAjaxController;
+use SGalinski\SgComments\Domain\Service\FileAndFolderService;
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/***************************************************************
+ *  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!
+ ***************************************************************/
+
+/**
+ * Uploads for the applicationForm
+ */
+class UploadController  extends AbstractAjaxController {
+
+	public const TYPO3_TMP_FOLDER = 'typo3temp/';
+
+	/**
+	 * Returns the calculated max file size.
+	 *
+	 * @return int
+	 */
+	protected function getMaxFileSize(): int {
+		$maxFileSize = (int) $this->settings['fileUpload']['maxfileSize'];
+		if ($maxFileSize <= 0 && isset($GLOBALS['TYPO3_CONF_VARS']['BE']['maxFileSize'])) {
+			$maxFileSize = (int) $GLOBALS['TYPO3_CONF_VARS']['BE']['maxFileSize']; // Example value: 800000
+			return ($maxFileSize * 1000);
+		}
+
+		return $maxFileSize * 1000 * 1000;
+	}
+
+	/**
+	 * Uploads a new image.
+	 *
+	 * @return void
+	 * @throws \TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException
+	 * @throws \InvalidArgumentException
+	 */
+	public function uploadCoverletterAction() {
+		$success = FALSE;
+		$filePath = '';
+		if (\count($_FILES) > 0) {
+			$firstFile = current($_FILES);
+			$pathInfo = pathinfo($firstFile['name']);
+
+			$fileAndFolderService = $this->objectManager->get(FileAndFolderService::class);
+			$storage = $fileAndFolderService->getStorage();
+			$fileName = $storage->sanitizeFileName(
+				strtolower(
+					str_replace(' ', '_', trim($pathInfo['filename'] . '.' . strtolower($pathInfo['extension'])))
+				)
+			);
+
+			$filePath = Environment::getPublicPath() . '/' . self::TYPO3_TMP_FOLDER . $fileName;
+			$tempFilePath = $firstFile['tmp_name'];
+			$success = GeneralUtility::upload_copy_move($tempFilePath, $filePath);
+		}
+
+		$this->returnData(
+			[
+				'success' => $success,
+				'path' => $filePath,
+			]
+		);
+	}
+}
diff --git a/Classes/Controller/JoblistController.php b/Classes/Controller/JoblistController.php
index c245284e..581b1dfc 100644
--- a/Classes/Controller/JoblistController.php
+++ b/Classes/Controller/JoblistController.php
@@ -392,6 +392,9 @@ class JoblistController extends ActionController {
 	 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
 	 */
 	protected function initializeApplyAction() {
+		$arguments = ($this->request->getArguments());
+		$parsedBody = $this->request->getParsedBody();
+		$uniqueFolderName = $arguments['folderName'] ?? '';
 		try {
 			$uniqueFolderName = $this->request->getArgument('folderName');
 		} catch (NoSuchArgumentException $exception) {
@@ -547,6 +550,20 @@ class JoblistController extends ActionController {
 		}
 	}
 
+	/**
+	 * Debug Action for us to check for all Data from form
+	 *
+	 *
+	 */
+	public function emptyAction(): ?\Psr\Http\Message\ResponseInterface {
+		$request = $this->request;
+		$arguments = $this->request->getArguments();
+		$parsedBody = $this->request->getParsedBody();
+		$uploadedFiles = $this->request->getUploadedFiles();
+		$basicBody = $this->request->getBody()->getContents();
+		return $this->htmlResponse('');
+	}
+
 	/**
 	 * Assign filter values
 	 *
diff --git a/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php b/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php
index af8bf23f..6b132333 100644
--- a/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php
+++ b/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php
@@ -374,7 +374,7 @@ class UploadedFileReferenceConverter implements TypeConverterInterface {
 		$filePathInfo = PathUtility::pathinfo($uploadInfo['name']);
 		$fileName = $this->getTargetUploadFileName();
 		$finalFileName = $fileName . '.' . strtolower($filePathInfo['extension']);
-		if (!$uploadInfo['wasUploaded']) {
+		if (!array_key_exists('wasUploaded', $uploadInfo) || !$uploadInfo['wasUploaded']) {
 			if (!\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Security\FileNameValidator::class)->isValid($uploadInfo['name'])) {
 				throw new TypeConverterException(
 					'Uploading files with PHP file extensions is not allowed!',
diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf
index 109d061f..3ca916f1 100644
--- a/Resources/Private/Language/locallang.xlf
+++ b/Resources/Private/Language/locallang.xlf
@@ -339,6 +339,21 @@
 			<trans-unit id="frontend.teaserHeadline">
 				<source><![CDATA[Vacancies worldwide]]></source>
 			</trans-unit>
+			<trans-unit id="frontend.DropFiles">
+				<source><![CDATA[Drop file here]]></source>
+			</trans-unit>
+			<trans-unit id="frontend.CancelUpload">
+				<source><![CDATA[Stop upload]]></source>
+			</trans-unit>
+			<trans-unit id="frontend.FileType">
+				<source><![CDATA[Filetype]]></source>
+			</trans-unit>
+			<trans-unit id="frontend.coverLetterUplodadButton">
+				<source><![CDATA[Upload coverletter]]></source>
+			</trans-unit>
+			<trans-unit id="frontend.cvUplodadButton">
+				<source><![CDATA[Upload cv]]></source>
+			</trans-unit>
 		</body>
 	</file>
 </xliff>
diff --git a/Resources/Private/Templates/Joblist/ApplyForm.html b/Resources/Private/Templates/Joblist/ApplyForm.html
index 349d6392..776ac0a5 100644
--- a/Resources/Private/Templates/Joblist/ApplyForm.html
+++ b/Resources/Private/Templates/Joblist/ApplyForm.html
@@ -1,8 +1,10 @@
 {namespace h=SGalinski\SgJobs\ViewHelpers}
 {namespace base=SGalinski\ProjectBase\ViewHelpers}
+{namespace sgajax=SGalinski\SgAjax\ViewHelpers}
 
 <f:layout name="Default"/>
 <f:section name="main">
+	<f:asset.script identifier="dropzone-js" crossorigin="anonymous" src="EXT:sg_jobs/node_modules/dropzone/dist/dropzone-min.js" priority="1" />
 	<f:if condition="{job}">
 		<f:render partial="ApplyFormSchema" arguments="{_all}"/>
 	</f:if>
@@ -479,9 +481,18 @@
 									<f:translate key="frontend.apply.allowed_file_extensions"/>
 									{allowedFileExtensions})
 								</label>
-
-								<h:form.upload property="coverLetter" resourceName="coverLetter" id="apply-cover-letter"
-											   class="form-control" additionalAttributes="{accept: '{allowedMimeTypes}'}"/>
+								<button type="button" class="coverLetter-upload-button">
+									<span class="sr-only"><f:translate key="frontend.coverLetterUplodadButton"/></span>
+								</button>
+								<div class="coverLetter-upload hidden" data-max-file-amount="1"
+									 data-valid-file-extensions="{settings.fileUpload.fileTypes}" data-max-file-size="{maxFileSize}"
+									 data-pid="{storagePid}"
+									 data-inner-text="{f:translate(key: 'frontend.DropFiles', extensionName: 'sg_jobs')}"
+									 data-cancel-upload="{f:translate(key: 'frontend.CancelUpload', extensionName: 'sg_jobs')}"
+									 data-remove-file="{f:translate(key: 'frontend.RemoveFile', extensionName: 'sg_jobs')}"
+									 data-file-type-error="{f:translate(key: 'frontend.FileType', extensionName: 'sg_jobs')}"
+									 data-upload-ajax="{sgajax:uri.ajax(extensionName: 'SgJobs', controller: 'Ajax\\Upload', action: 'uploadCoverletter', format: 'json', parameters: '{pageId: storagePid}')}"
+								></div>
 								<f:if condition="{coverLetter.name}">
 									<p class="help-block">
 										Aktuell: {coverLetter.name}
@@ -525,8 +536,6 @@
 									{allowedFileExtensions})
 								</label>
 
-								<h:form.upload property="cv" resourceName="cv" id="apply-cv" class="form-control"
-											   additionalAttributes="{accept: '{allowedMimeTypes}'}"/>
 								<f:if condition="{cv.name}">
 									<p class="help-block">
 										Aktuell: {cv.name}
diff --git a/Resources/Public/JavaScript/Form/upload.js b/Resources/Public/JavaScript/Form/upload.js
new file mode 100644
index 00000000..3c0cffa6
--- /dev/null
+++ b/Resources/Public/JavaScript/Form/upload.js
@@ -0,0 +1,80 @@
+const { Dropzone } = window;
+
+/**
+ * This module handles the integration of Dropzone
+ */
+export default () => {
+	if (!Dropzone) {
+		return;
+	}
+
+	document.querySelectorAll('#apply').forEach((item) => {
+		console.log('init sg_jobs');
+		addFileUpload(item, 'coverLetter');
+	});
+
+	function addFileUpload(item, uploadName) {
+		const uploadBox = item.querySelector('.' + uploadName + '-upload');
+		const uploadButton = item.querySelector('.' + uploadName + '-upload-button');
+
+		console.log(item);
+		console.log(uploadBox);
+		console.log(uploadButton);
+
+		if (!uploadBox || !uploadButton || !item) {
+			return;
+		}
+		console.log('all elements found');
+		const {
+			maxFileAmount,
+			validFileExtensions,
+			maxFileSize,
+			innerText,
+			cancelUpload,
+			removeFile,
+			fileTypeError,
+			uploadAjax
+		} = uploadBox.dataset;
+
+		console.log(uploadBox.dataset);
+		console.log(uploadAjax);
+		uploadBox.classList.add('dropzone');
+
+		uploadButton.addEventListener('click', () => {
+			uploadBox.classList.toggle('hidden');
+		});
+
+		const dropzone = new Dropzone('#apply .' + uploadName + '-upload', {
+			url: uploadAjax,
+			addRemoveLinks: true,
+			paramName: 'files',
+			dictDefaultMessage: innerText,
+			maxFilesize: maxFileSize,
+			maxFiles: maxFileAmount,
+			acceptedFiles: validFileExtensions,
+			dictInvalidFileType: fileTypeError,
+			dictCancelUpload: cancelUpload,
+			dictRemoveFile: removeFile,
+		});
+		dropzone.on('success', (file) => handleUpload(file, item, uploadName));
+		dropzone.on('removedfile', (file) => handleRemoval(file, item, uploadName));
+	}
+
+	function handleUpload(file, item, uploadName) {
+		const { uuid } = file.upload;
+		const response = JSON.parse(file.xhr.response);
+
+		item.insertAdjacentHTML(
+			'beforeend',
+			`<input data-uuid='${uuid}' type='hidden' name='${uploadName}[${uuid}][uuid]' value='${uuid}'>
+			<input data-uuid='${uuid}' type='hidden' name='${uploadName}[${uuid}][path]' value='${response.path}'>`,
+		);
+	}
+
+	function handleRemoval(file, item, uploadName) {
+		const { uuid } = file.upload;
+		item.querySelectorAll(`[data-uuid='${uuid}']`).forEach((item) => {
+			item.remove();
+		});
+	}
+};
diff --git a/Resources/Public/JavaScript/sgJobs.js b/Resources/Public/JavaScript/sgJobs.js
new file mode 100644
index 00000000..fa43ee8c
--- /dev/null
+++ b/Resources/Public/JavaScript/sgJobs.js
@@ -0,0 +1,15 @@
+import Upload from './Form/upload';
+
+/**
+ * This module handles the Isotope integration for sg_teaser
+ */
+export default class SgJobs {
+	/**
+	 * Kicks things off
+	 */
+	constructor(_root = {}) {
+
+		console.log('sg_jobs js constructed');
+		Upload();
+	}
+}
diff --git a/ext_localconf.php b/ext_localconf.php
index b9eda9ce..5d2d56ee 100644
--- a/ext_localconf.php
+++ b/ext_localconf.php
@@ -48,11 +48,11 @@ call_user_func(
 			'JobApplication',
 			[
 				// Available actions
-				\SGalinski\SgJobs\Controller\JoblistController::class => 'applyForm, apply'
+				\SGalinski\SgJobs\Controller\JoblistController::class => 'applyForm, apply, empty'
 			],
 			[
 				// Uncacheable actions
-				\SGalinski\SgJobs\Controller\JoblistController::class => 'applyForm, apply'
+				\SGalinski\SgJobs\Controller\JoblistController::class => 'applyForm, apply, empty'
 			]
 		);
 		\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
@@ -67,6 +67,14 @@ call_user_func(
 				\SGalinski\SgJobs\Controller\JobTeaserController::class => ''
 			]
 		);
+
+		\SGalinski\SgAjax\Service\AjaxRegistration::configureAjaxFrontendPlugin(
+			'sg_jobs',
+			[
+				\SGalinski\SgJobs\Controller\Ajax\UploadController::class => 'uploadCoverletter',
+			]
+		);
+
 		// Backend preview for plugins
 		$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['sg_jobs']
 			= \SGalinski\SgJobs\Hooks\PageLayoutView\PluginRenderer::class;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..709eddf2
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,26 @@
+{
+  "name": "sg_comments",
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "@swc/helpers": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
+      "integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA=="
+    },
+    "dropzone": {
+      "version": "6.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
+      "integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
+      "requires": {
+        "@swc/helpers": "^0.2.13",
+        "just-extend": "^5.0.0"
+      }
+    },
+    "just-extend": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
+      "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ=="
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..87133c03
--- /dev/null
+++ b/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "sg_comments",
+  "description": "",
+  "dependencies": {
+    "dropzone": "^6.0.0-beta.2"
+  }
+}
-- 
GitLab