diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php index 3e03467c418cc6af3bd5f5178e8c56b0cce78cec..2f39901ea860341a78ac596416996e96d1f8efc0 100644 --- a/extension/Classes/Core/Constants.php +++ b/extension/Classes/Core/Constants.php @@ -242,7 +242,7 @@ const ERROR_STORE_KEY_EXIST = 1201; // I/O Error const ERROR_IO_COPY = 1300; - +const ERROR_IO_ZIP_OPEN = 1301; const ERROR_IO_RMDIR = 1302; const ERROR_IO_WRITE = 1303; const ERROR_IO_OPEN = 1304; @@ -1129,6 +1129,8 @@ const FE_FILE_REPLACE_MODE = 'fileReplace'; // Flag if a) QFQ throw an error if const FE_FILE_REPLACE_MODE_ALWAYS = 'always'; // Value for flag FE_FILE_REPLACE_MODE const FE_FILE_MIME_TYPE_ACCEPT = 'accept'; // Comma separated list of mime types const FE_FILE_MAX_FILE_SIZE = SYSTEM_FILE_MAX_FILE_SIZE; // Max upload file size +const FE_FILE_UNZIP = 'fileUnzip'; // 0|1|dir|{{SELECT ...}} +const FE_FILE_UNPACK_DIR = 'unpack'; // default dir if not specified const FE_FILE_CAPTURE = 'capture'; // On a smartphone opens the camera const FE_FILE_SPLIT = 'fileSplit'; diff --git a/extension/Classes/Core/Form/FormAction.php b/extension/Classes/Core/Form/FormAction.php index c452db09b1dd668f508d9893a9117e2650bfbd01..aed3b24c53451c714668b113f9f7ef3116139232 100644 --- a/extension/Classes/Core/Form/FormAction.php +++ b/extension/Classes/Core/Form/FormAction.php @@ -200,7 +200,7 @@ class FormAction { $this->store->setStore($arr, STORE_LDAP, true); } - $this->sqlValidate($fe); + HelperFormElement::sqlValidate($this->evaluate, $fe); if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) { $this->doSendMail($fe); @@ -291,57 +291,6 @@ class FormAction { $sendMail->process($mailConfig); } - /** - * If there is a query defined in fe.parameter.FE_SQL_VALIDATE: fire them. - * Count the selected records and compare them with fe.parameter.FE_EXPECT_RECORDS. - * If match: everything is fine, do nothing. - * Else throw \UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL - * - * @param array $fe - * - * @throws \CodeException - * @throws \DbException - * @throws \UserFormException - * @throws \UserReportException - */ - private function sqlValidate(array $fe) { - - // Is there something to check? - if ($fe[FE_SQL_VALIDATE] === '') { - return; - } - - if ($fe[FE_EXPECT_RECORDS] === '') { - throw new \UserFormException("Missing parameter '" . FE_EXPECT_RECORDS . "'", ERROR_MISSING_EXPECT_RECORDS); - } - $expect = $this->evaluate->parse($fe[FE_EXPECT_RECORDS]); - - if ($fe[FE_MESSAGE_FAIL] === '') { - throw new \UserFormException("Missing parameter '" . FE_MESSAGE_FAIL . "'", ERROR_MISSING_MESSAGE_FAIL); - } - - // Do the check - $result = $this->evaluate->parse($fe[FE_SQL_VALIDATE], ROW_REGULAR); - if (!is_array($result)) { - throw new \UserFormException("Expected an array for '" . FE_SQL_VALIDATE . "', got a scalar. Please check for {{!...", ERROR_EXPECTED_ARRAY); - } - - // If there is at least one record count given, who matches: return 'check succeeded' - $countRecordsArr = explode(',', $expect); - foreach ($countRecordsArr as $count) { - if (count($result) == $count) { - return; // check successfully passed - } - } - - $msg = $this->evaluate->parse($fe[FE_MESSAGE_FAIL]); // Replace possible dynamic parts - - // Throw user error message - throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => $msg - , ERROR_MESSAGE_TO_DEVELOPER => 'validate() failed']), ERROR_REPORT_FAILED_ACTION); - - } - /** * Process slaveId, sqlBefore, sqlInsert|sqlUpdate|sqlDelete, sqlAfter. * flagFeAction=false: for Native Elements diff --git a/extension/Classes/Core/Helper/HelperFile.php b/extension/Classes/Core/Helper/HelperFile.php index 5619f9c0c6cf2510d4c215ad5adcbd673690e00d..a9b1935276d6c9d7bcfc8286bd722dc84045514a 100644 --- a/extension/Classes/Core/Helper/HelperFile.php +++ b/extension/Classes/Core/Helper/HelperFile.php @@ -101,6 +101,8 @@ class HelperFile { /** * Returns an array with filestat information to $pathFileName + * - mimeType + * - fileSize * * @param $pathFileName * @return array @@ -540,5 +542,43 @@ class HelperFile { return $pre . $separator . $post; } + + /** + * Translates ZIP error codes to text. + * + * @param $errno + * @return string + */ + public static function zipFileErrMsg($errno) { + + // using constant name as a string to make this function PHP4 compatible + $zipFileFunctionsErrors = array( + 'ZIPARCHIVE::ER_MULTIDISK' => 'Multi-disk zip archives not supported.', + 'ZIPARCHIVE::ER_RENAME' => 'Renaming temporary file failed.', + 'ZIPARCHIVE::ER_CLOSE' => 'Closing zip archive failed', + 'ZIPARCHIVE::ER_SEEK' => 'Seek error', + 'ZIPARCHIVE::ER_READ' => 'Read error', + 'ZIPARCHIVE::ER_WRITE' => 'Write error', + 'ZIPARCHIVE::ER_CRC' => 'CRC error', + 'ZIPARCHIVE::ER_ZIPCLOSED' => 'Containing zip archive was closed', + 'ZIPARCHIVE::ER_NOENT' => 'No such file.', + 'ZIPARCHIVE::ER_EXISTS' => 'File already exists', + 'ZIPARCHIVE::ER_OPEN' => 'Can\'t open file', + 'ZIPARCHIVE::ER_TMPOPEN' => 'Failure to create temporary file.', + 'ZIPARCHIVE::ER_ZLIB' => 'Zlib error', + 'ZIPARCHIVE::ER_MEMORY' => 'Memory allocation failure', + 'ZIPARCHIVE::ER_CHANGED' => 'Entry has been changed', + 'ZIPARCHIVE::ER_COMPNOTSUPP' => 'Compression method not supported.', + 'ZIPARCHIVE::ER_EOF' => 'Premature EOF', + 'ZIPARCHIVE::ER_INVAL' => 'Invalid argument', + 'ZIPARCHIVE::ER_NOZIP' => 'Not a zip archive', + 'ZIPARCHIVE::ER_INTERNAL' => 'Internal error', + 'ZIPARCHIVE::ER_INCONS' => 'Zip archive inconsistent', + 'ZIPARCHIVE::ER_REMOVE' => 'Can\'t remove file', + 'ZIPARCHIVE::ER_DELETED' => 'Entry has been deleted', + ); + + return $zipFileFunctionsErrors[$errno] ?? 'unknown'; + } } diff --git a/extension/Classes/Core/Helper/HelperFormElement.php b/extension/Classes/Core/Helper/HelperFormElement.php index 33c8dcd1575de89af0d55dc7d3ee38d24737dd3d..cb467d8f0d1d79743c60548d08e685116f480387 100644 --- a/extension/Classes/Core/Helper/HelperFormElement.php +++ b/extension/Classes/Core/Helper/HelperFormElement.php @@ -9,6 +9,7 @@ namespace IMATHUZH\Qfq\Core\Helper; use IMATHUZH\Qfq\Core\Store\Store; +use IMATHUZH\Qfq\Core\Evaluate; /** @@ -861,5 +862,51 @@ EOF; return '<div class="help-block with-errors hidden"></div>'; } + /** + * If there is a query defined in fe.parameter.FE_SQL_VALIDATE: fire them. + * Count the selected records and compare them with fe.parameter.FE_EXPECT_RECORDS. + * If match: everything is fine, do nothing. + * Else throw \UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL + * + * @param array $fe + * @param Evaluate $evaluate + * @throws \UserFormException + */ + public static function sqlValidate(Evaluate $evaluate, array $fe) { + // Is there something to check? + if ($fe[FE_SQL_VALIDATE] === '') { + return; + } + + if ($fe[FE_EXPECT_RECORDS] === '') { + throw new \UserFormException("Missing parameter '" . FE_EXPECT_RECORDS . "'", ERROR_MISSING_EXPECT_RECORDS); + } + $expect = $evaluate->parse($fe[FE_EXPECT_RECORDS]); + + if ($fe[FE_MESSAGE_FAIL] === '') { + throw new \UserFormException("Missing parameter '" . FE_MESSAGE_FAIL . "'", ERROR_MISSING_MESSAGE_FAIL); + } + + // Do the check + $result = $evaluate->parse($fe[FE_SQL_VALIDATE], ROW_REGULAR); + if (!is_array($result)) { + throw new \UserFormException("Expected an array for '" . FE_SQL_VALIDATE . "', got a scalar. Please check for {{!...", ERROR_EXPECTED_ARRAY); + } + + // If there is at least one record count given, who matches: return 'check succeeded' + $countRecordsArr = explode(',', $expect); + foreach ($countRecordsArr as $count) { + if (count($result) == $count) { + return; // check successfully passed + } + } + + $msg = $evaluate->parse($fe[FE_MESSAGE_FAIL]); // Replace possible dynamic parts + + // Throw user error message + throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => $msg + , ERROR_MESSAGE_TO_DEVELOPER => 'validate() failed']), ERROR_REPORT_FAILED_ACTION); + + } } \ No newline at end of file diff --git a/extension/Classes/Core/Save.php b/extension/Classes/Core/Save.php index 886444cae4b6274f6ee793ced7195c907f2799be..5f98928f6a6da49346788964a5b7c48c3d3dd5e7 100644 --- a/extension/Classes/Core/Save.php +++ b/extension/Classes/Core/Save.php @@ -20,6 +20,7 @@ use IMATHUZH\Qfq\Core\Helper\Support; use IMATHUZH\Qfq\Core\Store\FillStoreForm; use IMATHUZH\Qfq\Core\Store\Sip; use IMATHUZH\Qfq\Core\Store\Store; +use ZipArchive; /** * Class Save @@ -254,7 +255,7 @@ class Save { $formValues = $this->createEmptyTemplateGroupElements($formValues); // Iterate over all table.columns. Built an assoc array $newValues. - foreach ($tableColumns AS $column) { + foreach ($tableColumns as $column) { // Never save a predefined 'id': autoincrement values will be given by database.. if ($column === COLUMN_ID) { @@ -408,7 +409,7 @@ class Save { */ private function isColumnUploadField($feName) { - foreach ($this->feSpecNative AS $formElement) { + foreach ($this->feSpecNative as $formElement) { if ($formElement[FE_NAME] === $feName && $formElement[FE_TYPE] == FE_TYPE_UPLOAD) return true; } @@ -501,12 +502,22 @@ class Save { $sip = new Sip(false); $newValues = array(); - $vars = array(); + + $flagDoUnzip = false; $formValues = $this->store->getStore(STORE_FORM); $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table. - foreach ($this->feSpecNative AS $formElement) { + // Upload - Take care the necessary target directories exist. + $cwd = getcwd(); + $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM); + if ($cwd === false || $sitePath === false || !HelperFile::chdir($sitePath)) { + throw new \UserFormException( + json_encode([ERROR_MESSAGE_TO_USER => 'getcwd() failed or SITE_PATH undefined or chdir() failed', ERROR_MESSAGE_TO_DEVELOPER => "getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed."]), + ERROR_IO_CHDIR); + } + + foreach ($this->feSpecNative as $formElement) { // skip non upload formElements if ($formElement[FE_TYPE] != FE_TYPE_UPLOAD) { continue; @@ -523,7 +534,32 @@ class Save { } $column = $formElement[FE_NAME]; + + $statusUpload = $this->store->getVar($formValues[$column] ?? '', STORE_EXTRA); + // Get file stats + $vars = array(); + $vars[VAR_FILE_SIZE] = $statusUpload[FILES_SIZE]; + $vars[VAR_FILE_MIME_TYPE] = $statusUpload[FILES_TYPE]; + + // Check for 'unzip'. + if (isset($formElement[FE_FILE_UNZIP]) && $formElement[FE_FILE_UNZIP] != '0' && $vars[VAR_FILE_MIME_TYPE] = 'application/zip') { + $flagDoUnzip = true; + } + + // Do upload $pathFileName = $this->doUpload($formElement, ($formValues[$column] ?? ''), $sip, $modeUpload); + if ($flagDoUnzip) { + if ($formElement[FE_FILE_UNZIP] == '' || $formElement[FE_FILE_UNZIP] == '1') { + // Set default dir. + $formElement[FE_FILE_UNZIP] = HelperFile::joinPathFilename(dirname($pathFileName), FE_FILE_UNPACK_DIR); + } + + // Backup STORE_VAR - will be changed in doUnzip() + $tmpStoreVar = $this->store->getStore(STORE_VAR); + $this->doUnzip($formElement, $pathFileName); + // Restore STORE_VAR + $this->store->setStore($tmpStoreVar, STORE_VAR, true); + } if ($modeUpload == UPLOAD_MODE_DELETEOLD && $pathFileName == '') { $pathFileNameTmp = ''; // see '4' @@ -540,15 +576,15 @@ class Save { // No new upload and no existing: take care to remove previous upload file statistics. $this->store->unsetVar(VAR_FILE_MIME_TYPE, STORE_VAR); $this->store->unsetVar(VAR_FILE_SIZE, STORE_VAR); - $vars[VAR_FILE_SIZE] = 0; - $vars[VAR_FILE_MIME_TYPE] = ''; } else { - $vars = HelperFile::getFileStat($pathFileNameTmp); + $this->store->appendToStore($vars, STORE_VAR); } // If given: fire a sqlBefore query - $this->evaluate->parse($formElement[FE_SQL_BEFORE]); + if (!$flagDoUnzip) { + $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! @@ -567,22 +603,97 @@ class Save { } } elseif (isset($formElement[FE_IMPORT_TO_TABLE]) && !isset($formElement[FE_SLAVE_ID])) { // Excel import on nonexisting column -> no upload + } elseif ($flagDoUnzip) { + // If ZIP and advanced upload: process it not here but via doUnzip. } else { // 'Advanced Upload' $this->doUploadSlave($formElement, $modeUpload); } // If given: fire a sqlAfter query - $this->evaluate->parse($formElement[FE_SQL_AFTER]); - + if (!$flagDoUnzip) { + $this->evaluate->parse($formElement[FE_SQL_AFTER]); + } } + // Clean up + HelperFile::chdir($cwd); + // Only used in 'Simple Upload' if (count($newValues) > 0) { $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId, $this->formSpec[F_PRIMARY_KEY]); } } + /** + * Unzip $pathFileName to $formElement[FE_FILE_UNZIP]. Before final extract, fire FE_SQL_VALIDATE. + * For each file in ZIP: + * - Fill STORE_VAR with VAR_FILENAME, VAR_FILENAME_ONLY, VAR_FILENAME_BASE, VAR_FILENAME_EXT, VAR_FILE_MIME_TYPE, VAR_FILE_SIZE. + * - Fire $formElement[FE_SQL_VALIDATE] + * - Fire FE_SLAVE_ID, FE_SQL_BEFORE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE, FE_SQL_AFTER + * + * @param array $formElement + * @param string $pathFileName + * @throws \CodeException + * @throws \DbException + * @throws \UserFormException + * @throws \UserReportException + */ + private function doUnzip(array $formElement, $pathFileName) { + + if (!is_readable($pathFileName)) { + throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "Open ZIP file failed", + ERROR_MESSAGE_TO_DEVELOPER => "File: " . $pathFileName]), + ERROR_IO_ZIP_OPEN); + } + + $zip = new ZipArchive(); + $res = $zip->open($pathFileName); + if ($res !== true) { + throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "Open ZIP file failed" . HelperFile::zipFileErrMsg($res), + ERROR_MESSAGE_TO_DEVELOPER => "File: " . $pathFileName]), ERROR_IO_ZIP_OPEN); + } + + // Do sqlValidate() before final extraction. + if (!empty($formElement[FE_SQL_VALIDATE])) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + + $itemPathFileName = HelperFile::joinPathFilename($formElement[FE_FILE_UNZIP], $stat['name']); + $this->store->appendToStore(HelperFile::getFileStat($itemPathFileName), STORE_VAR); + $this->store->appendToStore(HelperFile::pathinfo($itemPathFileName), STORE_VAR); + + HelperFormElement::sqlValidate($this->evaluate, $formElement); + } + } + + // Extract + if (false === $zip->extractTo($formElement[FE_FILE_UNZIP])) { + throw new \UserFormException("Failed to extract ZIP.", ERROR_IO_ZIP_OPEN); + } + // Close Zip + if (false === $zip->close()) { + throw new \UserFormException("Failed to close ZIP.", ERROR_IO_ZIP_OPEN); + } + + // Process + if (!empty($formElement[FE_SLAVE_ID] . $formElement[FE_SQL_BEFORE] . $formElement[FE_SQL_INSERT] . + $formElement[FE_SQL_UPDATE] . $formElement[FE_SQL_DELETE] . $formElement[FE_SQL_AFTER])) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + + $itemPathFileName = HelperFile::joinPathFilename($formElement[FE_FILE_UNZIP], $stat['name']); + $this->store->appendToStore(HelperFile::getFileStat($itemPathFileName), STORE_VAR); + $this->store->appendToStore(HelperFile::pathinfo($itemPathFileName), STORE_VAR); + + $this->evaluate->parse($formElement[FE_SQL_BEFORE]); + $this->doUploadSlave($formElement, UPLOAD_MODE_NEW); + $this->evaluate->parse($formElement[FE_SQL_AFTER]); + print_r(basename($stat['name']) . PHP_EOL); + } + } + } + /** * Process all Upload FormElements for the given $recordId. * After processing, &$formValues will be updated with the final filename. @@ -592,7 +703,7 @@ class Save { */ public function processAllImageCutFE() { - foreach ($this->feSpecNative AS $formElement) { + foreach ($this->feSpecNative as $formElement) { // skip non upload formElements if ($formElement[FE_TYPE] != FE_TYPE_IMAGE_CUT) { continue; @@ -631,7 +742,7 @@ class Save { $flagAllRequiredGiven = 1; - foreach ($this->feSpecNative AS $key => $formElement) { + foreach ($this->feSpecNative as $key => $formElement) { // Do not check retype slave FE. if (isset($formElement[FE_RETYPE_SOURCE_NAME])) { @@ -748,26 +859,26 @@ class Save { * 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. * - * Check also: doc/CODING.md + * Check also: Documentation-develop/CODING.md * * @param array $formElement FormElement 'upload' * @param string $sipUpload SIP * @param Sip $sip * @param string $modeUpload UPLOAD_MODE_UNCHANGED | UPLOAD_MODE_NEW | UPLOAD_MODE_DELETEOLD | * UPLOAD_MODE_DELETEOLD_NEW - * * @return false|string New pathFilename or false on error * @throws \CodeException * @throws \DbException - * @throws \UserFormException - * @throws \UserReportException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception + * @throws \UserFormException + * @throws \UserReportException * @internal param $recordId */ private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) { $flagDelete = false; $modeUpload = UPLOAD_MODE_UNCHANGED; + $pathFileName = ''; // Status information about upload file $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA); @@ -781,15 +892,6 @@ class Save { $this->doImport($formElement, $tmpFile); } - // Upload - Take care the necessary target directories exist. - $cwd = getcwd(); - $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM); - if ($cwd === false || $sitePath === false || !HelperFile::chdir($sitePath)) { - throw new \UserFormException( - json_encode([ERROR_MESSAGE_TO_USER => 'getcwd() failed or SITE_PATH undefined or chdir() failed', ERROR_MESSAGE_TO_DEVELOPER => "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') { $arr = $sip->getVarsFromSip($sipUpload); @@ -819,8 +921,6 @@ class Save { Logger::logMessageWithPrefix($msg, $this->qfqLogFilename); } - HelperFile::chdir($cwd); - // Delete current used uniq SIP $this->store->setVar($sipUpload, array(), STORE_EXTRA); @@ -828,6 +928,8 @@ class Save { } /** + * Excel Import + * * @param $formElement * @param $fileName * @throws \CodeException @@ -977,7 +1079,7 @@ class Save { } // Import the data - foreach ($worksheetData AS $rowIndex => $row) { + foreach ($worksheetData as $rowIndex => $row) { $columnList = '`' . implode('`,`', $columnListArr) . '`'; $paramPlaceholders = str_repeat('?,', count($worksheetData[0]) - 1) . '?'; $insertSql = "INSERT INTO `$tableName` ($columnList) VALUES ($paramPlaceholders)"; @@ -989,7 +1091,7 @@ class Save { /** * Copy uploaded file from temporary location to final location. * - * Check also: doc/CODING.md + * Check also: Documentation-develop/CODING.md * * @param array $formElement * @param array $statusUpload @@ -1087,9 +1189,9 @@ class Save { } /** - * Check's if the file $pathFileName should be splitted in one file per page. If no: do nothing and return. + * Check's if the file $pathFileName should be split'ed in one file per PDF page. If no: do nothing and return. * The only possible split target file format is 'svg': fileSplit=svg. - * The splitted files will be saved under fileDestinationSplit=some/path/to/file.%02d.svg. A printf style token, + * The split'ed files will be saved under fileDestinationSplit=some/path/to/file.%02d.svg. A printf style token, * like '%02d', is needed to create distinguished filename's. See 'man pdf2svg' for further details. * For every created file, a record in table 'Split' is created (see splitSvg() ), storing the pathFileName of the * current page/file. @@ -1221,7 +1323,7 @@ class Save { * Create/update or delete the slave record. * * @param array $fe - * @param $modeUpload + * @param string $modeUpload UPLOAD_MODE_NEW|UPLOAD_MODE_DELETEOLD_NEW|UPLOAD_MODE_DELETEOLD * @return int * @throws \CodeException * @throws \DbException