diff --git a/CODING.md b/CODING.md index c14b9849e914c2a206d1c8d3d983155e799ae79b..9c969543347893005f867a23215083b3ba5b020c 100644 --- a/CODING.md +++ b/CODING.md @@ -158,8 +158,8 @@ New Button * Client stays on current page. -File Handling -------------- +File Handling: Upload +--------------------- * No previous uploaded file present 1. User presses the Browse button 1. User selects file @@ -173,6 +173,39 @@ File Handling 1. File delete button gets disabled and hidden 1. Browse button gets enabled and displayed +Form Build (load) +................. +* The upload functionality consist of three elements + * 1) A <div> tag with a) an optional filename of an earlier uploaded file plus and b) a trash Button. + * 2) The 'browse' button (<input type='file' name='_upload_<feName>'>). This element will not be send by post. + * 3) A HTML hidden element with name=<feName> containing the <sipUpload>. +* A new uniq SIP (sipUpload) will be created for every upload formElement. These 'sipUpload' will be assigned to the browse button and to the delete button. + * The individual sipUpload is necessary to correctly handle multiple simustaenously forms when using r=0. Also, through this uniq id it's easy to distinguish between asynchron uploaded files. + * The SIP contains the '_FILES' information submitted during the upload. +* Via the hidden element <feName> 'save()' access the form individual upload status informations. + +Upload to server, before 'save' +............................... +* If a user open's a file for upload via the browse button, that file is immediately transmitted to the server. The user will see a turning wheel during the upload time. +* On success the 'Browse; Button disappears and the filename plus the delete button will be displayed (client logic). +* The uploaded file will be checked: maxsize, mime type, check script. +* The uploaded file is still temporary. It has been renamed from $_SESSION['X'][<uploadSip>][FILES_TMP_NAME] to $_SESSION['X'][<uploadSip>][FILES_TMP_NAME].cached +* The upload action will be saved in the user session. + * $_SESSION['X'][<uploadSip>][FILES_TMP_NAME|FILES_NAME|FILES_ERROR|FILE_SIZE] +* Clicks the user on delete button. + * In the usersession a flagDelete will be set: $_SESSION['X'][<uploadSip>]['flagDelete']='1' + * An optional previous upload file (still not saved on the final place) will be deleted. + * An optional existing variable $_SESSION['X'][<uploadSip>][FILES_TMP_NAME] will be deleted. The 'flagDelete' must not be change - it's later needed to detect to delete earlier uploaded files. + +Form save +......... +* Before building the insert/update, process all 'uploads'. +* Get every uniq sipUpload to every upload formElement. Get the corresponding temporary uploaded filename. +* If $_SESSION['X'][<uploadSip>]['flagDelete']='1' is set, delete prefious uploaded file. +* Calculate <destination> +* mv <file>.cached <destination> +* clientvalue[<feName>] = <destination> +* delete $_SESSION['X'][<uploadSip>] Formelement type: DATE / DATETIME / TIME ---------------------------------------- diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index 1fdadd1e9f213f5b117c57e2870dd5f1b8ca07f9..757173ae2109ac2cbdb4537879c76c9cfca97ed7 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -42,8 +42,16 @@ abstract class AbstractBuildForm { // protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]"> + /** + * @var string + */ private $formId = null; + /** + * @var Sip + */ + private $sip = null; + /** * AbstractBuildForm constructor. * @@ -60,7 +68,7 @@ abstract class AbstractBuildForm { $this->evaluate = new Evaluate($this->store, $this->db); $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes'); -// $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT); + $this->sip = $this->store->getSipInstance(); // render mode specific $this->fillWrap(); @@ -1505,8 +1513,16 @@ abstract class AbstractBuildForm { public function buildFile(array $formElement, $htmlFormElementId, $value, &$json) { $attribute = ''; - $sip = $this->store->getVar(SIP_SIP, STORE_SIP); - $value = basename($value); // Strip directories + $arr = array(); + $arr['fake_uniq_never_use_this'] = uniqid(); // make sure we get a new SIP. This is needed for multiple forms (same user) with r=0 + $arr[CLIENT_SIP_FOR_FORM] = $this->store->getVar(SIP_SIP, STORE_SIP); + $arr[CLIENT_FE_NAME] = $formElement['name']; + $arr[CLIENT_FORM] = $this->formSpec['name']; + $arr[CLIENT_RECORD_ID] = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); + $arr[CLIENT_PAGE_ID] = 'fake'; + $sipUpload = $this->sip->queryStringToSip(OnArray::toString($arr), RETURN_SIP); + + $hiddenSipUpload = $this->buildNativeHidden($htmlFormElementId, $sipUpload); $attribute .= Support::doAttribute('name', $htmlFormElementId); // $attribute .= Support::doAttribute('class', 'form-control'); @@ -1514,6 +1530,7 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute('title', $formElement['tooltip']); $attribute .= $this->getAttributeList($formElement, ['autofocus', 'accept']); $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); + $attribute .= Support::doAttribute('data-sip', $sipUpload); if ($value === '') { $textDeleteClass = 'hidden'; @@ -1528,7 +1545,7 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute('class', $uploadClass, true); $htmlInputFile = '<input ' . $attribute . '>' . $this->getHelpBlock(); - $deleteButton = Support::wrapTag("<button class='delete-file' data-sip='$sip' name='delete-$htmlFormElementId'>", $this->symbol[SYMBOL_DELETE]); + $deleteButton = Support::wrapTag("<button class='delete-file' data-sip='$sipUpload' name='delete-$htmlFormElementId'>", $this->symbol[SYMBOL_DELETE]); $htmlFilename = Support::wrapTag("<span class='uploaded-file-name'>", $value, false); $htmlTextDelete = Support::wrapTag("<div class='uploaded-file $textDeleteClass'>", $htmlFilename . ' ' . $deleteButton); @@ -1537,7 +1554,7 @@ abstract class AbstractBuildForm { $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); - return $htmlTextDelete . $htmlInputFile; + return $htmlTextDelete . $htmlInputFile . $hiddenSipUpload; } /** diff --git a/extension/qfq/qfq/BuildFormBootstrap.php b/extension/qfq/qfq/BuildFormBootstrap.php index db8465282e3d7866ed3099b98e6b79e966f0ccbf..3ab2562bd254f880b87968df82782dd7e295c1ee 100644 --- a/extension/qfq/qfq/BuildFormBootstrap.php +++ b/extension/qfq/qfq/BuildFormBootstrap.php @@ -311,6 +311,9 @@ class BuildFormBootstrap extends AbstractBuildForm { $deleteUrl = $this->createDeleteUrl($this->formSpec['tableName'], $recordId); } + $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD; + $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE; + $html .= '</form>'; // <form class="form-horizontal" ... $html .= <<<EOF <script type="text/javascript"> @@ -324,7 +327,8 @@ class BuildFormBootstrap extends AbstractBuildForm { submitTo: 'typo3conf/ext/qfq/qfq/api/save.php', deleteUrl: '$deleteUrl', refreshUrl: "typo3conf/ext/qfq/qfq/api/load.php", - fileUploadTo: 'typo3conf/ext/qfq/qfq/api/file.php' + fileUploadTo: 'typo3conf/ext/qfq/qfq/api/file.php?$actionUpload', + fileDeleteUrl: 'typo3conf/ext/qfq/qfq/api/file.php?$actionDelete' }); var qfqRecordList = new QfqNS.QfqRecordList('typo3conf/ext/qfq/qfq/api/delete.php'); diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index a483238b60bfa19da88c4c4bb9d14c475ac24f68..dc8d4b1f9120c44eb189a32b1b6c14ca348f4296 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -143,6 +143,9 @@ const ERROR_STORE_KEY_EXIST = 1101; const ERROR_IO_READ_FILE = 1200; const ERROR_IO_WRITE = 1203; const ERROR_IO_OPEN = 1204; +const ERROR_IO_UNLINK = 1205; +const ERROR_IO_FILE_EXIST = 1206; +const ERROR_IO_RENAME = 1207; //Report const ERROR_UNKNOWN_LINK_QUALIFIER = 1300; @@ -153,7 +156,7 @@ const ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION = 1304; // Upload const ERROR_UPLOAD = 1400; -const ERROR_DELETE_TMP_UPLOAD = 1401; +const ERROR_UNKNOWN_ACTION = 1402; // KeyValueParser const ERROR_KVP_VALUE_HAS_NO_KEY = 1900; @@ -207,6 +210,9 @@ const CLIENT_PAGE_TYPE = 'type'; const CLIENT_PAGE_LANGUAGE = 'L'; const CLIENT_UPLOAD_FE_NAME = 'name'; +const CLIENT_SIP_FOR_FORM = '_sipForForm'; +const CLIENT_FE_NAME = '_feName'; + // ALL $_SERVER variables: http://php.net/manual/en/reserved.variables.server.php // The following exist and might be the most used ones. const CLIENT_SCRIPT_URL = 'SCRIPT_URL'; @@ -386,5 +392,9 @@ const FILES_NAME = 'name'; const FILES_TMP_NAME = 'tmp_name'; const FILES_ERROR = 'error'; const FILES_SIZE = 'size'; +const FILES_FLAG_DELETE = 'flagDelete'; -const UPLOAD_CACHED = '.cached'; \ No newline at end of file +const UPLOAD_CACHED = '.cached'; +const FILE_ACTION = 'action'; +const FILE_ACTION_UPLOAD = 'upload'; +const FILE_ACTION_DELETE = 'delete'; \ No newline at end of file diff --git a/extension/qfq/qfq/File.php b/extension/qfq/qfq/File.php index 1c56bafcc1b7995246ece9bf5cdf79a6a432ddff..8bc59e7b9e068bf40b11b6e740bc52ce0ebf3527 100644 --- a/extension/qfq/qfq/File.php +++ b/extension/qfq/qfq/File.php @@ -16,13 +16,16 @@ class File { private $uploadErrMsg = array(); /** - * @var null|Store + * @var Store */ private $store = null; public function __construct($phpUnit = false) { $this->store = Store::getInstance('', $phpUnit); +// $sessionName = $this->store->getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM); +// $this->sip = new Sip($sessionName); + $this->uploadErrMsg = [ UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini", UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form", @@ -34,38 +37,74 @@ class File { ]; } + /** + * @throws UserFormException + */ public function process() { - $sipClient = $this->store->getVar(CLIENT_SIP, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); - $sipStore = $this->store->getVar(SIP_SIP, STORE_SIP); - if ($sipClient === false || $sipStore === false || $sipClient !== $sipStore) { - throw new UserFormException('SIP invalid', ERROR_SIP_INVALID); + $sipUpload = $this->store->getVar(SIP_SIP, STORE_SIP); + if ($sipUpload === false) { + throw new UserFormException('SIP invalid: ' . $sipUpload, ERROR_SIP_INVALID); + } + $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA); + if ($statusUpload === false) { + $statusUpload = array(); } - $uploadFeName = $this->store->getVar(CLIENT_UPLOAD_FE_NAME, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); + $action = $this->store->getVar(FILE_ACTION, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); + switch ($action) { + case FILE_ACTION_UPLOAD: + $this->doUpload($sipUpload, $statusUpload); + break; + case FILE_ACTION_DELETE: + $this->doDelete($sipUpload, $statusUpload); + break; + default: + throw new UserFormException("Unknown FILE_ACTION: $action", ERROR_UNKNOWN_ACTION); + } + } - $keyStoreExtra = $sipStore . '-' . $uploadFeName; + /** + * @param string $keyStoreExtra + * @throws CodeException + * @throws UserFormException + */ + private function doUpload($sipUpload, $statusUpload) { list($dummy, $newArr) = each($_FILES); - $oldArr = $this->store->getVar($keyStoreExtra, STORE_EXTRA, SANITIZE_ALLOW_ALL); + $statusUpload = array_merge($statusUpload, $newArr); - // Exist old cached upload? remove. - $filenameCached = Support::extendFilename($oldArr[FILES_TMP_NAME], UPLOAD_CACHED); - - if (isset($oldArr[FILES_TMP_NAME]) && $oldArr[FILES_TMP_NAME] != '' && file_exists($filenameCached)) { - if (!unlink($oldArr[FILES_TMP_NAME] . '.cached')) { - throw new UserFormException("Failed to delete cached uploaded file: " . $filenameCached, ERROR_DELETE_TMP_UPLOAD); - } - } - - if ($newArr[FILES_ERROR] !== UPLOAD_ERR_OK) { + if ($statusUpload[FILES_ERROR] !== UPLOAD_ERR_OK) { throw new UserFormException($this->uploadErrMsg[$newArr[FILES_ERROR]], ERROR_UPLOAD); } - $filenameCached = Support::extendFilename($newArr[FILES_TMP_NAME], UPLOAD_CACHED); + //TODO: do necessary checks with uploaded file HERE!!! + + + // rename uploaded file: ?.cached + $filenameCached = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); move_uploaded_file($newArr[FILES_TMP_NAME], $filenameCached); - $this->store->setVar($keyStoreExtra, $newArr, STORE_EXTRA); + $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA); } + /** + * @param string $keyStoreExtra + * @throws CodeException + * @throws UserFormException + */ + private function doDelete($sipUpload, $statusUpload) { + + if (isset($statusUpload[FILES_TMP_NAME]) && $statusUpload[FILES_TMP_NAME] != '') { + $file = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); + if (file_exists($file)) { + if (!unlink($file)) { + throw new UserFormException('unlink file: ' . $file, ERROR_IO_UNLINK); + } + } + $statusUpload[FILES_TMP_NAME] = ''; + } + $statusUpload[FILES_FLAG_DELETE] = '1'; + $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA); + } } \ No newline at end of file diff --git a/extension/qfq/qfq/Save.php b/extension/qfq/qfq/Save.php index 033917a74ec0b0bccc38c01f421822c7219ac727..ae8f9e66ade39b1ca153550a16a73301c231a66a 100644 --- a/extension/qfq/qfq/Save.php +++ b/extension/qfq/qfq/Save.php @@ -82,8 +82,11 @@ class Save { $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES)); $formValues = $this->store->getStore(STORE_FORM); + $this->processAllUploads($recordId, $formValues); + // Iterate over all table.columns. Built an assoc array $newValues. foreach ($tableColumns AS $column) { + // Never save a predefined 'id': autoincrement values will be given by database.. if ($column === 'id') continue; @@ -96,11 +99,8 @@ class Save { // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); - if (isset($formValues[$column])) { - $newValues[$column] = $formValues[$column]; - } else { - $newValues[$column] = ''; - } + Support::setIfNotSet($formValues, $column); + $newValues[$column] = $formValues[$column]; } if ($recordId == 0) { @@ -113,6 +113,75 @@ class Save { return $rc; } + /** + * Process all Upload Formelements for the given $recordId + * + * @param $recordId + * @param array $formValues + */ + private function processAllUploads($recordId, array &$formValues) { + + foreach ($this->feSpecNative AS $formElement) { + // skip non upload formElements + if ($formElement['type'] != 'upload') { + continue; + } + + $column = $formElement['name']; + $file = $this->doUpload($formElement, $recordId, $formValues[$column]); + if ($file !== false) { + $formValues[$column] = $file; + } + } + } + + /** + * Process upload for the given Formelement. + * - Check if there is an upload: + * yes: do the upload + * no: if there is an old file: delete it. + * @param $formElement + * @param $sipUpload + * @return string|false New filename or false on error + * @throws CodeException + * @throws UserFormException + * @internal param $recordId + */ + private function doUpload($formElement, $sipUpload) { + + $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA); + if ($statusUpload === false) { + return false; + } + + // Delete existing old file. + if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') { + $oldFile = $this->store->getVar($formElement['name'], STORE_RECORD); + if (file_exists($oldFile)) { + if (!unlink($oldFile)) { + throw new UserFormException('unlink file: ' . $oldFile, ERROR_IO_UNLINK); + } + } + } + + // TODO: calculate destination + $targetFile = $statusUpload[FILES_TMP_NAME] . '.final'; + + if (file_exists($targetFile)) { + throw new UserFormException('Copy upload failed - file already exist: ' . $targetFile, ERROR_IO_FILE_EXIST); + } + + $uploadFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); + if (!rename($uploadFile, $targetFile)) { + throw new UserFormException("Rename file: '$uploadFile' > '$targetFile'", ERROR_IO_RENAME); + } + + $this->store->setVar($sipUpload, array(), STORE_EXTRA); + + return $targetFile; + + } + /** * Get the complete FormElement for $name * diff --git a/extension/qfq/qfq/store/Store.php b/extension/qfq/qfq/store/Store.php index 143042b9db636de68268c224fd58a49f3ae716a0..94969d604ab6903c069c777bd632a8851fc35579 100644 --- a/extension/qfq/qfq/store/Store.php +++ b/extension/qfq/qfq/store/Store.php @@ -366,6 +366,7 @@ class Store { * @throws \qfq\CodeException */ private static function fillStoreExtra() { + if (isset($_SESSION[STORE_EXTRA])) self::setVarArray($_SESSION[STORE_EXTRA], STORE_EXTRA, true); else @@ -454,9 +455,8 @@ class Store { // The STORE_EXTRA saves arrays and is persistent if ($store === STORE_EXTRA) { - foreach ($value as $k1 => $v1) { - $_SESSION[STORE_EXTRA][$key][$k1] = $v1; - } + + $_SESSION[STORE_EXTRA][$key] = $value; } }