<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 3/6/17
 * Time: 8:47 PM
 */

namespace IMATHUZH\Qfq\Core\Store;

use IMATHUZH\Qfq\Core\Exception\ErrorHandler;
use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/**
 * Class Config
 * @package qfq
 */
class Config {

    private static $config = null;

    const CONFIG_REQUIRED_TEMPLATE = [
        SYSTEM_DB_1_SERVER => "localhost",
        SYSTEM_DB_1_NAME => "<dbname>",
        SYSTEM_DB_1_USER => "<username>",
        SYSTEM_DB_1_PASSWORD => "<password>",
    ];

    /**
     * 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 = '') {
        $updateT3QfqConfig = false;
        $baseUrl = '';

        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), LOCK_EX);
            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");
            }
        }

        // Initialize array for custom variables
        $configT3qfqCustoms = array();
        if ($PhpUnitOverloadAbsoluteConfigFilePath === '') {
            $extraArr = array();
            $configT3qfq = self::readTypo3QfqConfig($configT3qfqCustoms, $extraArr);

            // Settings in qfq.json overwrite T3 settings
            $config = array_merge($configT3qfq, $config, $extraArr);
        }

        // Check minimum security length
        if (($config[SYSTEM_SECURITY_GET_MAX_LENGTH] ?? '') < 32) {
            // On some places we use MD5 hashes with 32chars. Therefore this should be the minimum. If too low, set default.
            $config[SYSTEM_SECURITY_GET_MAX_LENGTH] = 50;
            $updateT3QfqConfig = true;
        }

        // Set default for encryption method if empty in typo3 configuration
        if (($config[SYSTEM_ENCRYPTION_METHOD] ?? '') === '') {
            $config[SYSTEM_ENCRYPTION_METHOD] = ENCRYPTION_METHOD_AES_128;
            $updateT3QfqConfig = true;
        }

        // Check for valid encryption method and show error in page if not valid
        if (!EncryptDecrypt::checkForValidEncryptMethod($config[SYSTEM_ENCRYPTION_METHOD])) {
            Thrower::userFormException("Defined default for encryption method is not valid: '" .
                $config[SYSTEM_ENCRYPTION_METHOD] . "' . Please define a valid method.");
        }

        // Set default from baseUrl if not given, OR if a previous bootstrap
        // accidentally persisted the FAILED_TO_SET_BASE_URL placeholder to
        // the extension's LocalConfiguration.php - that placeholder is
        // never a valid runtime URL and must be re-derived on the next
        // request that has proper $_SERVER vars.
        $storedBaseUrl = $config[SYSTEM_BASE_URL] ?? '';
        $storedIsPlaceholder = $storedBaseUrl !== '' && strpos($storedBaseUrl, FAILED_TO_SET_BASE_URL) === 0;
        if (empty($storedBaseUrl) || $storedIsPlaceholder) {
            // Prefer Apache-style full URI if present
            $baseUrl = $_SERVER['SCRIPT_URI'] ?? '';
            // nginx / PHP-FPM does not populate SCRIPT_URI - reconstruct
            // from scheme + host + script path so the fallback to
            // FAILED_TO_SET_BASE_URL only fires in truly empty contexts
            // (CLI without HTTP-Server env, etc.).
            if ($baseUrl === '') {
                $scheme = 'http';
                if (($_SERVER['HTTPS'] ?? '') !== '' && ($_SERVER['HTTPS'] ?? 'off') !== 'off') {
                    $scheme = 'https';
                } elseif (($_SERVER['REQUEST_SCHEME'] ?? '') === 'https') {
                    $scheme = 'https';
                } elseif (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') {
                    $scheme = 'https';
                }
                $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';
                $script = $_SERVER['SCRIPT_NAME'] ?? '';
                if ($host !== '') {
                    $baseUrl = $scheme . '://' . $host . $script;
                }
            }
            if ($baseUrl === '') {
                $baseUrl = FAILED_TO_SET_BASE_URL;
            }
            if (false !== ($pos = strpos($baseUrl, 'index.php'))) {
                $baseUrl = substr($baseUrl, 0, $pos);
            }
            $config[SYSTEM_BASE_URL] = $baseUrl;
            // Only persist to LocalConfiguration.php when we actually
            // derived a real URL. The placeholder must never be written
            // back - otherwise QFQ reads it on every subsequent request
            // (storedBaseUrl is no longer empty) and the site is stuck
            // redirecting to https://failed.to.set.baseurl.
            if (strpos($baseUrl, FAILED_TO_SET_BASE_URL) !== 0) {
                $updateT3QfqConfig = true; // Legacy behaviour.
            }
        }

        if (defined('PHPUNIT_QFQ')) {
            $config[SYSTEM_BASE_URL] = $baseUrl = HTTP_EXAMPLE_COM;
        }

        $config = self::renameConfigElements($config);
        $config = self::setDefaults($config);
        self::checkDeprecated($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, $updateT3QfqConfig);
        $config = self::setAutoConfigValue($config);

        self::checkMandatoryParameter($config);

        if ($updateT3QfqConfig) {
            T3Handler::updateT3QfqConfig($config, false, $baseUrl);
        }

        // Add custom variables after T3QfqConfig is updated and written
        $config = array_merge($configT3qfqCustoms, $config);
        self::$config = $config;

        // Set log paths
        Path::overrideLogPathsFromConfig(self::$config);
        self::checkForAttack($config);
    }

    /**
     * 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) {
        // Legacy part
        $configT3qfqCustomsLegacy = array();
        for ($i = 1; $i <= 30; $i++) {
            if (isset($config[SYSTEM_FILL_STORE_SYSTEM_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);
                    }

                    $configT3qfqCustomsLegacy[$arr[0]] = $arr[1];
                }
            }
        }
        // New approach: parse the inline INI document
        $configT3qfqCustoms = parse_ini_string(
            $config[SYSTEM_FILL_STORE_SYSTEM_CUSTOM] ?? '',
            false,
            INI_SCANNER_RAW
        );

        // Legacy options has lower priority
        return array_merge($configT3qfqCustomsLegacy, $configT3qfqCustoms);
    }

    /**
     * Overwrite the CONFIG_QFQ_JSON (db credentials) 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), LOCK_EX);
        chmod(Path::join($absoluteConf, CONFIG_QFQ_JSON), 0640);
    }

    /**
     * Add or overwrite existing SYSTEM_ENCRYPTION_KEY in CONFIG_QFQ_JSON file with value from given string.
     *
     * @param string $encryptionKey
     * @throws \UserFormException
     * @throws \CodeException
     */
    private static function setEncryptionKey(string $encryptionKey): void {
        $absoluteConf = Path::absoluteConf();
        $configFile = Path::join($absoluteConf, CONFIG_QFQ_JSON);

        // Read existing config
        $config = [];
        if (file_exists($configFile)) {
            $content = file_get_contents($configFile);
            if ($content !== false && $content !== '') {
                $config = json_decode($content, true);
                if ($config === null) {
                    throw new \CodeException('Failed to parse ' . CONFIG_QFQ_JSON . ': ' . json_last_error_msg());
                }
            }
        }

        // Add or update encryption key
        $config[SYSTEM_ENCRYPTION_KEY] = $encryptionKey;

        // Write back
        HelperFile::createPathRecursive($absoluteConf);
        HelperFile::file_put_contents($configFile, json_encode($config, JSON_PRETTY_PRINT), LOCK_EX);
    }

