<?php
/**
 * This file is a part of the CIDRAM package.
 * Homepage: https://cidram.github.io/
 *
 * CIDRAM COPYRIGHT 2016 and beyond by Caleb Mazalevskis (Maikuolan).
 *
 * License: GNU/GPLv2
 * @see LICENSE.txt
 *
 * This file: The CIDRAM front-end (last modified: 2026.01.14).
 */

namespace CIDRAM\CIDRAM;

class FrontEnd extends Core
{
    use AuxiliaryRules;
    use CLI;
    use Configuration;
    use FrontEndMethods;
    use Logs;
    use RangeTables;
    use SimulateBlockEvent;
    use Updater;

    /**
     * @var array For any front-end working data without dedicated properties.
     */
    public $FE = [];

    /**
     * @var array Used by the updater and file manager to store and work with
     *      local data about components.
     */
    public $Components = [];

    /**
     * @var array The path for front-end pages.
     */
    public $PagesPath = '';

    /**
     * @var bool Whether we're calling CIDRAM through an alternative pathway.
     */
    private $Alternate = false;

    /**
     * @var bool Whether to use colours for CLI output.
     * @link https://no-color.org/
     */
    private $NoColor = false;

    /**
     * @var int Lowest possible two-factor authentication code.
     */
    private const TWO_FACTOR_MIN_INT = 10000000;

    /**
     * @var int Highest possible two-factor authentication code.
     */
    private const TWO_FACTOR_MAX_INT = 99999999;

    /**
     * @var string Regular expression used to separate signature sections and tags.
     */
    private const REGEX_TAGS = '~(?<=\n)(?:\n|Expires: \d{4}\.\d\d\.\d\d|Origin: [A-Z]{2}|(?:\#|Tag: |Profile: |Defers to: )[^\n]+| *\/\*\*(?:\n *\*[^\n]*)*\/| *\/\*\*? [^\n*]+\*\/|---\n(?:[^\n:]+:(?:\n +[^\n:]+: [^\n]+)+)+)+\n~';

    /**
     * View the front-end.
     *
     * @return void
     */
    public function view(): void
    {
        /** Pages path. */
        $this->PagesPath = $this->Vault . 'pages' . DIRECTORY_SEPARATOR;

        /** Checks whether we're calling CIDRAM through an alternative pathway (e.g., Cronable). */
        $this->Alternate = class_exists('\Maikuolan\Cronable\Cronable');

        /** Load CIDRAM front-end L10N data. */
        $this->loadL10N($this->Vault . 'l10n' . DIRECTORY_SEPARATOR . 'frontend' . DIRECTORY_SEPARATOR);

        /** Initialise stages. */
        if ($this->Stages === []) {
            $this->Stages = array_flip(explode("\n", $this->Configuration['general']['stages']));
        }

        /** Initialise shorthand options. */
        if ($this->Shorthand === []) {
            $this->Shorthand = array_flip(explode("\n", $this->Configuration['signatures']['shorthand']));
        }

        /** Set page selector if not already set. */
        if (empty($this->CIDRAM['QueryVars']['cidram-page'])) {
            $this->CIDRAM['QueryVars']['cidram-page'] = '';
        }

        /** Populate common front-end variables. */
        $this->FE = [
            /** "Generated by ..." line. */
            'GeneratedBy' => sprintf(
                $this->L10N->getString('label.Generated by %s'),
                '<div class="txtRd" dir="ltr">' . $this->ScriptIdent . '</div>'
            ),

            /** Main front-end HTML template file. */
            'Template' => $this->readFile($this->getAssetPath('frontend.html')),

            /** Populated by front-end JavaScript data as per needed. */
            'JS' => '',

            /** Populated by any other header data required for the request (usually nothing). */
            'OtherHead' => '',

            /** Custom header data. */
            'CustomHeader' => $this->Configuration['frontend']['custom_header'],

            /** Custom footer data. */
            'CustomFooter' => $this->Configuration['frontend']['custom_footer'],

            /** Default password hash ("password"). */
            'DefaultPassword' => '$2y$10$FPF5Im9MELEvF5AYuuRMSO.QKoYVpsiu1YU9aDClgrU57XtLof/dK',

            /** Current default language. */
            'FE_Lang' => $this->L10NAccepted,

            /** Font magnification. */
            'magnification' => $this->Configuration['frontend']['magnification'],

            /** Define active configuration file. */
            'ActiveConfigFile' => $this->ConfigurationPath,

            /** The current time and date. */
            'DateTime' => $this->timeFormat($this->Now, $this->Configuration['general']['time_format']),

            /** How the script identifies itself. */
            'ScriptIdent' => $this->ScriptIdent,

            /** The currently configured theme. */
            'theme' => $this->Configuration['frontend']['theme'],

            /** The currently configured theme mode. */
            'theme_mode' => $this->Configuration['frontend']['theme_mode'],

            /** The currently configured theme mode's effects. */
            'theme_mode_effects' => $this->CIDRAM['Config Defaults']['frontend']['theme_mode']['effects'][$this->Configuration['frontend']['theme_mode']] ?? '',

            /**
             * The current user state.
             * -1 = Attempted and failed to log in.
             * 0 = Not logged in.
             * 1 = Logged in.
             * 2 = Logged in, but awaiting two-factor authentication.
             */
            'UserState' => 0,

            /** The currently logged in user. */
            'User' => '',

            /**
             * User permissions.
             * 0 = Not logged in, or awaiting two-factor authentication.
             * 1 = Complete access.
             * 2 = Logs access only.
             * 3 = Cronable.
             */
            'Permissions' => 0,

            /** Will be populated by messages reflecting the current request state. */
            'state_msg' => '',

            /** Will be populated by the current session data. */
            'ThisSession' => '',

            /** Used to log out. */
            'LogoutButton' => sprintf(
                '<form action="?cidram-page=logout" method="POST" class="inline">%s%s<input type="submit" id="logoutbutton" value="%s" class="auto" /></form>',
                '<input name="hostname" id="hostnameoverride" type="hidden" value="" />',
                '<script type="text/javascript">document.getElementById(\'hostnameoverride\').value=window.location.hostname;</script>',
                $this->L10N->getString('link.Log Out')
            ),

            /** Used to return home. */
            'HomeButton' => sprintf(
                '<form action="" method="GET" class="inline"><input type="hidden" name="cidram-page" value="" /><input type="submit" id="homebutton" value="%s" class="auto" /></form>',
                $this->L10N->getString('link.Home')
            ),

            /** State reflecting whether the current request is cronable. */
            'CronMode' => $_POST['CronMode'] ?? '',

            /** The user agent of the current request. */
            'UA' => $_SERVER['HTTP_USER_AGENT'] ?? '',

            /** The IP address of the current request. */
            'YourIP' => $this->ipAddr,

            /** Asynchronous mode. */
            'ASYNC' => !empty($_POST['ASYNC']),

            /** Will be populated by the page title. */
            'FE_Title' => '',

            /** Make some of the link references available to the main front-end array. */
            'Links.Discussions' => $this->CIDRAM['Links']['Discussions'],
            'Links.Documentation' => $this->CIDRAM['Links']['Documentation'],
            'Links.Website' => $this->CIDRAM['Links']['Website']
        ];

        if ($this->CIDRAM['QueryVars']['cidram-page'] === 'config') {
            /** Fix for immediate display of freshly updated theme selection. */
            if (!empty($_POST['config_frontend_theme']) && !preg_match('~[^a-z]~', $_POST['config_frontend_theme']) && $this->filterThemeFrontEnd($_POST['config_frontend_theme'])) {
                $this->FE['theme'] = $_POST['config_frontend_theme'];
            }

            /** Fix for immediate display of freshly updated theme mode selection. */
            if (!empty($_POST['config_frontend_theme_mode']) && isset($this->CIDRAM['Config Defaults']['frontend']['theme_mode']['choices'][$_POST['config_frontend_theme_mode']])) {
                $this->FE['theme_mode'] = $_POST['config_frontend_theme_mode'];
            }
        }

        /** Used by rate limiting exceptions. */
        $this->CIDRAM['ViewCalled'] = true;

        /** Header appended by integration. */
        if (isset($this->CIDRAM['IntegrationHeader'])) {
            if ($this->FE['CustomHeader'] !== '') {
                $this->FE['CustomHeader'] .= '<br /><br />' . $this->CIDRAM['IntegrationHeader'];
            } else {
                $this->FE['CustomHeader'] = $this->CIDRAM['IntegrationHeader'];
            }
        }

        /** Footer appended by integration. */
        if (isset($this->CIDRAM['IntegrationFooter'])) {
            if ($this->FE['CustomFooter'] !== '') {
                $this->FE['CustomFooter'] .= '<br /><br />' . $this->CIDRAM['IntegrationFooter'];
            } else {
                $this->FE['CustomFooter'] = $this->CIDRAM['IntegrationFooter'];
            }
        }

        /** Trace to determine the type of cron operation. */
        if ($this->FE['CronMode'] !== '') {
            $this->FE['CronType'] = 'update';
            $this->CIDRAM['CronDebug'] = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 4);
            foreach ($this->CIDRAM['CronDebug'] as $this->CIDRAM['ThisCronDebug']) {
                if (
                    isset($this->CIDRAM['ThisCronDebug']['function'], $this->CIDRAM['ThisCronDebug']['class']) &&
                    $this->CIDRAM['ThisCronDebug']['function'] === 'localUpdate' &&
                    $this->CIDRAM['ThisCronDebug']['class'] === 'Maikuolan\Cronable\Cronable'
                ) {
                    $this->FE['CronType'] = 'localUpdate';
                }
            }
            unset($this->CIDRAM['CronDebug'], $this->CIDRAM['ThisCronDebug']);
        } else {
            $this->FE['CronType'] = '';
        }

