Commit 7f6ce338 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Feature #5571 / File Upload: save filesize and mimetype

- STORE_VARS contains now 'mimeType' and 'fileSize'.
- sqlBefore and sqlAfter will be fired in Upload Advanced and new in Upload Simple as well.
- STORE_VARS contains now `filenameOnly`. It can be used in downloadButton=....
parent 7c664cd8
......@@ -1543,14 +1543,29 @@ Store: *VARS* - V
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| slaveId | see *FormElement* `action` |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
.. _`store_vars_form_element_upload`:
* FormElement 'upload':
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| Name | Explanation |
+=========================+============================================================================================================================================+
| filename | Original filename of an uploaded file via an 'upload'-FormElement. Valid only during processing of the current 'upload'-formElement. |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| filenameOnly | Like filename, but without path. |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| filenameBase | Like `filename`, but without an optional extension. E.g. filename='image.png' comes to filenameBase='image' |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| filenameExt | Like `filename`, but only the optional extension. E.g. filename='image.png' comes to filenameExt='png' |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| fileDestination | Destination (path & filename) for an uploaded file. Defined in an 'upload'-FormElement.parameter. Valid: same as 'filename'. |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| fileSize | Size of the uploaded file. |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| mimeType | Mimetype of the uploaded file. |
+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
The directive `fillStoreVar` will fill the store VARS with custom values. Existing Store VARS values will be merged together with them.
E.g.: ::
......@@ -3351,7 +3366,7 @@ current record, either to finalize the upload and/or to delete a previous upload
The FormElement behaves like a
* 'native FormElement' (showing controls/text on the form) as well as an
* 'action FormElement' by fireing queries and doing some additional actions during form save.
* 'action FormElement' by firing queries and doing some additional actions during form save.
Inside the *Form editor* it's shown as a 'native FormElement'.
During saving the current record, it behaves like an action FormElement
......@@ -3392,6 +3407,8 @@ See also `downloadButton`_ to offer a download of an uploaded file.
fileDestination={{SELECT 'fileadmin/user/pictures/', p.name, '-{{filename}}' FROM Person AS p WHERE p.id={{id:R0}} }}
* Several more variants of the filename and also mimetype and filesize are available. See `store_vars_form_element_upload`_.
* The original filename will be sanitized: only '<alnum>', '.' and '_' characters are allowed. German 'umlaut' will
be replaced by 'ae', 'ue', 'oe'. All non valid characters will be replaced by '_'.
......@@ -3408,7 +3425,12 @@ See also `downloadButton`_ to offer a download of an uploaded file.
by an attacker. Therefore it's recommended to use a `fileDestination`-directory, which is secured against script
execution (even if the file has been uploaded, the webserver won't execute it) - see `SecureDirectFileAccess`_.
* *slaveId*, *sqlBefore*, *sqlInsert*, *sqlUpdate*, *sqlDelete*, *sqlUpdate*, *sqlAfter*: Only used in :ref:`Upload advanced mode`.
* *sqlBefore*, *sqlAfter*: available in :ref:`Upload simple mode` and :ref:`Upload advanced mode`.
* *slaveId*, *sqlInsert*, *sqlUpdate*, *sqlDelete*, *sqlUpdate*: available only in :ref:`Upload advanced mode`.
* To save the `fileSize` and / or `mimeType` (upload simple mode) in the same record as the upload: ::
sqlAfter = {{UPDATE Data SET mimeType='{{mimeType:V}}', fileSize={{fileSize:V}} WHERE id={{id:R}} }}
* *fileReplace=always*: If `fileDestination` exist - replace it by the new one.
......@@ -3417,9 +3439,9 @@ See also `downloadButton`_ to offer a download of an uploaded file.
* *downloadButton*: If given, shows a button to download the previous uploaded file - instead of the string given in
`fe.value`. It's important that `fe.value` points to a readable file on the server.
If `downloadButton` ist empty, just shows the regular download glyph.
Additional attributes might be given like `downloadButton = t:Download|o:check file`. Please check `download`_.
* If `downloadButton` ist empty, just shows the regular download glyph.
* To just show the filename: `downloadButton = t:{{filenameOnly:V}}`
* Additional attributes might be given like `downloadButton = t:Download|o:check file`. Please check `download`_.
* fileSplit, fileDestinationSplit, tableNameSplit: see split-pdf-upload_
......@@ -3430,7 +3452,6 @@ The maximum size is defined by the minimum of `upload_max_filesize`, `post_max_s
In case of broken uploads, please also check `max_input_time` in php.ini.
Deleting a record and the referenced file
'''''''''''''''''''''''''''''''''''''''''
......
......@@ -27,6 +27,7 @@ require_once(__DIR__ . '/helper/OnArray.php');
require_once(__DIR__ . '/helper/Ldap.php');
require_once(__DIR__ . '/report/Link.php');
require_once(__DIR__ . '/helper/Sanitize.php');
require_once(__DIR__ . '/helper/HelperFile.php');
require_once(__DIR__ . '/report/Report.php');
/**
......@@ -519,7 +520,7 @@ abstract class AbstractBuildForm {
$html = '';
// The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
$skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER, FE_FILL_STORE_VAR];
$skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER, FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON];
// get current data record
if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
......@@ -2711,6 +2712,8 @@ abstract class AbstractBuildForm {
public function buildFile(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
$attribute = '';
$this->store->appendToStore(HelperFile::pathinfo($value), STORE_VAR);
if (empty($formElement[FE_FILE_MIME_TYPE_ACCEPT])) {
$formElement[FE_FILE_MIME_TYPE_ACCEPT] = UPLOAD_DEFAULT_MIME_TYPE;
}
......@@ -2791,7 +2794,7 @@ abstract class AbstractBuildForm {
if (Support::isEnabled($formElement, FE_FILE_DOWNLOAD_BUTTON)) {
if (is_readable($value)) {
$link = new Link($this->sip, $this->dbIndexData);
$value = $link->renderLink('s|M:file|d|F:' . $value . '|' . $formElement[FE_FILE_DOWNLOAD_BUTTON]);
$value = $link->renderLink('s|M:file|d|F:' . $value . '|' . $this->evaluate->parse($formElement[FE_FILE_DOWNLOAD_BUTTON]));
} else {
// In case debugging is off: showing download button means 'never show the real pathfilename'
if (!$this->showDebugInfoFlag) {
......
......@@ -585,8 +585,12 @@ const VAR_RANDOM = 'random';
const VAR_FILE_DESTINATION = 'fileDestination';
const VAR_SLAVE_ID = ACTION_KEYWORD_SLAVE_ID;
const VAR_FILENAME = 'filename'; // Original filename of an uploaded file.
const VAR_FILENAME_ONLY = 'filenameOnly'; // Original filename of an uploaded file, without directories.
const VAR_FILENAME_BASE = 'filenameBase'; // Original filename of an uploaded file, without the extension.
const VAR_FILENAME_EXT = 'filenameExt'; // Extension of the original filename of an uploaded file, .
const VAR_FILE_MIME_TYPE = 'mimeType';
const VAR_FILE_SIZE = 'fileSize';
// PHP class Typeahead
const TYPEAHEAD_API_QUERY = 'query'; // Name of parameter in API call of typeahead.php?query=...&s=... - See also FE_TYPE_AHEAD_SQL
......
......@@ -10,6 +10,7 @@ namespace qfq;
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/helper/HelperFile.php');
class File {
......@@ -124,10 +125,8 @@ class File {
$return_var = 0;
// E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
$fileMimeType = exec('file --brief --mime ' . $tmp_name, $output, $return_var);
if ($return_var != 0) {
throw new UserFormException('Error get mime type of upload.', ERROR_UPLOAD_GET_MIME_TYPE);
}
$fileMimeType = HelperFile::getMimeType($tmp_name);
// Strip optional '; charset=binary'
$arr = explode(';', $fileMimeType, 2);
$fileMimeType = $arr[0];
......
......@@ -8,10 +8,11 @@
namespace qfq;
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/store/Sip.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/Evaluate.php');
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/store/Sip.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/Evaluate.php');
require_once(__DIR__ . '/helper/HelperFile.php');
//require_once(__DIR__ . '/../qfq/exceptions/UserException.php');
//require_once(__DIR__ . '/../qfq/exceptions/CodeException.php');
//require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
......@@ -284,9 +285,6 @@ class Save {
throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
}
// $paramList = str_repeat('?, ', count($values));
// $paramList = substr($paramList, 0, strlen($paramList) - 2);
$sql = 'UPDATE `' . $tableName . '` SET ';
foreach ($values as $column => $value) {
......@@ -332,6 +330,14 @@ class Save {
$column = $formElement[FE_NAME];
$pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload);
$pathFileNameTmp = empty($pathFileName) ? $primaryRecord[$column] : $pathFileName;
// Get latest file information
$vars = HelperFile::getFileStat($pathFileNameTmp);
$this->store->appendToStore($vars, STORE_VAR);
// If given: fire a sqlBefore query
$this->evaluate->parse($formElement[FE_SQL_BEFORE]);
// Upload Type: Simple or Advanced
// If (isset($primaryRecord[$column])) { - see #5048 - isset does not deal correctly with NULL!
if (array_key_exists($column, $primaryRecord)) {
......@@ -343,6 +349,10 @@ class Save {
// 'Advanced Upload'
$this->doUploadSlave($formElement, $modeUpload);
}
// If given: fire a sqlAfter query
$this->evaluate->parse($formElement[FE_SQL_AFTER]);
}
// Only used in 'Simple Upload'
......@@ -517,24 +527,20 @@ class Save {
*/
private function copyUploadFile(array $formElement, array $statusUpload) {
$pathFileName = '';
$vars = array();
if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') {
// nothing to upload: e.g. user has deleted a previous uploaded file.
return '';
}
$srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
if (isset($formElement[FE_FILE_DESTINATION])) {
// Provide variable 'filename'. Might be substituted in $formElement[FE_PATH_FILE_NAME].
$origFilename = Sanitize::safeFilename($statusUpload[FILES_NAME]);
$pathParts = pathinfo($origFilename);
$this->store->setVar(VAR_FILENAME, $origFilename, STORE_VAR);
if (isset($pathParts['filename'])) {
$this->store->setVar(VAR_FILENAME_BASE, $pathParts['filename'], STORE_VAR);
}
if (isset($pathParts['extension'])) {
$this->store->setVar(VAR_FILENAME_EXT, $pathParts['extension'], STORE_VAR);
}
$this->store->appendToStore(HelperFile::pathinfo($origFilename), STORE_VAR);
$pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
......@@ -558,7 +564,6 @@ class Save {
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);
}
......@@ -720,9 +725,6 @@ class Save {
throw new CodeException('Unknown mode: ' . $mode, ERROR_UNKNOWN_MODE);
}
// If given: fire a sqlBefore query
$this->evaluate->parse($fe[FE_SQL_BEFORE]);
$rc = $this->evaluate->parse($sql);
// Check if the slave record has been deleted: if yes, set slaveId=0
if ($flagSlaveDeleted && $rc > 0) {
......@@ -736,9 +738,6 @@ class Save {
$slaveId = $rc;
}
// If given: fire a sqlAfter query
$this->evaluate->parse($fe[FE_SQL_AFTER]);
return $slaveId;
}
}
\ No newline at end of file
......@@ -56,4 +56,95 @@ class HelperFile {
return $name;
}
/**
* Return the mimetype of $pathFilename
*
* @param string $pathFilename
* @return string
* @throws UserFormException
*/
public static function getMimeType($pathFilename) {
// E.g.: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary'
$fileMimeType = exec('file --brief --mime ' . $pathFilename, $output, $return_var);
if ($return_var != 0) {
throw new UserFormException('Error get mime type of upload.', ERROR_UPLOAD_GET_MIME_TYPE);
}
return $fileMimeType;
}
/**
* Returns an array with filestat information to $pathFileName
*
* @param $pathFileName
* @return array
* @throws UserFormException
*/
public static function getFileStat($pathFileName) {
$vars = array();
if (empty($pathFileName)) {
return array();
}
$pathFileName = self::correctRelativPathFileName($pathFileName);
$vars[VAR_FILE_MIME_TYPE] = self::getMimeType($pathFileName);
$vars[VAR_FILE_SIZE] = filesize($pathFileName);
return $vars;
}
/**
* Correct $pathFilename, if the cwd is .../qfq/api'
*
* @param $pathFileName
* @return string
*/
public static function correctRelativPathFileName($pathFileName) {
if (empty($pathFileName)) {
return '';
}
if ($pathFileName[0] == '/') {
return $pathFileName;
}
if (substr(getcwd(), -8) == '/qfq/api') {
return '../../../../../' . $pathFileName;
}
return $pathFileName;
}
/**
* Split $pathFileName into it's components and fill an array, with array keys like used in STORE_VAR.
*
* @param string $pathFileName
* @return array
*/
public static function pathInfo($pathFileName) {
$vars = array();
$pathParts = pathinfo($pathFileName);
$vars[VAR_FILENAME] = $pathFileName;
if (isset($pathParts['basename'])) {
$vars[VAR_FILENAME_ONLY] = $pathParts['basename'];
}
if (isset($pathParts['filename'])) {
$vars[VAR_FILENAME_BASE] = $pathParts['filename'];
}
if (isset($pathParts['extension'])) {
$vars[VAR_FILENAME_EXT] = $pathParts['extension'];
}
return $vars;
}
}
\ No newline at end of file
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