    /**
     * 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(&$configT3qfqCustoms, array &$extraArr): array {
        $configT3qfq = array();
        if (isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_CONF_EXTENSIONS][EXT_KEY])) {
            $configT3qfq = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_CONF_EXTENSIONS][EXT_KEY];
            $extraArr[SYSTEM_DB_NAME_T3] = self::getDbName($GLOBALS['TYPO3_CONF_VARS'][TYPO3_CONF_DB]);

        } else {
            // Probably API Call: read config directly from File. TYPO3
            // changed the default config file location across versions:
            //   - v11 and older        typo3conf/LocalConfiguration.php
            //   - v12+ Classic Mode    typo3conf/system/settings.php
            //                          + typo3conf/system/additional.php
            //   - v12+ Composer Mode   config/system/settings.php
            //                          + config/system/additional.php
            // Try all candidates; later layers override earlier ones so
            // that values set by additional.php (typical override file)
            // win over settings.php and LocalConfiguration.php.
            $candidates = [
                Path::absoluteApp(Path::APP_TO_TYPO3_CONF, CONFIG_T3),
                Path::absoluteApp(Path::APP_TO_TYPO3_CONF, 'system', 'settings.php'),
                Path::absoluteApp(Path::APP_TO_TYPO3_CONF, 'system', 'additional.php'),
                Path::absoluteApp('config', 'system', 'settings.php'),
                Path::absoluteApp('config', 'system', 'additional.php'),
            ];
            $configT3 = [];
            foreach ($candidates as $candidate) {
                if (is_readable($candidate)) {
                    $layer = HelperFile::include($candidate);
                    if (is_array($layer)) {
                        $configT3 = array_replace_recursive($configT3, $layer);
                    }
                }
            }
            if (!empty($configT3)) {
                $configT3qfq = $configT3[TYPO3_CONF_EXTENSIONS][EXT_KEY] ?? [];
                if (!is_array($configT3qfq)) {
                    Thrower::userFormException('Error read file', "Error while reading qfq config from TYPO3 config files.");
                }
                $extraArr[SYSTEM_DB_NAME_T3] = self::getDbName($configT3[TYPO3_CONF_DB] ?? []);
                $extraArr[SYSTEM_T3_SITE_NAME] = $configT3[TYPO3_CONF_SYS][TYPO3_CONF_SITENAME] ?? 'unknown';
                unset($configT3);
            }
        }

        $configT3qfqCustoms = self::getCustomVariable($configT3qfq);

        // Custom variables do not overwrite standard entries
        // Make a warning if an overlap is detected!
        $overlap = implode(", ", array_intersect(
            array_keys($configT3qfqCustoms),
            array_keys($configT3qfq)
        ));
        if ($overlap) {
            $message = "Custom variables cannot override existing variables. Overlapping keys: $overlap";
            Logger::logMessage($message, Path::absoluteQfqLogFile());
        }

        return array_merge($configT3qfqCustoms, $configT3qfq);
    }

    /**
     * Returns T3 DB-Name, depending on T3 version
     *
     * @param array $db
     * @return mixed
     */
    private static function getDbName(array $db) {
        return $db[TYPO3_CONF_CONNECTIONS][TYPO3_CONF_DEFAULT][TYPO3_DB_NAME] ?? '';
    }