        /** Populated by [Home | Log Out] by default; Replaced by [Log Out] for some specific pages (e.g., the homepage). */
        $this->FE['bNav'] = $this->FE['HomeButton'] . $this->FE['LogoutButton'];

        /** To be populated by warnings. */
        $this->CIDRAM['Warnings'] = [];

        /** Modes for disabled termination. */
        if (!isset($this->Stages['Terminate:Enable'])) {
            if (isset(
                $this->Stages['Tests:Enable'],
                $this->Stages['Modules:Enable'],
                $this->Stages['SearchEngineVerification:Enable'],
                $this->Stages['SocialMediaVerification:Enable'],
                $this->Stages['OtherVerification:Enable'],
                $this->Stages['Aux:Enable'],
                $this->Stages['PrepareFields:Enable'],
                $this->Stages['WriteLogs:Enable']
            )) {
                /** Dry run mode. */
                $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.Operating in dry run mode');
            } elseif (
                !isset($this->Stages['Tests:Enable']) &&
                !isset($this->Stages['Modules:Enable']) &&
                !isset($this->Stages['SearchEngineVerification:Enable']) &&
                !isset($this->Stages['SocialMediaVerification:Enable']) &&
                !isset($this->Stages['OtherVerification:Enable']) &&
                !isset($this->Stages['Aux:Enable']) &&
                !isset($this->Stages['Reporting:Enable']) &&
                !isset($this->Stages['Tracking:Enable']) &&
                !isset($this->Stages['RL:Enable']) &&
                !isset($this->Stages['CAPTCHA:Enable']) &&
                !isset($this->Stages['Statistics:Enable']) &&
                !isset($this->Stages['Webhooks:Enable']) &&
                !isset($this->Stages['PrepareFields:Enable']) &&
                !isset($this->Stages['Output:Enable']) &&
                !isset($this->Stages['WriteLogs:Enable']) &&
                !isset($this->Stages['AuxRedirect:Enable']) &&
                !isset($this->Stages['NonBlockedCAPTCHA:Enable'])
            ) {
                /** Maintenance mode. */
                $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.Operating in maintenance mode');
            } else {
                /** Termination disabled. */
                $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.Page termination is disabled');
            }
        } else {
            if (isset($this->Stages['Tests:Enable'])) {
                if (
                    $this->Configuration['components']['ipv4'] === '' &&
                    $this->Configuration['components']['ipv6'] === ''
                ) {
                    /** No active signature files. */
                    $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.No signature files are active');
                }
            } elseif (
                $this->Configuration['components']['ipv4'] === '' &&
                $this->Configuration['components']['ipv6'] === ''
            ) {
                /** IP tests disabled. */
                $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.IP tests are disabled');
            }
            if (isset($this->Stages['Modules:Enable'])) {
                if ($this->Configuration['components']['modules'] === '') {
                    /** No active modules. */
                    $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.No modules are active');
                }
            } elseif ($this->Configuration['components']['modules'] === '') {
                /** Modules disabled. */
                $this->CIDRAM['Warnings'][] = $this->L10N->getString('warning.Modules are disabled');
            }
        }

