<?php
/*
 * This file is part of MODX Revolution.
 *
 * Copyright (c) MODX, LLC. All Rights Reserved.
 *
 * For complete copyright and license information, see the COPYRIGHT and LICENSE
 * files found in the top-level directory of this distribution.
 */

/**
 * Runs tests on the server to determine if MODX can be installed
 *
 * @package setup
 * @subpackage tests
 */
abstract class modInstallTest
{
    public $results = [
        'pass' => [],
        'fail' => [],
        'warn' => [],
    ];
    /** @var modInstall $install */
    public $install;
    public $mode;
    public $success = true;

    public function __construct(&$install)
    {
        $this->install =& $install;
    }

    /**
     * Run tests.
     *
     * @param int $mode The install mode.
     * @return array An array of result messages collected during the process.
     */
    public function run($mode = modInstall::MODE_NEW)
    {
        $this->results = [];
        $this->mode = $mode;

        $this->_checkPHPVersion();
        $this->_checkDependencies();
        $this->_checkMemoryLimit();
        $this->_checkCache();
        $this->_checkExport();
        $this->_checkPackages();
        $this->_checkContexts();
        $this->_checkConfig();
        $this->_checkDatabase();
        $this->_checkSuhosin();
        $this->_checkSessionsGarbageCollector();
        $this->_checkNoCompress();
        $this->_checkDocumentRoot();

        return $this->results;
    }

    /**
     * Checks PHP version
     */
    protected function _checkPHPVersion()
    {
        $this->title('php_version', $this->install->lexicon('test_php_version_start') . ' ');
        $phpVersion = phpversion();

        $recommended_version = "8.0.0";
        $required_version = "7.4.0";

        $php_ver_comp = version_compare($phpVersion, $required_version, '>=');

        /* -1 if left is less, 0 if equal, +1 if left is higher */
        if (!$php_ver_comp) {
            $this->fail(
                'php_version',
                '',
                $this->install->lexicon('test_php_version_fail', [
                    'version' => $phpVersion
                    ,
                    'required' => $required_version
                    ,
                    'recommended' => $recommended_version
                ])
            );
        } else {
            $this->pass('php_version', $this->install->lexicon('test_php_version_success', ['version' => $phpVersion]));
        }
    }

    /**
     * Titles the check message
     *
     * @param string $key The check being titled
     * @param string $title The title of the check
     */
    protected function title($key, $title)
    {
        if (!isset($this->results[$key])) {
            $this->results[$key] = [];
        }
        $this->results[$key]['msg'] = '<p>' . $title;
    }

    /**
     * Denotes a check message that is a failure
     *
     * @param string $key The check being performed
     * @param string $title The failure message title.
     * @param string $message A detailed failure message.
     * @return bool
     */
    protected function fail($key, $title = '', $message = '')
    {
        if (empty($title)) {
            $title = $this->install->lexicon('failed');
        }
        $msg = '<p class="notok">' . $title . '</p>';
        if (!empty($message)) {
            $msg .= '<p><strong>' . $message . '</strong></p>';
        }
        if (!isset($this->results[$key])) {
            $this->results[$key] = [];
        }
        if (!isset($this->results[$key]['msg'])) {
            $this->results[$key]['msg'] = '';
        }
        $this->results[$key]['msg'] .= $msg;
        $this->results[$key]['class'] = 'testFailed';


        if (!isset($this->results['fail'][$key])) {
            $this->results['fail'][$key] = [];
        }
        if (!isset($this->results['fail'][$key]['msg'])) {
            $this->results['fail'][$key]['msg'] = '';
        }
        $this->results['fail'][$key]['msg'] .= $msg;
        $this->results['fail'][$key]['class'] = 'testFailed';
        $this->results['fail'][$key]['title'] = $title;
        $this->results['fail'][$key]['message'] = $message;
        $this->success = false;
        return true;
    }

    /**
     * Denotes a check passed successfully
     *
     * @param string $key The check being performed
     * @param string $message The success message.
     */
    protected function pass($key, $message = '')
    {
        if (empty($message)) {
            $message = $this->install->lexicon('ok');
        }

        if (!isset($this->results[$key])) {
            $this->results[$key] = [];
        }
        if (!isset($this->results[$key]['msg'])) {
            $this->results[$key]['msg'] = '';
        }
        $this->results[$key]['msg'] .= '<span class="ok">' . $message . '</span></p>';
        $this->results[$key]['class'] = 'testPassed';

        if (!isset($this->results['pass'][$key])) {
            $this->results['pass'][$key] = [];
        }
        if (!isset($this->results['pass'][$key]['msg'])) {
            $this->results['pass'][$key]['msg'] = '';
        }
        $this->results['pass'][$key]['msg'] .= '<span class="ok">' . $message . '</span></p>';
        $this->results['pass'][$key]['class'] = 'testPassed';
        $this->results['pass'][$key]['message'] = $message;
    }

    /**
     * Check to see if PHP has zlib and SimpleXML installed.
     *
     * @access public
     */
    protected function _checkDependencies()
    {
        $this->title('dependencies', $this->install->lexicon('test_dependencies') . ' ');
        /* check for zlib */
        if (!extension_loaded('zlib')) {
            $this->fail('dependencies', '', $this->install->lexicon('test_dependencies_fail_zlib'));
        } else {
            $this->pass('dependencies');
        }

        /* check for SimpleXML */
        $this->title('simplexml', $this->install->lexicon('test_simplexml') . ' ');
        if (!function_exists('simplexml_load_string')) {
            $this->warn(
                'simplexml',
                '',
                $this->install->lexicon('test_simplexml_nf_msg'),
                $this->install->lexicon('test_simplexml_nf')
            );
        } else {
            $this->pass('simplexml');
        }
    }

    /**
     * Denotes a check message that is a warning
     *
     * @param string $key The check being performed
     * @param string $title The warning message title.
     * @param string $message A detailed warning message.
     * @param string $messageTitle An optional title for the detail panel.
     * @return bool
     */
    protected function warn($key, $title, $message = '', $messageTitle = '')
    {
        if (empty($title)) {
            $title = $this->install->lexicon('warning');
        }
        $msg = '<p class="notok">' . $title . '</p>';
        if (!empty($message)) {
            $msg .= '<div class="notes">';
            if (!empty($messageTitle)) {
                $msg .= '<h3>' . $messageTitle . '</h3>';
            }
            $msg .= '<p>' . $message . '</p></div>';
        }
        if (!isset($this->results[$key])) {
            $this->results[$key] = [];
        }
        if (!isset($this->results[$key]['msg'])) {
            $this->results[$key]['msg'] = '';
        }

        $this->results[$key]['msg'] .= $msg;
        $this->results[$key]['class'] = 'testWarn';

        if (!isset($this->results['warn'][$key])) {
            $this->results['warn'][$key] = [];
        }
        if (!isset($this->results['warn'][$key]['msg'])) {
            $this->results['warn'][$key]['msg'] = '';
        }
        $this->results['warn'][$key]['msg'] .= $msg;
        $this->results['warn'][$key]['class'] = 'testPassed';
        $this->results['warn'][$key]['title'] = $title;
        $this->results['warn'][$key]['message'] = $message;
        $this->results['warn'][$key]['messageTitle'] = $messageTitle;
        return true;
    }

    /**
     * Check memory limit, to make sure it is set at least to 32M
     */
    protected function _checkMemoryLimit()
    {
        $success = false;
        $ml = @ini_get('memory_limit');
        $bytes = $this->_returnBytes($ml);

        if ($bytes < 25165824) { /* 24M = 25165824, 32M = 33554432, 64M = 67108864 */
            $success = @ini_set('memory_limit', '24M');
            $success = $success !== false ? true : false;
        } else {
            $success = true;
        }

        $this->title('memory_limit', $this->install->lexicon('test_memory_limit') . ' ');
        if ($success) {
            $this->pass('memory_limit', $this->install->lexicon('test_memory_limit_success', ['memory' => $ml]));
        } else {
            $this->fail('memory_limit', '', $this->install->lexicon('test_memory_limit_fail', ['memory' => $ml]));
        }
    }

    /**
     * Helper function that converts php.ini memory string settings to bytes
     *
     * @access private
     * @param string $val The byte string
     * @return int The string converted into a proper integer bytes
     */
    protected function _returnBytes($val)
    {
        if (is_integer($val)) {
            return $val;
        }
        $lval = strtolower(trim($val));
        if (strpos($lval, 'm') === false && strpos($lval, 'g') === false && strpos($lval, 'k') === false && intval(
                trim($lval)
            ) > 0) {
            return intval($lval);
        }
        $num = intval(substr($lval, 0, strlen($lval) - 1));
        $last = substr($lval, -1);
        switch ($last) {
            case 'g':
                $num *= 1024;
            case 'm':
                $num *= 1024;
            case 'k':
                $num *= 1024;
        }
        return $num;
    }

    /**
     * Check if cache exists and is writable. Should in theory never fail.
     */
    protected function _checkCache()
    {
        /* cache exists? */
        $this->title(
            'cache_exists',
            $this->install->lexicon(
                'test_directory_exists',
                ['dir' => MODX_CORE_PATH . 'cache']
            )
        );
        if (!file_exists(MODX_CORE_PATH . 'cache')) {
            $this->fail('cache_exists');
        } else {
            $this->pass('cache_exists');
        }

        /* cache writable? */
        $this->title(
            'cache_writable',
            $this->install->lexicon(
                'test_directory_writable',
                ['dir' => MODX_CORE_PATH . 'cache']
            )
        );
        if (!$this->is_writable2(MODX_CORE_PATH . 'cache')) {
            $this->fail('cache_writable');
        } else {
            $this->pass('cache_writable');
        }
    }

    /**
     * Custom is_writable function to test on problematic servers
     *
     * @param string $path
     * @return bool True if write was successful
     */
    protected function is_writable2($path)
    {
        return $this->install->is_writable2($path);
    }

    /**
     * Check if core/export exists and is writable
     */
    protected function _checkExport()
    {
        /* export exists? */
        $this->title(
            'assets_export_exists',
            $this->install->lexicon(
                'test_directory_exists',
                ['dir' => MODX_CORE_PATH . 'export']
            )
        );
        if (!file_exists(MODX_CORE_PATH . 'export')) {
            $this->fail('assets_export_exists');
        } else {
            $this->pass('assets_export_exists');
        }

        /* export writable? */
        $this->title(
            'assets_export_writable',
            $this->install->lexicon(
                'test_directory_writable',
                ['dir' => MODX_CORE_PATH . 'export']
            )
        );
        if (!$this->is_writable2(MODX_CORE_PATH . 'export')) {
            $this->fail('assets_export_writable');
        } else {
            $this->pass('assets_export_writable');
        }
    }

    /**
     * Verify if core/packages exists and is writable
     */
    protected function _checkPackages()
    {
        /* core/packages exists? */
        $this->title(
            'core_packages_exists',
            $this->install->lexicon(
                'test_directory_exists',
                ['dir' => MODX_CORE_PATH . 'packages']
            )
        );
        if (!file_exists(MODX_CORE_PATH . 'packages')) {
            $this->fail('core_packages_exists');
        } else {
            $this->pass('core_packages_exists');
        }

        /* packages writable? */
        $this->title(
            'core_packages_writable',
            $this->install->lexicon(
                'test_directory_writable',
                ['dir' => MODX_CORE_PATH . 'packages']
            )
        );
        if (!$this->is_writable2(MODX_CORE_PATH . 'packages')) {
            $this->fail('core_packages_writable');
        } else {
            $this->pass('core_packages_writable');
        }
    }

    /**
     * Check context paths if inplace, else make sure paths can be written
     */
    protected function _checkContexts()
    {
        $coreConfigsExist = false;
        if ($this->install->settings->get('inplace')) {
            /* web_path */
            $this->title(
                'context_web_exists',
                $this->install->lexicon(
                    'test_directory_exists',
                    ['dir' => $this->install->settings->get('context_web_path')]
                )
            );
            if (!file_exists($this->install->settings->get('context_web_path'))) {
                $this->fail('context_web_exists');
            } else {
                $this->pass('context_web_exists');
            }

            /* mgr_path */
            $this->title(
                'context_mgr_exists',
                $this->install->lexicon(
                    'test_directory_exists',
                    ['dir' => $this->install->settings->get('context_mgr_path')]
                )
            );
            if (!file_exists($this->install->settings->get('context_mgr_path'))) {
                $this->fail('context_mgr_exists');
            } else {
                $this->pass('context_mgr_exists');
            }

            /* connectors_path */
            $this->title(
                'context_connectors_exists',
                $this->install->lexicon(
                    'test_directory_exists',
                    ['dir' => $this->install->settings->get('context_connectors_path')]
                )
            );
            if (!file_exists($this->install->settings->get('context_connectors_path'))) {
                $this->fail('context_connectors_exists');
            } else {
                $this->pass('context_connectors_exists');
            }
            if (file_exists($this->install->settings->get('context_web_path') . 'config.core.php') &&
                file_exists($this->install->settings->get('context_connectors_path') . 'config.core.php') &&
                file_exists($this->install->settings->get('context_mgr_path') . 'config.core.php')) {
                $coreConfigsExist = true;
            }
        }
    }

    /**
     * Config file writable?
     */
    protected function _checkConfig()
    {
        $configFileDisplay = 'config/' . MODX_CONFIG_KEY . '.inc.php';
        $configFilePath = MODX_CORE_PATH . $configFileDisplay;
        $this->title('config_writable', $this->install->lexicon('test_config_file', ['file' => $configFilePath]));
        if (!file_exists($configFilePath)) {
            /* make an attempt to create the file */
            @ $hnd = fopen($configFilePath, 'w');
            @ fwrite($hnd, '<?php // ' . $this->install->lexicon('modx_configuration_file') . ' ?>');
            @ fclose($hnd);
        }
        $isWriteable = @is_writable($configFilePath);
        if (!$isWriteable) {
            $this->fail(
                'config_writable',
                $this->install->lexicon('failed'),
                $this->install->lexicon(
                    'test_config_file_nw',
                    ['key' => MODX_CONFIG_KEY]
                )
            );
        } else {
            $this->pass('config_writable');
        }
    }

    /**
     * Check connection to database, as well as table prefix
     */
    protected function _checkDatabase()
    {
        /* connect to the database */
        $this->title('dbase_connection', $this->install->lexicon('test_db_check'));
        $GLOBALS['database_dsn'] = $this->install->settings->get('database_dsn');
        $GLOBALS['config_options'] = $this->install->settings->get('config_options');
        $xpdo = $this->install->getConnection();
        if (!$xpdo || !$xpdo->connect()) {
            if ($this->mode > modInstall::MODE_NEW) {
                $this->fail(
                    $this->install->lexicon('dbase_connection'),
                    $this->install->lexicon('test_db_failed'),
                    $this->install->lexicon('test_db_check_conn')
                );
            } else {
                $this->warn(
                    $this->install->lexicon('dbase_connection'),
                    $this->install->lexicon('test_db_failed'),
                    $this->install->lexicon('test_db_setup_create')
                );
            }
        } else {
            $this->pass('dbase_connection');
        }
    }

    /**
     * Check for suhosin extension, and if found and suhosin.get.max_value_length is less than 1024, throw a warning
     * and force compress_js off.
     */
    public function _checkSuhosin()
    {
        $this->title('test_suhosin_max_length', $this->install->lexicon('test_suhosin'));
        $maxLength = (int)@ini_get('suhosin.get.max_value_length');
        if ($maxLength != 0 && $maxLength !== null && $maxLength > 0 && $maxLength < 1024) {
            $this->warn(
                $this->install->lexicon('test_suhosin_max_length'),
                $this->install->lexicon('warning'),
                $this->install->lexicon('test_suhosin_max_length_err')
            );
            $this->install->settings->set('compress_js', 0);
        } else {
            $this->pass('test_suhosin_max_length');
            $this->install->settings->set('compress_js', 1);
        }
        $this->install->settings->store();
    }

    /**
     * Check sessions garbage collector
     */
    protected function _checkSessionsGarbageCollector()
    {
        $status = 'success';
        $gc_probability = (int)@ini_get('session.gc_probability');
        $gc_divisor = (int)@ini_get('session.gc_divisor');

        if (!$gc_probability) {
            $status = @ini_set('session.gc_probability', 1) !== false ? 'success' : 'fail';
        }

        $this->title('session_gc', $this->install->lexicon('test_session_gc'));
        $this->warn('session_gc', '', $this->install->lexicon("test_session_gc_$status", ['gc_probability' => $gc_probability, 'gc_divisor' => $gc_divisor]));
    }

    /**
     * Check if the user requested css/js compression to be off, regardless of Suhosin check result.
     * Force css/js compression to be off if the option was checked during install (adv options).
     */
    public function _checkNoCompress()
    {
        $this->title('test_nocompress', $this->install->lexicon('test_nocompress'));
        if ($this->install->settings->get('nocompress') == 1) {
            $this->pass('test_nocompress', $this->install->lexicon('test_nocompress_disabled'));
            $this->install->settings->set('compress_js', 0);
            $this->install->settings->set('compress_css', 0);
        } else {
            $this->pass('test_nocompress', $this->install->lexicon('test_nocompress_skip'));
        }
        $this->install->settings->store();
    }

    /**
     * If the server document root is empty, don't do JS/CSS compression until they set it up manually.
     * @return void
     */
    public function _checkDocumentRoot()
    {
        $documentRoot = $_SERVER['DOCUMENT_ROOT'];
        if (empty($documentRoot)) {
            $this->install->settings->set('compress_js', 0);
            $this->install->settings->set('compress_css', 0);
        }
        $this->install->settings->store();
    }
}