    /**
     * 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.

                    // Typeahead parameter do have special extended limitation.
                    if ($key == TYPEAHEAD_API_QUERY || $key == TYPEAHEAD_API_PREFETCH) {
                        if ($maxLength < TYPEAHEAD_API_MAX_LENGTH) {
                            $maxLength = TYPEAHEAD_API_MAX_LENGTH;
                        }
                    }
                }

                $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 = ''): void {

        if (count($config) == 0) {
            $config = self::getConfigArray();
        }

        //Generate code and timestamp
        $code = substr(md5(uniqid()), 0, 8);
        $timestamp = date('Y-m-d H:i:s');

        Logger::logMessage(Logger::linePre() . "Security: attack detected [Code: $code]" . PHP_EOL . $reason, Path::absoluteQfqLogFile());

        // 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 ($penalty == -1) {
            return; // no destroy, no sleep, no exit
        }

        if (defined('PHPUNIT_QFQ')) {
            throw new \UserFormException('Attack detected', 1);
        }

        Session::destroy();
        sleep($penalty);

        if ($config[SYSTEM_SECURITY_SHOW_MESSAGE] == 1) {
            //Security Config in Typo 3 is enabled, Message with information about attack
            $errorMessage = "Attack detected - exiting. Code: $code <p>$reason</p>";
        } else {
            //Security Config in Typo 3 is disabled, Generic Attack Message
            $errorMessage = "The request could not be processed. Error code: $code | $timestamp";
        }

        if (defined('QFQ_API')) {
            // API-Call: JSON Response
            header("Content-Type: application/json");
            $errorMessage = json_encode([
                API_STATUS => API_ANSWER_STATUS_ERROR,
                API_MESSAGE => $errorMessage
            ]);
        }
        die($errorMessage);
    }

    /**
     * @param array $config
     *
     * @return array
     * @throws \UserFormException
     */
    public static function setDefaults(array $config) {

        // Set defaults. Important in case of new variables: new variables do not exist in QFQ extension config and do not
        // get the default defined in ext_conf_template.txt
        $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 => 'fileadmin/protected/qfqProject/log/mail.log',
            SYSTEM_QFQ_LOG_PATHFILENAME => 'fileadmin/protected/qfqProject/log/qfq.log',
            SYSTEM_SQL_LOG_PATHFILENAME => 'fileadmin/protected/qfqProject/log/sql.log',
            SYSTEM_MERGE_LOG_PATHFILENAME => Path::absoluteMergeLogFile(),
            SYSTEM_SIP_ERROR_LOG_MODE => SYSTEM_SIP_ERROR_LOG_MODE_FE_USER,
            SYSTEM_SQL_LOG_MODE => FORM_SUBMIT_LOG_MODE_MODIFY,
            SYSTEM_SQL_LOG_MODE_AUTOCRON => 'error',
            SYSTEM_IMAP_LOG_MODE => 'error',
            SYSTEM_IMAP_LOG_PATHFILENAME => 'fileadmin/protected/qfqProject/log/imap.log',

            F_BS_COLUMNS => 'col-md-12 col-lg-8',
            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',

            SYSTEM_ENCRYPTION_METHOD => ENCRYPTION_METHOD_AES_128,

            F_CLASS => 'qfq-notify',

            F_CLASS_PILL => 'qfq-color-grey-1',
            F_CLASS_BODY => 'qfq-color-grey-2',

            F_NOTE_BUTTON_CLASS => 'btn btn-default navbar-btn',

            // Deprecated
            F_SAVE_BUTTON_TEXT => '',
            F_SAVE_BUTTON_TOOLTIP => 'Save', // Save
            F_SAVE_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_SAVE_BUTTON_GLYPH_ICON => GLYPH_ICON_CHECK, // GLYPH_ICON_CHECK

            F_CLOSE_BUTTON_TEXT => '',
            F_CLOSE_BUTTON_TOOLTIP => 'Close', // Close
            F_CLOSE_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_CLOSE_BUTTON_GLYPH_ICON => GLYPH_ICON_CLOSE, // GLYPH_ICON_CLOSE

            F_DELETE_BUTTON_TEXT => '',
            F_DELETE_BUTTON_TOOLTIP => 'Delete', // Delete
            F_DELETE_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_DELETE_BUTTON_GLYPH_ICON => GLYPH_ICON_DELETE, // GLYPH_ICON_DELETE

            F_NEW_BUTTON_TEXT => '',
            F_NEW_BUTTON_TOOLTIP => 'New', // New
            F_NEW_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_NEW_BUTTON_GLYPH_ICON => GLYPH_ICON_NEW, // GLYPH_ICON_NEW
            F_UPLOAD_SUCCESS_MESSAGE => 'Upload complete',
            F_UPLOAD_IDLE_TEXT => 'Drag & drop or choose file',
            //-------------------------------------------

            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_FIELDSET_CLASS => SYSTEM_FIELDSET_CLASS_DEFAULT,

            SYSTEM_ESCAPE_TYPE_DEFAULT => TOKEN_ESCAPE_MYSQL,
            SYSTEM_EXTRA_BUTTON_INFO_INLINE => '<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>',
            SYSTEM_EXTRA_BUTTON_INFO_BELOW => '<span class="glyphicon glyphicon-info-sign text-info" aria-hidden="true"></span>',
            SYSTEM_EXTRA_BUTTON_INFO_CLASS => '',

            SYSTEM_SUBRECORD_EMPTY => 'show',

            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_ENTER_AS_SUBMIT => 1,
            SYSTEM_REMEMBER_LAST_PILL => 1,
            SYSTEM_SHOW_HISTORY => 0,

            SYSTEM_CMD_WKHTMLTOPDF => '/opt/wkhtmltox/bin/wkhtmltopdf',
            SYSTEM_CMD_QFQPDF => '/opt/qfqpdf/qfqpdf',
            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_CMD_HEIF_CONVERT => 'heif-convert',
            SYSTEM_CMD_PDF2PS => 'pdf2ps',
            SYSTEM_CMD_PS2PDF => 'ps2pdf',
            SYSTEM_CMD_WGET => 'wget >/dev/null 2>&1',
            SYSTEM_CMD_PDF_INFO => 'pdfinfo',
            SYSTEM_CMD_PDF_TO_TEXT => 'pdftotext',

            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_DATE_TIME_PICKER_TYPE => 'qfq',
            SYSTEM_UPLOAD_TYPE => UPLOAD_TYPE_DND,
            SYSTEM_FORCE_SMTP_SENDER => '',

            SYSTEM_CACHE_DIR_SECURE => SYSTEM_CACHE_DIR_SECURE_DEFAULT,
            SYSTEM_CACHE_PURGE_FILES_OLDER_DAYS => SYSTEM_CACHE_PURGE_FILES_OLDER_DAYS_DEFAULT,

            SYSTEM_FILE_MAX_FILE_SIZE => min(Support::returnBytes(ini_get('post_max_size')), Support::returnBytes(ini_get('upload_max_filesize'))),
            SYSTEM_IMAGE_UPLOAD_DIR => 'fileadmin/imageUploadDir',

            SYSTEM_PROTECTED_FOLDER_CHECK => 1,
            SYSTEM_RECENT_LOG => 1,

            SYSTEM_FILE_NAME_TO_LOWER => 1,
            SYSTEM_MAX_IMAGE_DIMENSION => '5000x5000',
        ];

        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, &$updateT3QfqConfig) {

        $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';
        }

        // Check for multiple baseUrl definitions. If given, search for those, who matches current request.
        // $config[SYSTEM_BASE_URL] = 'http://localhost,https://proxy.example.com,http://example.com'.
        // Dynamic updating is important to fulfil CORS requirements for AJAX calls - no special CORS Headers necessary than.
        $arr = explode(',', $config[SYSTEM_BASE_URL]);
        if (count($arr) > 1) {
            // Be careful: call via auto-cron.php does not have any $_SERVER vars!
            if (isset($_SERVER[CLIENT_HTTP_HOST])) {
                // http://example1.com,http://new.domain.com,
                $finalBaseUrl = OnArray::strposArr($arr, $_SERVER[CLIENT_HTTP_HOST]);
                if (empty($finalBaseUrl)) {
                    // If none matching found, take the first one as best guess.
                    $finalBaseUrl = $arr[0] ?? FAILED_TO_SET_BASE_URL;
                }
            } else {
                // No $_SERVER vars: take first as default.
                $finalBaseUrl = $arr[0];
            }
        } else {
            $finalBaseUrl = $config[SYSTEM_BASE_URL];
        }

        // Check for existing SYSTEM_ENCRYPTION_KEY and set default if not given. Max. 20 chars and alphanumeric.
        if (!isset($config[SYSTEM_ENCRYPTION_KEY]) || $config[SYSTEM_ENCRYPTION_KEY] === '') {
            // Generate random alphanumeric string (20 chars)
            $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            $encryptionKey = '';
            for ($i = 0; $i < 20; $i++) {
                $encryptionKey .= $characters[random_int(0, strlen($characters) - 1)];
            }
            // Overwrite qfq.json file with encryption key
            $config[SYSTEM_ENCRYPTION_KEY] = $encryptionKey;
            self::setEncryptionKey($encryptionKey);
        }

        // Check for existing slash and add it if not given.
        if (substr($finalBaseUrl, -1) !== '/') {
            $finalBaseUrl .= '/';
        }
        $config[SYSTEM_BASE_URL] = $finalBaseUrl;

        // In case there is no scheme: use the scheme of the current called url. If there is no $_SERVER[CLIENT_REQUEST_SCHEME], use 'http'.
        if (substr($config[SYSTEM_BASE_URL], 0, 4) != 'http') {
            $config[SYSTEM_BASE_URL] = ($_SERVER[CLIENT_REQUEST_SCHEME] ?? 'http') . '://' . $config[SYSTEM_BASE_URL];
        }

        if (isset($_SERVER[CLIENT_REQUEST_SCHEME])) {
            $url = $_SERVER[CLIENT_REQUEST_SCHEME] ?? '';
            $url .= '://';
            $url .= $_SERVER[CLIENT_HTTP_HOST] ?? '';
            $finalUrl = OnString::urlStripFile($url);

            // Changes should be saved in config file for different reasons.
            // If it's executed over autocron, SERVER vars don't exist.
            // Is used for tinyMce image uploads
            if ($finalUrl !== ($config[SYSTEM_HTTP_ORIGIN] ?? '')) {
                $config[SYSTEM_HTTP_ORIGIN] = $finalUrl;
                T3Handler::updateT3QfqConfig($finalUrl, SYSTEM_HTTP_ORIGIN);
            }
        }

        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, SYSTEM_IMAP_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;
    }

    /** Get extension version information from its ext_emconf.php
     * @param array $extensionArray Array of extensions [key => path]
     * @param string|false $extensionKey Specific extension key to get, or false for all
     * @param bool $invalidateCache Invalidate OPcache before reading (use after deployment)
     * @return mixed
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function getExtVersion($extensionArray = array(), $extensionKey = false, bool $invalidateCache = false): mixed {
        if (defined('PHPUNIT_QFQ')) {
            return [EXT_KEY => QFQ_VERSION_EXAMPLE];
        }

        $versionInfo = [];

        // If extensionArray is empty, get qfq version
        if (empty($extensionArray)) {
            $extensionArray = [EXT_KEY => 'typo3conf/ext/qfq/'];
        }

        foreach ($extensionArray as $key => $path) {
            // Skip if a specific key is requested
            if ($extensionKey && $key !== $extensionKey) {
                continue;
            }

            // Get qfq version from ext_emconf.php
            $extEmconfPath = Path::absoluteApp() . '/' . $path . 'ext_emconf.php';

            // Check if ext_emconf.php exists
            if (file_exists($extEmconfPath)) {
                $EM_CONF = null;
                // Set the $_EXTKEY variable for proper context
                $_EXTKEY = $key;

                // Invalidate OPcache to get fresh version after deployment
                if ($invalidateCache && function_exists('opcache_invalidate')) {
                    opcache_invalidate($extEmconfPath, true);
                }

                include $extEmconfPath;

                if (isset($EM_CONF[$key]['version'])) {
                    $versionInfo[$key] = $EM_CONF[$key]['version'];
                } else {
                    $versionInfo[$key] = 'unknown' . $extEmconfPath;
                }
            } else {
                $versionInfo[$key] = 'not found';
            }
        }

        return $versionInfo;
    }

    /**
     * Get server information
     * @return string
     */
    public static function getServerInfo(): string {
        $dpkgSoftware = ['apache2', 'nginx', 'mariadb-client', 'mariadb-server', 'wkhtmltopdf'];

        $binarySoftware = [
            'wkhtmltopdf' => ['cmd' => 'wkhtmltopdf --version 2>&1', 'regex' => '/(\d+\.\d+\.\d+(\.\d+)?)/'],
        ];

        $result = [];

        // Check dpkg packages
        $command = "dpkg-query -W -f='\${Package}:\${Version}\\n' " . implode(' ', $dpkgSoftware) . " 2>/dev/null";
        $output = shell_exec($command);
        $lines = explode("\n", trim($output));

        foreach ($dpkgSoftware as $name) {
            $version = self::findDpkgVersion($lines, $name);
            $result[$name] = $version;
        }

        // Only check binaries for packages not found in dpkg
        foreach ($binarySoftware as $name => $config) {
            if ($result[$name] === null) {
                $version = self::getBinaryVersion($config['cmd'], $config['regex']);
                if ($version !== null) {
                    $result[$name] = $version . ' (manual)';
                }
            }
        }

        // Add binary-specific metadata
        $result = self::addBinaryMetadata($result);

        // Format output
        $output = [];
        foreach ($result as $name => $version) {
            $output[] = "$name:" . ($version ?? 'not installed');
        }

        $output[] = self::getOsInfo();
        $output[] = "php:" . phpversion();

        return implode(',', $output);
    }

    /**
     * Get OS information
     * @return string
     */
    private static function getOsInfo(): string {
        // Try /etc/os-release first (standard on modern Linux)
        if (file_exists('/etc/os-release')) {
            $content = file_get_contents('/etc/os-release');
            $id = '';
            $version = '';

            if (preg_match('/^ID=(.*)$/m', $content, $matches)) {
                $id = trim($matches[1], '"');
            }
            if (preg_match('/^VERSION_ID=(.*)$/m', $content, $matches)) {
                $version = trim($matches[1], '"');
            }

            if ($id && $version) {
                return "$id:$version";
            }
        }

        // Fallback to php_uname
        return 'os:' . php_uname('s') . ' ' . php_uname('r');
    }

    /**
     * @param array $lines
     * @param string $name
     * @return string|null
     */
    private static function findDpkgVersion(array $lines, string $name): ?string {
        foreach ($lines as $line) {
            if (strpos($line, $name . ':') === 0) {
                $parts = explode(':', $line, 2);
                $version = trim($parts[1]);
                return $version !== '' ? $version : null;
            }
        }
        return null;
    }

    /**
     * @param string $command
     * @param string $regex
     * @return string|null
     */
    private static function getBinaryVersion(string $command, string $regex): ?string {
        $output = shell_exec($command);
        if ($output && preg_match($regex, $output, $matches)) {
            return $matches[1];
        }
        return null;
    }

    /**
     * Add binary specific metadata
     *
     * @param array $result
     * @return array
     */
    private static function addBinaryMetadata(array $result): array {
        // wkhtmltopdf: check for patched Qt
        if (!empty($result['wkhtmltopdf'])) {
            $output = shell_exec('wkhtmltopdf --version 2>&1');
            if ($output && strpos($output, 'with patched qt') !== false) {
                $result['wkhtmltopdf'] .= ' (qt)';
            }
        }

        // Add more binary-specific checks here as needed

        return $result;
    }
}