        /** Menu toggle JavaScript, needed by some front-end pages. */
        $this->CIDRAM['MenuToggle'] =
            '<script type="text/javascript">var i,toggler=document.getElementsByClassName("comCat");' .
            'for(i=0;i<toggler.length;i++)toggler[i].addEventListener("click",function(){' .
            'this.parentElement.querySelector(".comSub").classList.toggle("active"),' .
            'this.getAttribute("aria-expanded")!==null&&this.setAttribute("aria-expanded",this.getAttribute("aria-expanded")==="false"?"true":"false"),' .
            '!this.classList.toggle("caret-down")&&this.classList.toggle("caret-up")&&setTimeout(function(t){t.classList.toggle("caret-up")},500,this)})</script>';

        /** A fix for correctly displaying LTR/RTL text. */
        if ($this->L10N->Directionality !== 'rtl') {
            $this->L10N->Data['Text Direction'] = 'ltr';
            $this->FE['FE_Align'] = 'left';
            $this->FE['FE_Align_Reverse'] = 'right';
            $this->FE['FE_Align_Mode'] = 'lr';
            $this->FE['Half_Border'] = 'solid solid none none';
            $this->FE['90deg'] = '90deg';
            $this->FE['caretTransformBefore'] = 'transform:rotate(0) scaleX(1);position:relative;top:2px;left:0px;filter:drop-shadow(0 0 0 rgba(0,0,0,0)) drop-shadow(0 0 0 rgba(0,0,0,0))';
            $this->FE['caretTransformAfter'] = 'transform:rotate(90deg) scaleX(0.4);position:relative;top:4px;left:10px;filter:drop-shadow(0 6px 0 rgba(0,0,0,0.7)) drop-shadow(0 12px 0 rgba(0,0,0,0.4))';
            $this->FE['cnrRadius'] = '0 12px';
        } else {
            $this->L10N->Data['Text Direction'] = 'rtl';
            $this->FE['FE_Align'] = 'right';
            $this->FE['FE_Align_Reverse'] = 'left';
            $this->FE['FE_Align_Mode'] = 'rl';
            $this->FE['Half_Border'] = 'solid none none solid';
            $this->FE['90deg'] = '270deg';
            $this->FE['caretTransformBefore'] = 'transform:rotate(0) scaleX(1);position:relative;top:2px;right:0px;filter:drop-shadow(0 0 0 rgba(0,0,0,0)) drop-shadow(0 0 0 rgba(0,0,0,0))';
            $this->FE['caretTransformAfter'] = 'transform:rotate(90deg) scaleX(0.4);position:relative;top:4px;right:10px;filter:drop-shadow(0 -6px 0 rgba(0,0,0,0.7)) drop-shadow(0 -12px 0 rgba(0,0,0,0.4))';
            $this->FE['cnrRadius'] = '12px 0';
        }

        /** A simple passthru for non-private theme images and related data. */
        if (!empty($this->CIDRAM['QueryVars']['cidram-asset'])) {
            $this->eTaggable($this->CIDRAM['QueryVars']['cidram-asset']);
        }

        /** A simple passthru for the front-end CSS. */
        if ($this->CIDRAM['QueryVars']['cidram-page'] === 'css') {
            $this->eTaggable('frontend.css', function ($AssetData) {
                if (!empty($this->CIDRAM['QueryVars']['theme-mode'])) {
                    $this->FE['theme_mode_effects'] = $this->CIDRAM['Config Defaults']['frontend']['theme_mode']['effects'][$this->CIDRAM['QueryVars']['theme-mode']] ?? '';
                }
                return $this->embedAssets($this->parseVars($this->FE, $AssetData, true));
            });
        }

        /** Set the current request's form target. */
        $this->FE['FormTarget'] = $_POST['cidram-form-target'] ?? '';

        /** Form data for SVG buttons. */
        $this->FE['SVGsFormData'] = isset($this->CIDRAM['SVGs form data']) && is_array($this->CIDRAM['SVGs form data']) ? implode('', $this->CIDRAM['SVGs form data']) : '';

        /** Initialise cache. */
        $this->initialiseCache();

        /** Warnings from imports and modules. */
        if (isset($this->CIDRAM['Problems']) && is_array($this->CIDRAM['Problems'])) {
            /** Prefetch any needed cache entries. */
            if (isset($this->CIDRAM['Cache Prefetch']) && is_array($this->CIDRAM['Cache Prefetch'])) {
                foreach ($this->CIDRAM['Cache Prefetch'] as $Key => &$Value) {
                    $Value = $this->Cache->getEntry($Key);
                }
            }

            /** Process all warning conditions. */
            $this->executor($this->CIDRAM['Problems']);
        }

        /** Prepare warnings. */
        $this->FE['Warnings'] = count($this->CIDRAM['Warnings']) ? "\n<div class=\"center\"><div class=\"warning\">" . implode(
            "</div>\n<div class=\"warning\">",
            $this->CIDRAM['Warnings']
        ) . '</div></div><hr />' : '';

        /** Cleanup. */
        unset($this->CIDRAM['Warnings']);

