<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Install\SystemEnvironment;

use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Check system environment status.
 *
 * This class is a hardcoded requirement check of the underlying
 * server and PHP system.
 *
 * The class *must not* check for any TYPO3 specific things like
 * specific configuration values or directories.
 *
 * This class is instantiated as a very early during installation.
 *
 * Be picky with dependencies here:
 * * No hooks or anything like that
 * * No localization
 * * Only low level ext:core classes if free of side effects
 *
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
 */
class Check implements CheckInterface
{
    /**
     * @var FlashMessageQueue
     */
    protected $messageQueue;

    /**
     * @var array List of required PHP extensions
     */
    protected $requiredPhpExtensions = [
        'filter',
        'gd',
        'intl',
        'json',
        'libxml',
        'mbstring',
        'PDO',
        'session',
        'SPL',
        'standard',
        'tokenizer',
        'xml',
        'zip',
        'zlib',
    ];

    /**
     * @var string[]
     */
    protected $suggestedPhpExtensions = [
        'exif' => 'This extension is used to detect the orientation of uploaded images.',
        'fileinfo' => 'This extension is used for proper file type detection in the File Abstraction Layer.',
        'openssl' => 'This extension is used for sending SMTP mails over an encrypted channel endpoint.',
    ];

    public function __construct()
    {
        $this->messageQueue = new FlashMessageQueue('install');
    }

    public function getMessageQueue(): FlashMessageQueue
    {
        return $this->messageQueue;
    }

    /**
     * Get all status information as array with status objects
     */
    public function getStatus(): FlashMessageQueue
    {
        $this->checkCurrentDirectoryIsInIncludePath();
        $this->checkFileUploadEnabled();
        $this->checkPostUploadSizeIsHigherOrEqualMaximumFileUploadSize();
        $this->checkMemorySettings();
        $this->checkPhpVersion();
        $this->checkMaxExecutionTime();
        $this->checkDisableFunctions();
        $this->checkDocRoot();
        $this->checkOpenBaseDir();
        $this->checkXdebugMaxNestingLevel();

        $this->checkMaxInputVars();
        $this->checkReflectionDocComment();
        $this->checkWindowsApacheThreadStackSize();

        foreach ($this->requiredPhpExtensions as $extension) {
            $this->checkPhpExtension($extension);
        }

        foreach ($this->suggestedPhpExtensions as $extension => $purpose) {
            $this->checkPhpExtension($extension, false, $purpose);
        }

        $this->checkPcreVersion();
        $this->checkGdLibTrueColorSupport();
        $this->checkGdLibGifSupport();
        $this->checkGdLibJpgSupport();
        $this->checkGdLibPngSupport();
        $this->checkGdLibFreeTypeSupport();

        return $this->messageQueue;
    }

    /**
     * Checks if current directory (.) is in PHP include path
     */
    protected function checkCurrentDirectoryIsInIncludePath()
    {
        $includePath = (string)ini_get('include_path');
        $delimiter = $this->isWindowsOs() ? ';' : ':';
        $pathArray = GeneralUtility::trimExplode($delimiter, $includePath, true);
        if (!in_array('.', $pathArray)) {
            $this->messageQueue->enqueue(new FlashMessage(
                'include_path = ' . implode(' ', $pathArray) . LF
                    . 'Normally the current path \'.\' is included in the'
                    . ' include_path of PHP. Although TYPO3 does not rely on this,'
                    . ' it is an unusual setting that may introduce problems for'
                    . ' some extensions.',
                'Current directory (./) is not within PHP include path',
                ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'Current directory (./) is within PHP include path.'
            ));
        }
    }

    /**
     * Check if file uploads are enabled in PHP
     */
    protected function checkFileUploadEnabled()
    {
        if (!ini_get('file_uploads')) {
            $this->messageQueue->enqueue(new FlashMessage(
                'file_uploads=' . ini_get('file_uploads') . LF
                    . 'TYPO3 uses the ability to upload files from the browser in various cases.'
                    . ' If this flag is disabled in PHP, you won\'t be able to upload files.'
                    . ' But it doesn\'t end here, because not only are files not accepted by'
                    . ' the server - ALL content in the forms are discarded and therefore'
                    . ' nothing at all will be editable if you don\'t set this flag!',
                'File uploads not allowed in PHP',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'File uploads allowed in PHP'
            ));
        }
    }

