diff --git a/extension/Documentation/Manual.rst b/extension/Documentation/Manual.rst index ff45774165042db3324e8828ce572e8e6e0bbcda..1e3778f01ccf9703d1c8cecba6a852bc43bac7c7 100644 --- a/extension/Documentation/Manual.rst +++ b/extension/Documentation/Manual.rst @@ -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 ''''''''''''''''''''''''''''''''''''''''' diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index 68304e777fba7a60a361b2f38a0fd89d73c889d6..a4a24ece9126be3c0887c6099013b1a570ab34e7 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -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) { diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 8b39c5a880ea7316a57b50bdf9664d8e5aa833bb..3dbc550ac3aabb5a1af4d15371faef2f04f14d74 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -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 diff --git a/extension/qfq/qfq/File.php b/extension/qfq/qfq/File.php index 1fb0044ad62d458d2130a1e74c0b3f9b50165273..9aded19bbda8d4faffab51055eb81c24237c7975 100644 --- a/extension/qfq/qfq/File.php +++ b/extension/qfq/qfq/File.php @@ -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]; diff --git a/extension/qfq/qfq/Save.php b/extension/qfq/qfq/Save.php index f87231c124cc26dde525ce81914ce455b5331430..db89fa213d6e6d0bf1c9498a945f63a454207599 100644 --- a/extension/qfq/qfq/Save.php +++ b/extension/qfq/qfq/Save.php @@ -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 diff --git a/extension/qfq/qfq/helper/HelperFile.php b/extension/qfq/qfq/helper/HelperFile.php index 9e08a28f0eb57301cafe5b57b844ecf535bc6b45..875ad1e061751f57ba76b514036f3f6f74c20bb2 100644 --- a/extension/qfq/qfq/helper/HelperFile.php +++ b/extension/qfq/qfq/helper/HelperFile.php @@ -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