<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 2/2/20
 * Time: 9:02 AM
 */

namespace IMATHUZH\Qfq\Core\Typo3;

use IMATHUZH\Qfq\Core\Exception\ErrorHandler;
use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Store\Config;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
use TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility;

/**
 * Class T3Handler
 * @package IMATHUZH\Qfq\Core\Typo3
 */
class T3Handler {

    /**
     * Returns the login status of the frontend user
     * @return bool
     */
    public static function isFeUserLoggedIn(): bool
    {
        try {
            return class_exists(GeneralUtility::class) &&
                   GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'isLoggedIn');
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Call logoff of current FE User
     */
    public static function feLogOff() {

        // Restore T3 ErrorHandler. T3 throws exceptions - those should be handled by T3!
        ErrorHandler::suspend();

        $GLOBALS['TSFE']->fe_user->logoff();

        // Activate QFQ ErrorHandler again.
        ErrorHandler::resume();
    }


    /**
     * Convert a cleartext password to a hash. Respects if 'salted passwords' are enabled.
     * The password hash type is taken from QFQ config, Argon2i is used by default.
     *
     * @param string $newPassword
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function getHash($newPassword) {

        // Typo3 version <=8
        // Legacy code based on https://docs.typo3.org/typo3cms/extensions/saltedpasswords/8.7/DevelopersGuide/Index.html
        self::t3AutoloadIfNotRunning();
        if (!class_exists(PasswordHashFactory::class)
            && class_exists(SaltedPasswordsUtility::class)
            && class_exists(SaltFactory::class)) {
            $saltedPassword = null;
            // Restore T3 ErrorHandler. T3 throws exceptions - those should be handled by T3!
            ErrorHandler::suspend();
            if (SaltedPasswordsUtility::isUsageEnabled('FE')) {
                $objSalt = SaltFactory::getSaltingInstance(NULL);
                if (is_object($objSalt)) {
                    $saltedPassword = $objSalt->getHashedPassword($newPassword);
                }
            }
            ErrorHandler::resume(); // Activate QFQ ErrorHandler again.
            if (!is_null($saltedPassword)) {
                return $saltedPassword;
            }
        }

        // On ZI RedHat hosts, PHP module for argon2i might be missing. As a consequence, even the constants are not defined.
        if(!defined('PASSWORD_ARGON2I')){
            define('PASSWORD_ARGON2I', 'argon2i');
        }
        // Typo3 version >=9
        $type = Config::get(SYSTEM_PASWORD_HASH_TYPE) ?? PASSWORD_BCRYPT;
        if ($type === PASSWORD_ARGON2I) {
            // Use Argon2i algorithm (with current default options of typo3 in year 2020)
            $saltedPassword = password_hash($newPassword, PASSWORD_ARGON2I, ['memory_cost' => 65536, 'time_cost' => 16, 'threads' => 1]);
        } elseif ($type === PASSWORD_BCRYPT) {
            $hash = base64_encode(hash('sha384', $newPassword, true));
            $saltedPassword = password_hash($hash, PASSWORD_BCRYPT, ['cost' => 12]);
        } else {
            Thrower::userFormException('Password hashing type not recognized.', 'The following password hashing type is not recognized: ' . $type);
        }
        return $saltedPassword;
    }

    /**
     * Check if the salted password corresponds to the password.
     *
     * @param string $saltedPassword
     * @param string $password
     * @return bool
     * @throws \UserFormException
     */
    public static function checkPassword($saltedPassword, $password) {
        return password_verify($password, $saltedPassword);
    }

    /**
     * Load Typo3 autoloader if Typo3 is not instantiated
     *
     * @throws \UserFormException
     */
    public static function t3AutoloadIfNotRunning() {

        if (!self::isTypo3Loaded()) {

            if (file_exists(Path::absoluteApp('vendor/autoload.php'))) {
                // Typo3 new installation since version >=9
                $classLoader = require Path::absoluteApp('vendor/autoload.php');
            } else {
                // Typo3 version <=8 or upgraded >=9
                $classLoader = require Path::absoluteApp('typo3_src/vendor/autoload.php');
            }

            // run typo3 bootstrap if not yet happened. Necessary if run in unittest.
            if (!defined('TYPO3_MODE')) {

                try {
                    // NOTE: Deprecated: Deprecation: #85821 - bootstrap methods
                    Bootstrap::getInstance()
                        ->setRequestType(TYPO3_REQUESTTYPE_AJAX)
                        ->baseSetup(0);
                } catch (\Throwable $e) {
                    try {
                        // The new way to bootstrap Typo3
                        // Documentation: https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Bootstrapping/Index.html
                        // Typo3 testing framework example bootstrap: https://github.com/TYPO3/testing-framework/blob/9e8e2700bef2281af5d0b8d0693dd0b0cc1bfe04/Classes/Core/Functional/Framework/Frontend/RequestBootstrap.php
                        SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_FE);

                        // The following line might be necessary but it aborts the execution without catchable error:
                        // Bootstrap::init($classLoader)->get(\TYPO3\CMS\Frontend\Http\Application::class)->run();

                    } catch (\Throwable $e) {
                        Thrower::userFormException('Manual Typo3 bootstrap failed.');
                    }
                }

//              Alternate error fix if you don't want to run Typo3 bootstrap:
//              error_reporting(E_ALL & ~(E_STRICT | E_NOTICE | E_DEPRECATED));
                if (!defined('TYPO3_MODE')) {
                    define('TYPO3_MODE', 'FE');
                }
            }
        }
    }

    /**
     * Update in `typo3conf/LocalConfiguration.php` QFQ config.
     * a) key!==false: a single $key/$value pair (not used anymore)
     * b) key===false: array in $value
     *
     * @param mixed $value
     * @param mixed $key
     * @param string $baseUrl
     * @throws \UserFormException
     */
    public static function updateT3QfqConfig($value, $key = false, $baseUrl = '') {

        if (defined('PHPUNIT_QFQ') || defined('QFQ_API') || defined('QFQ_EXTERNAL')) {
            // a) There is no typo3conf/LocalConfiguration.php in phpunit.
            // b) During QFQ_API: we won't update LocalConfiguration.php. In T3 V8: broken path  for LocalConfiguration if update during API call: just skip this now.
            return;
        }


        // Restore T3 ErrorHandler. T3 throws exceptions - those should be handled by T3!
        ErrorHandler::suspend();

        self::t3AutoloadIfNotRunning();

        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);

        // Same as $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['qfq']
        $configT3 = $configurationManager->getLocalConfiguration();

        if (isset($configT3['EXTENSIONS'][EXT_KEY])) {
            // Typo3 version >=9
            $configExtQfq = $configT3['EXTENSIONS'][EXT_KEY];
        } else {
            // Typo3 version <=8
            $configExtQfq = unserialize($configT3['EXT']['extConf'][EXT_KEY]);
        }

        if ($key === false) {
            // merge both arrays. $value overwrites $configExtQfq
            $tmpArr = array_merge($configExtQfq, $value);

            // Specific values are dynamically replaced: restore saved value to store again. Unit tests are an example for this reason.
            foreach ([SYSTEM_THROW_GENERAL_ERROR, SYSTEM_SHOW_DEBUG_INFO, SYSTEM_REPORT_MIN_PHP_VERSION, SYSTEM_BASE_URL, SYSTEM_DB_UPDATE] as $key) {
                $tmpArr[$key] = $configExtQfq[$key];
            }

            if ($baseUrl != '') {
                $tmpArr[SYSTEM_BASE_URL] = $baseUrl;
            }

            $localConfQfq = $configExtQfq;
            // Take care to only write those elements who originally defined in QFQ Extension LocalConfiguration.php.
            $configExtQfq = array();
            // Take only such arguments which exist in T3/QFQ config
            foreach ($localConfQfq as $key => $value) {
                $configExtQfq[$key] = ($tmpArr[$key] ?? '');
            }

        } else {
            // key given: set indiviudal key/value
            $configExtQfq[$key] = $value;
        }

        // Prepare
        if (isset($configT3['EXTENSIONS'][EXT_KEY])) {
            // Typo3 version >=9
            $configT3['EXTENSIONS'][EXT_KEY] = $configExtQfq;
        } else {
            // Typo3 version <=8
            $configT3['EXT']['extConf'][EXT_KEY] = serialize($configExtQfq);
        }

        // In #18793/#16717 we seem to have concurrent write access to LocalConfigration.php, initiated by QFQ,  which destroys the config.
        // Create a local qfq lock. With this, we can detect if there is another QFQ thread who tries to write to it.
        $configT3qfqLock = Path::absoluteConf(CONFIG_T3) . '.qfq.lock';
        $fp = @fopen($configT3qfqLock, "x");
        if ($fp !== false) {
            // Lock acquired - Write new config to typo3conf/LocalConfiguration.php
            $configurationManager->writeLocalConfiguration($configT3);
            // Release lock
            fclose($fp);
        } else {
            // Lock couldn't be acquired (another QFQ thread is running) - Check if lock is expired:
            if (false !== ($fp = @fopen("$configT3qfqLock", "r"))) {

                $fstat = fstat($fp);
                fclose($fp);
                $ctime = $fstat['ctime'] ?? 0;
                // Expire: after 30 seconds
                if (time() - $ctime > 30) {
                    // Lock expired - try to remove it.
                    HelperFile::unlink($configT3qfqLock);
                }
            }
        }