    /**
     * Check maximum post upload size correlates with maximum file upload
     */
    protected function checkPostUploadSizeIsHigherOrEqualMaximumFileUploadSize()
    {
        $maximumUploadFilesize = $this->getBytesFromSizeMeasurement((string)ini_get('upload_max_filesize'));
        $maximumPostSize = $this->getBytesFromSizeMeasurement((string)ini_get('post_max_size'));
        if ($maximumPostSize > 0 && $maximumPostSize < $maximumUploadFilesize) {
            $this->messageQueue->enqueue(new FlashMessage(
                'upload_max_filesize=' . ini_get('upload_max_filesize') . LF
                    . 'post_max_size=' . ini_get('post_max_size') . LF
                    . 'You have defined a maximum size for file uploads in PHP which'
                    . ' exceeds the allowed size for POST requests. Therefore the'
                    . ' file uploads can also not be larger than ' . ini_get('post_max_size') . '.',
                'Maximum size for POST requests is smaller than maximum upload filesize in PHP',
                ContextualFeedbackSeverity::ERROR
            ));
        } elseif ($maximumPostSize === $maximumUploadFilesize) {
            $this->messageQueue->enqueue(new FlashMessage(
                'The maximum size for file uploads is set to ' . ini_get('upload_max_filesize'),
                'Maximum post upload size correlates with maximum upload file size in PHP'
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                'The maximum size for file uploads is set to ' . ini_get('upload_max_filesize'),
                'Maximum post upload size is higher than maximum upload file size in PHP, which is fine.'
            ));
        }
    }

    /**
     * Check memory settings
     */
    protected function checkMemorySettings()
    {
        $minimumMemoryLimit = 256;
        $recommendedMemoryLimit = 512;
        $memoryLimit = $this->getBytesFromSizeMeasurement((string)ini_get('memory_limit'));
        if ($memoryLimit <= 0) {
            if (Environment::isCli()) {
                // "0" memory limit for CLI is usually "just fine". Do not cause a report for this in CLI mode (but in web mode)
                $this->messageQueue->enqueue(new FlashMessage(
                    'Maximum PHP memory limit is set to zero; this is commonly set in PHP CLI mode, which is currently active for this check.',
                    'Unlimited memory limit for PHP (CLI)',
                ));
                return;
            }
            $this->messageQueue->enqueue(new FlashMessage(
                'PHP is configured not to limit memory usage at all. This is a risk'
                    . ' and should be avoided in production setup. In general it\'s best practice to limit this.'
                    . ' To be safe, set a limit in PHP, but with a minimum of ' . $recommendedMemoryLimit . 'MB:' . LF
                    . 'memory_limit=' . $recommendedMemoryLimit . 'M',
                'Unlimited memory limit for PHP',
                ContextualFeedbackSeverity::WARNING
            ));
        } elseif ($memoryLimit < 1024 * 1024 * $minimumMemoryLimit) {
            $this->messageQueue->enqueue(new FlashMessage(
                'memory_limit=' . ini_get('memory_limit') . LF
                    . 'Your system is configured to enforce a memory limit for PHP scripts lower than '
                    . $minimumMemoryLimit . 'MB. It is required to raise the limit.'
                    . ' We recommend a minimum PHP memory limit of ' . $recommendedMemoryLimit . 'MB:' . LF
                    . 'memory_limit=' . $recommendedMemoryLimit . 'M',
                'PHP Memory limit below ' . $minimumMemoryLimit . 'MB',
                ContextualFeedbackSeverity::ERROR
            ));
        } elseif ($memoryLimit < 1024 * 1024 * $recommendedMemoryLimit) {
            $this->messageQueue->enqueue(new FlashMessage(
                'memory_limit=' . ini_get('memory_limit') . LF
                    . 'Your system is configured to enforce a memory limit for PHP scripts lower than '
                    . $recommendedMemoryLimit . 'MB.'
                    . ' A slim TYPO3 instance without many extensions will probably work, but you should monitor your'
                    . ' system for "allowed memory size of X bytes exhausted" messages, especially if using the backend.'
                    . ' To be on the safe side, we recommend a minimum PHP memory limit of '
                    . $recommendedMemoryLimit . 'MB:' . LF
                    . 'memory_limit=' . $recommendedMemoryLimit . 'M',
                'PHP Memory limit below ' . $recommendedMemoryLimit . 'MB',
                ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP Memory limit is equal to or more than ' . $recommendedMemoryLimit . 'MB'
            ));
        }
    }

    /**
     * Check minimum PHP version
     */
    protected function checkPhpVersion()
    {
        $minimumPhpVersion = '8.5.0';
        $currentPhpVersion = PHP_VERSION;
        if (version_compare($currentPhpVersion, $minimumPhpVersion) < 0) {
            $this->messageQueue->enqueue(new FlashMessage(
                'Your PHP version ' . $currentPhpVersion . ' is too old. TYPO3 CMS does not run'
                    . ' with this version. Update to at least PHP ' . $minimumPhpVersion,
                'PHP version too low',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP version is fine'
            ));
        }
    }

    /**
     * Check PRCE module is loaded and minimum version
     */
    protected function checkPcreVersion()
    {
        $minimumPcreVersion = '8.38';
        if (!extension_loaded('pcre')) {
            $this->messageQueue->enqueue(new FlashMessage(
                'TYPO3 CMS uses PHP extension pcre but it is not loaded'
                    . ' in your environment. Change your environment to provide this extension'
                    . ' in with minimum version ' . $minimumPcreVersion . '.',
                'PHP extension pcre not loaded',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            $installedPcreVersionString = trim(PCRE_VERSION); // '8.39 2016-06-14'
            $mainPcreVersionString = explode(' ', $installedPcreVersionString);
            $mainPcreVersionString = $mainPcreVersionString[0]; // '8.39'
            if (version_compare($mainPcreVersionString, $minimumPcreVersion) < 0) {
                $this->messageQueue->enqueue(new FlashMessage(
                    'Your PCRE version ' . PCRE_VERSION . ' is too old. TYPO3 CMS may trigger PHP segmentantion'
                        . ' faults with this version. Update to at least PCRE ' . $minimumPcreVersion,
                    'PCRE version too low',
                    ContextualFeedbackSeverity::ERROR
                ));
            } else {
                $this->messageQueue->enqueue(new FlashMessage(
                    '',
                    'PHP extension PCRE is loaded and version is fine'
                ));
            }
        }
    }

    /**
     * Check maximum execution time
     */
    protected function checkMaxExecutionTime()
    {
        $minimumMaximumExecutionTime = 30;
        $recommendedMaximumExecutionTime = 240;
        $currentMaximumExecutionTime = ini_get('max_execution_time');
        if ($currentMaximumExecutionTime == 0) {
            if (Environment::isCli()) {
                // "0" execution time for CLI is usually "just fine". Do not cause a report for this in CLI mode (but in web mode)
                $this->messageQueue->enqueue(new FlashMessage(
                    'Maximum PHP script execution time is set to zero; this is commonly set in PHP CLI mode, which is currently active for this check.',
                    'Infinite PHP script execution time',
                ));
                return;
            }
            $this->messageQueue->enqueue(new FlashMessage(
                'max_execution_time=0' . LF
                    . 'While TYPO3 is fine with this, you risk a denial-of-service for your system if for whatever'
                    . ' reason some script hangs in an infinite loop. You are usually on the safe side '
                    . ' if it is reduced to ' . $recommendedMaximumExecutionTime . ' seconds:' . LF
                    . 'max_execution_time=' . $recommendedMaximumExecutionTime,
                'Infinite PHP script execution time',
                ContextualFeedbackSeverity::WARNING
            ));
        } elseif ($currentMaximumExecutionTime < $minimumMaximumExecutionTime) {
            $this->messageQueue->enqueue(new FlashMessage(
                'max_execution_time=' . $currentMaximumExecutionTime . LF
                    . 'Your max_execution_time is too low. Some expensive operations in TYPO3 can take longer than that.'
                    . ' It is recommended to raise the limit to ' . $recommendedMaximumExecutionTime . ' seconds:' . LF
                    . 'max_execution_time=' . $recommendedMaximumExecutionTime,
                'Low PHP script execution time',
                ContextualFeedbackSeverity::ERROR
            ));
        } elseif ($currentMaximumExecutionTime < $recommendedMaximumExecutionTime) {
            $this->messageQueue->enqueue(new FlashMessage(
                'max_execution_time=' . $currentMaximumExecutionTime . LF
                    . 'Your max_execution_time is low. While TYPO3 often runs without problems'
                    . ' with ' . $minimumMaximumExecutionTime . ' seconds,'
                    . ' it may still happen that script execution is stopped before finishing'
                    . ' calculations. You should monitor the system for messages in this area'
                    . ' and maybe raise the limit to ' . $recommendedMaximumExecutionTime . ' seconds:' . LF
                    . 'max_execution_time=' . $recommendedMaximumExecutionTime,
                'Low PHP script execution time',
                ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'Maximum PHP script execution time is equal to or more than ' . $recommendedMaximumExecutionTime
            ));
        }
    }

    /**
     * Check for disabled functions
     */
    protected function checkDisableFunctions()
    {
        $disabledFunctions = trim((string)ini_get('disable_functions'));

        // Filter "disable_functions"
        $disabledFunctionsArray = GeneralUtility::trimExplode(',', $disabledFunctions, true);

        // Array with strings to find
        $findStrings = [
            // Disabled by default on Ubuntu OS but this is okay since the Core does not use them
            'pcntl_',
        ];

        // List of disable_functions which would not trigger an error, but only a warning
        $configuredAllowedDisableFunctions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions'] ?? [];
        if (!is_array($configuredAllowedDisableFunctions)) {
            $configuredAllowedDisableFunctions = [];
        }
        $foundAllowedDisableFunctions = [];

        // Iterate all functions that are currently disabled by PHP ($disabledFunctionsArray).
        // Each disabled function ($disabledFunction) is checked whether it may be acceptable
        // to be disabled (based on $configuredAllowedDisableFunctions).
        // If good to be disabled: Remove from $disabledFunctionsArray (which is emitted later on),
        //                         and also add to $foundAllowedDisableFunctions for reporting.
        // Else: Check if the function is maybe whitelisted because unused by core ($findStrings)
        //       and if so, also remove from $disabledFunctionsArray
        // What remains then in $disabledFunctionsArray is the list of disabled functions that
        // need to be reported.
        foreach ($disabledFunctionsArray as $key => $disabledFunction) {
            if (in_array($disabledFunction, $configuredAllowedDisableFunctions, true)) {
                unset($disabledFunctionsArray[$key]);
                $foundAllowedDisableFunctions[] = $disabledFunction;
                continue;
            }

            foreach ($findStrings as $findString) {
                if (str_contains($disabledFunction, $findString)) {
                    unset($disabledFunctionsArray[$key]);
                }
            }
        }

        if ($disabledFunctions !== '') {
            // Error for disable_functions which are not explicitly allowed
            if ($disabledFunctionsArray !== []) {
                $this->messageQueue->enqueue(new FlashMessage(
                    'disable_functions=' . implode(' ', $disabledFunctionsArray) . LF
                    . '- These function(s) are disabled. TYPO3 uses some of those, so there might be trouble.'
                    . ' TYPO3 is designed to use the default set of PHP functions plus some common extensions.'
                    . ' Possibly these functions are disabled'
                    . ' due to security considerations and often the list would include a function like'
                    . ' exec() which is used by TYPO3 at various places. Depending on which exact functions'
                    . ' are disabled, some parts of the system may just break without further notice. Known acceptable'
                    . ' exemptions can be muted by adding those to'
                    . ' $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'allowedPhpDisableFunctions\'] in your system'
                    . ' configuration.',
                    'Some PHP functions disabled',
                    ContextualFeedbackSeverity::ERROR
                ));
            }
            // Warning for disable_functions which are explicitly allowed
            if ($foundAllowedDisableFunctions !== []) {
                $this->messageQueue->enqueue(new FlashMessage(
                    'disable_functions=' . implode(' ', $foundAllowedDisableFunctions) . LF
                    . '- These function(s) are disabled. TYPO3 or installed extensions may use some of those, but the'
                    . ' error reporting for them is explicitly muted in your installation via configuration variable'
                    . ' $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'allowedPhpDisableFunctions\'].'
                    . ' Please ensure by yourself that these functions are not used by TYPO3 or any other installed package.',
                    'Some PHP functions disabled, but considered irrelevant',
                    ContextualFeedbackSeverity::WARNING
                ));
            }
            // Notice for known irrelevant functions (e.g. pcntl_*)
            // Only shown if none of the two FlashMessages above are emitted.
            if ($disabledFunctionsArray === [] && $foundAllowedDisableFunctions === []) {
                $this->messageQueue->enqueue(new FlashMessage(
                    'disable_functions=' . implode(' ', explode(',', $disabledFunctions)) . LF
                        . '- These function(s) are disabled. TYPO3 uses currently none of those, so you are good to go.',
                    'Some PHP functions currently disabled but OK'
                ));
            }
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'No disabled PHP functions'
            ));
        }
    }

    /**
     * Check for doc_root ini setting
     */
    protected function checkDocRoot()
    {
        $docRootSetting = trim((string)ini_get('doc_root'));
        if ($docRootSetting !== '') {
            $this->messageQueue->enqueue(new FlashMessage(
                'doc_root=' . $docRootSetting . LF
                    . 'PHP cannot execute scripts'
                    . ' outside this directory. This setting is seldom used and must correlate'
                    . ' with your actual document root. You might be in trouble if your'
                    . ' TYPO3 CMS core code is linked to some different location.'
                    . ' If that is a problem, the setting must be changed.',
                'doc_root is set',
                ContextualFeedbackSeverity::NOTICE
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP doc_root is not set'
            ));
        }
    }

    /**
     * Check open_basedir
     */
    protected function checkOpenBaseDir()
    {
        $openBaseDirSetting = trim((string)ini_get('open_basedir'));
        if ($openBaseDirSetting !== '') {
            $this->messageQueue->enqueue(new FlashMessage(
                'open_basedir = ' . ini_get('open_basedir') . LF
                    . 'This restricts TYPO3 to open and include files only in this'
                    . ' path. Please make sure that this does not prevent TYPO3 from running,'
                    . ' if for example your TYPO3 CMS core is linked to a different directory'
                    . ' not included in this path.',
                'PHP open_basedir is set',
                ContextualFeedbackSeverity::NOTICE
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP open_basedir is off'
            ));
        }
    }

