From 39e742bbe59a38421dec3d52d3f5e08e15915060 Mon Sep 17 00:00:00 2001 From: Carsten Rose Date: Thu, 4 Mar 2021 18:05:47 +0100 Subject: [PATCH] Implements #12022 New Escape class HtmlSpecialChar 'h' --- extension/Classes/Core/Constants.php | 1 + extension/Classes/Core/Evaluate.php | 3 + .../Core/Exception/AbstractException.php | 24 ++-- extension/Classes/Core/Form/FormAsFile.php | 112 +++++++++--------- extension/Classes/Core/Helper/Support.php | 2 +- 5 files changed, 74 insertions(+), 68 deletions(-) diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php index d189d3fb..53885aa2 100644 --- a/extension/Classes/Core/Constants.php +++ b/extension/Classes/Core/Constants.php @@ -756,6 +756,7 @@ const TOKEN_ESCAPE_PASSWORD_T3FE = 'p'; const TOKEN_ESCAPE_NONE = '-'; const TOKEN_ESCAPE_WIPE = 'w'; const TOKEN_ESCAPE_TIMEZONE = 't'; +const TOKEN_ESCAPE_HTML_SPECIAL_CHAR = 'h'; const TOKEN_ESCAPE_STOP_REPLACE = 'S'; const TOKEN_ESCAPE_EXCEPTION = 'X'; diff --git a/extension/Classes/Core/Evaluate.php b/extension/Classes/Core/Evaluate.php index 3ca450a4..00a2bf88 100644 --- a/extension/Classes/Core/Evaluate.php +++ b/extension/Classes/Core/Evaluate.php @@ -465,6 +465,9 @@ class Evaluate { case TOKEN_ESCAPE_TIMEZONE: $value = $this->getEuropeanTimezone($value); break; + case TOKEN_ESCAPE_HTML_SPECIAL_CHAR: + $value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value); + break; default: throw new \UserFormException("Unknown escape qualifier: $escape", ERROR_UNKNOW_SANITIZE_CLASS); break; diff --git a/extension/Classes/Core/Exception/AbstractException.php b/extension/Classes/Core/Exception/AbstractException.php index 71827dd6..4d4d26ff 100644 --- a/extension/Classes/Core/Exception/AbstractException.php +++ b/extension/Classes/Core/Exception/AbstractException.php @@ -8,16 +8,16 @@ namespace IMATHUZH\Qfq\Core\Exception; +use IMATHUZH\Qfq\Core\Database\Database; +use IMATHUZH\Qfq\Core\Helper\Logger; +use IMATHUZH\Qfq\Core\Helper\OnArray; use IMATHUZH\Qfq\Core\Helper\Path; +use IMATHUZH\Qfq\Core\Helper\Support; use IMATHUZH\Qfq\Core\QuickFormQuery; -use IMATHUZH\Qfq\Core\Store\Store; +use IMATHUZH\Qfq\Core\Report\Link; use IMATHUZH\Qfq\Core\Store\Sip; +use IMATHUZH\Qfq\Core\Store\Store; use IMATHUZH\Qfq\Core\Store\T3Info; -use IMATHUZH\Qfq\Core\Report\Link; -use IMATHUZH\Qfq\Core\Database\Database; -use IMATHUZH\Qfq\Core\Helper\OnArray; -use IMATHUZH\Qfq\Core\Helper\Logger; -use IMATHUZH\Qfq\Core\Helper\Support; /** @@ -29,11 +29,11 @@ use IMATHUZH\Qfq\Core\Helper\Support; * * Throw with message for User and message for Support. * - throw new \UserFormException( json_encode( - [ERROR_MESSAGE_TO_USER => 'Failed: chmod', - ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'", - ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]), - ERROR_IO_CHMOD); + * throw new \UserFormException( json_encode( + * [ERROR_MESSAGE_TO_USER => 'Failed: chmod', + * ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'", + * ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]), + * ERROR_IO_CHMOD); * * @package qfq */ @@ -174,7 +174,7 @@ class AbstractException extends \Exception { $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] = $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] ?? ''; try { $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] = QuickFormQuery::buildInlineReport(\UserReportException::$report_uid, - \UserReportException::$report_pathFileName, \UserReportException::$report_bodytext, + \UserReportException::$report_pathFileName, \UserReportException::$report_bodytext, \UserReportException::$report_header) . $arrMerged[ERROR_MESSAGE_TO_DEVELOPER]; } catch (\Error | \Exception $e) { $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] .= "
(inline report editor not available)"; diff --git a/extension/Classes/Core/Form/FormAsFile.php b/extension/Classes/Core/Form/FormAsFile.php index cc79fee7..8711789e 100644 --- a/extension/Classes/Core/Form/FormAsFile.php +++ b/extension/Classes/Core/Form/FormAsFile.php @@ -11,10 +11,12 @@ use IMATHUZH\Qfq\Core\Helper\OnArray; use IMATHUZH\Qfq\Core\Helper\OnString; use IMATHUZH\Qfq\Core\Helper\Path; use IMATHUZH\Qfq\Core\Helper\SqlQuery; -use IMATHUZH\Qfq\Core\Store\Store; -class FormAsFile -{ +/** + * Class FormAsFile + * @package IMATHUZH\Qfq\Core\Form + */ +class FormAsFile { /** * Remove the form from the DB and insert it using the form file. (only if the form file was changed) * If the form file can't be read, then the form is deleted from the DB and an exception is thrown. @@ -33,22 +35,32 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - public static function importForm(string $formName, Database $database, bool $keepIfNeverExported = true): bool - { + public static function importForm(string $formName, Database $database, bool $keepIfNeverExported = true): bool { // Get file stats from database form $formFromDb = $database->selectFormByName($formName, [F_ID, F_FILE_STATS]); // Get file stats from file system $absoluteFormFilePath = self::formPathFileName($formName, $database); $fileReadException = function () use ($absoluteFormFilePath, $database, $formName) { + + // Search for form name: convert all to lowercase and compare. + $addHint = implode(', ', array_filter(self::formFileNames($database), + function ($f) use ($formName) { + return strtolower($f) === strtolower($formName); + })); + + if ($addHint != '') { + $addHint = 'Similar forms: ' . $addHint; + } + Thrower::userFormException( - "Form file not found or missing permission: '" . baseName($absoluteFormFilePath) . "'. Form names are case sensitive. Similar forms: " - . implode(', ', array_filter(self::formFileNames($database), function ($f) use ($formName) {return strtolower($f) === strtolower($formName);})), + "Form file not found or missing permission: '" . baseName($absoluteFormFilePath) . "'. " . $addHint, "Form definition file not found or no permission to read file: '$absoluteFormFilePath'" ); }; - if(!file_exists($absoluteFormFilePath)) { - if ($keepIfNeverExported && isset($formFromDb[F_ID]) && (!isset($formFromDb[F_FILE_STATS]) || !self::isValidFileStats($formFromDb[F_FILE_STATS]))) { + if (!file_exists($absoluteFormFilePath)) { + if ($keepIfNeverExported && isset($formFromDb[F_ID]) && (!isset($formFromDb[F_FILE_STATS]) || + !self::isValidFileStats($formFromDb[F_FILE_STATS]))) { // if file not exists and form was never exported, then export self::exportForm($formName, $database); } else { @@ -85,14 +97,17 @@ class FormAsFile foreach ($formFromFile[F_FILE_FORM_ELEMENT] as $formElementFromFile) { $keysNotSet = OnArray::keysNotSet([FE_CLASS, FE_NAME], $formElementFromFile); if (!empty($keysNotSet)) { - Thrower::userFormException('Failed to import form file.', "One or more required keys are missing in FormElement definition in file: '$absoluteFormFilePath'. Missing keys: " . implode(', ', $keysNotSet)); + Thrower::userFormException('Failed to import form file.', + "One or more required keys are missing in FormElement definition in file: '$absoluteFormFilePath'. Missing keys: " . implode(', ', $keysNotSet)); } if ($formElementFromFile[FE_CLASS] === FE_CLASS_CONTAINER) { if (in_array($formElementFromFile[FE_NAME], $containerNames)) { - Thrower::userFormException('Failed to import form file.', "Multiple formElements of class container with the same name '" . $formElementFromFile[FE_NAME] . "' in form file '$absoluteFormFilePath'"); + Thrower::userFormException('Failed to import form file.', + "Multiple formElements of class container with the same name '" . $formElementFromFile[FE_NAME] . "' in form file '$absoluteFormFilePath'"); } if ($formElementFromFile[FE_NAME] == '') { - Thrower::userFormException('Failed to import form file.', "Found formElement of class container with empty name in form file '$absoluteFormFilePath'"); + Thrower::userFormException('Failed to import form file.', + "Found formElement of class container with empty name in form file '$absoluteFormFilePath'"); } $containerNames[] = $formElementFromFile[FE_NAME]; } @@ -122,7 +137,7 @@ class FormAsFile $feId = self::insertFormElement($formElementFromFile, $formIdNew, $database); $formElementFromFile[FE_ID] = $feId; if ($formElementFromFile[FE_CLASS] === FE_CLASS_CONTAINER) { - $containerIds[$formElementFromFile[FE_NAME]] = $feId; + $containerIds[$formElementFromFile[FE_NAME]] = $feId; } } @@ -131,7 +146,8 @@ class FormAsFile if (array_key_exists(FE_FILE_CONTAINER_NAME, $formElementFromFile)) { $containerName = $formElementFromFile[FE_FILE_CONTAINER_NAME]; if (!isset($containerIds[$containerName])) { - Thrower::userFormException('Failed to import form file.', "Key '" . FE_FILE_CONTAINER_NAME . "' points to non-existing container with name '$containerName' in definition of formElement with name '" . $formElementFromFile[FE_NAME] . "' in file '$absoluteFormFilePath'"); + Thrower::userFormException('Failed to import form file.', + "Key '" . FE_FILE_CONTAINER_NAME . "' points to non-existing container with name '$containerName' in definition of formElement with name '" . $formElementFromFile[FE_NAME] . "' in file '$absoluteFormFilePath'"); } $containerId = $containerIds[$containerName]; $feId = $formElementFromFile[FE_ID]; @@ -186,7 +202,7 @@ class FormAsFile Logger::logMessage(date('Y.m.d H:i:s ') . ": Importing form file '$pathFileName'. Reason: Empty or non-unique names of container formElements where adjusted during form export.", Path::absoluteQfqLogFile()); self::importForm($formName, $database); - // otherwise => Update column fileStats + // otherwise => Update column fileStats } else { $fileStats = self::formFileStatsJson($pathFileName); list($sql, $parameterArray) = SqlQuery::updateRecord(TABLE_NAME_FORM, [F_FILE_STATS => $fileStats, F_MODIFIED => $form[F_MODIFIED]], $formId); @@ -201,10 +217,8 @@ class FormAsFile * @param string $absoluteFormFilePath * @throws \UserFormException */ - private static function backupFormFile(string $absoluteFormFilePath) - { - if (file_exists($absoluteFormFilePath)) - { + private static function backupFormFile(string $absoluteFormFilePath) { + if (file_exists($absoluteFormFilePath)) { if (!is_readable($absoluteFormFilePath)) { Thrower::userFormException('Error while trying to backup form file.', "Form file is not readable: $absoluteFormFilePath"); } @@ -235,7 +249,7 @@ class FormAsFile { $recordFormName = self::formNameFromFormRelatedRecord($recordId, $tableName, $database); if ($recordFormName !== null) { - if(self::importForm($recordFormName, $database)) { + if (self::importForm($recordFormName, $database)) { throw new \UserFormException(json_encode([ ERROR_MESSAGE_TO_USER => 'Form file was changed.', ERROR_MESSAGE_TO_DEVELOPER => "Form definition file has been changed. Please close tab and reload the form list and Form-Editor from scratch."]), @@ -260,7 +274,7 @@ class FormAsFile { self::enforceFormFileWritable($formName, $database); $pathFileName = self::formPathFileName($formName, $database); - if(file_exists($pathFileName)) { + if (file_exists($pathFileName)) { self::backupFormFile($pathFileName); $success = unlink($pathFileName); if ($success === false) { @@ -280,8 +294,7 @@ class FormAsFile * @param string $tableName * @return string */ - public static function errorHintFormImport (string $tableName = TABLE_NAME_FORM): string - { + public static function errorHintFormImport(string $tableName = TABLE_NAME_FORM): string { $message = ''; if (in_array($tableName, [TABLE_NAME_FORM, TABLE_NAME_FORM_ELEMENT])) { $message .= "Hint: Form definition file might have changed. Please reopen the form list and Form-Editor from scratch."; @@ -352,16 +365,14 @@ class FormAsFile * @param string $sql * @return bool */ - public static function isFormQuery(string $sql): bool - { + public static function isFormQuery(string $sql): bool { // find substrings which start with FROM and are followed by Form or FormElement preg_match_all('/(?i)FROM(?-i)(.*?)\b(' . TABLE_NAME_FORM . '|' . TABLE_NAME_FORM_ELEMENT . ')\b/s', $sql, $matches); // Check if no other SQL keywords are in between FROM and the table name $keywordsAfterFrom = ['WHERE', 'GROUP BY', 'HAVING', 'WINDOW', 'ORDER BY', 'LIMIT', 'FOR', 'INTO']; - foreach($matches[0] as $match) - { - if(!OnString::containsOneOfWords($keywordsAfterFrom, $match)) { + foreach ($matches[0] as $match) { + if (!OnString::containsOneOfWords($keywordsAfterFrom, $match)) { return true; } } @@ -417,11 +428,11 @@ class FormAsFile self::exportForm($formNameDB, $database); } if ($deleteFiles) { - $formFileNames = self::formFileNames($database); - $filesToDelete = array_diff($formFileNames, $formNamesDB); - foreach ($filesToDelete as $fileToDelete) { - self::deleteFormFile($fileToDelete, $database, "Export all forms from database. No form with name '$fileToDelete' in database."); - } + $formFileNames = self::formFileNames($database); + $filesToDelete = array_diff($formFileNames, $formNamesDB); + foreach ($filesToDelete as $fileToDelete) { + self::deleteFormFile($fileToDelete, $database, "Export all forms from database. No form with name '$fileToDelete' in database."); + } } } @@ -437,8 +448,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function insertFormElement(array $values, int $formId, Database $database): int - { + private static function insertFormElement(array $values, int $formId, Database $database): int { // filter allowed formElement columns (remove id, formId, feIdContainer) $formElementSchema = $database->getTableDefinition(TABLE_NAME_FORM_ELEMENT); $formElementColumns = array_column($formElementSchema, 'Field'); @@ -471,7 +481,7 @@ class FormAsFile { $formFromDb = $database->selectFormByName($formName, [F_ID, F_FILE_STATS]); - if ($keepIfNeverExported && (isset($formFromDb[F_ID]) && !isset($formFromDb[F_FILE_STATS])) || (isset($formFromDb[F_FILE_STATS]) && !self::isValidFileStats($formFromDb[F_FILE_STATS]))) { + if ($keepIfNeverExported && (isset($formFromDb[F_ID]) && !isset($formFromDb[F_FILE_STATS])) || (isset($formFromDb[F_FILE_STATS]) && !self::isValidFileStats($formFromDb[F_FILE_STATS]))) { // export form instead of deleting since it was never exported before self::exportForm($formName, $database); } else if (array_key_exists(F_ID, $formFromDb)) { @@ -487,7 +497,7 @@ class FormAsFile * @return bool */ private static function isValidFileStats(string $fileStats) { - return substr($fileStats, 0, 1 ) === '{'; + return substr($fileStats, 0, 1) === '{'; } /** @@ -522,8 +532,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function backupFormDb(int $formId, Database $database) - { + private static function backupFormDb(int $formId, Database $database) { list($formName, $formId, $formJson) = self::formToJson('', $database, $formId); $absoluteBackupFilePath = self::newBackupPathFileName($formName, 'db'); HelperFile::file_put_contents($absoluteBackupFilePath, $formJson); @@ -557,9 +566,8 @@ class FormAsFile * @param string $pathFileName * @return false|string */ - private static function formFileStatsJson(string $pathFileName) - { - clearstatcache (true, $pathFileName); + private static function formFileStatsJson(string $pathFileName) { + clearstatcache(true, $pathFileName); $stats = stat($pathFileName); if ($stats === false) { return false; @@ -582,8 +590,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function formPathFileName(string $formName, Database $database): string - { + private static function formPathFileName(string $formName, Database $database): string { // validate form name if (!HelperFile::isValidFileName($formName)) { throw new \UserFormException(json_encode([ @@ -604,8 +611,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function formPath(Database $database): string - { + private static function formPath(Database $database): string { $absoluteFormPath = Path::absoluteProject(Path::projectToForm()); if (!is_dir($absoluteFormPath)) { @@ -633,8 +639,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function queryAllFormNames(Database $database): array - { + private static function queryAllFormNames(Database $database): array { $NAME = F_NAME; $FORM = TABLE_NAME_FORM; return array_column($database->sql("SELECT `$NAME` FROM `$FORM`", ROW_REGULAR), $NAME); @@ -649,8 +654,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function formFileNames(Database $database): array - { + private static function formFileNames(Database $database): array { $formPath = self::formPath($database); $files = scandir($formPath); if ($files === false) { @@ -684,8 +688,7 @@ class FormAsFile * @throws \DbException * @throws \UserFormException */ - private static function formToJson(string $formName, Database $database, ?int $formId = null): array - { + private static function formToJson(string $formName, Database $database, ?int $formId = null): array { // Get form from DB (either by id or by name) if ($formId !== null) { list($sql, $parameterArray) = SqlQuery::selectFormById($formId); @@ -768,8 +771,7 @@ class FormAsFile * @return string * @throws \UserFormException */ - private static function newBackupPathFileName(string $formName, string $tag): string - { + private static function newBackupPathFileName(string $formName, string $tag): string { // create backup path if not exists $absoluteBackupPath = Path::absoluteProject(Path::projectToForm(), Path::FORM_TO_FORM_BACKUP); if (!is_dir($absoluteBackupPath)) { @@ -785,7 +787,7 @@ class FormAsFile $index = 1; while (file_exists($absoluteBackupFilePath)) { $absoluteBackupFilePath = Path::join($absoluteBackupPath, $formName . '.json.' . date('Y-m-d_H-i-s') . ".$index.$tag"); - $index ++; + $index++; if ($index > 20) { Thrower::userFormException('Error while trying to backup form file.', 'Infinite loop.'); } diff --git a/extension/Classes/Core/Helper/Support.php b/extension/Classes/Core/Helper/Support.php index e96cb15c..6a266e12 100644 --- a/extension/Classes/Core/Helper/Support.php +++ b/extension/Classes/Core/Helper/Support.php @@ -1475,7 +1475,7 @@ class Support { } /** - * @param $mode + * @param $mode // MODE_ENCODE|MODE_DECODE|MODE_NONE * @param $data * @return string * @throws \UserFormException -- GitLab