        /** Initialise statistics tracked. */
        $this->StatisticsTracked = array_flip(explode("\n", $this->Configuration['general']['statistics']));

        /** Brute-force protection. */
        if ((
            ($this->CIDRAM['LoginAttempts'] = (int)$this->Cache->getEntry('LoginAttempts' . $this->ipAddr)) &&
            ($this->CIDRAM['LoginAttempts'] >= $this->Configuration['frontend']['max_login_attempts'])
        ) || (
            ($this->CIDRAM['Failed2FA'] = (int)$this->Cache->getEntry('Failed2FA' . $this->ipAddr)) &&
            ($this->CIDRAM['Failed2FA'] >= $this->Configuration['frontend']['max_login_attempts'])
        )) {
            header('Content-Type: text/plain');
            die('[CIDRAM] ' . $this->L10N->getString('response.Maximum number of login attempts exceeded'));
        }

        /** Attempt to log in the user. */
        if ($this->FE['FormTarget'] === 'login' || $this->FE['CronMode'] !== '') {
            if (!empty($_POST['username']) && empty($_POST['password'])) {
                $this->FE['UserState'] = -1;
                $this->FE['state_msg'] = $this->L10N->getString('response.Password field empty');
            } elseif (empty($_POST['username']) && !empty($_POST['password'])) {
                $this->FE['UserState'] = -1;
                $this->FE['state_msg'] = $this->L10N->getString('response.Username field empty');
            } elseif (!empty($_POST['username']) && !empty($_POST['password'])) {
                $this->FE['UserState'] = -1;
                $this->FE['LP'] = ['ConfigUserPath' => 'user.' . $this->desabotage($_POST['username'])];
                if (isset(
                    $this->Configuration[$this->FE['LP']['ConfigUserPath']],
                    $this->Configuration[$this->FE['LP']['ConfigUserPath']]['password'],
                    $this->Configuration[$this->FE['LP']['ConfigUserPath']]['permissions']
                ) &&
                    !empty($this->Configuration[$this->FE['LP']['ConfigUserPath']]['password']) &&
                    !empty($this->Configuration[$this->FE['LP']['ConfigUserPath']]['permissions'])
                ) {
                    if (password_verify($_POST['password'], $this->Configuration[$this->FE['LP']['ConfigUserPath']]['password'])) {
                        $this->Cache->deleteEntry('LoginAttempts' . $this->ipAddr);
                        $this->FE['Permissions'] = $this->Configuration[$this->FE['LP']['ConfigUserPath']]['permissions'];
                        if (($this->FE['Permissions'] === 3 && (
                            $this->FE['CronMode'] === '' || substr($this->FE['UA'], 0, 10) !== 'Cronable v'
                        )) || !($this->FE['Permissions'] > 0 && $this->FE['Permissions'] <= 3)) {
                            $this->FE['Permissions'] = 0;
                            $this->FE['state_msg'] = $this->L10N->getString('response.Wrong endpoint');
                        } else {
                            $this->FE['User'] = $this->desabotage($_POST['username']);
                            if ($this->FE['CronMode'] === '') {
                                $this->FE['SessionKey'] = hash('sha256', $this->generateSalt());
                                $this->FE['Cookie'] = $this->FE['User'] . $this->FE['SessionKey'];
                                setcookie('CIDRAM-ADMIN', $this->FE['Cookie'], $this->Now + 604800, '/', $this->CIDRAM['HostnameOverride'] ?: $this->CIDRAM['HTTP_HOST'], false, true);
                                $this->FE['ThisSession'] = $this->FE['User'] . ',' . password_hash($this->FE['SessionKey'], $this->DefaultAlgo);

                                /** Prepare 2FA email. */
                                if (
                                    $this->Configuration['frontend']['enable_two_factor'] &&
                                    preg_match('~^.+@.+$~', $this->FE['User']) &&
                                    ($this->FE['LP']['TwoFactorMessage'] = $this->L10N->getString('msg_template_2fa')) &&
                                    ($this->FE['LP']['TwoFactorSubject'] = $this->L10N->getString('msg_subject_2fa'))
                                ) {
                                    $this->FE['LP']['TwoFactorState'] = ['Number' => $this->twoFactorNumber()];
                                    $this->FE['LP']['TwoFactorState']['Hash'] = password_hash($this->FE['LP']['TwoFactorState']['Number'], $this->DefaultAlgo);
                                    $this->Cache->setEntry('TwoFactorState:' . $this->FE['Cookie'], '0' . $this->FE['LP']['TwoFactorState']['Hash'], 600);
                                    $this->FE['LP']['TwoFactorState']['Template'] = sprintf(
                                        $this->FE['LP']['TwoFactorMessage'],
                                        $this->FE['User'],
                                        $this->FE['LP']['TwoFactorState']['Number']
                                    );
                                    if (preg_match('~^[^<>]+<[^<>]+>$~', $this->FE['User'])) {
                                        $this->FE['LP']['TwoFactorState']['Name'] = trim(preg_replace('~^([^<>]+)<[^<>]+>$~', '\1', $this->FE['User']));
                                        $this->FE['LP']['TwoFactorState']['Address'] = trim(preg_replace('~^[^<>]+<([^<>]+)>$~', '\1', $this->FE['User']));
                                    } else {
                                        $this->FE['LP']['TwoFactorState']['Name'] = trim($this->FE['User']);
                                        $this->FE['LP']['TwoFactorState']['Address'] = $this->FE['LP']['TwoFactorState']['Name'];
                                    }
                                    $EventData = [
                                        [['Name' => $this->FE['LP']['TwoFactorState']['Name'], 'Address' => $this->FE['LP']['TwoFactorState']['Address']]],
                                        $this->FE['LP']['TwoFactorSubject'],
                                        $this->FE['LP']['TwoFactorState']['Template'],
                                        strip_tags($this->FE['LP']['TwoFactorState']['Template']),
                                        ''
                                    ];
                                    $this->Events->fireEvent('sendEmail', '', ...$EventData);
                                    unset($EventData);
                                    $this->FE['UserState'] = 2;
                                } else {
                                    $this->FE['UserState'] = 1;
                                }

                                /** Need to set a cache item to correspond with the cookie value. */
                                if ($this->FE['UserState'] === 1 || $this->FE['UserState'] === 2) {
                                    $this->Cache->setEntry($this->FE['Cookie'], $this->FE['ThisSession'], 604800);
                                } else {
                                    $this->FE['Permissions'] = 0;
                                }
                            } else {
                                $this->FE['UserState'] = 1;
                            }
                        }
                    } else {
                        $this->FE['Permissions'] = 0;
                        $this->FE['state_msg'] = $this->L10N->getString('response.Invalid password');
                    }
                } else {
                    $this->FE['state_msg'] = $this->L10N->getString('response.Username doesn_t exist');
                }

                /** Cleanup. */
                unset($this->FE['LP']);
            }

            if ($this->FE['state_msg']) {
                $this->CIDRAM['LoginAttempts']++;
                $this->CIDRAM['TimeToAdd'] = ($this->CIDRAM['LoginAttempts'] > 4) ? ($this->CIDRAM['LoginAttempts'] - 4) * 86400 : 86400;
                $this->Cache->setEntry('LoginAttempts' . $this->ipAddr, $this->CIDRAM['LoginAttempts'], $this->CIDRAM['TimeToAdd']);
                if ($this->Configuration['frontend']['frontend_log']) {
                    $LoggerMessage = $this->FE['state_msg'];
                }
                if ($this->FE['CronMode'] === '') {
                    $this->FE['state_msg'] = '<div class="txtRd">' . $this->FE['state_msg'] . '<br /><br /></div>';
                }
            } elseif ($this->Configuration['frontend']['frontend_log']) {
                $LoggerMessage = $this->L10N->getString((
                    $this->Configuration['frontend']['enable_two_factor'] &&
                    $this->FE['Permissions'] === 0
                ) ? 'label.Logged in, 2FA pending' : 'label.Logged in');
            }

            /** Safer for the front-end logger. */
            $NameToLog = preg_replace('~[\x00-\x1F]~', '', $this->desabotage($_POST['username'] ?? ''));

            /** Handle front-end logging. */
            $this->frontendLogger($this->ipAddr, $NameToLog, $LoggerMessage ?? '');

            /** Cleanup. */
            unset($NameToLog, $LoggerMessage);
        } elseif (!empty($_COOKIE['CIDRAM-ADMIN'])) {
            $this->FE['UserState'] = -1;
            $this->FE['LP'] = [];
            if (
                ($this->FE['LP']['TrySession'] = $this->Cache->getEntry($_COOKIE['CIDRAM-ADMIN'])) &&
                ($this->FE['LP']['SessionDel'] = strpos($this->FE['LP']['TrySession'], ',')) !== false
            ) {
                $this->FE['LP']['SessionHash'] = substr($this->FE['LP']['TrySession'], $this->FE['LP']['SessionDel'] + 1);
                $this->FE['LP']['SessionUser'] = substr($this->FE['LP']['TrySession'], 0, $this->FE['LP']['SessionDel']);
            }
            if (!empty($this->FE['LP']['SessionHash']) && !empty($this->FE['LP']['SessionUser'])) {
                $this->FE['LP']['SessionUserLen'] = strlen($this->FE['LP']['SessionUser']);
                $this->FE['LP']['SessionKey'] = substr($_COOKIE['CIDRAM-ADMIN'], $this->FE['LP']['SessionUserLen']);
                $this->FE['LP']['CookieUser'] = substr($_COOKIE['CIDRAM-ADMIN'], 0, $this->FE['LP']['SessionUserLen']);
                $this->FE['LP']['ConfigUserPath'] = 'user.' . $this->FE['LP']['CookieUser'];
                if (
                    $this->FE['LP']['CookieUser'] === $this->FE['LP']['SessionUser'] &&
                    password_verify($this->FE['LP']['SessionKey'], $this->FE['LP']['SessionHash']) &&
                    isset(
                        $this->Configuration[$this->FE['LP']['ConfigUserPath']],
                        $this->Configuration[$this->FE['LP']['ConfigUserPath']]['permissions']
                    )
                ) {
                    $this->FE['Permissions'] = $this->Configuration[$this->FE['LP']['ConfigUserPath']]['permissions'];
                    $this->FE['User'] = $this->FE['LP']['SessionUser'];

                    /** Handle 2FA stuff here. */
                    if ($this->Configuration['frontend']['enable_two_factor'] && preg_match('~^.+@.+$~', $this->FE['LP']['SessionUser'])) {
                        $this->FE['LP']['TwoFactorState'] = $this->Cache->getEntry('TwoFactorState:' . $_COOKIE['CIDRAM-ADMIN']);
                        $this->FE['LP']['Try'] = (int)substr($this->FE['LP']['TwoFactorState'], 0, 1);
                        $this->FE['UserState'] = ((int)$this->FE['LP']['TwoFactorState'] === 1) ? 1 : 2;
                        if ($this->FE['UserState'] === 2 && $this->FE['FormTarget'] === '2fa' && !empty($_POST['2fa'])) {
                            /** User has submitted a 2FA code. Attempt to verify it. */
                            if (password_verify($_POST['2fa'], substr($this->FE['LP']['TwoFactorState'], 1))) {
                                $this->Cache->setEntry('TwoFactorState:' . $_COOKIE['CIDRAM-ADMIN'], '1', 604800);
                                $this->FE['UserState'] = 1;
                            }
                        }
                        unset($this->FE['LP']['TwoFactorState']);
                    } else {
                        $this->FE['UserState'] = 1;
                    }

                    /** Revert permissions if not authenticated. */
                    if ($this->FE['UserState'] !== 1) {
                        $this->FE['Permissions'] = 0;
                    }
                }
            }

            /** In case of 2FA form submission. */
            if ($this->FE['FormTarget'] === '2fa' && !empty($_POST['2fa'])) {
                if ($this->FE['UserState'] === 2) {
                    $this->CIDRAM['Failed2FA']++;
                    $this->CIDRAM['TimeToAdd'] = ($this->CIDRAM['Failed2FA'] > 4) ? ($this->CIDRAM['Failed2FA'] - 4) * 86400 : 86400;
                    $this->Cache->setEntry('Failed2FA' . $this->ipAddr, $this->CIDRAM['Failed2FA'], $this->CIDRAM['TimeToAdd']);
                    if ($this->Configuration['frontend']['frontend_log']) {
                        $this->frontendLogger($this->ipAddr, $this->FE['User'], $this->L10N->getString('response.Incorrect 2FA code entered'));
                    }
                    $this->FE['state_msg'] = '<div class="txtRd">' . $this->L10N->getString('response.Incorrect 2FA code entered') . '<br /><br /></div>';
                } else {
                    $this->Cache->deleteEntry('Failed2FA' . $this->ipAddr);
                    if ($this->Configuration['frontend']['frontend_log']) {
                        $this->frontendLogger($this->ipAddr, $this->FE['User'], $this->L10N->getString('response.Successfully authenticated'));
                    }
                }
            }

            /** Cleanup. */
            unset($this->FE['LP']);
        }

        /** The user is attempting an asynchronous request without adequate permissions. */
        if ($this->FE['UserState'] !== 1 && $this->FE['ASYNC']) {
            $this->Events->fireEvent('final');
            header('HTTP/1.0 403 Forbidden');
            header('HTTP/1.1 403 Forbidden');
            header('Status: 403 Forbidden');
            die($this->L10N->getString('response.Permissions not adequate to perform asynchronous requests'));
        }

        /** Major version notice. */
        $this->CIDRAM['MajorVersionNotice'] = '';

        /** Executed only for users that are logged in or awaiting two-factor authentication. */
        if (($this->FE['UserState'] === 1 || $this->FE['UserState'] === 2) && $this->FE['CronMode'] === '') {
            /** Log the user out. */
            if ($this->CIDRAM['QueryVars']['cidram-page'] === 'logout') {
                $this->Cache->deleteEntry($_COOKIE['CIDRAM-ADMIN']);
                $this->Cache->deleteEntry('TwoFactorState:' . $_COOKIE['CIDRAM-ADMIN']);
                $this->FE['ThisSession'] = '';
                $this->FE['UserState'] = 0;
                $this->FE['Permissions'] = 0;
                setcookie('CIDRAM-ADMIN', '', -1, '/', $this->CIDRAM['HostnameOverride'] ?: $this->CIDRAM['HTTP_HOST'], false, true);
                $this->frontendLogger($this->ipAddr, $this->FE['User'], $this->L10N->getString('label.Logged out'));
                $this->FE['User'] = '';
            }

            if ($this->FE['Permissions'] === 1) {
                /** If the user has complete access. */
                $this->FE['nav'] = $this->parseVars($this->FE, $this->readFile($this->getAssetPath('_nav_complete_access.html')), true);
            } elseif ($this->FE['Permissions'] === 2) {
                /** If the user has logs access only. */
                $this->FE['nav'] = $this->parseVars($this->FE, $this->readFile($this->getAssetPath('_nav_logs_access_only.html')), true);
            } else {
                /** No valid navigation state. */
                $this->FE['nav'] = '';
            }
        }

