Commit e3da2abe authored by Carsten  Rose's avatar Carsten Rose
Browse files

Rewrote upload to be concurrent save.

Store.php: fills arrays direct in $_SESSION - not sure if this works.
AbstractBuildForm.php: buildFile() extended to create hidden sipUplaod element.
BuildFormBootstrap.php: support different actions on calling file.php
File.php, Save.php: rewrote whole logic of uploading files. See CODING.md
parent 76fa218e
......@@ -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
----------------------------------------
......
......@@ -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;
}
/**
......
......@@ -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');
......
......@@ -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
......@@ -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
......@@ -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
*
......
......@@ -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;
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment