<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    health_check
 */

/**
 * Hook class.
 */
class Hook_health_check_install_env extends Hook_Health_Check
{
    protected $category_label = 'Installation environment';

    /**
     * Standard hook run function to run this category of health checks.
     *
     * @param  ?array $sections_to_run Which check sections to run (null: all)
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     * @param  boolean $show_unusable_categories Whether to include categories that might not be accessible for some reason
     * @return array A pair: category label, list of results
     */
    public function run(?array $sections_to_run, int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null, bool $show_unusable_categories = false) : array
    {
        $this->process_checks_section('testServerSoftware', 'Server software', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testSuExec', 'suEXEC', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testDirectoryName', 'Directory naming', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testBaseURL', 'Base URL', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testPHPVersion', 'PHP version', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testPHPPlatform', 'PHP platform', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testMySQLVersion', 'MySQL version', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testMySQLSettings', 'MySQL settings', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testForInjectedAdScripts', 'Injected Ad Scripts', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testModSecurity', 'ModSecurity', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testDiskSpaceInstallation', 'Disk Space (Installation)', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testUnicode', 'Database Unicode settings', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testPCRE', 'PCRE settings', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testSELinux', 'SELinux settings', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testUmask', 'Umask settings', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);
        $this->process_checks_section('testLocaleStability', 'Locale stability', $sections_to_run, $check_context, $manual_checks, $automatic_repair, $use_test_data_for_pass, $urls_or_page_links, $comcode_segments);

        return [$this->category_label, $this->results];
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testServerSoftware(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $server_software = $_SERVER['SERVER_SOFTWARE'];

        if ($server_software == '') {
            $this->stateCheckSkipped('Could not detect web server software');
            return;
        }

        /*$unsupported_server_software = ['lighttpd', 'Tengine', 'nginx', 'IdeaWebServer'];
        foreach ($unsupported_server_software as $_server_software) {
            $this->assertTrue((stripos($server_software, $_server_software) === false), do_lang('WARNING_SERVER_SOFTWARE', $_server_software));
        }*/

        $supported_server_software = ['LiteSpeed', 'Apache', 'nginx',  'Microsoft-IIS'];
        $supported = false;
        foreach ($supported_server_software as $_server_software) {
            if (stripos($server_software, $_server_software) !== false) {
                $supported = true;
            }
        }
        $this->assertTrue($supported, do_lang('WARNING_SERVER_SOFTWARE', $server_software));

        $matches = [];
        if (preg_match('#^Apache/([\d\.]*)#', $server_software, $matches) != 0) {
            $minimum_version = '2.4'; // LEGACY also maintain in tut_webhosting.txt

            $version = $matches[1];
            $apache_too_old = version_compare($version, $minimum_version, '<');
            $this->assertTrue(!$apache_too_old, do_lang('APACHE_TOO_OLD', $minimum_version, $version));

            $max_tested_apache_version = '2.4'; // LEGACY needs maintaining
            if (!is_maintained('platform_apache')) {
                $apache_too_new = version_compare($version, $max_tested_apache_version . '.1000', '>');
                $this->assertTrue(
                    !$apache_too_new,
                    '[html]' . do_lang('WARNING_NON_MAINTAINED', do_lang('APACHE_TOO_NEW', escape_html($max_tested_apache_version)), escape_html(get_brand_base_url()), escape_html('apache')) . '[/html]'
                );
            }
        }

        if (!is_maintained('platform_nginx')) {
            $this->assertTrue(
                (stripos($server_software, 'nginx') === false),
                '[html]' . do_lang('WARNING_NON_MAINTAINED', escape_html('NGINX'), escape_html(get_brand_base_url()), escape_html('platform_nginx')) . '[/html]'
            );
        }
        if (!is_maintained('platform_litespeed')) {
            $this->assertTrue(
                (stripos($server_software, 'LiteSpeed') === false),
                '[html]' . do_lang('WARNING_NON_MAINTAINED', escape_html('LiteSpeed'), escape_html(get_brand_base_url()), escape_html('platform_litespeed')) . '[/html]'
            );
        }
        if (!is_maintained('platform_iis')) {
            $this->assertTrue(
                (stripos($server_software, 'Microsoft-IIS') === false),
                '[html]' . do_lang('WARNING_NON_MAINTAINED', escape_html('Microsoft IIS'), escape_html(get_brand_base_url()), escape_html('platform_iis')) . '[/html]'
            );
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testSuExec(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (!is_maintained('platform_chmod')) {
            $this->assertTrue(is_suexec_like(), '[html]' . do_lang('WARNING_SUEXEC', escape_html(get_brand_base_url()), escape_html('platform_chmod')) . '[/html]');
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testDirectoryName(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $this->assertTrue((strpos(get_file_base(), '_custom') === false), 'You should not have _custom in the base directory path, due to an internal coding limitation.');
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testBaseURL(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $installing = ($check_context == CHECK_CONTEXT__INSTALL);

        if (file_exists(get_file_base() . '/data/empty.php')) {
            $test_url = get_base_url() . '/data/empty.php'; // Should normally exist, simple static URL call
        } elseif ($installing) {
            $test_url = get_base_url() . '/install.php'; // But this definitely must exist if running the installer
        } else {
            $test_url = static_evaluate_tempcode(build_url(['page' => ''], '', [], false, false, true)); // This absolutely must exist otherwise
        }

        $test = cms_http_request($test_url, ['byte_limit' => 1, 'trigger_error' => false, 'no_redirect' => true]); // Should return a 200 blank, not an HTTP error or a redirect; actual data would be a software error

        $has_www = (strpos(get_base_url(), '://www.') !== false);

        if (in_array($test->message, ['200'])) {
            // Is okay
        }

        if (in_array($test->message, ['401', '403'])) {
            // Is access denied, which could happen so isn't an error from our point of view
        }

        if (($installing) && ($test->message == '500')) {
            // May be the final configuration isn't placed yet by the installer
        }

        // Redirect
        $a = do_lang($installing ? '_HTTP_REDIRECT_PROBLEM_INSTALLING' : '_HTTP_REDIRECT_PROBLEM_RUNNING', get_base_url() . '/config_editor.php');
        $b = do_lang($has_www ? '_WITH_WWW' : '_WITHOUT_WWW', get_base_url() . '/config_editor.php');
        $this->assertTrue(
            !in_array($test->message, ['301', '302', '307']),
            do_lang('HTTP_REDIRECT_PROBLEM', $a, $b, $test->message)
        );

        // Some kind of error state that we shouldn't ever be expecting
        if ($installing) {
            $a = do_lang('_IP_FORWARDING_INSTALLING');
        } else {
            $has_ip_forwarding = !((get_option('ip_forwarding') == '0') || (get_option('ip_forwarding') == ''));
            $a = do_lang($has_ip_forwarding ? '_IP_FORWARDING_ENABLED' : '_IP_FORWARDING_DISABLED');
        }
        $this->assertTrue(
            (!in_array($test->message, ['400', '404', '500', 'no-data', '408', '502', '503', '504'])) && ($test->data !== null),
            do_lang('IP_FORWARDING_CHANGE', $a, do_lang('config:IP_FORWARDING'), $test->message)
        );
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testPHPVersion(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $php_too_old = version_compare(PHP_VERSION, '7.2', '<'); // LEGACY: Also maintain in tut_webhosting.txt, install.php, restore.php.pre, _standard_dir_files.php
        $this->assertTrue(!$php_too_old, do_lang('PHP_TOO_OLD', '7.2'));

        $max_tested_php_version = '8.3'; // LEGACY: This needs to keep raising (also it is in tut_webhosting.txt, _standard_dir_files.php)
        if (!is_maintained('php')) {
            $php_too_new = version_compare(PHP_VERSION, $max_tested_php_version . '.1000', '>'); // LEGACY needs maintaining
            $this->assertTrue(
                !$php_too_new,
                '[html]' . do_lang('WARNING_NON_MAINTAINED', do_lang('PHP_TOO_NEW', escape_html($max_tested_php_version)), escape_html(get_brand_base_url()), escape_html('php')) . '[/html]'
            );
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testPHPPlatform(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (!is_maintained('platform_gae')) {
            $this->assertTrue(
                !GOOGLE_APPENGINE,
                '[html]' . do_lang('WARNING_NON_MAINTAINED', escape_html('Google App Engine'), escape_html(get_brand_base_url()), escape_html('platform_gae')) . '[/html]'
            );
        }

        if (!is_maintained('platform_peachpie')) {
            $this->assertTrue(
                !defined('PEACHPIE_VERSION'),
                '[html]' . do_lang('WARNING_NON_MAINTAINED', escape_html('PeachPie'), escape_html(get_brand_base_url()), escape_html('platform_peachpie')) . '[/html]'
            );
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testMySQLVersion(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $minimum_version = '5.5.3'; // LEGACY also maintain in tut_webhosting.txt
        // ^ Why? We need this for proper Unicode support: https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-utf8mb4.html

        // If you really need to fiddle it and don't care about emoji, add this to _config.php while installing (before step 5 runs):   $SITE_INFO['database_charset'] = 'utf8';

        $version = null;

        if (isset($GLOBALS['SITE_DB']->connection_write)) {
            $GLOBALS['SITE_DB']->ensure_connected();

            if (strpos(get_db_type(), 'mysql') !== false) {
                $version = $GLOBALS['SITE_DB']->query_value_if_there('SELECT version()');
            } else {
                $this->stateCheckSkipped('Not running MySQL (or MariaDB)');
            }
        } else {
            // Probably running from the installer, let's see if we can detect MySQL client via command line (we'll assume user is a MySQL user as that is the default)
            if (php_function_allowed('shell_exec')) {
                $_version = shell_exec('mysql -V');
                if ($_version !== null) {
                    $matches = [];
                    if (preg_match('#Distrib ([^\s]*)#', $_version, $matches) != 0) {
                        $version = $matches[1];
                    } elseif (preg_match('#Ver ([^\s]*)#', $_version, $matches) != 0) {
                        $version = $matches[1];
                    }
                }
            }

            if ($version === null) {
                $this->stateCheckSkipped('Not running MySQL (or MariaDB) / No active database connection');
            }
        }

        if ($version !== null) {
            if (stripos($version, 'maria') !== false) {
                $mariadb_too_old = version_compare($version, $minimum_version, '<');
                $this->assertTrue(!$mariadb_too_old, do_lang('MARIADB_TOO_OLD', $minimum_version, $version));

                $max_tested_mariadb_version = '10.6'; // LEGACY needs maintaining
                if (!is_maintained('mariadb')) {
                    $mariadb_too_new = version_compare($version, $max_tested_mariadb_version . '.1000', '>');
                    $this->assertTrue(
                        !$mariadb_too_new,
                        '[html]' . do_lang('WARNING_NON_MAINTAINED', do_lang('MARIADB_TOO_NEW', escape_html($max_tested_mariadb_version)), escape_html(get_brand_base_url()), escape_html('mariadb')) . '[/html]'
                    );
                }
            } else {
                $mysql_too_old = version_compare($version, $minimum_version, '<');
                $this->assertTrue(!$mysql_too_old, do_lang('MYSQL_TOO_OLD', $minimum_version, $version));

                $max_tested_mysql_version = '8.2'; // LEGACY needs maintaining
                if (!is_maintained('mysql')) {
                    $mysql_too_new = version_compare($version, $max_tested_mysql_version . '.1000', '>');
                    $this->assertTrue(
                        !$mysql_too_new,
                        '[html]' . do_lang('WARNING_NON_MAINTAINED', do_lang('MYSQL_TOO_NEW', escape_html($max_tested_mysql_version)), escape_html(get_brand_base_url()), escape_html('mysql')) . '[/html]'
                    );
                }
            }
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testMySQLSettings(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return;
        }

        if (strpos(get_db_type(), 'mysql') === false) {
            $this->stateCheckSkipped('Not running MySQL (or MariaDB)');
            return;
        }

        $min = 1024 * 1024 * 4; // 4MB; if you change this value, also change it in the installer confirm_db_credentials.

        if (isset($GLOBALS['SITE_DB']->connection_write)) {
            $vars = $GLOBALS['SITE_DB']->query('SHOW VARIABLES LIKE \'max_allowed_packet\'');
            foreach ($vars as $var) {
                $current = intval($var['Value']);
                $this->assertTrue($current >= $min, do_lang('MAX_ALLOWED_PACKET_TOO_LOW', integer_format($min), integer_format($current)));
            }
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testForInjectedAdScripts(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $url = get_base_url() . '/install.php?type=test_blank_result';
        } else {
            $url = find_script('empty') . '?truly=1';
        }

        $blank = (http_get_contents($url, ['trigger_error' => false, 'timeout' => 1.0]) === '');
        $this->assertTrue($blank, do_lang('INTERFERING_AD_SCRIPT'));
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testModSecurity(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        // Test to see if we have any ModSecurity issue that blocks config form submissions, via posting through some perfectly legitimate things that it might be paranoid about
        $test_url = get_custom_base_url() . '/data/empty.php';
        $test_a = cms_http_request($test_url, ['byte_limit' => 0, 'trigger_error' => false, 'no_redirect' => true, 'post_params' => ['test_a' => '/usr/bin/unzip -o @_SRC_@ -x -d @_DST_@', 'test_b' => '<iframe src="http://example.com/"></iframe>', 'test_c' => '<script>console.log(document.cookie);</script>']]);
        $message_a = $test_a->message;
        if ($message_a != '200') {
            $test_b = cms_http_request($test_url, ['byte_limit' => 0, 'trigger_error' => false, 'no_redirect' => true]);
            $message_b = $test_b->message;
            if ($message_b == '200') {
                $this->assertTrue(false, do_lang('MODSECURITY', $message_a));
            } else {
                $this->assertTrue(true, do_lang('MODSECURITY', do_lang('NA')));
            }
        } else {
            $this->assertTrue(true, do_lang('MODSECURITY', do_lang('NA')));
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testDiskSpaceInstallation(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context != CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are not running from installer.');
            return;
        }

        if (php_function_allowed('disk_free_space')) {
            require_code('files');

            $disk_space = @disk_free_space(get_file_base());
            $min_disk_space = 250 * 1024 * 1024;
            $low_disk_space = ((is_integer($disk_space)) && ($disk_space < $min_disk_space));
            $this->assertTrue(!$low_disk_space, do_lang('WARNING_DISK_SPACE', clean_file_size($min_disk_space)));
        } else {
            $this->stateCheckSkipped('PHP [tt]disk_free_space[/tt] function not available');
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testUnicode(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if (!isset($GLOBALS['SITE_DB'])) {
            return;
        }

        $cnt = $GLOBALS['SITE_DB']->query_value_if_there('SELECT ' . db_function('LENGTH', ['\'' . db_escape_string("\u{00A3}") . '\'']));
        $this->assertTrue($cnt == 1, 'Database connection is not encoding Unicode properly (non-latin characters, utf8)');

        $cnt = $GLOBALS['SITE_DB']->query_value_if_there('SELECT ' . db_function('LENGTH', ['\'' . db_escape_string("\u{1F60E}") . '\'']));
        $this->assertTrue($cnt == 1, 'Database connection is not encoding Unicode properly (emojis, utf8mb4)');
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testPCRE(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        $this->assertTrue(preg_replace('#\n#', '', "\n") == '', 'PHP is using a non-default copy of the PCRE library that is not configured with the usual line endings, which will cause some major problems'); // Checks correct line endings
        $this->assertTrue(@preg_replace('#\n#u', '', "\n") !== false, 'PCRE does not have inbuilt Unicode support, which means Unicode may not work correctly'); // Checks correct line endings
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testSELinux(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context != CHECK_CONTEXT__LIVE_SITE) {
            $this->log('Skipped; we are not running from a live site.');
            return;
        }

        if (!$manual_checks) {
            return;
        }

        if (php_function_allowed('shell_exec')) {
            $result = @strval(shell_exec('sestatus'));
            if (strpos($result, 'enabled') !== false) {
                $this->stateCheckManual('SELinux is enabled, check the httpd_sys_rw_content_t context is recursively applied to your webroot');
            } elseif (strpos($result, 'disabled') !== false) {
                $this->stateCheckSkipped('SELinux not enabled');
            } else {
                $this->stateCheckSkipped('SELinux probably not enabled');
            }
        } else {
            $this->stateCheckSkipped('PHP [tt]shell_exec[/tt] function not available');
        }
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    public function testUmask(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context != CHECK_CONTEXT__LIVE_SITE) {
            $this->log('Skipped; we are not running from a live site.');
            return;
        }

        // Test problems with lack of umask-derived permissions when Apache is not running as web user (possible in various cases, e.g. suPHP causes PHP to use web user but not therefore for static requests)...

        // File test
        $filename = 'hc-test.html';
        $subdir = '';
        $result = $this->runUmaskTest($filename, $subdir);
        if ($result !== null) {
            list($ok, $perms, $dir_perms) = $result;
            $has_group_read_perms = (($perms & 0040) != 0);
            $this->assertTrue($ok, 'Could not access test static file created with default permissions, group read permissions ' . ($has_group_read_perms ? 'were' : 'were NOT') . ' set by default umask' . ((!$has_group_read_perms) ? ' (likely the web server itself is running as a different server to PHP is executed with so relies on the group permissions)' : ''));
        }

        // Directory test
        if ($ok) { // Only for this test if previous test passed
            $filename = 'hc-test.html';
            $subdir = 'test/';
            $result = $this->runUmaskTest($filename, $subdir);
            if ($result !== null) {
                list($ok, $perms, $dir_perms) = $result;
                $has_group_execute_perms = (($perms & 0010) != 0);
                $this->assertTrue($ok, 'Could not access test static file in directory created with default permissions, group execute permissions ' . ($has_group_execute_perms ? 'were' : 'were NOT') . ' set by default umask' . ((!$has_group_execute_perms) ? ' (likely the web server itself is running as a different server to PHP is executed with so relies on the group permissions)' : ''));
            }
        }

        // Test problems with excess of permissions (possible in various cases, e.g. suPHP allow_file_group_writeable/allow_directory_group_writeable/allow_file_others_writeable/allow_directory_others_writeable)...

        // File test
        $filename = 'hc-test.php';
        $subdir = '';
        $result = $this->runUmaskTest($filename, $subdir);
        if ($result !== null) {
            list($ok, $perms, $dir_perms) = $result;
            $has_excess_write_perms = (($perms & 0022) != 0);
            $this->assertTrue($ok, 'Could not access test PHP file created with default permissions, excess write permissions ' . ($has_excess_write_perms ? 'were' : 'were NOT') . ' set by default umask' . ((!$has_excess_write_perms) ? ' (likely you are using suPHP and wisely have allow_file_group_writeable or allow_file_others_writeable not set)' : ''));
        }

        // Directory test
        if ($ok) { // Only for this test if previous test passed
            $filename = 'hc-test.php';
            $subdir = 'test/';
            $result = $this->runUmaskTest($filename, $subdir);
            if ($result !== null) {
                list($ok, $perms, $dir_perms) = $result;
                $has_excess_write_perms = (($perms & 0022) != 0);
                $this->assertTrue($ok, 'Could not access test PHP file in directory created with default permissions, excess write permissions ' . ($has_excess_write_perms ? 'were' : 'were NOT') . ' set by default umask' . ((!$has_group_execute_perms) ? ' (likely you are using suPHP and wisely have allow_directory_group_writeable or allow_directory_others_writeable not set)' : ''));
            }
        }
    }

    /**
     * Run a umask check (creates a file and sees if web server can access it).
     *
     * @param  string $filename Filename
     * @param  string $subdir Subdirectory
     * @return ?array A tuple: Success status, Permissions, Directory permissions (null: error)
     */
    protected function runUmaskTest(string $filename, string $subdir) : ?array
    {
        $dir_path = get_file_base() . '/uploads/' . $subdir;
        $path = $dir_path . $filename;
        $url = get_base_url() . '/uploads/' . $subdir . $filename;

        if ($subdir != '') {
            // Create a directory using the default umask
            @call_user_func('mkdir', $dir_path); // In call_user_func to trick CQC, which demands a permissions parameter that we explicitly don't want for this test (testing for what happens with default umask)
            if (!is_dir($dir_path)) {
                $this->stateCheckSkipped('Could not create test directory');
                return null;
            }
        }

        @file_put_contents($path, 'test');  // Create a file using the default umask
        if (!is_file($path)) {
            $this->stateCheckSkipped('Could not create test file');
            return null;
        }

        $data = http_get_contents($url, ['trigger_error' => false]);
        $ok = (is_string($data)) && (strpos($data, 'test') !== false);

        $perms = fileperms($path);
        $dir_perms = fileperms($dir_path);

        // Cleanup
        if ($subdir != '') {
            @rmdir($dir_path);
        }
        @unlink($path);

        return [$ok, $perms, $dir_perms];
    }

    /**
     * Run a section of health checks.
     *
     * @param  integer $check_context The current state of the website (a CHECK_CONTEXT__* constant)
     * @param  boolean $manual_checks Mention manual checks
     * @param  boolean $automatic_repair Do automatic repairs where possible
     * @param  ?boolean $use_test_data_for_pass Should test data be for a pass [if test data supported] (null: no test data)
     * @param  ?array $urls_or_page_links List of URLs and/or page-links to operate on, if applicable (null: those configured)
     * @param  ?array $comcode_segments Map of field names to Comcode segments to operate on, if applicable (null: N/A)
     */
    protected function testLocaleStability(int $check_context, bool $manual_checks = false, bool $automatic_repair = false, ?bool $use_test_data_for_pass = null, ?array $urls_or_page_links = null, ?array $comcode_segments = null)
    {
        if ($check_context == CHECK_CONTEXT__SPECIFIC_PAGE_LINKS) {
            $this->log('Skipped; running on specific page links.');
            return;
        }

        if ($check_context == CHECK_CONTEXT__INSTALL) {
            $this->log('Skipped; we are running from installer.');
            return; // Too pedantic to confuse people at installation
        }

        if (function_exists('php_sapi_name')) {
            $current_locale = setlocale(LC_ALL, '0');
            setlocale(LC_ALL, 'tr_TR');
            $set_locale = setlocale(LC_ALL, '0');
            setlocale(LC_ALL, $current_locale);

            $this->assertTrue((php_sapi_name() != 'apache2handler') || (strpos($set_locale, 'tr_TR') === false), 'Threaded mode of PHP on a server with Turkish locale installed on server');
        }
    }
}