        /** Only execute this code block for already logged in users. */
        if ($this->FE['UserState'] === 1) {
            /** Where to find remote version information? */
            $RemoteVerPath = 'https://raw.githubusercontent.com/Maikuolan/Compatibility-Charts/gh-pages/';

            /** Fetch remote CIDRAM version information and cache it if necessary. */
            if (($this->CIDRAM['Remote-YAML-CIDRAM'] = $this->Cache->getEntry('cidram-ver.yaml')) === false) {
                $this->CIDRAM['Remote-YAML-CIDRAM'] = $this->Request->request($RemoteVerPath . 'cidram-ver.yaml', [], 8);
                $this->Cache->setEntry('cidram-ver.yaml', $this->CIDRAM['Remote-YAML-CIDRAM'] ?: '-', 86400);
            }

            /** Process remote CIDRAM version information. */
            if (empty($this->CIDRAM['Remote-YAML-CIDRAM'])) {
                /** CIDRAM latest stable. */
                $this->FE['info_cidram_stable'] = $this->L10N->getString('response.Error');

                /** CIDRAM latest unstable. */
                $this->FE['info_cidram_unstable'] = $this->L10N->getString('response.Error');

                /** CIDRAM branch latest stable. */
                $this->FE['info_cidram_branch'] = $this->L10N->getString('response.Error');
            } else {
                $RemoteYAMLCIDRAMArray = [];
                $this->YAML->process($this->CIDRAM['Remote-YAML-CIDRAM'], $RemoteYAMLCIDRAMArray);

                /** CIDRAM latest stable. */
                if (empty($RemoteYAMLCIDRAMArray['Stable'])) {
                    $this->FE['info_cidram_stable'] = $this->L10N->getString('response.Error');
                } else {
                    $this->FE['MajorVersionCurrent'] = (int)substr($this->ScriptVersion, 0, strpos($this->ScriptVersion, '.') ?: strlen($this->ScriptVersion));
                    $this->FE['MajorVersionLatest'] = (int)substr($RemoteYAMLCIDRAMArray['Stable'], 0, strpos($RemoteYAMLCIDRAMArray['Stable'], '.') ?: strlen($RemoteYAMLCIDRAMArray['Stable']));
                    if (
                        $this->FE['MajorVersionCurrent'] < $this->FE['MajorVersionLatest'] &&
                        !empty($RemoteYAMLCIDRAMArray['Stable Minimum PHP Required']) &&
                        is_string($RemoteYAMLCIDRAMArray['Stable Minimum PHP Required']) &&
                        version_compare(\PHP_VERSION, $RemoteYAMLCIDRAMArray['Stable Minimum PHP Required'], '>=')
                    ) {
                        $this->CIDRAM['MajorVersionNotice'] = sprintf(
                            $this->L10N->getString('notice_new_major_version'),
                            'CIDRAM v' . $RemoteYAMLCIDRAMArray['Stable'],
                            $this->ScriptIdent
                        );
                    }
                    $this->FE['info_cidram_stable'] = $RemoteYAMLCIDRAMArray['Stable'];
                }

                /** CIDRAM latest unstable. */
                $this->FE['info_cidram_unstable'] = empty($RemoteYAMLCIDRAMArray['Unstable']) ?
                    $this->L10N->getString('response.Error') : $RemoteYAMLCIDRAMArray['Unstable'];

                /** CIDRAM branch latest stable. */
                if ($ThisBranch = substr($this->ScriptVersion, 0, strpos($this->ScriptVersion, '.') ?: 1)) {
                    $ThisBranch = 'v' . ($ThisBranch ?: 1);
                    if (empty($RemoteYAMLCIDRAMArray['Branch'][$ThisBranch]['Latest'])) {
                        $this->FE['info_cidram_branch'] = $this->L10N->getString('response.Error');
                    } else {
                        $this->FE['info_cidram_branch'] = $RemoteYAMLCIDRAMArray['Branch'][$ThisBranch]['Latest'];
                    }
                } else {
                    $this->FE['info_php_branch'] = $this->L10N->getString('response.Error');
                }
            }

            /** Cleanup. */
            unset($RemoteYAMLCIDRAMArray, $this->CIDRAM['Remote-YAML-CIDRAM']);

            /** Fetch remote PHP version information and cache it if necessary. */
            if (($RemoteYAMLPHP = $this->Cache->getEntry('php-ver.yaml')) === false) {
                $RemoteYAMLPHP = $this->Request->request($RemoteVerPath . 'php-ver.yaml', [], 8);
                $this->Cache->setEntry('php-ver.yaml', $RemoteYAMLPHP ?: '-', 86400);
            }

            /** Process remote PHP version information. */
            if (empty($RemoteYAMLPHP)) {
                /** PHP latest stable. */
                $this->FE['info_php_stable'] = $this->L10N->getString('response.Error');

                /** PHP latest unstable. */
                $this->FE['info_php_unstable'] = $this->L10N->getString('response.Error');

                /** PHP branch latest stable. */
                $this->FE['info_php_branch'] = $this->L10N->getString('response.Error');
            } else {
                $RemoteYAMLPHPArray = [];
                $this->YAML->process($RemoteYAMLPHP, $RemoteYAMLPHPArray);

                /** PHP latest stable. */
                $this->FE['info_php_stable'] = $RemoteYAMLPHPArray['Stable'] ?? $this->L10N->getString('response.Error');

                /** PHP latest unstable. */
                $this->FE['info_php_unstable'] = $RemoteYAMLPHPArray['Unstable'] ?? $this->L10N->getString('response.Error');

                /** PHP branch latest stable. */
                $ThisBranch = 'php' . \PHP_MAJOR_VERSION . \PHP_MINOR_VERSION;
                $this->FE['info_php_branch'] = $RemoteYAMLPHPArray['Branch'][$ThisBranch]['Latest'] ?? $this->L10N->getString('response.Error');
            }

            /** Get cached logs link. */
            $this->FE['CachedLogsLink'] = $this->Cache->getEntry('CachedLogsLink-' . $this->FE['User']) ?: '?cidram-page=logs';

            /** Cleanup. */
            unset($RemoteYAMLPHPArray, $RemoteYAMLPHP, $ThisBranch, $RemoteVerPath);
        }

