null, SYSTEM_DB_1_PASSWORD => null, SYSTEM_DB_1_SERVER => "localhost", SYSTEM_DB_1_NAME => null, ]; /** * Get config value with given key. Throws exception if config has not been read. * * @param string $key * @return mixed * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ public static function get(string $key) { self::readConfig(); // only reads once return self::$config[$key] ?? null; } /** * Returns a copy of the config array. Throws exception if config has not been read. * * @param string $PhpUnitOverloadAbsoluteConfigFilePath * @return array * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ public static function getConfigArray($PhpUnitOverloadAbsoluteConfigFilePath = ''): array { self::readConfig($PhpUnitOverloadAbsoluteConfigFilePath); // only reads once, except if argument !='' return self::$config; } /** * Read qfq.json (merge with Typo3-qfq config if exists). * Note: Deprecated config file typo3conf/config.qfq.php is translated to JSON in PATH:findAppToProject(..) * * @param string $PhpUnitOverloadAbsoluteConfigFilePath * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ private static function readConfig($PhpUnitOverloadAbsoluteConfigFilePath = '') { if (self::$config !== null && $PhpUnitOverloadAbsoluteConfigFilePath === '') { // only read once, except phpUnit return; } // read and parse config. Throw exception if not exists. $absoluteConfigFilePath = $PhpUnitOverloadAbsoluteConfigFilePath === '' ? Path::absoluteConf(CONFIG_QFQ_JSON) : $PhpUnitOverloadAbsoluteConfigFilePath; if (!file_exists($absoluteConfigFilePath)) { HelperFile::createPathRecursive(Path::absoluteConf()); HelperFile::file_put_contents(Path::absoluteConf(CONFIG_QFQ_JSON_EXAMPLE), json_encode(self::CONFIG_REQUIRED_TEMPLATE, JSON_PRETTY_PRINT)); Thrower::userFormException("Please create qfq config file '" . CONFIG_QFQ_JSON . "' in the conf directory which is inside the project directory. Example config file '" . CONFIG_QFQ_JSON_EXAMPLE . "' was created in conf directory.", "Project directory: " . Path::absoluteProject()); } $config = HelperFile::json_decode(HelperFile::file_get_contents($absoluteConfigFilePath)); // check required keys foreach (self::CONFIG_REQUIRED_TEMPLATE as $key => $value) { if (!array_key_exists($key, $config) || is_null($config[$key]) || $config[$key] === '') { Thrower::userFormException("Required key '$key' missing in config file " . CONFIG_QFQ_JSON, "Config file: $absoluteConfigFilePath"); } } if ($PhpUnitOverloadAbsoluteConfigFilePath === '') { $configT3qfq = self::readTypo3QfqConfig(); // Settings in qfq.json overwrite T3 settings $config = array_merge($configT3qfq, $config); } $config = self::renameConfigElements($config); $config = self::setDefaults($config); self::checkDeprecated($config); self::checkForAttack($config); // Copy values to detect custom settings later $config[F_FE_DATA_PATTERN_ERROR_SYSTEM] = $config[F_FE_DATA_PATTERN_ERROR]; $config = self::adjustConfig($config); $config = self::setAutoConfigValue($config); self::checkMandatoryParameter($config); self::$config = $config; Path::setUrlApp(self::get(SYSTEM_BASE_URL)); // Set log paths Path::overrideLogPathsFromConfig(); } /** * Iterates over all 30 custom vars, explode them to split between key and value, append to $config. * * @param array $config * @return array * @throws \UserReportException */ private static function getCustomVariable(array $config) { for ($i = 1; $i <= 30; $i++) { if (isset($config['custom' . $i])) { $arr = explode('=', $config['custom' . $i], 2); if (!empty($arr[0]) && !empty($arr[1])) { $arr[0] = trim($arr[0]); $arr[1] = OnString::trimQuote(trim($arr[1])); if (isset($config[$arr[0]])) { throw new \UserReportException("Variable '$arr[0]' already defined", ERROR_INVALID_OR_MISSING_PARAMETER); } $config[$arr[0]] = $arr[1]; } } } return $config; } /** * Overwrite the qfq config file with data from given array. * * @param array $config * @throws \UserFormException * @throws \CodeException */ private static function writeConfig(array $config) { $absoluteConf = Path::absoluteConf(); HelperFile::createPathRecursive($absoluteConf); HelperFile::file_put_contents(Path::join($absoluteConf, CONFIG_QFQ_JSON), json_encode($config, JSON_PRETTY_PRINT)); } /** * Replace typo3conf/config.qfq.php with fileadmin/protected/qfqProject/qfq.json * * @throws \CodeException * @throws \UserFormException */ public static function migrateConfigPhpToJson(): void { // read old config.qfq.php $absoluteOldConfigFilePath = Path::absoluteApp(Path::APP_TO_TYPO3_CONF, CONFIG_QFQ_PHP); if (!is_writeable($absoluteOldConfigFilePath)) { throw new \UserFormException(json_encode([ ERROR_MESSAGE_TO_USER => "Can't migrate to new config: Legacy config file `config.qfq.php` not writable.", ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file/directory '$absoluteOldConfigFilePath'"])); } $config = include($absoluteOldConfigFilePath); // In case the database credentials are given in the old style: rename the keys $config = OnArray::renameKeys([ SYSTEM_DB_USER => SYSTEM_DB_1_USER, SYSTEM_DB_SERVER => SYSTEM_DB_1_SERVER, SYSTEM_DB_PASSWORD => SYSTEM_DB_1_PASSWORD, SYSTEM_DB_NAME => SYSTEM_DB_1_NAME ], $config); // write new qfq.config.json self::writeConfig($config); // remove old HelperFile::unlink($absoluteOldConfigFilePath); } /** * Read Typo3-QFQ config first from global variable, then from typo3conf/Localconfig.php. * If both not exist, return empty array. * * @return array Empty if config not found. * @throws \UserFormException * @throws \UserReportException */ private static function readTypo3QfqConfig(): array { $configT3qfq = array(); if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][EXT_KEY])) { // Typo3 version >=9 $configT3qfq = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][EXT_KEY]; $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($GLOBALS['TYPO3_CONF_VARS']['DB']); } elseif (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][EXT_KEY])) { // Typo3 version <=8 $configT3qfq = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][EXT_KEY]); $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($GLOBALS['TYPO3_CONF_VARS']['DB']); } elseif (is_readable(Path::absoluteApp(Path::APP_TO_TYPO3_CONF, CONFIG_T3))) { $absoluteTypo3ConfigFile = Path::absoluteApp(Path::APP_TO_TYPO3_CONF, CONFIG_T3); $configT3 = HelperFile::include($absoluteTypo3ConfigFile); if (isset($configT3['EXTENSIONS'][EXT_KEY])) { // Typo3 version >=9 $configT3qfq = $configT3['EXTENSIONS'][EXT_KEY]; } else { // Typo3 version <=8 $configT3qfq = unserialize($configT3['EXT']['extConf'][EXT_KEY]); } if (!is_array($configT3qfq)) { Thrower::userFormException('Error read file', "Error while reading qfq config from: $absoluteTypo3ConfigFile"); } $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($configT3['DB']); unset($configT3); } $configT3qfq = self::getCustomVariable($configT3qfq); return $configT3qfq; } /** * Returns T3 DB-Name, depending on T3 version * * @param array $db * @return mixed */ private static function getDbName(array $db) { // T3 7.x: $GLOBALS['TYPO3_CONF_VARS']['DB']['database'], T3 8.x: $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] return isset($db['database']) ? $db['database'] : $db['Connections']['Default']['dbname']; } /** * Checks for deprecated options. * * @param array $config * @throws \UserFormException */ private static function checkDeprecated(array $config) { foreach ([SYSTEM_VAR_ADD_BY_SQL] as $key) { if (isset($config[$key])) { $msg = ''; switch ($key) { case SYSTEM_VAR_ADD_BY_SQL: $msg = 'Replaced by: ' . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . '1|2|3'; } throw new \UserFormException ("Deprecated option in " . CONFIG_QFQ_PHP . ": " . SYSTEM_VAR_ADD_BY_SQL . " - " . $msg); } } } /** * Check for attack * * @param array $config * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ public static function checkForAttack(array $config) { $attack = false; $key = ''; $reason = 'Problem: '; // Iterate over all fake vars $arr = explode(',', $config[SYSTEM_SECURITY_VARS_HONEYPOT]); foreach ($arr as $key) { $key = trim($key); if ($key === '') { continue; } if (!empty($_POST[$key])) { $attack = true; $reason .= "Post/Get Honeypot variable '$key' detected: " . htmlentities($_POST[$key]) . PHP_EOL; } } // Limit length of all get vars: protect against SQL injection based on long ...%34%34%24%34... $maxLength = $config[SYSTEM_SECURITY_GET_MAX_LENGTH]; if ($maxLength > 0 && $attack === false) { foreach ($_GET as $key => $value) { if (!is_string($value)) { continue; } // Check if the variable is something like 'my_name_100' - if the part after the last '_' is numerical, this means a valid, non standard length. $arr = explode(GET_EXTRA_LENGTH_TOKEN, $key); $cnt = count($arr); if ($cnt > 1 && is_numeric($arr[$cnt - 1])) { $maxLength = $arr[$cnt - 1]; } else { $maxLength = $config[SYSTEM_SECURITY_GET_MAX_LENGTH]; // might change again. } $len = strlen($value); if ($len > $maxLength) { $attack = true; $reason .= "Value of GET variable '$key' too long. Allowed: $maxLength, Length: $len. Value: '" . htmlentities($_GET[$key]) . "'" . PHP_EOL; } } } // Nothing found? if ($attack === false) { return; } self::attackDetectedExitNow($config, $reason); } /** * @param array $config * @param string $reason * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ public static function attackDetectedExitNow(array $config = array(), $reason = '') { if (count($config) == 0) { $config = self::getConfigArray(); } Logger::logMessage(Logger::linePre() . 'Security: attack detected' . PHP_EOL . $reason, Path::absoluteQfqLogFile()); // In case of an attack: log out the current user. Session::destroy(); // Sleep $penalty = (empty($config[SYSTEM_SECURITY_ATTACK_DELAY]) || !is_numeric($config[SYSTEM_SECURITY_ATTACK_DELAY])) ? SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT : $config[SYSTEM_SECURITY_ATTACK_DELAY]; if (!defined('PHPUNIT_QFQ')) { sleep($penalty); } if ($config[SYSTEM_SECURITY_SHOW_MESSAGE] == 'true' || $config[SYSTEM_SECURITY_SHOW_MESSAGE] == 1) { echo "Attack detected - stop process

" . $reason . '

'; // $answer[API_STATUS] = API_ANSWER_STATUS_ERROR; // $answer[API_MESSAGE] = 'Attack detected - stop process.'; // if($getParamName!='') { // $answer[API_MESSAGE] .= " Attack parameter: $getParamName"; // } // header("Content-Type: application/json"); // echo json_encode($answer); } if (defined('PHPUNIT_QFQ')) { throw new \UserFormException('Attack detected', 1); } exit; } /** * @param array $config * * @return array */ public static function setDefaults(array $config) { $default = [ SYSTEM_DB_INIT => 'set names utf8', SYSTEM_DB_INDEX_DATA => DB_INDEX_DEFAULT, SYSTEM_DB_INDEX_QFQ => DB_INDEX_DEFAULT, SYSTEM_RENDER => SYSTEM_RENDER_SINGLE, SYSTEM_DATE_FORMAT => 'yyyy-mm-dd', SYSTEM_SHOW_DEBUG_INFO => SYSTEM_SHOW_DEBUG_INFO_AUTO, SYSTEM_REPORT_MIN_PHP_VERSION => SYSTEM_REPORT_MIN_PHP_VERSION_AUTO, SYSTEM_MAIL_LOG_PATHFILENAME => '', SYSTEM_QFQ_LOG_PATHFILENAME => '', SYSTEM_SQL_LOG_PATHFILENAME => '', SYSTEM_SQL_LOG_MODE => 'modify', SYSTEM_SQL_LOG_MODE_AUTOCRON => 'error', F_BS_COLUMNS => 'col-md-12 col-lg-10', F_BS_LABEL_COLUMNS => 'col-md-3 col-lg-3', F_BS_INPUT_COLUMNS => 'col-md-6 col-lg-6', F_BS_NOTE_COLUMNS => 'col-md-3 col-lg-3', F_CLASS => 'qfq-notify', F_CLASS_PILL => 'qfq-color-grey-1', F_CLASS_BODY => 'qfq-color-grey-2', F_SAVE_BUTTON_TEXT => '', F_SAVE_BUTTON_TOOLTIP => '', F_SAVE_BUTTON_CLASS => 'btn btn-default navbar-btn', F_SAVE_BUTTON_GLYPH_ICON => GLYPH_ICON_CHECK, F_CLOSE_BUTTON_TEXT => '', F_CLOSE_BUTTON_TOOLTIP => 'Close', F_CLOSE_BUTTON_CLASS => 'btn btn-default navbar-btn', F_CLOSE_BUTTON_GLYPH_ICON => GLYPH_ICON_CLOSE, F_DELETE_BUTTON_TEXT => '', F_DELETE_BUTTON_TOOLTIP => 'Delete', F_DELETE_BUTTON_CLASS => 'btn btn-default navbar-btn', F_DELETE_BUTTON_GLYPH_ICON => GLYPH_ICON_DELETE, F_NEW_BUTTON_TEXT => '', F_NEW_BUTTON_TOOLTIP => 'New', F_NEW_BUTTON_CLASS => 'btn btn-default navbar-btn', F_NEW_BUTTON_GLYPH_ICON => GLYPH_ICON_NEW, F_BUTTON_ON_CHANGE_CLASS => 'btn-info alert-info', SYSTEM_EDIT_FORM_PAGE => 'form', SYSTEM_SECURITY_VARS_HONEYPOT => SYSTEM_SECURITY_VARS_HONEYPOT_NAMES, SYSTEM_SECURITY_ATTACK_DELAY => SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT, SYSTEM_SECURITY_SHOW_MESSAGE => '0', SYSTEM_SECURITY_GET_MAX_LENGTH => SYSTEM_SECURITY_GET_MAX_LENGTH_DEFAULT, SYSTEM_LABEL_ALIGN => SYSTEM_LABEL_ALIGN_LEFT, SYSTEM_ESCAPE_TYPE_DEFAULT => TOKEN_ESCAPE_MYSQL, SYSTEM_EXTRA_BUTTON_INFO_INLINE => '', SYSTEM_EXTRA_BUTTON_INFO_BELOW => '', SYSTEM_EXTRA_BUTTON_INFO_CLASS => '', SYSTEM_DB_UPDATE => SYSTEM_DB_UPDATE_AUTO, SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS => SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT, SYSTEM_SESSION_TIMEOUT_SECONDS => Session::getPhpSessionTimeout(), SYSTEM_DOCUMENTATION_QFQ => SYSTEM_DOCUMENTATION_QFQ_URL, SYSTEM_ENTER_AS_SUBMIT => 1, SYSTEM_CMD_WKHTMLTOPDF => '/opt/wkhtmltox/bin/wkhtmltopdf', SYSTEM_CMD_INKSCAPE => 'inkscape', SYSTEM_CMD_CONVERT => 'convert', SYSTEM_CMD_PDF2SVG => 'pdf2svg', SYSTEM_CMD_PDFTOCAIRO => 'pdftocairo', SYSTEM_CMD_QPDF => 'qpdf', SYSTEM_CMD_GS => 'gs', SYSTEM_CMD_PDFUNITE => 'pdfunite', SYSTEM_CMD_IMG2PDF => 'img2pdf', SYSTEM_THUMBNAIL_DIR_SECURE_REL_TO_APP => Path::APP_TO_SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT, SYSTEM_THUMBNAIL_DIR_PUBLIC_REL_TO_APP => Path::APP_TO_SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT, F_FE_DATA_REQUIRED_ERROR => F_FE_DATA_REQUIRED_ERROR_DEFAULT, F_FE_DATA_MATCH_ERROR => F_FE_DATA_MATCH_ERROR_DEFAULT, F_FE_DATA_ERROR => F_FE_DATA_ERROR_DEFAULT, F_FE_DATA_PATTERN_ERROR => F_FE_DATA_PATTERN_ERROR_DEFAULT, SYSTEM_FLAG_PRODUCTION => 'yes', SYSTEM_THROW_GENERAL_ERROR => 'auto', SYSTEM_SECURITY_FAILED_AUTH_DELAY => '3', SYSTEM_FILE_MAX_FILE_SIZE => min(Support::returnBytes(ini_get('post_max_size')), Support::returnBytes(ini_get('upload_max_filesize'))), ]; foreach ($default as $key => $value) { if (!isset($config[$key]) || $config[$key] == '') { $config[$key] = $value; } } return $config; } /** * Rename Elements defined in config.qfq.ini to more appropriate in user interaction. * E.g.: in config.qfq.ini everything is in upper case and word space is '_'. In Form.parameter it's lowercase and * camel hook. * * @param array $config * * @return array */ private static function renameConfigElements(array $config) { // oldname > newname $setting = [ [SYSTEM_FORM_BS_COLUMNS, F_BS_COLUMNS], [SYSTEM_FORM_BS_LABEL_COLUMNS, F_BS_LABEL_COLUMNS], [SYSTEM_FORM_BS_INPUT_COLUMNS, F_BS_INPUT_COLUMNS], [SYSTEM_FORM_BS_NOTE_COLUMNS, F_BS_NOTE_COLUMNS], [SYSTEM_FORM_DATA_PATTERN_ERROR, F_FE_DATA_PATTERN_ERROR], [SYSTEM_FORM_DATA_REQUIRED_ERROR, F_FE_DATA_REQUIRED_ERROR], [SYSTEM_FORM_DATA_MATCH_ERROR, F_FE_DATA_MATCH_ERROR], [SYSTEM_FORM_DATA_ERROR, F_FE_DATA_ERROR], [SYSTEM_CSS_CLASS_QFQ_FORM, F_CLASS], [SYSTEM_CSS_CLASS_QFQ_FORM_PILL, F_CLASS_PILL], [SYSTEM_CSS_CLASS_QFQ_FORM_BODY, F_CLASS_BODY], [SYSTEM_SAVE_BUTTON_CLASS_ON_CHANGE, F_BUTTON_ON_CHANGE_CLASS], ]; foreach ($setting as $row) { $oldName = $row[0]; $newName = $row[1]; if (isset($config[$oldName])) { $config[$newName] = $config[$oldName]; if ($oldName != $newName) { unset($config[$oldName]); } } } return $config; } /** * Depending on some configuration value, update corresponding values. * * @param array $config * @return array */ private static function adjustConfig(array $config) { $config[SYSTEM_SHOW_DEBUG_INFO] = self::adjustConfigDebugInfoAuto($config[SYSTEM_SHOW_DEBUG_INFO], T3Info::beUserLoggedIn()); if ($config[SYSTEM_REPORT_MIN_PHP_VERSION] == SYSTEM_REPORT_MIN_PHP_VERSION_AUTO && T3Info::beUserLoggedIn()) { $config[SYSTEM_REPORT_MIN_PHP_VERSION] = SYSTEM_REPORT_MIN_PHP_VERSION_YES; } // In case the database credentials are given in the old style: copy them to the new style if (!isset($config[SYSTEM_DB_1_USER]) && isset($config[SYSTEM_DB_USER])) { $config[SYSTEM_DB_1_USER] = $config[SYSTEM_DB_USER]; $config[SYSTEM_DB_1_SERVER] = $config[SYSTEM_DB_SERVER]; $config[SYSTEM_DB_1_PASSWORD] = $config[SYSTEM_DB_PASSWORD]; $config[SYSTEM_DB_1_NAME] = $config[SYSTEM_DB_NAME]; } if ($config[SYSTEM_THROW_GENERAL_ERROR] == 'auto') { $config[SYSTEM_THROW_GENERAL_ERROR] = $config[SYSTEM_FLAG_PRODUCTION] == 'yes' ? 'no' : 'yes'; } return $config; } /** * @param string $value * * @param $flag * @return string */ public static function adjustConfigDebugInfoAuto($value, $flag) { // Check if SHOW_DEBUG_INFO contains 'auto'. Replace with appropriate. if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_AUTO, $value) && $flag) { $value = str_replace(SYSTEM_SHOW_DEBUG_INFO_AUTO, SYSTEM_SHOW_DEBUG_INFO_YES, $value); } return $value; } /** * Set automatic filled values * * @param array $config * @return array */ private static function setAutoConfigValue(array $config) { $config[SYSTEM_DB_NAME_DATA] = $config['DB_' . $config[SYSTEM_DB_INDEX_DATA] . '_NAME'] ?? ''; $config[SYSTEM_DB_NAME_QFQ] = $config['DB_' . $config[SYSTEM_DB_INDEX_QFQ] . '_NAME'] ?? ''; return $config; } /** * Iterate over all Parameter which have to exist in the config. Throw an array if any is missing. * * @param array $config * * @throws \UserFormException */ private static function checkMandatoryParameter(array $config) { // Check mandatory config vars. $names = array_merge([SYSTEM_SQL_LOG_MODE], self::dbCredentialName($config[SYSTEM_DB_INDEX_DATA]), self::dbCredentialName($config[SYSTEM_DB_INDEX_QFQ])); foreach ($names as $name) { if (!isset($config[$name])) { throw new \UserFormException ("Missing configuration in `" . CONFIG_QFQ_JSON . "`: $name", ERROR_MISSING_CONFIG_INI_VALUE); } } } /** * @param $index * @return array */ private static function dbCredentialName($index) { $names = array(); $names[] = 'DB_' . $index . '_USER'; $names[] = 'DB_' . $index . '_SERVER'; $names[] = 'DB_' . $index . '_PASSWORD'; $names[] = 'DB_' . $index . '_NAME'; return $names; } }