        // Activate QFQ ErrorHandler again.
        ErrorHandler::resume();
    }

    /**
     * Note: Loads Typo3 if not running.
     *
     * @return string
     */
    public static function getTypo3Version() {
        self::t3AutoloadIfNotRunning();
        return VersionNumberUtility::getNumericTypo3Version();
    }

    /**
     * Note: Loads Typo3 if not running.
     * Return value is only computed once, then cached.
     *
     * @return bool
     */
    public static function typo3VersionGreaterEqual9() {

        if (defined('PHPUNIT_QFQ')) {
            // assume Typo3 version grater than 8 in unittests
            return true;
        }

        // Warning: caching!!! Do not try to do version compare with different version number during one run and than cache such result.
        static $cache = null;
        if (is_null($cache)) {
            // use pageSlug notation after 10.0
            $cache = version_compare(self::getTypo3Version(), '9.0.0') >= 0;
        }
        return $cache;
    }

    /**
     * Note: Loads Typo3 if not running.
     * Return value is only computed once, then cached.
     *
     * @return bool
     */
    public static function typo3VersionGreaterEqual10() {
        if (defined('PHPUNIT_QFQ')) {
            // assume Typo3 version grater than 8 in unittests
            return true;
        }

        // Warning: caching!!! Do not try to do version compare with different version number during one run and than cache such result.
        static $cache = null;
        if (is_null($cache)) {
            $cache = version_compare(self::getTypo3Version(), '10.0.0') >= 0;
        }
        return $cache;
    }

    /**
     * Alias for typo3VersionGreaterEqual9()
     * Introduced in case we want to change the version number at which slugs are mandatory.
     * Return value is only computed once, then cached.
     *
     * @return bool
     */
    public static function useSlugsInsteadOfPageAlias() {
        static $cache = null;
        if (is_null($cache)) {
            $cache = self::typo3VersionGreaterEqual10();
        }
        return $cache;
    }

    /**
     * Returns true if Typo3 classes are available.
     *
     * @return bool
     */
    public static function isTypo3Loaded() {
        return class_exists(GeneralUtility::class);
    }

    /**
     * Note: Loads Typo3 if not running.
     * Return value is only computed once, then cached.
     * Used for Controller handling.
     *
     * @return bool
     */
    public static function typo3VersionGreaterEqual11(): ?bool {
        if (defined('PHPUNIT_QFQ')) {
            // assume Typo3 version grater than 8 in unittests
            return true;
        }

        // Warning: caching!!! Do not try to do version compare with different version number during one run and than cache such result.
        static $cache = null;
        if (is_null($cache)) {
            $cache = version_compare(self::getTypo3Version(), '11.0.0') >= 0;
        }
        return $cache;
    }

    /**
     * Get typo3 database configuration.
     *
     * @param int $dbIndexData
     * @param int $dbIndexQfq
     * @param int $dbIndexT3
     * @return array
     * @throws \UserFormException
     */
    public static function getTypo3DbConfig(int $dbIndexData, int $dbIndexQfq, int &$dbIndexT3): array {
        $config = array();
        for ($i = 1; $i <= 3; $i++) {
            if ($dbIndexData !== $i && $dbIndexQfq !== $i) {
                $dbIndexT3 = $i;
                continue;
            }
        }

        self::t3AutoloadIfNotRunning();

        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);

        // Same as $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['qfq']
        if (isset($GLOBALS['TYPO3_CONF_VARS']) && !defined('PHPUNIT_QFQ')) {
            $configT3 = $configurationManager->getLocalConfiguration();
            $typo3DbCredentials = $configT3[TYPO3_CONF_DB][TYPO3_CONF_CONNECTIONS][TYPO3_CONF_DEFAULT];
            $config['DB_' . $dbIndexT3 . '_USER'] = $typo3DbCredentials[TYPO3_DB_USER];
            $config['DB_' . $dbIndexT3 . '_PASSWORD'] = $typo3DbCredentials[TYPO3_DB_PASSWORD];
            $config['DB_' . $dbIndexT3 . '_SERVER'] = $typo3DbCredentials[TYPO3_DB_HOST];
            $config['DB_' . $dbIndexT3 . '_NAME'] = $typo3DbCredentials[TYPO3_DB_NAME];
        }

        return $config;
    }
}