        /** Useful for avoiding excessive IO operations when dealing with components. */
        $this->CIDRAM['Updater-IO'] = new \Maikuolan\Common\DelayedIO();

        if ($this->FE['UserState'] !== 1 && $this->FE['CronMode'] === '') {
            /** The user hasn't logged in, or hasn't authenticated yet. */
            $this->initialPrepwork($this->L10N->getString('label.Login'), '', false);

            /** Hide warnings from non-logged in users. */
            $this->FE['Warnings'] = '';

            if ($this->FE['UserState'] === 2) {
                /** Provide the option to log out (omit home link). */
                $this->FE['bNav'] = $this->FE['LogoutButton'];

                /** Aesthetic spacer. */
                $this->FE['2fa_status_spacer'] = empty($this->FE['state_msg']) ? '' : '<br /><br />';

                /** Show them the two-factor authentication page. */
                $this->FE['FE_Content'] = $this->parseVars($this->FE, $this->readFile($this->getAssetPath('_2fa.html')), true);
            } else {
                /** Omit the log out and home links. */
                $this->FE['bNav'] = '';

                /** Show them the login page. */
                $this->FE['FE_Content'] = $this->parseVars($this->FE, $this->readFile($this->getAssetPath('_login.html')), true);
            }

            /** Send output. */
            echo $this->sendOutput();
        } elseif ($this->CIDRAM['QueryVars']['cidram-page'] === '' && $this->FE['CronMode'] === '') {
            /**
             * The user has logged in, but hasn't selected anything to view. Show them the
             * front-end home page.
             */
            $this->initialPrepwork($this->L10N->getString('link.Home'), $this->L10N->getString('tip.Home'), false);

            /** CIDRAM version used. */
            $this->FE['ScriptVersion'] = $this->ScriptVersion;

            /** PHP version used. */
            $this->FE['info_php'] = \PHP_VERSION;

            /** SAPI used. */
            $this->FE['info_sapi'] = php_sapi_name();

            /** Operating system used. */
            $this->FE['info_os'] = php_uname();

            /** Provide the option to log out (omit home link). */
            $this->FE['bNav'] = $this->FE['LogoutButton'];

            /** Extension availability. */
            $this->FE['Extensions'] = [];
            $ExtensionsCopyData = '';
            foreach ([
                ['Lib' => 'pcre', 'Name' => 'PCRE'],
                ['Lib' => 'curl', 'Name' => 'cURL'],
                ['Lib' => 'apcu', 'Name' => 'APCu'],
                ['Lib' => 'memcached', 'Name' => 'Memcached'],
                ['Lib' => 'redis', 'Name' => 'Redis'],
                ['Lib' => 'pdo', 'Name' => 'PDO', 'Drivers' => (class_exists('\PDO') ? \PDO::getAvailableDrivers() : [])]
            ] as $this->CIDRAM['ThisExtension']) {
                if (extension_loaded($this->CIDRAM['ThisExtension']['Lib'])) {
                    $this->CIDRAM['ExtVer'] = (new \ReflectionExtension($this->CIDRAM['ThisExtension']['Lib']))->getVersion();
                    $this->CIDRAM['ThisResponse'] = $this->L10N->getString('response._Yes') . ' (' . $this->CIDRAM['ExtVer'] . ')';
                    if (!empty($this->CIDRAM['ThisExtension']['Drivers'])) {
                        $this->CIDRAM['ThisResponse'] .= ', {' . implode(', ', $this->CIDRAM['ThisExtension']['Drivers']) . '}';
                    }
                    $ExtensionsCopyData .= $this->ltrInRtf(
                        sprintf('- %s➡%s\n', $this->CIDRAM['ThisExtension']['Name'], $this->CIDRAM['ThisResponse'])
                    );
                    $this->CIDRAM['ThisResponse'] = '<span class="txtGn">' . $this->CIDRAM['ThisResponse'] . '</span>';
                } else {
                    $ExtensionsCopyData .= $this->ltrInRtf(
                        sprintf('- %s➡%s\n', $this->CIDRAM['ThisExtension']['Name'], $this->L10N->getString('response._No'))
                    );
                    $this->CIDRAM['ThisResponse'] = '<span class="txtRd">' . $this->L10N->getString('response._No') . '</span>';
                }
                $this->FE['Extensions'][] = '    <li><small>' . $this->ltrInRtf(sprintf(
                    '%s➡%s',
                    $this->CIDRAM['ThisExtension']['Name'],
                    $this->CIDRAM['ThisResponse']
                )) . '</small></li>';
            }
            $this->FE['Extensions'] = implode("\n", $this->FE['Extensions']);
            $this->FE['ExtensionIsAvailable'] = $this->ltrInRtf(
                $this->L10N->getString('label.Extension') . '➡' . $this->L10N->getString('label.Installed Available')
            );
            unset($this->CIDRAM['ExtVer'], $this->CIDRAM['ThisResponse'], $this->CIDRAM['ThisExtension']);

            /** Build clipboard data. */
            $this->FE['HomeCopyData'] = sprintf(
                '%1$s\n\n- %2$s %3$s\n- %4$s %5$s\n- %6$s %7$s\n- %8$s %9$s\n\n- %10$s %11$s\n- %4$s %12$s\n- %6$s %13$s\n- %8$s %14$s\n- %15$s %16$s\n\n%17$s\n%18$s',
                $this->L10N->getString('label.System information'),
                $this->L10N->getString('label.CIDRAM version used'),
                $this->FE['ScriptVersion'],
                $this->L10N->getString('label.Branch latest stable'),
                $this->FE['info_cidram_branch'],
                $this->L10N->getString('label.Latest stable'),
                $this->FE['info_cidram_stable'],
                $this->L10N->getString('label.Latest unstable'),
                $this->FE['info_cidram_unstable'],
                $this->L10N->getString('label.PHP version used'),
                $this->FE['info_php'],
                $this->FE['info_php_branch'],
                $this->FE['info_php_stable'],
                $this->FE['info_php_unstable'],
                $this->L10N->getString('label.SAPI used'),
                $this->FE['info_sapi'],
                $this->FE['ExtensionIsAvailable'],
                $ExtensionsCopyData
            );

            /** Parse output. */
            $this->FE['FE_Content'] = $this->parseVars($this->FE, $this->readFile($this->getAssetPath('_home.html')), true) . $this->CIDRAM['MenuToggle'];

            /** Send output. */
            echo $this->sendOutput();
        } elseif ($this->CIDRAM['QueryVars']['cidram-page'] === 'icon' && $this->FE['Permissions'] === 1) {
            /** A simple passthru for the file manager icons. */
            if (
                !empty($this->CIDRAM['QueryVars']['file']) &&
                $this->pathSecurityCheck($this->CIDRAM['QueryVars']['file']) &&
                is_readable($this->Vault . $this->CIDRAM['QueryVars']['file'])
            ) {
                header('Content-Type: image/x-icon');
                echo $this->readFile($this->Vault . $this->CIDRAM['QueryVars']['file']);
            } elseif (!empty($this->CIDRAM['QueryVars']['icon'])) {
                $this->eTaggable($this->CIDRAM['QueryVars']['icon'] . '.gif');
            }
        } elseif ($this->CIDRAM['QueryVars']['cidram-page'] === 'flags' && $this->FE['Permissions']) {
            /** A simple passthru for the flags CSS. */
            $this->eTaggable('flags.css');
        } else {
            /** Strip out unexpected bytes here for security. */
            $Page = preg_replace('~[^\dA-Za-z-]~', '', $this->CIDRAM['QueryVars']['cidram-page']);

            /** Loading the specific requested page. */
            if (!$this->isReserved($Page) && is_readable($this->PagesPath . $Page . '.php')) {
                require_once $this->PagesPath . $Page . '.php';
            }
        }

        /** Finalise IO operations all at once. */
        unset($this->CIDRAM['Updater-IO']);

        /** Print Cronable failure state messages here. */
        if ($this->FE['CronMode'] !== '' && $this->FE['state_msg'] !== '' && $this->FE['UserState'] !== 1) {
            if ($this->FE['CronType'] === 'localUpdate') {
                $GLOBALS['Results'] = ['state_msg' => $this->FE['state_msg']];
            } else {
                echo json_encode(['state_msg' => $this->FE['state_msg']]);
            }
        }

        /** Final event before we exit. */
        $this->Events->fireEvent('final');

        /** Exit front-end. */
        if (empty($this->Alternate) && $this->FE['CronType'] !== 'localUpdate') {
            die;
        }
    }
}