    /**
     * If xdebug is loaded, the default max_nesting_level of 100 must be raised
     */
    protected function checkXdebugMaxNestingLevel()
    {
        if (extension_loaded('xdebug')) {
            $recommendedMaxNestingLevel = 400;
            $errorThreshold = 250;
            $currentMaxNestingLevel = ini_get('xdebug.max_nesting_level');
            if ($currentMaxNestingLevel < $errorThreshold) {
                $this->messageQueue->enqueue(new FlashMessage(
                    'xdebug.max_nesting_level=' . $currentMaxNestingLevel . LF
                        . 'This setting controls the maximum number of nested function calls to protect against'
                        . ' infinite recursion. The current value is too low for TYPO3 CMS and must'
                        . ' be either raised or xdebug has to be unloaded. A value of ' . $recommendedMaxNestingLevel
                        . ' is recommended. Warning: Expect fatal PHP errors in central parts of the CMS'
                        . ' if the value is not raised significantly to:' . LF
                        . 'xdebug.max_nesting_level=' . $recommendedMaxNestingLevel,
                    'PHP xdebug.max_nesting_level is critically low',
                    ContextualFeedbackSeverity::ERROR
                ));
            } elseif ($currentMaxNestingLevel < $recommendedMaxNestingLevel) {
                $this->messageQueue->enqueue(new FlashMessage(
                    'xdebug.max_nesting_level=' . $currentMaxNestingLevel . LF
                        . 'This setting controls the maximum number of nested function calls to protect against'
                        . ' infinite recursion. The current value is high enough for the TYPO3 CMS core to work'
                        . ' fine, but still some extensions could raise fatal PHP errors if the setting is not'
                        . ' raised further. A value of ' . $recommendedMaxNestingLevel . ' is recommended.' . LF
                        . 'xdebug.max_nesting_level=' . $recommendedMaxNestingLevel,
                    'PHP xdebug.max_nesting_level is low',
                    ContextualFeedbackSeverity::WARNING
                ));
            } else {
                $this->messageQueue->enqueue(new FlashMessage(
                    '',
                    'PHP xdebug.max_nesting_level ok'
                ));
            }
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP xdebug extension not loaded'
            ));
        }
    }

    /**
     * Get max_input_vars status
     */
    protected function checkMaxInputVars()
    {
        $recommendedMaxInputVars = 1500;
        $minimumMaxInputVars = 1000;
        $currentMaxInputVars = ini_get('max_input_vars');

        if ($currentMaxInputVars < $minimumMaxInputVars) {
            $this->messageQueue->enqueue(new FlashMessage(
                'max_input_vars=' . $currentMaxInputVars . LF
                    . 'This setting can lead to lost information if submitting forms with lots of data in TYPO3 CMS'
                    . ' (as the install tool does). It is highly recommended to raise this'
                    . ' to at least ' . $recommendedMaxInputVars . ':' . LF
                    . 'max_input_vars=' . $recommendedMaxInputVars,
                'PHP max_input_vars too low',
                ContextualFeedbackSeverity::ERROR
            ));
        } elseif ($currentMaxInputVars < $recommendedMaxInputVars) {
            $this->messageQueue->enqueue(new FlashMessage(
                'max_input_vars=' . $currentMaxInputVars . LF
                    . 'This setting can lead to lost information if submitting forms with lots of data in TYPO3 CMS'
                    . ' (as the install tool does). It is highly recommended to raise this'
                    . ' to at least ' . $recommendedMaxInputVars . ':' . LF
                    . 'max_input_vars=' . $recommendedMaxInputVars,
                'PHP max_input_vars very low',
                ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP max_input_vars ok'
            ));
        }
    }

    /**
     * Check doc comments can be fetched by reflection
     */
    protected function checkReflectionDocComment()
    {
        $testReflection = new \ReflectionMethod(static::class, __FUNCTION__);
        if ($testReflection->getDocComment() === false) {
            $this->messageQueue->enqueue(new FlashMessage(
                'TYPO3 CMS core extensions like extbase and fluid heavily rely on method'
                    . ' comment parsing to fetch annotations and add magic belonging to them.'
                    . ' This does not work in the current environment and so we cannot install'
                    . ' TYPO3 CMS.' . LF
                    . ' Here are some possibilities: ' . LF
                    . '* In Zend OPcache you can disable saving/loading comments. If you are using'
                    . ' Zend OPcache (included since PHP 5.5) then check your php.ini settings for'
                    . ' opcache.save_comments and opcache.load_comments and enable them.' . LF
                    . '* In Zend Optimizer+ you can disable saving comments. If you are using'
                    . ' Zend Optimizer+ then check your php.ini settings for'
                    . ' zend_optimizerplus.save_comments and enable it.' . LF
                    . '* The PHP extension eaccelerator is known to break this if'
                    . ' it is compiled without --with-eaccelerator-doc-comment-inclusion flag.'
                    . ' This compile flag must be specified, otherwise TYPO3 CMS will not work.' . LF
                    . 'For more information take a look in our documentation '
                    . Typo3Information::getDocsLink('t3coreapi:troubleshooting-php-troubleshooting-opcode') . '.',
                'PHP Doc comment reflection broken',
                ContextualFeedbackSeverity::ERROR
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP Doc comment reflection works'
            ));
        }
    }

    /**
     * Checks thread stack size if on windows with apache
     */
    protected function checkWindowsApacheThreadStackSize()
    {
        if (Environment::isCli()) {
            return;
        }
        if ($this->isWindowsOs()
            && str_starts_with($_SERVER['SERVER_SOFTWARE'], 'Apache')
        ) {
            $this->messageQueue->enqueue(new FlashMessage(
                'This current value cannot be checked by the system, so please ignore this warning if it'
                    . ' is already taken care of: Fluid uses complex regular expressions which require a lot'
                    . ' of stack space during the first processing.'
                    . ' On Windows the default stack size for Apache is a lot smaller than on UNIX.'
                    . ' You can increase the size to 8MB (default on UNIX) by adding the following configuration'
                    . ' to httpd.conf and restarting Apache afterwards:' . LF
                    . '<IfModule mpm_winnt_module>ThreadStackSize 8388608</IfModule>',
                'Windows apache thread stack size',
                ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'Apache ThreadStackSize is not an issue on UNIX systems'
            ));
        }
    }

    /**
     * Checks if a specific PHP extension is loaded.
     */
    public function checkPhpExtension(string $extension, bool $required = true, string $purpose = '')
    {
        if (!extension_loaded($extension)) {
            $this->messageQueue->enqueue(new FlashMessage(
                'TYPO3 uses the PHP extension "' . $extension . '" but it is not loaded'
                    . ' in your environment. Change your environment to provide this extension. '
                    . $purpose,
                'PHP extension "' . $extension . '" not loaded',
                $required ? ContextualFeedbackSeverity::ERROR : ContextualFeedbackSeverity::WARNING
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP extension "' . $extension . '" loaded'
            ));
        }
    }

    /**
     * Check imagecreatetruecolor to verify gdlib works as expected
     */
    protected function checkGdLibTrueColorSupport()
    {
        if (function_exists('imagecreatetruecolor')) {
            $imageResource = @imagecreatetruecolor(50, 100);
            if ($this->checkImageResource($imageResource)) {
                $this->messageQueue->enqueue(new FlashMessage(
                    '',
                    'PHP GD library true color works'
                ));
            } else {
                $this->messageQueue->enqueue(new FlashMessage(
                    'GD is loaded, but calling imagecreatetruecolor() fails.'
                        . ' This must be fixed, TYPO3 CMS won\'t work well otherwise.',
                    'PHP GD library true color support broken',
                    ContextualFeedbackSeverity::ERROR
                ));
            }
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                'Gdlib is essential for TYPO3 CMS to work properly.',
                'PHP GD library true color support missing',
                ContextualFeedbackSeverity::ERROR
            ));
        }
    }

    /**
     * Check gif support of GD library
     */
    protected function checkGdLibGifSupport()
    {
        if (function_exists('imagecreatefromgif')
            && function_exists('imagegif')
            && (imagetypes() & IMG_GIF)
        ) {
            // Do not use data:// wrapper to be independent of allow_url_fopen
            $imageResource = @imagecreatefromgif(__DIR__ . '/../../Resources/Public/Images/TestInput/Test.gif');
            if ($this->checkImageResource($imageResource)) {
                $this->messageQueue->enqueue(new FlashMessage(
                    '',
                    'PHP GD library has gif support'
                ));
            } else {
                $this->messageQueue->enqueue(new FlashMessage(
                    'GD is loaded, but calling imagecreatefromgif() fails. This must be fixed, TYPO3 CMS won\'t work well otherwise.',
                    'PHP GD library gif support broken',
                    ContextualFeedbackSeverity::ERROR
                ));
            }
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                'GD must be compiled with gif support. This is essential for TYPO3 CMS to work properly.',
                'PHP GD library gif support missing',
                ContextualFeedbackSeverity::ERROR
            ));
        }
    }

    /**
     * Check jpg support of GD library
     */
    protected function checkGdLibJpgSupport()
    {
        if (function_exists('imagecreatefromjpeg')
            && function_exists('imagejpeg')
            && (imagetypes() & IMG_JPG)
        ) {
            $this->messageQueue->enqueue(new FlashMessage(
                '',
                'PHP GD library has jpg support'
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                'GD must be compiled with jpg support. This is essential for TYPO3 CMS to work properly.',
                'PHP GD library jpg support missing',
                ContextualFeedbackSeverity::ERROR
            ));
        }
    }

    /**
     * Check png support of GD library
     */
    protected function checkGdLibPngSupport()
    {
        if (function_exists('imagecreatefrompng')
            && function_exists('imagepng')
            && (imagetypes() & IMG_PNG)
        ) {
            // Do not use data:// wrapper to be independent of allow_url_fopen
            $imageResource = @imagecreatefrompng(__DIR__ . '/../../Resources/Public/Images/TestInput/Test.png');
            if ($this->checkImageResource($imageResource)) {
                $this->messageQueue->enqueue(new FlashMessage(
                    '',
                    'PHP GD library has png support'
                ));
            } else {
                $this->messageQueue->enqueue(new FlashMessage(
                    'GD is compiled with png support, but calling imagecreatefrompng() fails.'
                        . ' Check your environment and fix it, png in GD lib is important'
                        . ' for TYPO3 CMS to work properly.',
                    'PHP GD library png support broken',
                    ContextualFeedbackSeverity::ERROR
                ));
            }
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                'GD must be compiled with png support. This is essential for TYPO3 CMS to work properly',
                'PHP GD library png support missing',
                ContextualFeedbackSeverity::ERROR
            ));
        }
    }

    /**
     * Check gdlib supports freetype
     */
    protected function checkGdLibFreeTypeSupport()
    {
        if (function_exists('imagettftext')) {
            $this->messageQueue->enqueue(new FlashMessage(
                'There is a difference between the font size setting which the GD'
                    . ' library should be supplied  with. If installation is completed'
                    . ' a test in the install tool helps to find out the value you need.',
                'PHP GD library has freetype font support'
            ));
        } else {
            $this->messageQueue->enqueue(new FlashMessage(
                'Some core functionality and extension rely on the GD'
                    . ' to render fonts on images. This support is missing'
                    . ' in your environment. Install it.',
                'PHP GD library freetype support missing',
                ContextualFeedbackSeverity::ERROR
            ));
        }
    }

    /**
     * Helper methods
     */

    /**
     * Validate a given IP address.
     *
     * @param string $ip IP address to be tested
     * @return bool
     */
    protected function isValidIp($ip)
    {
        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
    }

    /**
     * Test if this instance runs on windows OS
     *
     * @return bool TRUE if operating system is windows
     */
    protected function isWindowsOs()
    {
        $windowsOs = false;
        if (stripos(PHP_OS, 'darwin') === false && stripos(PHP_OS, 'win') !== false) {
            $windowsOs = true;
        }
        return $windowsOs;
    }

    /**
     * Helper method to get the bytes value from a measurement string like "100k".
     *
     * @param string $measurement The measurement (e.g. "100k")
     * @return int The bytes value (e.g. 102400)
     */
    protected function getBytesFromSizeMeasurement($measurement)
    {
        $bytes = (float)$measurement;
        if (stripos($measurement, 'G')) {
            $bytes *= 1024 * 1024 * 1024;
        } elseif (stripos($measurement, 'M')) {
            $bytes *= 1024 * 1024;
        } elseif (stripos($measurement, 'K')) {
            $bytes *= 1024;
        }
        return (int)$bytes;
    }

    private function checkImageResource($imageResource): bool
    {
        return $imageResource instanceof \GdImage;
    }
}
