Commit 9c24d2d3 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Upload: first version which really uploads file, saving to destination.

Sanitize.php: new sanitize class ALLBUT. New function safeFilename().
Support.php: New function mkDirParent().
Store.php: new system config variable SYSTEM_SITE_PATH.
QuickFormQuery.php: fills STORE_RECORD before saving the current record.
Save.php: moving the file to the final destination.
parent e4fade6f
......@@ -19,11 +19,9 @@ Features not implemented yet
----------------------------
* Multi Forms
* File upload
* FormElement:
* type=action (especially not *addNupdate*)
* field dependencies (activating a parent element, activates child elements and vice versa)
* Checkbox: some combinations not tested.
QFQ content element
......@@ -818,10 +816,10 @@ Typically not used. Useful if user wishes an explicit 'Submit' Button.
Type: time
^^^^^^^^^^
* Range time: '00:00:00' to '23:59:59' or '00:00:00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html)
* Optional:
* *showSeconds*: 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed '1' or not '0'.
* *showZero*: 0|1 - For an empty timestamp, With '0' nothing is displayed. With '1' the string '00:00[:00]' is displayed.
* Range time: '00:00:00' to '23:59:59' or '00:00:00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html)
* Optional:
* *showSeconds*: 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed '1' or not '0'.
* *showZero*: 0|1 - For an empty timestamp, With '0' nothing is displayed. With '1' the string '00:00[:00]' is displayed.
Type: upload
^^^^^^^^^^^^
......@@ -829,6 +827,25 @@ Type: upload
* See: https://www.w3.org/TR/html5/forms.html#file-upload-state-(type=file)
* parameter:accept: *image/*,video/*,audio/*,.doc,.docx,.pdf,<mime type>*
An upload element is based on a file browse button and a delete button. Only one of them is shown at a time.
If there is no file already uploaded, the file browse button is displayed.
The user can than select a file from the local filesystem.
After choosing the file, the upload starts immediately, shown by a turning wheel. When the server received the whole file
and accepts the file, the browse button dissappears and the filename is shown, followed by a delete button.
Now, the user can delete the uploaded file (and maybe upload another one) or leave it as it is.
Until this point, the file is cached on the server but not copied to the final destination. The user have to save the
current record either to finalize the upload or to delete a previous uploaded file.
* FormElement. *parameter*:
* *pathFileName*: Destination where to copy the file. A good practice is to specify a relative pathFileName - such an
installation (filesystem and database) are moveable. If the final pathFileName should contain the original filename,
the variable *{{_filename}}* can be used. Example::
pathFileName={{SELECT 'fileadmin/user/pictures/', p.name, '-{{_filename}}' FROM Person AS p WHERE p.id={{r}} }}
.. _class-action:
Class: Action
......
......@@ -146,6 +146,9 @@ const ERROR_IO_OPEN = 1204;
const ERROR_IO_UNLINK = 1205;
const ERROR_IO_FILE_EXIST = 1206;
const ERROR_IO_RENAME = 1207;
const ERROR_IO_INVALID_LINK = 1208;
const ERROR_IO_DIR_EXIST_AS_FILE = 1209;
const ERROR_IO_CHDIR = 1210;
//Report
const ERROR_UNKNOWN_LINK_QUALIFIER = 1300;
......@@ -157,6 +160,7 @@ const ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION = 1304;
// Upload
const ERROR_UPLOAD = 1400;
const ERROR_UNKNOWN_ACTION = 1402;
const ERROR_NO_TARGET_PATH_FILE_NAME = 1403;
// KeyValueParser
const ERROR_KVP_VALUE_HAS_NO_KEY = 1900;
......@@ -212,6 +216,7 @@ const CLIENT_UPLOAD_FE_NAME = 'name';
const CLIENT_SIP_FOR_FORM = '_sipForForm';
const CLIENT_FE_NAME = '_feName';
const CLIENT_UPLOAD_FILENAME = '_filename';
// ALL $_SERVER variables: http://php.net/manual/en/reserved.variables.server.php
// The following exist and might be the most used ones.
......@@ -260,6 +265,7 @@ const SYSTEM_CSS_CLASS_QFQ_CONTAINER = 'CSS_CLASS_QFQ_CONTAINER';
// computed automatically during runtime
const SYSTEM_PATH_EXT = 'EXT_PATH';
const SYSTEM_SITE_PATH = 'SITE_PATH';
// Information for: Log / Debug / Exception
const SYSTEM_SQL_RAW = 'sqlRaw'; // Type: SANITIZE_ALL / String. SQL Query (before substitute). Useful for error reporting.
......@@ -364,6 +370,7 @@ const FE_TYPE = 'type';
const FE_DATE_FORMAT = 'dateFormat'; // value: FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN
const FE_SHOW_SECONDS = 'showSeconds'; // value: 0|1
const FE_SHOW_ZERO = 'showZero'; // value: 0|1
const FE_PATH_FILE_NAME = 'pathFileName'; // Target pathFilename for an uploaded file.
// SUPPORT
const PARAM_T3_ALL = 't3 all';
......
......@@ -46,7 +46,7 @@ class File {
if ($sipUpload === false) {
throw new UserFormException('SIP invalid: ' . $sipUpload, ERROR_SIP_INVALID);
}
$statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
$statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA, SANITIZE_ALLOW_ALL);
if ($statusUpload === false) {
$statusUpload = array();
}
......
......@@ -225,12 +225,14 @@ class QuickFormQuery {
break;
case FORM_SAVE:
// If an old record exist: load it. Necessary to delete uploaded files which should be overwritten.
$this->fillStoreRecord($this->formSpec['tableName'], $this->store->getVar(SIP_RECORD_ID, STORE_SIP));
$save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative);
$rc = $save->process();
// Reload fresh saved record and fill STORE_RECORD with it
$record = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, [$rc]);
$this->store->setVarArray($record, STORE_RECORD, true);
$this->fillStoreRecord($this->formSpec['tableName'], $rc);
$htmlElementNameIdZero = false;
// Retrieve current STORE_SIP.
......@@ -435,6 +437,21 @@ class QuickFormQuery {
return $sipFound;
}
/**
* @param string $table
* @param string $recordId
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
private function fillStoreRecord($table, $recordId) {
if ($recordId !== false && $recordId > 0) {
$record = $this->db->sql("SELECT * FROM $table WHERE id = ?", ROW_EXPECT_1, [$recordId]);
$this->store->setVarArray($record, STORE_RECORD, true);
}
}
/**
* @param $sipArray
* @param $recordId
......
......@@ -62,7 +62,8 @@ class Save {
$rc = $this->elements($row['_id']);
}
} else {
$rc = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO));
$recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
$rc = $this->elements($recordId);
}
return $rc;
......@@ -82,7 +83,7 @@ class Save {
$tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
$formValues = $this->store->getStore(STORE_FORM);
$this->processAllUploads($recordId, $formValues);
$this->processAllUploads($formValues);
// Iterate over all table.columns. Built an assoc array $newValues.
foreach ($tableColumns AS $column) {
......@@ -114,12 +115,11 @@ class Save {
}
/**
* Process all Upload Formelements for the given $recordId
* Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the final filenames.
*
* @param $recordId
* @param array $formValues
*/
private function processAllUploads($recordId, array &$formValues) {
private function processAllUploads(array &$formValues) {
foreach ($this->feSpecNative AS $formElement) {
// skip non upload formElements
......@@ -127,8 +127,11 @@ class Save {
continue;
}
// Preparation for Log, Debug
$this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);
$column = $formElement['name'];
$file = $this->doUpload($formElement, $recordId, $formValues[$column]);
$file = $this->doUpload($formElement, $formValues[$column]);
if ($file !== false) {
$formValues[$column] = $file;
}
......@@ -136,10 +139,9 @@ class Save {
}
/**
* 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.
* Process upload for the given Formelement. If necessary, delete a previous uploaded file.
* Calculate the final path/filename and move the file to the new location.
*
* @param $formElement
* @param $sipUpload
* @return string|false New filename or false on error
......@@ -149,39 +151,77 @@ class Save {
*/
private function doUpload($formElement, $sipUpload) {
// Status information about upload file
$statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
if ($statusUpload === false) {
return false;
}
// Take care the necessary target directories exist.
$cwd = getcwd();
$sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
if ($cwd === false || $sitePath === false || !chdir($sitePath)) {
throw new UserFormException("getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed.", ERROR_IO_CHDIR);
}
// 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);
throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK);
}
}
}
// TODO: calculate destination
$targetFile = $statusUpload[FILES_TMP_NAME] . '.final';
$pathFileName = $this->copyUploadFile($formElement, $statusUpload);
chdir($cwd);
// Delete current used uniq SIP
$this->store->setVar($sipUpload, array(), STORE_EXTRA);
return $pathFileName;
}
/**
* @param array $formElement
* @param array $statusUpload
* @return array|mixed|null|string
* @throws CodeException
* @throws UserFormException
*/
private function copyUploadFile(array $formElement, array $statusUpload) {
$pathFileName = '';
if (isset($formElement[FE_PATH_FILE_NAME])) {
if (file_exists($targetFile)) {
throw new UserFormException('Copy upload failed - file already exist: ' . $targetFile, ERROR_IO_FILE_EXIST);
// Provide variable '_filename'. Might be substituted in $formElement[FE_PATH_FILE_NAME].
$origFilename = Sanitize::safeFilename($statusUpload[FILES_NAME]);
$this->store->setVar(CLIENT_UPLOAD_FILENAME, $origFilename, STORE_FORM);
$pathFileName = $this->evaluate->parse($formElement[FE_PATH_FILE_NAME]);
}
$uploadFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
if (!rename($uploadFile, $targetFile)) {
throw new UserFormException("Rename file: '$uploadFile' > '$targetFile'", ERROR_IO_RENAME);
if ($pathFileName === '') {
throw new UserFormException("Upload failed, no target '" . FE_PATH_FILE_NAME . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
}
$this->store->setVar($sipUpload, array(), STORE_EXTRA);
if (file_exists($pathFileName)) {
throw new UserFormException('Copy upload failed - file already exist: ' . $pathFileName, ERROR_IO_FILE_EXIST);
}
return $targetFile;
Support::mkDirParent($pathFileName);
}
$srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
if (!rename($srcFile, $pathFileName)) {
throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME);
}
return $pathFileName;
}
/**
* Get the complete FormElement for $name
*
......
......@@ -101,6 +101,7 @@ class Sanitize {
case SANITIZE_ALLOW_DIGIT:
case SANITIZE_ALLOW_EMAIL:
case SANITIZE_ALLOW_ALNUMX:
case SANITIZE_ALLOW_ALLBUT:
$arr = self::inputCheckPatternArray();
$pattern = $arr[$sanatizeClass];
break;
......@@ -133,8 +134,7 @@ class Sanitize {
/**
* @return array
*/
public
static function inputCheckPatternArray() {
public static function inputCheckPatternArray() {
//EMail Regex: http://www.regular-expressions.info/email.html
return [
SANITIZE_ALLOW_ALNUMX => '^[@\-_\.,;: \/\(\)a-zA-Z0-9]*$', // ':alnum:' does not work here in FF
......@@ -148,4 +148,34 @@ class Sanitize {
];
}
/**
* Sanatizes a filename. Copied from http://www.phpit.net/code/filename-safe/
*
* @param $filename
* @return mixed
*/
public static function safeFilename($filename) {
$search = array(
// Definition of German Umlauts START
'/ß/',
'/ä/', '/Ä/',
'/ö/', '/Ö/',
'/ü/', '/Ü/',
// Definition of German Umlauts ENDE
'([^[:alnum:]._])' // Disallow: Not alphanumeric, dot or underscore
);
$replace = array(
'ss',
'ae', 'Ae',
'oe', 'Oe',
'ue', 'Ue',
'_'
);
return preg_replace($search, $replace, $filename);
} // safeFilename()
}
\ No newline at end of file
......@@ -535,4 +535,41 @@ class Support {
return $filename . $extend;
}
/**
* Creates all necessary directories in $pathFileName, but not the last part, the filename. A filename has to be specified.
*
* @param string $pathFileName Path with Filename
* @throws UserFormException
*/
public static function mkDirParent($pathFileName) {
$path = "";
// Teile "Directory/File.Extension" auf
$pathParts = pathinfo($pathFileName);
// Zerlege Pfad in einzelne Directories
$arr = explode("/", $pathParts["dirname"]);
// Durchlaufe die einzelnen Dirs und überprüfe ob sie angelegt sind.
// Wenn nicht, lege sie an.
foreach ($arr as $part) {
$path .= $part;
// Check ob der Pfad ein Link ist
if ("link" == @filetype($path)) {
if ("0" == ($path1 = readlink($path))) {
throw new UserFormException("Can't create '$pathFileName': '$path' contains an invalid link.", ERROR_IO_INVALID_LINK);
}
} else {
if (file_exists($path)) {
if ("dir" != filetype($path)) {
throw new UserFormException("Can't create '$pathFileName': There is already a file with the same name as '$path'", ERROR_IO_DIR_EXIST_AS_FILE);
}
} else
mkdir($path, 0700);
}
$path .= "/";
}
}
}
\ No newline at end of file
......@@ -110,6 +110,7 @@ class Store {
CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL,
CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX,
CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX,
CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT,
// SYSTEM_DBUSER => SANITIZE_ALLOW_ALNUMX,
// SYSTEM_DBSERVER => SANITIZE_ALLOW_ALNUMX,
......@@ -142,7 +143,7 @@ class Store {
STORE_TYPO3 => false,
STORE_ZERO => false,
STORE_SYSTEM => false,
STORE_EXTRA => true
STORE_EXTRA => false
];
self::fillSystemStore();
......@@ -183,10 +184,13 @@ class Store {
$pos = strpos($_SERVER['SCRIPT_FILENAME'], $relExtDir);
if ($pos === false && isset($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php'])) {
// probably: index.php - THERE should be a TYPO3 environment.
// Typo3 extension: probably index.php
$config[SYSTEM_PATH_EXT] = dirname($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php']);
$config[SYSTEM_SITE_PATH] = dirname($_SERVER['SCRIPT_FILENAME']);
} else {
// API
$config[SYSTEM_PATH_EXT] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos + strlen($relExtDir));
$config[SYSTEM_SITE_PATH] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos);
}
}
}
......
Supports Markdown
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