File: vault/frontend_functions.php

Recommend this page to a friend!
  Classes of Caleb  >  CIDRAM  >  vault/frontend_functions.php  >  Download  
File: vault/frontend_functions.php
Role: Example script
Content type: text/plain
Description: Example script
Class: CIDRAM
Check if an IP address is a bad source of traffic
Author: By
Last change: Add an events orchestrator and refactor.
Bug-fix.

Invalid argument warning at the front-end updates page.
Date: 2 years ago
Size: 145,581 bytes
 

Contents

Class file image Download
<?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: Front-end functions file (last modified: 2019.09.17).
 */

/**
 * Syncs downstream metadata with upstream metadata to remove superfluous
 * entries. Installed components are ignored.
 *
 * @param string $Downstream Downstream/local data.
 * @param string $Upstream Upstream/remote data.
 * @return string Patched/synced data (or an empty string on failure).
 */
$CIDRAM['Congruency'] = function (string $Downstream, string $Upstream) use (&$CIDRAM): string {
    if (empty($Downstream) || empty($Upstream)) {
        return '';
    }
    $DownstreamArray = (new \Maikuolan\Common\YAML($Downstream))->Data;
    $UpstreamArray = (new \Maikuolan\Common\YAML($Upstream))->Data;
    foreach ($DownstreamArray as $Element => $Data) {
        if (!isset($Data['Version']) && !isset($Data['Files']) && !isset($UpstreamArray[$Element])) {
            $Downstream = preg_replace("~\n" . preg_quote($Element) . ":?(\n [^\n]*)*\n~i", "\n", $Downstream);
        }
    }
    return $Downstream;
};

/**
 * Can be used to delete some files via the front-end.
 *
 * @param string $File The file to delete.
 * @return bool Success or failure.
 */
$CIDRAM['Delete'] = function (string $File) use (&$CIDRAM): bool {
    if (preg_match('~^(\'.*\'|".*")$~', $File)) {
        $File = substr($File, 1, -1);
    }
    if (!empty($File) && file_exists($CIDRAM['Vault'] . $File) && $CIDRAM['Traverse']($File)) {
        if (!unlink($CIDRAM['Vault'] . $File)) {
            return false;
        }
        $CIDRAM['DeleteDirectory']($File);
        return true;
    }
    return false;
};

/**
 * Can be used to patch parts of files after updating via the front-end.
 *
 * @param string $Query The instruction to execute.
 * @return bool Success or failure.
 */
$CIDRAM['In'] = function (string $Query) use (&$CIDRAM): bool {
    if (!isset($CIDRAM['Updater-IO']) || !$Delimiter = substr($Query, 0, 1)) {
        return false;
    }
    $QueryParts = explode($Delimiter, $Query);
    $CountParts = count($QueryParts);
    if (!($CountParts % 2)) {
        return false;
    }
    $Arr = [];
    for ($Iter = 0; $Iter < $CountParts; $Iter++) {
        if ($Iter % 2) {
            $Arr[] = $QueryParts[$Iter];
            continue;
        }
        $QueryParts[$Iter] = preg_split('~ +~', $QueryParts[$Iter], -1, PREG_SPLIT_NO_EMPTY);
        foreach ($QueryParts[$Iter] as $ThisPart) {
            $Arr[] = $ThisPart;
        }
    }
    $QueryParts = $Arr;
    unset($ThisPart, $Iter, $Arr, $CountParts);

    /** Safety mechanism. */
    if (empty($QueryParts[0]) || empty($QueryParts[1]) || !file_exists($CIDRAM['Vault'] . $QueryParts[0]) || !is_readable($CIDRAM['Vault'] . $QueryParts[0])) {
        return false;
    }

    /** Fetch file content. */
    $Data = $CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . $QueryParts[0]);

    /** Normalise main instruction. */
    $QueryParts[1] = strtolower($QueryParts[1]);

    /** Replace file content. */
    if ($QueryParts[1] === 'replace' && !empty($QueryParts[3]) && strtolower($QueryParts[3]) === 'with') {
        $Data = preg_replace($QueryParts[2], ($QueryParts[4] ?? ''), $Data);
        return $CIDRAM['Updater-IO']->writeFile($CIDRAM['Vault'] . $QueryParts[0], $Data);
    }

    /** Nothing done. Return false (failure). */
    return false;
};

/**
 * Adds integer values; Returns zero if the sum total is negative or if any
 * contained values aren't integers, and otherwise, returns the sum total.
 */
$CIDRAM['ZeroMin'] = function (...$Values): int {
    $Sum = 0;
    foreach ($Values as $Value) {
        $IntValue = (int)$Value;
        if ($IntValue !== $Value) {
            return 0;
        }
        $Sum += $IntValue;
    }
    return $Sum < 0 ? 0 : $Sum;
};

/**
 * Format filesize information.
 *
 * @param int $Filesize
 */
$CIDRAM['FormatFilesize'] = function (int &$Filesize) use (&$CIDRAM) {
    $Scale = ['field_size_bytes', 'field_size_KB', 'field_size_MB', 'field_size_GB', 'field_size_TB'];
    $Iterate = 0;
    while ($Filesize > 1024) {
        $Filesize = $Filesize / 1024;
        $Iterate++;
        if ($Iterate > 4) {
            break;
        }
    }
    $Filesize = $CIDRAM['NumberFormatter']->format($Filesize, ($Iterate === 0) ? 0 : 2) . ' ' . $CIDRAM['L10N']->getPlural($Filesize, $Scale[$Iterate]);
};

/**
 * Remove an entry from the front-end cache data.
 *
 * @param string $Source Variable containing cache file data.
 * @param bool $Rebuild Flag indicating to rebuild cache file.
 * @param string $Entry Name of the cache entry to be deleted.
 */
$CIDRAM['FECacheRemove'] = function (string &$Source, bool &$Rebuild, string $Entry) use (&$CIDRAM) {

    /** Override if using a different preferred caching mechanism. */
    if ($CIDRAM['Cache']->Using && $CIDRAM['Cache']->Using !== 'FF') {
        $CIDRAM['Cache']->deleteEntry($Entry);
        return;
    }

    /** Default process. */
    $Entry64 = base64_encode($Entry);
    while (($EntryPos = strpos($Source, "\n" . $Entry64 . ',')) !== false) {
        $EoL = strpos($Source, "\n", $EntryPos + 1);
        if ($EoL !== false) {
            $Line = substr($Source, $EntryPos, $EoL - $EntryPos);
            $Source = str_replace($Line, '', $Source);
            $Rebuild = true;
        }
    }
};

/**
 * Add an entry to the front-end cache data.
 *
 * @param string $Source Variable containing cache file data.
 * @param bool $Rebuild Flag indicating to rebuild cache file.
 * @param string $Entry Name of the cache entry to be added.
 * @param string $Data Cache entry data (what should be cached).
 * @param int $Expires When should the cache entry expire (be deleted).
 */
$CIDRAM['FECacheAdd'] = function (string &$Source, bool &$Rebuild, string $Entry, string $Data, int $Expires) use (&$CIDRAM) {

    /** Override if using a different preferred caching mechanism. */
    if ($CIDRAM['Cache']->Using && $CIDRAM['Cache']->Using !== 'FF') {
        $CIDRAM['Cache']->setEntry($Entry, $Data, $Expires - $CIDRAM['Now']);
        return;
    }

    /** Default process. */
    $CIDRAM['FECacheRemove']($Source, $Rebuild, $Entry);
    $NewLine = base64_encode($Entry) . ',' . base64_encode($Data) . ',' . $Expires . "\n";
    $Source .= $NewLine;
    $Rebuild = true;
};

/**
 * Get an entry from the front-end cache data.
 *
 * @param string $Source Variable containing cache file data.
 * @param bool $Rebuild Flag indicating to rebuild cache file.
 * @param string $Entry Name of the cache entry to get.
 * @return string|bool Returned cache entry data (or false on failure).
 */
$CIDRAM['FECacheGet'] = function (string &$Source, string $Entry) use (&$CIDRAM) {

    /** Override if using a different preferred caching mechanism. */
    if ($CIDRAM['Cache']->Using && $CIDRAM['Cache']->Using !== 'FF') {
        return $CIDRAM['Cache']->getEntry($Entry);
    }

    /** Default process. */
    $Entry = base64_encode($Entry);
    $EntryPos = strpos($Source, "\n" . $Entry . ',');
    if ($EntryPos !== false) {
        $EoL = strpos($Source, "\n", $EntryPos + 1);
        if ($EoL !== false) {
            $Line = substr($Source, $EntryPos, $EoL - $EntryPos);
            $Entry = explode(',', $Line);
            if (!empty($Entry[1])) {
                return base64_decode($Entry[1]);
            }
        }
    }
    return false;
};

/**
 * Compare two different versions of CIDRAM, or two different versions of a
 * component for CIDRAM, to see which is newer (mostly used by the updater).
 *
 * @param string $A The 1st version string.
 * @param string $B The 2nd version string.
 * @return bool True if the 2nd version is newer than the 1st version, and false
 *      otherwise (i.e., if they're the same, or if the 1st version is newer).
 */
$CIDRAM['VersionCompare'] = function (string $A, string $B): bool {
    $Normalise = function (&$Ver) {
        $Ver =
            preg_match('~^v?(\d+)$~i', $Ver, $Matches) ?:
            preg_match('~^v?(\d+)\.(\d+)$~i', $Ver, $Matches) ?:
            preg_match('~^v?(\d+)\.(\d+)\.(\d+)(alpha\d|RC\d{1,2}|-[.\d\w_+\\/]+)?$~i', $Ver, $Matches) ?:
            preg_match('~^(\d{1,4})[.-](\d{1,2})[.-](\d{1,4})(RC\d{1,2}|[.+-][\d\w_+\\/]+)?$~i', $Ver, $Matches) ?:
            preg_match('~^([\w]+)-([\d\w]+)-([\d\w]+)$~i', $Ver, $Matches);
        $Ver = [
            'Major' => isset($Matches[1]) ? $Matches[1] : 0,
            'Minor' => isset($Matches[2]) ? $Matches[2] : 0,
            'Patch' => isset($Matches[3]) ? $Matches[3] : 0,
            'Build' => isset($Matches[4]) ? $Matches[4] : 0
        ];
        if ($Ver['Build'] && substr($Ver['Build'], 0, 1) === '-') {
            $Ver['Build'] = substr($Ver['Build'], 1);
        }
        $Ver = array_map(function ($Var) {
            $VarInt = (int)$Var;
            $VarLen = strlen($Var);
            if ($Var == $VarInt && strlen($VarInt) === $VarLen && $VarLen > 1) {
                return $VarInt;
            }
            return strtolower($Var);
        }, $Ver);
    };
    $Normalise($A);
    $Normalise($B);
    return (
        $B['Major'] > $A['Major'] || (
            $B['Major'] === $A['Major'] &&
            $B['Minor'] > $A['Minor']
        ) || (
            $B['Major'] === $A['Major'] &&
            $B['Minor'] === $A['Minor'] &&
            $B['Patch'] > $A['Patch']
        ) || (
            $B['Major'] === $A['Major'] &&
            $B['Minor'] === $A['Minor'] &&
            $B['Patch'] === $A['Patch'] &&
            !empty($A['Build']) && (
                empty($B['Build']) || $B['Build'] > $A['Build']
            )
        )
    );
};

/**
 * Remove sub-arrays from an array.
 *
 * @param array $Arr An array.
 * @return array An array.
 */
$CIDRAM['ArrayFlatten'] = function (array $Arr): array {
    return array_filter($Arr, function () {
        return (!is_array(func_get_args()[0]));
    });
};

/**
 * Reduce an L10N array down to a single relevant string.
 *
 * @param array $Arr An L10N array.
 * @param string $Lang The language that we're hoping to isolate from the array.
 */
$CIDRAM['IsolateL10N'] = function (array &$Arr, string $Lang) {
    if (isset($Arr[$Lang])) {
        $Arr = $Arr[$Lang];
    } elseif (isset($Arr['en'])) {
        $Arr = $Arr['en'];
    } else {
        $Key = key($Arr);
        $Arr = $Arr[$Key];
    }
};

/**
 * Append one or two values to a string, depending on whether that string is
 * empty prior to calling the closure (allows cleaner code in some areas).
 *
 * @param string $String The string to work with.
 * @param string $Delimit Appended first, if the string is not empty.
 * @param string $Append Appended second, and always (empty or otherwise).
 */
$CIDRAM['AppendToString'] = function (string &$String, string $Delimit = '', string $Append = '') {
    if (!empty($String)) {
        $String .= $Delimit;
    }
    $String .= $Append;
};

/**
 * Performs some simple sanity checks on files (used by the updater).
 *
 * @param string $FileName The name of the file to be checked.
 * @param string $FileData The content of the file to be checked.
 * @return bool True when passed; False when failed.
 */
$CIDRAM['SanityCheck'] = function (string $FileName, string $FileData): bool {

    /** Check whether YAML is valid. */
    if (preg_match('~\.ya?ml$~i', $FileName)) {
        $ThisYAML = new \Maikuolan\Common\YAML();
        if (!($ThisYAML->process($FileData, $ThisYAML->Data))) {
            return false;
        }
    }

    /** A very simple, rudimentary check for unwanted, possibly maliciously inserted HTML. */
    if ($FileData && preg_match('~<(?:html|body)~i', $FileData)) {
        return false;
    }

    /** Passed. */
    return true;
};

/**
 * Used by the file manager to generate a list of the files contained in a
 * working directory (normally, the vault).
 *
 * @param string $Base The path to the working directory.
 * @return array A list of the files contained in the working directory.
 */
$CIDRAM['FileManager-RecursiveList'] = function (string $Base) use (&$CIDRAM): array {
    $Arr = [];
    $Key = -1;
    $Offset = strlen($Base);
    $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($Base), RecursiveIteratorIterator::SELF_FIRST);
    foreach ($List as $Item => $List) {
        $Key++;
        $ThisName = substr($Item, $Offset);
        if (preg_match('~^(?:/\.\.|./\.|\.{3})$~', str_replace("\\", '/', substr($Item, -3)))) {
            continue;
        }
        $Arr[$Key] = ['Filename' => $ThisName];
        if (is_dir($Item)) {
            $Arr[$Key]['CanEdit'] = false;
            $Arr[$Key]['Directory'] = true;
            $Arr[$Key]['Filesize'] = 0;
            $Arr[$Key]['Filetype'] = $CIDRAM['L10N']->getString('field_filetype_directory');
            $Arr[$Key]['Icon'] = 'icon=directory';
        } elseif (is_file($Item)) {
            $Arr[$Key]['CanEdit'] = true;
            $Arr[$Key]['Directory'] = false;
            $Arr[$Key]['Filesize'] = filesize($Item);
            if (isset($CIDRAM['FE']['TotalSize'])) {
                $CIDRAM['FE']['TotalSize'] += $Arr[$Key]['Filesize'];
            }
            if (isset($CIDRAM['Components']['Components'])) {
                $Component = $CIDRAM['L10N']->getString('field_filetype_unknown');
                $ThisNameFixed = str_replace("\\", '/', $ThisName);
                if (isset($CIDRAM['Components']['Files'][$ThisNameFixed])) {
                    if (!empty($CIDRAM['Components']['Names'][$CIDRAM['Components']['Files'][$ThisNameFixed]])) {
                        $Component = $CIDRAM['Components']['Names'][$CIDRAM['Components']['Files'][$ThisNameFixed]];
                    } else {
                        $Component = $CIDRAM['Components']['Files'][$ThisNameFixed];
                    }
                } elseif (preg_match('~(?:[^|/]\.ht|\.safety$|^salt\.dat$)~i', $ThisNameFixed)) {
                    $Component = $CIDRAM['L10N']->getString('label_fmgr_safety');
                } elseif (preg_match('/^config\.ini$/i', $ThisNameFixed)) {
                    $Component = $CIDRAM['L10N']->getString('link_config');
                } elseif ($CIDRAM['FileManager-IsLogFile']($ThisNameFixed)) {
                    $Component = $CIDRAM['L10N']->getString('link_logs');
                } elseif (preg_match('/(?:^auxiliary\.yaml|^ignore\.dat|_custom\.dat|\.sig|\.inc)$/i', $ThisNameFixed)) {
                    $Component = $CIDRAM['L10N']->getString('label_fmgr_other_sig');
                } elseif (preg_match('~(?:\.tmp|\.rollback|^(?:cache|hashes|ipbypass|fe_assets/frontend)\.dat)$~i', $ThisNameFixed)) {
                    $Component = $CIDRAM['L10N']->getString('label_fmgr_cache_data');
                } elseif (preg_match('/\.(?:dat|ya?ml)$/i', $ThisNameFixed)) {
                    $Component = $CIDRAM['L10N']->getString('label_fmgr_updates_metadata');
                }
                if (!isset($CIDRAM['Components']['Components'][$Component])) {
                    $CIDRAM['Components']['Components'][$Component] = 0;
                }
                $CIDRAM['Components']['Components'][$Component] += $Arr[$Key]['Filesize'];
                if (!isset($CIDRAM['Components']['ComponentFiles'][$Component])) {
                    $CIDRAM['Components']['ComponentFiles'][$Component] = [];
                }
                $CIDRAM['Components']['ComponentFiles'][$Component][$ThisNameFixed] = $Arr[$Key]['Filesize'];
            }
            if (($ExtDel = strrpos($Item, '.')) !== false) {
                $Ext = strtoupper(substr($Item, $ExtDel + 1));
                if (!$Ext) {
                    $Arr[$Key]['Filetype'] = $CIDRAM['L10N']->getString('field_filetype_unknown');
                    $Arr[$Key]['Icon'] = 'icon=unknown';
                    $CIDRAM['FormatFilesize']($Arr[$Key]['Filesize']);
                    continue;
                }
                $Arr[$Key]['Filetype'] = $CIDRAM['ParseVars'](['EXT' => $Ext], $CIDRAM['L10N']->getString('field_filetype_info'));
                if ($Ext === 'ICO') {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'file=' . urlencode($Arr[$Key]['Filename']);
                    $CIDRAM['FormatFilesize']($Arr[$Key]['Filesize']);
                    continue;
                }
                if (preg_match(
                    '/^(?:.?[BGL]Z.?|7Z|A(CE|LZ|P[KP]|R[CJ]?)?|B([AH]|Z2?)|CAB|DMG|' .
                    'I(CE|SO)|L(HA|Z[HOWX]?)|P(AK|AQ.?|CK|EA)|RZ|S(7Z|EA|EN|FX|IT.?|QX)|' .
                    'X(P3|Z)|YZ1|Z(IP.?|Z)?|(J|M|PH|R|SH|T|X)AR)$/'
                , $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=archive';
                } elseif (preg_match('/^[SDX]?HT[AM]L?$/', $Ext)) {
                    $Arr[$Key]['Icon'] = 'icon=html';
                } elseif (preg_match('/^(?:CSV|JSON|NEON|SQL|YAML)$/', $Ext)) {
                    $Arr[$Key]['Icon'] = 'icon=ods';
                } elseif (preg_match('/^(?:PDF|XDP)$/', $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=pdf';
                } elseif (preg_match('/^DOC[XT]?$/', $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=doc';
                } elseif (preg_match('/^XLS[XT]?$/', $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=xls';
                } elseif (preg_match('/^(?:CSS|JS|OD[BFGPST]|P(HP|PT))$/', $Ext)) {
                    $Arr[$Key]['Icon'] = 'icon=' . strtolower($Ext);
                    if (!preg_match('/^(?:CSS|JS|PHP)$/', $Ext)) {
                        $Arr[$Key]['CanEdit'] = false;
                    }
                } elseif (preg_match('/^(?:FLASH|SWF)$/', $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=swf';
                } elseif (preg_match(
                    '/^(?:BM[2P]|C(D5|GM)|D(IB|W[FG]|XF)|ECW|FITS|GIF|IMG|J(F?IF?|P[2S]|PE?G?2?|XR)|P(BM|CX|DD|GM|IC|N[GMS]|PM|S[DP])|S(ID|V[AG])|TGA|W(BMP?|EBP|MP)|X(CF|BMP))$/'
                , $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=image';
                } elseif (preg_match(
                    '/^(?:H?264|3GP(P2)?|A(M[CV]|VI)|BIK|D(IVX|V5?)|F([4L][CV]|MV)|GIFV|HLV|' .
                    'M(4V|OV|P4|PE?G[4V]?|KV|VR)|OGM|V(IDEO|OB)|W(EBM|M[FV]3?)|X(WMV|VID))$/'
                , $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=video';
                } elseif (preg_match(
                    '/^(?:3GA|A(AC|IFF?|SF|U)|CDA|FLAC?|M(P?4A|IDI|KA|P[A23])|OGG|PCM|' .
                    'R(AM?|M[AX])|SWA|W(AVE?|MA))$/'
                , $Ext)) {
                    $Arr[$Key]['CanEdit'] = false;
                    $Arr[$Key]['Icon'] = 'icon=audio';
                } elseif (preg_match('/^(?:MD|NFO|RTF|TXT)$/', $Ext)) {
                    $Arr[$Key]['Icon'] = 'icon=text';
                }
            } else {
                $Arr[$Key]['Filetype'] = $CIDRAM['L10N']->getString('field_filetype_unknown');
            }
        }
        if (empty($Arr[$Key]['Icon'])) {
            $Arr[$Key]['Icon'] = 'icon=unknown';
        }
        if ($Arr[$Key]['Filesize']) {
            $CIDRAM['FormatFilesize']($Arr[$Key]['Filesize']);
            $Arr[$Key]['Filesize'] .= ' ? <em>' . $CIDRAM['TimeFormat'](filemtime($Item), $CIDRAM['Config']['general']['time_format']) . '</em>';
        } else {
            $Arr[$Key]['Filesize'] = '';
        }
    }
    return $Arr;
};

/**
 * Used by the file manager and the updates pages to fetch the components list.
 *
 * @param string $Base The path to the working directory.
 * @param array $Arr The array to use for rendering components file YAML data.
 */
$CIDRAM['FetchComponentsLists'] = function (string $Base, array &$Arr) use (&$CIDRAM) {
    $Files = new DirectoryIterator($Base);
    foreach ($Files as $ThisFile) {
        if (!empty($ThisFile) && preg_match('/\.(?:dat|inc|ya?ml)$/i', $ThisFile)) {
            $Data = $CIDRAM['ReadFile']($Base . $ThisFile);
            if (substr($Data, 0, 4) === "---\n" && ($EoYAML = strpos($Data, "\n\n")) !== false) {
                $CIDRAM['YAML']->process(substr($Data, 4, $EoYAML - 4), $Arr);
            }
        }
    }
};

/**
 * Checks paths for directory traversal and ensures that they only contain
 * expected characters.
 *
 * @param string $Path The path to check.
 * @return bool False when directory traversals and/or unexpected characters
 *      are detected, and true otherwise.
 */
$CIDRAM['FileManager-PathSecurityCheck'] = function (string $Path): bool {
    $Path = str_replace("\\", '/', $Path);
    if (
        preg_match('~(?://|[^!\d\w\._-]$)~i', $Path) ||
        preg_match('~^(?:/\.\.|./\.|\.{3})$~', str_replace("\\", '/', substr($Path, -3)))
    ) {
        return false;
    }
    $Path = preg_split('@/@', $Path, -1, PREG_SPLIT_NO_EMPTY);
    $Valid = true;
    array_walk($Path, function ($Segment) use (&$Valid) {
        if (empty($Segment) || preg_match('/(?:[\x00-\x1f\x7f]+|^\.+$)/i', $Segment)) {
            $Valid = false;
        }
    });
    return $Valid;
};

/**
 * Used by the logs viewer to generate a list of the logfiles contained in a
 * working directory (normally, the vault).
 *
 * @param string $Base The path to the working directory.
 * @return array A list of the logfiles contained in the working directory.
 */
$CIDRAM['Logs-RecursiveList'] = function (string $Base) use (&$CIDRAM): array {
    $Arr = [];
    $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($Base), RecursiveIteratorIterator::SELF_FIRST);
    foreach ($List as $Item => $List) {
        $ThisName = str_replace("\\", '/', substr($Item, strlen($Base)));
        if (!is_file($Item) || !is_readable($Item) || is_dir($Item) || !$CIDRAM['FileManager-IsLogFile']($ThisName)) {
            continue;
        }
        $Arr[$ThisName] = ['Filename' => $ThisName, 'Filesize' => filesize($Item)];
        $CIDRAM['FormatFilesize']($Arr[$ThisName]['Filesize']);
    }
    ksort($Arr);
    return $Arr;
};

/**
 * Checks whether a component is in use (front-end closure).
 *
 * @param array $Component An array of the component metadata.
 * @return bool True for when in use; False for when not in use.
 */
$CIDRAM['IsInUse'] = function (array $Component) use (&$CIDRAM): bool {
    $Files = $Component['Files']['To'] ?? [];
    $CIDRAM['Arrayify']($Files);
    $UsedWith = $Component['Used with'] ?? '';
    $Description = $Component['Extended Description'] ?? '';
    foreach ($Files as $File) {
        $File = preg_quote($File);
        if ((($UsedWith === 'ipv4' || strpos($Description, 'signatures-&gt;ipv4') !== false) && preg_match(
            '~,(?:[\w\d]+:)?' . $File . ',~',
            ',' . $CIDRAM['Config']['signatures']['ipv4'] . ','
        )) || (($UsedWith === 'ipv6' || strpos($Description, 'signatures-&gt;ipv6') !== false) && preg_match(
            '~,(?:[\w\d]+:)?' . $File . ',~',
            ',' . $CIDRAM['Config']['signatures']['ipv6'] . ','
        )) || (($UsedWith === 'modules' || strpos($Description, 'signatures-&gt;modules') !== false) && preg_match(
            '~,(?:[\w\d]+:)?' . $File . ',~',
            ',' . $CIDRAM['Config']['signatures']['modules'] . ','
        )) || (
            !$UsedWith &&
            strpos($Description, 'signatures-&gt;ipv4') === false &&
            strpos($Description, 'signatures-&gt;ipv6') === false &&
            strpos($Description, 'signatures-&gt;modules') === false && (
                preg_match('~,(?:[\w\d]+:)?' . $File . ',~', ',' . $CIDRAM['Config']['signatures']['ipv4'] . ',') ||
                preg_match('~,(?:[\w\d]+:)?' . $File . ',~', ',' . $CIDRAM['Config']['signatures']['ipv6'] . ',') ||
                preg_match('~,(?:[\w\d]+:)?' . $File . ',~', ',' . $CIDRAM['Config']['signatures']['modules'] . ',')
            )
        )) {
            return true;
        }
    }
    return false;
};

/**
 * Determine the final IP address covered by an IPv4 CIDR. This closure is used
 * by the CIDR Calculator.
 *
 * @param string $First The first IP address.
 * @param int $Factor The range number (or CIDR factor number).
 * @return string The final IP address.
 */
$CIDRAM['IPv4GetLast'] = function (string $First, int $Factor): string {
    $Octets = explode('.', $First);
    $Split = $Bracket = $Factor / 8;
    $Split -= floor($Split);
    $Split = (int)(8 - ($Split * 8));
    $Octet = floor($Bracket);
    if ($Octet < 4) {
        $Octets[$Octet] += pow(2, $Split) - 1;
    }
    while ($Octet < 3) {
        $Octets[$Octet + 1] = 255;
        $Octet++;
    }
    return implode('.', $Octets);
};

/**
 * Determine the final IP address covered by an IPv6 CIDR. This closure is used
 * by the CIDR Calculator.
 *
 * @param string $First The first IP address.
 * @param int $Factor The range number (or CIDR factor number).
 * @return string The final IP address.
 */
$CIDRAM['IPv6GetLast'] = function (string $First, int $Factor): string {
    if (strpos($First, '::') !== false) {
        $Abr = 7 - substr_count($First, ':');
        $Arr = [':0:', ':0:0:', ':0:0:0:', ':0:0:0:0:', ':0:0:0:0:0:', ':0:0:0:0:0:0:'];
        $First = str_replace('::', $Arr[$Abr], $First);
    }
    $Octets = explode(':', $First);
    $Octet = 8;
    while ($Octet > 0) {
        $Octet--;
        $Octets[$Octet] = hexdec($Octets[$Octet]);
    }
    $Split = $Bracket = $Factor / 16;
    $Split -= floor($Split);
    $Split = (int)(16 - ($Split * 16));
    $Octet = floor($Bracket);
    if ($Octet < 8) {
        $Octets[$Octet] += pow(2, $Split) - 1;
    }
    while ($Octet < 7) {
        $Octets[$Octet + 1] = 65535;
        $Octet++;
    }
    $Octet = 8;
    while ($Octet > 0) {
        $Octet--;
        $Octets[$Octet] = dechex($Octets[$Octet]);
    }
    $Last = implode(':', $Octets);
    if (strpos($Last . '/', ':0:0/') !== false) {
        $Last = preg_replace('/(\:0){2,}$/i', '::', $Last, 1);
    } elseif (strpos($Last, ':0:0:') !== false) {
        $Last = preg_replace('/(?:(\:0)+\:(0\:)+|\:\:0$)/i', '::', $Last, 1);
    }
    return $Last;
};

/** Fetch remote data (front-end updates page). */
$CIDRAM['FetchRemote'] = function () use (&$CIDRAM) {
    $CIDRAM['Components']['ThisComponent']['RemoteData'] = '';
    $CIDRAM['FetchRemote-ContextFree'](
        $CIDRAM['Components']['ThisComponent']['RemoteData'],
        $CIDRAM['Components']['ThisComponent']['Remote']
    );
};

/**
 * Fetch remote data (context-free).
 *
 * @param string $RemoteData Where to put the remote data.
 * @param string $Remote Where to get the remote data.
 */
$CIDRAM['FetchRemote-ContextFree'] = function (string &$RemoteData, string &$Remote) use (&$CIDRAM) {
    $RemoteData = $CIDRAM['FECacheGet']($CIDRAM['FE']['Cache'], $Remote);
    if (!$RemoteData) {
        $RemoteData = $CIDRAM['Request']($Remote);
        if (strtolower(substr($Remote, -2)) === 'gz' && substr($RemoteData, 0, 2) === "\x1f\x8b") {
            $RemoteData = gzdecode($RemoteData);
        }
        if (empty($RemoteData)) {
            $RemoteData = '-';
        }
        $CIDRAM['FECacheAdd'](
            $CIDRAM['FE']['Cache'],
            $CIDRAM['FE']['Rebuild'],
            $Remote,
            $RemoteData,
            $CIDRAM['Now'] + 3600
        );
    }
};

/**
 * Checks whether component is activable.
 *
 * @param array $Component An array of the component metadata.
 * @return bool True for when activable; False for when not activable.
 */
$CIDRAM['IsActivable'] = function (array &$Component): bool {
    return (!empty($Component['Used with']) || (!empty($Component['Extended Description']) && strpos($Component['Extended Description'], 'signatures-&gt;') !== false));
};

/**
 * Activate component (front-end updates page).
 *
 * @param string $Type Value can be ipv4, ipv6, or modules.
 * @param string $ID The ID of the component to activate.
 */
$CIDRAM['ActivateComponent'] = function (string $Type, string $ID) use (&$CIDRAM) {
    $CIDRAM['Activation'][$Type] = array_unique(array_filter(
        explode(',', $CIDRAM['Activation'][$Type]),
        function ($Component) use (&$CIDRAM) {
            $Component = (strpos($Component, ':') === false) ? $Component : substr($Component, strpos($Component, ':') + 1);
            return ($Component && file_exists($CIDRAM['Vault'] . $Component));
        }
    ));
    foreach ($CIDRAM['Components']['Meta'][$ID]['Files']['To'] as $ThisFile) {
        $Ext = (($DecPos = strpos($ThisFile, '.')) !== false) ? substr($ThisFile, $DecPos + 1) : '';
        if (
            !empty($ThisFile) &&
            !preg_match('~^(?:css|gif|html?|jpe?g|js|png|ya?ml)$~i', $Ext) &&
            file_exists($CIDRAM['Vault'] . $ThisFile) &&
            empty($CIDRAM['Activation'][$Type][$ThisFile]) &&
            $CIDRAM['Traverse']($ThisFile)
        ) {
            $CIDRAM['Activation'][$Type][] = $ThisFile;
        }
    }
    if (count($CIDRAM['Activation'][$Type])) {
        sort($CIDRAM['Activation'][$Type]);
    }
    $CIDRAM['Activation'][$Type] = implode(',', $CIDRAM['Activation'][$Type]);
    if ($CIDRAM['Activation'][$Type] !== $CIDRAM['Config']['signatures'][$Type]) {
        $CIDRAM['Activation']['Modified'] = true;
    }
};

/**
 * Deactivate component (front-end updates page).
 *
 * @param string $Type Value can be ipv4, ipv6, or modules.
 * @param string $ID The ID of the component to deactivate.
 */
$CIDRAM['DeactivateComponent'] = function (string $Type, string $ID) use (&$CIDRAM) {
    $CIDRAM['Deactivation'][$Type] = array_unique(array_filter(
        explode(',', $CIDRAM['Deactivation'][$Type]),
        function ($Component) use (&$CIDRAM) {
            $Component = (strpos($Component, ':') === false) ? $Component : substr($Component, strpos($Component, ':') + 1);
            return ($Component && file_exists($CIDRAM['Vault'] . $Component));
        }
    ));
    if (count($CIDRAM['Deactivation'][$Type])) {
        sort($CIDRAM['Deactivation'][$Type]);
    }
    $CIDRAM['Deactivation'][$Type] = ',' . implode(',', $CIDRAM['Deactivation'][$Type]) . ',';
    foreach ($CIDRAM['Components']['Meta'][$ID]['Files']['To'] as $CIDRAM['Deactivation']['ThisFile']) {
        $CIDRAM['Deactivation'][$Type] = preg_replace(
            '~,(?:[\w\d]+:)?' . preg_quote($CIDRAM['Deactivation']['ThisFile']) . ',~',
            ',',
            $CIDRAM['Deactivation'][$Type]
        );
    }
    $CIDRAM['Deactivation'][$Type] = substr($CIDRAM['Deactivation'][$Type], 1, -1);
    if ($CIDRAM['Deactivation'][$Type] !== $CIDRAM['Config']['signatures'][$Type]) {
        $CIDRAM['Deactivation']['Modified'] = true;
    }
};

/**
 * Prepares component extended description (front-end updates page).
 *
 * @param array $Arr Metadata of the component to be prepared.
 * @param string $Key A key to use to help find L10N data for the component description.
 */
$CIDRAM['PrepareExtendedDescription'] = function (array &$Arr, string $Key = '') use (&$CIDRAM) {
    $Key = 'Extended Description ' . $Key;
    if (isset($CIDRAM['L10N']->Data[$Key])) {
        $Arr['Extended Description'] = $CIDRAM['L10N']->getString($Key);
    } elseif (empty($Arr['Extended Description'])) {
        $Arr['Extended Description'] = '';
    }
    if (is_array($Arr['Extended Description'])) {
        $CIDRAM['IsolateL10N']($Arr['Extended Description'], $CIDRAM['Config']['general']['lang']);
    }
    if (!empty($Arr['Used with']) && strpos($Arr['Extended Description'], 'signatures-&gt;') === false) {
        $Arr['Extended Description'] .=
            '<br /><em>' . $CIDRAM['L10N']->getString('label_used_with') .
            '<code>signatures-&gt;' . $Arr['Used with'] . '</code></em>';
    }
    if (!empty($Arr['False Positive Risk'])) {
        if ($Arr['False Positive Risk'] === 'Low') {
            $State = $CIDRAM['L10N']->getString('state_risk_low');
            $Class = 'txtGn';
        } elseif ($Arr['False Positive Risk'] === 'Medium') {
            $State = $CIDRAM['L10N']->getString('state_risk_medium');
            $Class = 'txtOe';
        } elseif ($Arr['False Positive Risk'] === 'High') {
            $State = $CIDRAM['L10N']->getString('state_risk_high');
            $Class = 'txtRd';
        } else {
            return;
        }
        $Arr['Extended Description'] .=
            '<br /><em>' . $CIDRAM['L10N']->getString('label_false_positive_risk') .
            '<span class="' . $Class . '">' . $State . '</span></em>';
    }
};

/**
 * Prepares component name (front-end updates page).
 *
 * @param array $Arr Metadata of the component to be prepared.
 * @param string $Key A key to use to help find L10N data for the component name.
 */
$CIDRAM['PrepareName'] = function (array &$Arr, string $Key = '') use (&$CIDRAM) {
    $Key = 'Name ' . $Key;
    if (isset($CIDRAM['L10N']->Data[$Key])) {
        $Arr['Name'] = $CIDRAM['L10N']->getString($Key);
    } elseif (empty($Arr['Name'])) {
        $Arr['Name'] = '';
    }
    if (is_array($Arr['Name'])) {
        $CIDRAM['IsolateL10N']($Arr['Name'], $CIDRAM['Config']['general']['lang']);
    }
};

/**
 * Duplication avoidance (front-end updates page).
 *
 * @param string $Targets
 * @return bool Always false.
 */
$CIDRAM['ComponentFunctionUpdatePrep'] = function (string $Targets) use (&$CIDRAM): bool {
    if (!empty($CIDRAM['Components']['Meta'][$Targets]['Files'])) {
        $CIDRAM['PrepareExtendedDescription']($CIDRAM['Components']['Meta'][$Targets]);
        $CIDRAM['Arrayify']($CIDRAM['Components']['Meta'][$Targets]['Files']);
        $CIDRAM['Arrayify']($CIDRAM['Components']['Meta'][$Targets]['Files']['To']);
        return $CIDRAM['IsInUse']($CIDRAM['Components']['Meta'][$Targets]);
    }
    return false;
};

/**
 * Simulates block event (used by the IP tracking and IP test pages).
 *
 * @param string $Addr The IP address to test against.
 * @param bool $Modules Specifies whether to test against modules.
 * @param bool $Aux Specifies whether to test against auxiliary rules.
 * @param bool $Verification Specifies whether to test against search engine and social media verification.
 */
$CIDRAM['SimulateBlockEvent'] = function (string $Addr, bool $Modules = false, bool $Aux = false, bool $Verification = false) use (&$CIDRAM) {

    /** Reset bypass flags (needed to prevent falsing due to search engine verification). */
    $CIDRAM['ResetBypassFlags']();

    /** Reset "don't log" flag. */
    $CIDRAM['Flag Don\'t Log'] = false;

    /** Reset hostname (needed to prevent falsing due to repeat module calls involving hostname lookups). */
    $CIDRAM['Hostname'] = '';

    /** Populate BlockInfo. */
    $CIDRAM['BlockInfo'] = [
        'ID' => $CIDRAM['GenerateID'](),
        'IPAddr' => $Addr,
        'IPAddrResolved' => $CIDRAM['Resolve6to4']($Addr),
        'Query' => 'SimulateBlockEvent',
        'Referrer' => '',
        'UA' => !empty($CIDRAM['FE']['custom-ua']) ? $CIDRAM['FE']['custom-ua'] : 'SimulateBlockEvent',
        'ReasonMessage' => '',
        'SignatureCount' => 0,
        'Signatures' => '',
        'WhyReason' => '',
        'xmlLang' => $CIDRAM['Config']['general']['lang'],
        'rURI' => 'SimulateBlockEvent'
    ];
    $CIDRAM['BlockInfo']['UALC'] = strtolower($CIDRAM['BlockInfo']['UA']);

    /** Standard IP check. */
    try {
        $CIDRAM['Caught'] = false;
        $CIDRAM['TestResults'] = $CIDRAM['RunTests']($Addr);
    } catch (\Exception $e) {
        $CIDRAM['Caught'] = true;
    }

    /** Resolved IP check. */
    if ($CIDRAM['BlockInfo']['IPAddrResolved']) {
        if (!empty($CIDRAM['ThisIP']['IPAddress'])) {
            $CIDRAM['ThisIP']['IPAddress'] .= ' (' . $CIDRAM['BlockInfo']['IPAddrResolved'] . ')';
        }
        try {
            $CIDRAM['TestResults'] = ($CIDRAM['RunTests']($CIDRAM['BlockInfo']['IPAddrResolved']) || $CIDRAM['TestResults']);
        } catch (\Exception $e) {
            $CIDRAM['Caught'] = true;
        }
    }

    /** Instantiate report orchestrator (used by some modules). */
    $CIDRAM['Reporter'] = new \CIDRAM\Core\Reporter();

    /** Execute modules, if any have been enabled. */
    if ($Modules && $CIDRAM['Config']['signatures']['modules'] && empty($CIDRAM['Whitelisted'])) {
        $CIDRAM['InitialiseErrorHandler']();

        /** Explode module list and cycle through all modules. */
        $Modules = explode(',', $CIDRAM['Config']['signatures']['modules']);
        array_walk($Modules, function ($Module) use (&$CIDRAM) {
            $Module = (strpos($Module, ':') === false) ? $Module : substr($Module, strpos($Module, ':') + 1);
            $Infractions = $CIDRAM['BlockInfo']['SignatureCount'];
            if (file_exists($CIDRAM['Vault'] . $Module) && is_readable($CIDRAM['Vault'] . $Module)) {
                require $CIDRAM['Vault'] . $Module;
            }
        });

        $CIDRAM['ModuleErrors'] = $CIDRAM['Errors'];
        $CIDRAM['RestoreErrorHandler']();
    }

    /** Execute search engine verification. */
    if ($Verification && empty($CIDRAM['Whitelisted'])) {
        $CIDRAM['SearchEngineVerification']();
    }

    /** Execute social media verification. */
    if ($Verification && empty($CIDRAM['Whitelisted'])) {
        $CIDRAM['SocialMediaVerification']();
    }

    /** Process auxiliary rules, if any exist. */
    if ($Aux && empty($CIDRAM['Whitelisted'])) {
        $CIDRAM['InitialiseErrorHandler']();
        $CIDRAM['Aux']();
        $CIDRAM['AuxErrors'] = $CIDRAM['Errors'];
        $CIDRAM['RestoreErrorHandler']();
    }

    /**
     * Destroying the reporter (we won't process reports in this case, because we're only simulating block events,
     * as opposed to checking against actual, real requests; still needed to set it though to prevent errors).
     */
    unset($CIDRAM['Reporter']);

};

/**
 * Filter the available language options provided by the configuration page on
 * the basis of the availability of the corresponding language files.
 *
 * @param string $ChoiceKey Language code.
 * @return bool Valid/Invalid.
 */
$CIDRAM['FilterLang'] = function (string $ChoiceKey) use (&$CIDRAM): bool {
    $Path = $CIDRAM['Vault'] . 'lang/lang.' . $ChoiceKey;
    return (file_exists($Path . '.yaml') && file_exists($Path . '.fe.yaml'));
};

/**
 * Filter the available hash algorithms provided by the configuration page on
 * the basis of their availability.
 *
 * @param string $ChoiceKey Hash algorithm.
 * @return bool Valid/Invalid.
 */
$CIDRAM['FilterAlgo'] = function ($ChoiceKey) use (&$CIDRAM) {
    if ($ChoiceKey === 'PASSWORD_ARGON2ID') {
        return $CIDRAM['VersionCompare'](PHP_VERSION, '7.3.0');
    }
    return true;
};

/**
 * Filter the available theme options provided by the configuration page on
 * the basis of their availability.
 *
 * @param string $ChoiceKey Theme ID.
 * @return bool Valid/Invalid.
 */
$CIDRAM['FilterTheme'] = function (string $ChoiceKey) use (&$CIDRAM): bool {
    if ($ChoiceKey === 'default') {
        return true;
    }
    $Path = $CIDRAM['Vault'] . 'fe_assets/' . $ChoiceKey . '/';
    return (file_exists($Path . 'frontend.css') || file_exists($CIDRAM['Vault'] . 'template_' . $ChoiceKey . '.html'));
};

/**
 * Attempt to perform some simple formatting for the log data.
 *
 * @param string $In The log data to be formatted.
 * @param string $BlockLink Used as the basis for links inserted into displayed log data used for searching related log data.
 * @param string $Current The current search query (if it exists). Used to avoid inserting unnecessary links.
 * @param string $FieldSeparator Used to distinguish between a field's name and its content.
 * @param bool $Flags Tells the formatter whether the flags CSS file is available.
 */
$CIDRAM['Formatter'] = function (string &$In, string $BlockLink = '', string $Current = '', string $FieldSeparator = ': ', bool $Flags = false) {
    static $MaxBlockSize = 65536;
    if (strpos($In, "<br />\n") === false) {
        $In = '<div class="hB hFd s">' . $In . '</div>';
        return;
    }
    $Out = '';
    $In = "\n" . $In;
    $Len = strlen($In);
    $Caret = 0;
    $BlockSeparator = (strpos($In, "<br />\n<br />\n") !== false) ? "<br />\n<br />\n" : "<br />\n";
    $BlockSeparatorLen = strlen($BlockSeparator);
    $Demojibakefier = new \Maikuolan\Common\Demojibakefier();
    while ($Caret < $Len) {
        $Remainder = $Len - $Caret;
        if ($Remainder < $MaxBlockSize && $Remainder < ini_get('pcre.backtrack_limit')) {
            $Section = substr($In, $Caret) . $BlockSeparator;
            $Caret = $Len;
        } else {
            $SectionLen = strrpos(substr($In, $Caret, $MaxBlockSize), $BlockSeparator);
            $Section = substr($In, $Caret, $SectionLen) . $BlockSeparator;
            $Caret += $SectionLen;
        }
        preg_match_all('~(&lt;\?(?:(?!&lt;\?)[^\n])+\?&gt;|<\?(?:(?!<\?)[^\n])+\?>|\{\?(?:(?!\{\?)[^\n])+\?\})~i', $Section, $Parts);
        foreach ($Parts[0] as $ThisPart) {
            if (strlen($ThisPart) > 512 || strpos($ThisPart, "\n") !== false) {
                continue;
            }
            $Section = str_replace($ThisPart, '<code>' . $ThisPart . '</code>', $Section);
        }
        if (strpos($Section, "<br />\n<br />\n") !== false) {
            preg_match_all('~\n((?!?)[^\n:]+)' . $FieldSeparator . '((?:(?!<br />)[^\n])+)~i', $Section, $Parts);
            if (count($Parts[1])) {
                $Parts[1] = array_unique($Parts[1]);
                foreach ($Parts[1] as $ThisPart) {
                    $Section = str_replace(
                        "\n" . $ThisPart . $FieldSeparator,
                        "\n<span class=\"textLabel\">" . $ThisPart . '</span>' . $FieldSeparator,
                        $Section
                    );
                }
            }
            if (count($Parts[2]) && $BlockSeparatorLen === 14) {
                $Parts[2] = array_unique($Parts[2]);
                foreach ($Parts[2] as $ThisPart) {
                    $ThisPartUnsafe = str_replace(['&gt;', '&lt;'], ['>', '<'], $ThisPart);
                    $TestString = $Demojibakefier->guard($ThisPartUnsafe);
                    $Alternate = (
                        $TestString !== $ThisPartUnsafe && $Demojibakefier->Last
                    ) ? '<code dir="ltr">?' . $Demojibakefier->Last . '??UTF-8' . $FieldSeparator . '</code>' . str_replace(['<', '>'], ['&lt;', '&gt;'], $TestString) . "<br />\n" : '';
                    if (!$ThisPart || $ThisPart === $Current) {
                        $Section = str_replace(
                            $FieldSeparator . $ThisPart . "<br />\n",
                            $FieldSeparator . $ThisPart . "<br />\n" . $Alternate,
                            $Section
                        );
                        continue;
                    }
                    $Enc = str_replace('=', '_', base64_encode($ThisPart));
                    $Section = str_replace(
                        $FieldSeparator . $ThisPart . "<br />\n",
                        $FieldSeparator . $ThisPart . ' <a href="' . $BlockLink . '&search=' . $Enc . '"></a>' . "<br />\n" . $Alternate,
                        $Section
                    );
                }
            }
            preg_match_all('~\n((?:(?!?)[^\n:]+)' . $FieldSeparator . '(?:(?!<br />)[^\n])+)~i', $Section, $Parts);
            if (count($Parts[1])) {
                foreach ($Parts[1] as $ThisPart) {
                    $Section = str_replace("\n" . $ThisPart . "<br />\n", "\n<span class=\"s\">" . $ThisPart . "</span><br />\n", $Section);
                }
            }
            preg_match_all('~\("([^()"]+)", L~', $Section, $Parts);
            if (count($Parts[1])) {
                $Parts[1] = array_unique($Parts[1]);
                foreach ($Parts[1] as $ThisPart) {
                    $Section = str_replace(
                        '("' . $ThisPart . '", L',
                        '("<a href="' . $BlockLink . '&search=' . str_replace('=', '_', base64_encode($ThisPart)) . '">' . $ThisPart . '</a>", L',
                        $Section
                    );
                }
            }
            preg_match_all('~\[([A-Z]{2})\]~', $Section, $Parts);
            if (count($Parts[1])) {
                if ($Flags) {
                    $OuterOpen = '';
                    $OuterClose = '';
                    $InnerOpen = '<span class="flag ';
                    $InnerClose = '"><span></span></span>';
                } else {
                    $OuterOpen = '[';
                    $OuterClose = ']';
                    $InnerOpen = '';
                    $InnerClose = '';
                }
                foreach ($Parts[1] as $ThisPart) {
                    $Section = str_replace(
                        '[' . $ThisPart . ']',
                        $OuterOpen . '<a href="' . $BlockLink . '&search=' . str_replace('=', '_', base64_encode($ThisPart)) . '">' . $InnerOpen . $ThisPart . $InnerClose . '</a>' . $OuterClose,
                        $Section
                    );
                }
            }
        }
        $Out .= substr($Section, 0, $BlockSeparatorLen * -1);
    }
    $Out = substr($Out, 1);
    $In = '';
    $BlockStart = 0;
    $BlockEnd = 0;
    while ($BlockEnd !== false) {
        $Darken = empty($Darken);
        $Style = '<div class="h' . ($Darken ? 'B' : 'W') . ' hFd fW">';
        $BlockEnd = strpos($Out, $BlockSeparator, $BlockStart);
        $In .= $Style . substr($Out, $BlockStart, $BlockEnd - $BlockStart + $BlockSeparatorLen) . '</div>';
        $BlockStart = $BlockEnd + $BlockSeparatorLen;
    }
    $In = str_replace("<br />\n</div>", "<br /></div>\n", $In);
};

/**
 * Attempt to tally log data.
 *
 * @param string $In The log data to be tallied.
 * @param string $BlockLink Used as the basis for links inserted into displayed log data used for searching related log data.
 * @param array $Exclusions Instructs which fields should be excluded from the tally.
 * @return string A tally of the log data (or an empty string when valid log data isn't supplied).
 */
$CIDRAM['Tally'] = function (string $In, string $BlockLink, array $Exclusions = []) use (&$CIDRAM): string {
    if (empty($In)) {
        return '';
    }
    $Data = [];
    $PosA = $PosB = 0;
    while (($PosB = strpos($In, "\n", $PosA)) !== false) {
        $Line = substr($In, $PosA, $PosB - $PosA);
        $PosA = $PosB + 1;
        if (empty($Line)) {
            continue;
        }
        foreach ($Exclusions as $Exclusion) {
            $Len = strlen($Exclusion);
            if (substr($Line, 0, $Len) === $Exclusion) {
                continue 2;
            }
        }
        $Separator = (strpos($Line, '?') !== false) ? '?' : ': ';
        if (($SeparatorPos = strpos($Line, $Separator)) === false) {
            continue;
        }
        $Field = trim(substr($Line, 0, $SeparatorPos));
        $Entry = trim(substr($Line, $SeparatorPos + strlen($Separator)));
        if (!isset($Data[$Field])) {
            $Data[$Field] = [];
        }
        if (!isset($Data[$Field][$Entry])) {
            $Data[$Field][$Entry] = 0;
        }
        $Data[$Field][$Entry]++;
    }
    $Data['Origin'] = [];
    for ($A = 65; $A < 91; $A++) {
        for ($B = 65; $B < 91; $B++) {
            $Code = '[' . chr($A) . chr($B) . ']';
            if ($Count = substr_count($In, $Code)) {
                $Data['Origin'][$Code] = $Count;
            }
        }
    }
    if (empty($Data['Origin'])) {
        unset($Data['Origin']);
    }
    $Out = '<table>';
    foreach ($Data as $Field => $Entries) {
        $Out .= '<tr><td class="h2f" colspan="2"><div class="s">' . $Field . "</div></td></tr>\n";
        arsort($Entries, SORT_NUMERIC);
        foreach ($Entries as $Entry => $Count) {
            if (!(substr($Entry, 0, 1) === '[' && substr($Entry, 3, 1) === ']')) {
                $Entry .= ' <a href="' . $BlockLink . '&search=' . str_replace('=', '_', base64_encode($Entry)) . '"></a>';
            }
            preg_match_all('~\("([^()"]+)", L~', $Entry, $Parts);
            if (count($Parts[1])) {
                foreach ($Parts[1] as $ThisPart) {
                    $Entry = str_replace(
                        '("' . $ThisPart . '", L',
                        '("<a href="' . $BlockLink . '&search=' . str_replace('=', '_', base64_encode($ThisPart)) . '">' . $ThisPart . '</a>", L',
                        $Entry
                    );
                }
            }
            preg_match_all('~\[([A-Z]{2})\]~', $Entry, $Parts);
            if (count($Parts[1])) {
                foreach ($Parts[1] as $ThisPart) {
                    $Entry = str_replace('[' . $ThisPart . ']', $CIDRAM['FE']['Flags'] ? (
                        '<a href="' . $BlockLink . '&search=' . str_replace('=', '_', base64_encode($ThisPart)) . '"><span class="flag ' . $ThisPart . '"></span></a>'
                    ) : (
                        '[<a href="' . $BlockLink . '&search=' . str_replace('=', '_', base64_encode($ThisPart)) . '">' . $ThisPart . '</a>]'
                    ), $Entry);
                }
            }
            $Percent = $CIDRAM['FE']['EntryCount'] ? ($Count / $CIDRAM['FE']['EntryCount']) * 100 : 0;
            $Out .= '<tr><td class="h1 fW">' . $Entry . '</td><td class="h1f"><div class="s">' . $CIDRAM['NumberFormatter']->format($Count) . ' (' . $CIDRAM['NumberFormatter']->format($Percent, 2) . "%)</div></td></tr>\n";
        }
    }
    $Out .= '</table>';
    return $Out;
};

/**
 * Get the appropriate path for a specified asset as per the defined theme.
 *
 * @param string $Asset The asset filename.
 * @param bool $CanFail Is failure acceptable? (Default: False)
 * @throws Exception if the asset can't be found.
 * @return string The asset path.
 */
$CIDRAM['GetAssetPath'] = function (string $Asset, bool $CanFail = false) use (&$CIDRAM): string {
    if (
        $CIDRAM['Config']['template_data']['theme'] !== 'default' &&
        file_exists($CIDRAM['Vault'] . 'fe_assets/' . $CIDRAM['Config']['template_data']['theme'] . '/' . $Asset)
    ) {
        return $CIDRAM['Vault'] . 'fe_assets/' . $CIDRAM['Config']['template_data']['theme'] . '/' . $Asset;
    }
    if (file_exists($CIDRAM['Vault'] . 'fe_assets/' . $Asset)) {
        return $CIDRAM['Vault'] . 'fe_assets/' . $Asset;
    }
    if ($CanFail) {
        return '';
    }
    throw new \Exception('Asset not found');
};

/**
 * Determines whether to display warnings about the PHP version used (based
 * upon what we know at the time that the package was last updated; information
 * herein is likely to become stale very quickly when not updated frequently).
 *
 * References:
 * - secure.php.net/releases/
 * - secure.php.net/supported-versions.php
 * - cvedetails.com/vendor/74/PHP.html
 * - maikuolan.github.io/Compatibility-Charts/
 * - maikuolan.github.io/Vulnerability-Charts/php.html
 *
 * @param string $Version The PHP version used (defaults to PHP_VERSION).
 * @return int Warning level.
 */
$CIDRAM['VersionWarning'] = function (string $Version = PHP_VERSION) use (&$CIDRAM): int {
    $Level = 0;
    $Minor = (float)$Version;
    if (!empty($CIDRAM['ForceVersionWarning']) || $Minor < 7.1 || (
        $Minor === 7.1 && $CIDRAM['VersionCompare']($Version, '7.1.30')
    ) || (
        $Minor === 7.2 && $CIDRAM['VersionCompare']($Version, '7.2.19')
    ) || (
        $Minor === 7.3 && $CIDRAM['VersionCompare']($Version, '7.3.6')
    )) {
        $Level += 2;
    }
    if ($Minor < 7.2) {
        $Level += 1;
    }
    $CIDRAM['ForceVersionWarning'] = false;
    return $Level;
};

/**
 * Executes a list of closures or commands when specific conditions are met.
 *
 * @param string|array $Closures The list of closures or commands to execute.
 * @param bool $Queue Whether to queue the operation or perform immediately.
 */
$CIDRAM['FE_Executor'] = function ($Closures = false, bool $Queue = false) use (&$CIDRAM) {
    if ($Queue && $Closures !== false) {
        if (empty($CIDRAM['FE_Executor_Queue'])) {
            $CIDRAM['FE_Executor_Queue'] = [];
        }
        $CIDRAM['FE_Executor_Queue'][] = $Closures;
        return;
    }
    if ($Closures === false && !empty($CIDRAM['FE_Executor_Queue'])) {
        foreach ($CIDRAM['FE_Executor_Queue'] as $QueueItem) {
            $CIDRAM['FE_Executor']($QueueItem);
        }
        $CIDRAM['FE_Executor_Queue'] = [];
        return;
    }
    $CIDRAM['Arrayify']($Closures);
    foreach ($Closures as $Closure) {
        if (isset($CIDRAM[$Closure]) && is_object($CIDRAM[$Closure])) {
            $CIDRAM[$Closure]();
        } elseif (($Pos = strpos($Closure, ' ')) !== false) {
            $Params = substr($Closure, $Pos + 1);
            $Closure = substr($Closure, 0, $Pos);
            if (isset($CIDRAM[$Closure]) && is_object($CIDRAM[$Closure])) {
                $CIDRAM[$Closure]($Params);
            }
        }
    }
};

/**
 * Updates plugin version cited in the WordPress plugins dashboard, if this
 * copy of CIDRAM is running as a WordPress plugin.
 */
$CIDRAM['WP-Ver'] = function () use (&$CIDRAM) {
    if (
        !empty($CIDRAM['Components']['RemoteMeta']['CIDRAM']['Version']) &&
        ($ThisData = $CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . '../cidram.php'))
    ) {
        $PlugHead = "\x3C\x3Fphp\n/**\n * Plugin Name: CIDRAM\n * Version: ";
        if (substr($ThisData, 0, 45) === $PlugHead) {
            $PlugHeadEnd = strpos($ThisData, "\n", 45);
            $CIDRAM['Updater-IO']->writeFile(
                $CIDRAM['Vault'] . '../cidram.php',
                $PlugHead . $CIDRAM['Components']['RemoteMeta']['CIDRAM Core']['Version'] . substr($ThisData, $PlugHeadEnd)
            );
        }
    }
};

/** Used to format numbers according to the specified configuration. */
$CIDRAM['NumberFormatter'] = new \Maikuolan\Common\NumberFormatter($CIDRAM['Config']['general']['numbers']);

/** Used by the Number_L10N_JS closure (separated out to improve memory footprint). */
$CIDRAM['Number_L10N_JS_Sets'] = [
    'NoSep-1' => ['.', '', 3, 'return l10nd', 1],
    'NoSep-2' => [',', '', 3, 'return l10nd', 1],
    'Latin-1' => ['.', ',', 3, 'return l10nd', 1],
    'Latin-2' => ['.', '?', 3, 'return l10nd', 1],
    'Latin-3' => [',', '.', 3, 'return l10nd', 1],
    'Latin-4' => [',', '?', 3, 'return l10nd', 1],
    'Latin-5' => ['', ',', 3, 'return l10nd', 1],
    'China-1' => ['.', ',', 4, 'return l10nd', 1],
    'India-1' => ['.', ',', 2, 'return l10nd', 0],
    'India-2' => ['.', ',', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'India-3' => ['.', ',', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'India-4' => ['.', ',', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'India-5' => ['.', ',', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'India-6' => ['.', ',', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'Arabic-1' => ['?', '', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Arabic-2' => ['?', '?', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Arabic-3' => ['?', '?', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Arabic-4' => ['?', '?', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'Bengali-1' => ['.', ',', 2, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 0],
    'Burmese-1' => ['.', '', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Khmer-1' => [',', '.', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Lao-1' => ['.', '', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Thai-1' => ['.', ',', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    'Thai-2' => ['.', '', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
];

/**
 * Generates JavaScript code for localising numbers locally.
 *
 * @return string The JavaScript code.
 */
$CIDRAM['Number_L10N_JS'] = function () use (&$CIDRAM): string {
    $Base =
        'function l10nn(l10nd){%4$s};function nft(r){var x=r.indexOf(\'.\')!=-1?' .
        '\'%1$s\'+r.replace(/^.*\./gi,\'\'):\'\',n=r.replace(/\..*$/gi,\'\').rep' .
        'lace(/[^0-9]/gi,\'\'),t=n.length;for(e=\'\',b=%5$d,i=1;i<=t;i++){b>%3$d' .
        '&&(b=1,e=\'%2$s\'+e);var e=l10nn(n.substring(t-i,t-(i-1)))+e;b++}var t=' .
        'x.length;for(y=\'\',b=1,i=1;i<=t;i++){var y=l10nn(x.substring(t-i,t-(i-' .
        '1)))+y}return e+y}';
    if (
        !empty($CIDRAM['Config']['general']['numbers']) &&
        isset($CIDRAM['Number_L10N_JS_Sets'][$CIDRAM['Config']['general']['numbers']])
    ) {
        $Set = $CIDRAM['Number_L10N_JS_Sets'][$CIDRAM['Config']['general']['numbers']];
    } else {
        $Set = $CIDRAM['Number_L10N_JS_Sets']['Latin-1'];
    }
    return sprintf($Base, $Set[0], $Set[1], $Set[2], $Set[3], $Set[4]);
};

/**
 * Switch control for front-end page filters.
 *
 * @param array $Switches Names of available switches.
 * @param string $Selector Switch selector variable.
 * @param bool $StateModified Determines whether the filter state has been modified.
 * @param string $Redirect Reconstructed path to redirect to when the state changes.
 * @param string $Options Reconstructed filter controls.
 */
$CIDRAM['FilterSwitch'] = function (array $Switches, string $Selector, bool &$StateModified, string &$Redirect, string &$Options) use (&$CIDRAM) {
    foreach ($Switches as $Switch) {
        $State = (!empty($Selector) && $Selector === $Switch);
        $CIDRAM['FE'][$Switch] = empty($CIDRAM['QueryVars'][$Switch]) ? false : (
            ($CIDRAM['QueryVars'][$Switch] === 'true' && !$State) ||
            ($CIDRAM['QueryVars'][$Switch] !== 'true' && $State)
        );
        if ($State) {
            $StateModified = true;
        }
        if ($CIDRAM['FE'][$Switch]) {
            $Redirect .= '&' . $Switch . '=true';
            $LangItem = 'switch-' . $Switch . '-set-false';
        } else {
            $Redirect .= '&' . $Switch . '=false';
            $LangItem = 'switch-' . $Switch . '-set-true';
        }
        $Label = $CIDRAM['L10N']->getString($LangItem) ?: $LangItem;
        $Options .= '<option value="' . $Switch . '">' . $Label . '</option>';
    }
};

/**
 * Appends test data onto component metadata.
 *
 * @param array $Component The component to append test data onto.
 * @param bool $ReturnState Whether to operate as a function or a procedure.
 * @return bool|null Indicates whether tests have passed, when operating as a function.
 */
$CIDRAM['AppendTests'] = function (array &$Component, bool $ReturnState = false) use (&$CIDRAM) {
    $TestData = $CIDRAM['FECacheGet'](
        $CIDRAM['FE']['Cache'],
        $CIDRAM['Components']['RemoteMeta'][$Component['ID']]['Tests']
    );
    if (!$TestData) {
        $TestData = $CIDRAM['Request'](
            $CIDRAM['Components']['RemoteMeta'][$Component['ID']]['Tests']
        ) ?: '-';
        $CIDRAM['FECacheAdd'](
            $CIDRAM['FE']['Cache'],
            $CIDRAM['FE']['Rebuild'],
            $CIDRAM['Components']['RemoteMeta'][$Component['ID']]['Tests'],
            $TestData,
            $CIDRAM['Now'] + 1800
        );
    }
    if (substr($TestData, 0, 1) === '{' && substr($TestData, -1) === '}') {
        $TestData = json_decode($TestData, true, 5);
    }
    if (!empty($TestData['statuses']) && is_array($TestData['statuses'])) {
        $TestsTotal = 0;
        $TestsPassed = 0;
        $TestDetails = '';
        foreach ($TestData['statuses'] as $ThisStatus) {
            if (empty($ThisStatus['context']) || empty($ThisStatus['state'])) {
                continue;
            }
            $TestsTotal++;
            $StatusHead = '';
            if ($ThisStatus['state'] === 'success') {
                if ($TestsPassed !== '?') {
                    $TestsPassed++;
                }
                $StatusHead .= '<span class="txtGn">?? ';
            } elseif ($ThisStatus['state'] === 'pending') {
                $TestsPassed = '?';
                $StatusHead .= '<span class="txtOe">? ';
            } else {
                if ($ReturnState) {
                    return false;
                }
                $StatusHead .= '<span class="txtRd">? ';
            }
            $StatusHead .= empty($ThisStatus['target_url']) ? $ThisStatus['context'] : (
                '<a href="' . $ThisStatus['target_url'] . '">' . $ThisStatus['context'] . '</a>'
            );
            if (!$ReturnState) {
                $CIDRAM['AppendToString']($TestDetails, '<br />', $StatusHead . '</span>');
            }
        }
        if (!$ReturnState) {
            if ($TestsTotal === $TestsPassed) {
                $TestClr = 'txtGn';
            } else {
                $TestClr = ($TestsPassed === '?' || $TestsPassed >= ($TestsTotal / 2)) ? 'txtOe' : 'txtRd';
            }
            $TestsTotal = sprintf(
                '<span class="%1$s">%2$s/%3$s</span><br /><span id="%4$s-showtests">' .
                '<input class="auto" type="button" onclick="javascript:showid(\'%4$s-tests\');hideid(\'%4$s-showtests\');showid(\'%4$s-hidetests\')" value="+" />' .
                '</span><span id="%4$s-hidetests" style="display:none">' .
                '<input class="auto" type="button" onclick="javascript:hideid(\'%4$s-tests\');showid(\'%4$s-showtests\');hideid(\'%4$s-hidetests\')" value="-" />' .
                '</span><span id="%4$s-tests" style="display:none"><br />%5$s</span>',
                $TestClr,
                ($TestsPassed === '?' ? '?' : $CIDRAM['NumberFormatter']->format($TestsPassed)),
                $CIDRAM['NumberFormatter']->format($TestsTotal),
                $Component['ID'],
                $TestDetails
            );
            $CIDRAM['AppendToString'](
                $Component['StatusOptions'],
                '<hr />',
                '<div class="s">' . $CIDRAM['L10N']->getString('label_tests') . ' ' . $TestsTotal
            );
        }
    }
    if ($ReturnState) {
        return true;
    }
};

/**
 * Traversal detection.
 *
 * @param string $Path The path to check for traversal.
 * @return bool True when the path is traversal-free. False when traversal has been detected.
 */
$CIDRAM['Traverse'] = function (string $Path): bool {
    return !preg_match('~(?:[\./]{2}|[\x01-\x1f\[-^`?*$])~i', str_replace("\\", '/', $Path));
};

/**
 * Custom sort an array by key and then implode the results.
 *
 * @param array $Arr The array to sort.
 * @return string The sorted, imploded array.
 */
$CIDRAM['UpdatesSortFunc'] = function (array $Arr) use (&$CIDRAM) {
    $Type = $CIDRAM['FE']['sort-by-name'] ?? false;
    $Order = $CIDRAM['FE']['descending-order'] ?? false;
    uksort($Arr, function (string $A, string $B) use ($Type, $Order) {
        if (!$Type) {
            $Priority = '~^(?:CIDRAM|Common Classes Package|IPv[46]|l10n/)~i';
            $CheckA = preg_match($Priority, $A);
            $CheckB = preg_match($Priority, $B);
            if ($CheckA && !$CheckB) {
                return $Order ? 1 : -1;
            }
            if ($CheckB && !$CheckA) {
                return $Order ? -1 : 1;
            }
        }
        if ($A < $B) {
            return $Order ? 1 : -1;
        }
        if ($A > $B) {
            return $Order ? -1 : 1;
        }
        return 0;
    });
    return implode('', $Arr);
};

/**
 * Updates handler.
 *
 * @param string $Action The action to perform (update/install, verify,
 *      uninstall, activate, deactivate).
 * @return string|array The ID (or IDs) of the component (or components) to
 *      perform the specified action upon.
 */
$CIDRAM['UpdatesHandler'] = function (string $Action, $ID = '') use (&$CIDRAM) {

    /** Support for executor calls. */
    if (empty($ID) && ($Pos = strpos($Action, ' ')) !== false) {
        $ID = substr($Action, $Pos + 1);
        $Action = trim(substr($Action, 0, $Pos));
        $ID = (strpos($ID, ',') === false) ? trim($ID) : array_map('trim', explode(',', $ID));
    }

    /** Update a component. */
    if ($Action === 'update-component') {
        $CIDRAM['UpdatesHandler-Update']($ID);
    }

    /** Update (or install) and activate a component (one-step solution). */
    if (!is_array($ID) && $Action === 'update-and-activate-component') {
        $CIDRAM['UpdatesHandler-Update']($ID);
        $CIDRAM['UpdatesHandler-Activate']($ID);
    }

    /** Verify a component. */
    if ($Action === 'verify-component') {
        $CIDRAM['UpdatesHandler-Verify']($ID);
    }

    /** Uninstall a component. */
    if (!is_array($ID) && $Action === 'uninstall-component') {
        $CIDRAM['UpdatesHandler-Uninstall']($ID);
    }

    /** Activate a component. */
    if (!is_array($ID) && $Action === 'activate-component') {
        $CIDRAM['UpdatesHandler-Activate']($ID);
    }

    /** Deactivate a component. */
    if (!is_array($ID) && $Action === 'deactivate-component') {
        $CIDRAM['UpdatesHandler-Deactivate']($ID);
    }

    /** Deactivate and uninstall a component (one-step solution). */
    if (!is_array($ID) && $Action === 'deactivate-and-uninstall-component') {
        $CIDRAM['UpdatesHandler-Deactivate']($ID);
        $CIDRAM['UpdatesHandler-Uninstall']($ID);
    }

    /** Process and empty executor queue. */
    $CIDRAM['FE_Executor']();

};

/**
 * Updates handler: Update a component.
 *
 * @param string|array $ID The ID (or array of IDs) of the component(/s) to update.
 */
$CIDRAM['UpdatesHandler-Update'] = function ($ID) use (&$CIDRAM) {
    $CIDRAM['Arrayify']($ID);
    $Congruents = [];
    foreach ($ID as $ThisTarget) {
        if (!isset(
            $CIDRAM['Components']['Meta'][$ThisTarget]['Remote'],
            $CIDRAM['Components']['Meta'][$ThisTarget]['Reannotate']
        )) {
            continue;
        }
        if ($Reactivate = $CIDRAM['IsInUse']($CIDRAM['Components']['Meta'][$ThisTarget])) {
            $CIDRAM['UpdatesHandler-Deactivate']($ThisTarget);
        }
        $CIDRAM['Components']['BytesAdded'] = 0;
        $CIDRAM['Components']['BytesRemoved'] = 0;
        $CIDRAM['Components']['TimeRequired'] = microtime(true);
        $CIDRAM['Components']['RemoteMeta'] = [];
        $CIDRAM['Components']['Meta'][$ThisTarget]['RemoteData'] = '';
        $CIDRAM['FetchRemote-ContextFree'](
            $CIDRAM['Components']['Meta'][$ThisTarget]['RemoteData'],
            $CIDRAM['Components']['Meta'][$ThisTarget]['Remote']
        );
        $CIDRAM['UpdateFailed'] = false;
        if (
            substr($CIDRAM['Components']['Meta'][$ThisTarget]['RemoteData'], 0, 4) === "---\n" &&
            ($CIDRAM['Components']['EoYAML'] = strpos(
                $CIDRAM['Components']['Meta'][$ThisTarget]['RemoteData'], "\n\n"
            )) !== false &&
            $CIDRAM['YAML']->process(
                substr($CIDRAM['Components']['Meta'][$ThisTarget]['RemoteData'], 4, $CIDRAM['Components']['EoYAML'] - 4),
                $CIDRAM['Components']['RemoteMeta']
            ) &&
            !empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Minimum Required']) &&
            !$CIDRAM['VersionCompare'](
                $CIDRAM['ScriptVersion'],
                $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Minimum Required']
            ) &&
            (
                empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Minimum Required PHP']) ||
                !$CIDRAM['VersionCompare'](PHP_VERSION, $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Minimum Required PHP'])
            ) &&
            !empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['From']) &&
            !empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To']) &&
            !empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Reannotate']) &&
            $CIDRAM['Traverse']($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Reannotate']) &&
            ($ThisReannotate = $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Reannotate']) &&
            file_exists($CIDRAM['Vault'] . $ThisReannotate) &&
            ($OldMeta = $CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . $ThisReannotate)) &&
            preg_match("~(\n" . preg_quote($ThisTarget) . ":?)(\n [^\n]*)*\n~i", $OldMeta, $OldMetaMatches) &&
            ($OldMetaMatches = $OldMetaMatches[0]) &&
            ($NewMeta = $CIDRAM['Components']['Meta'][$ThisTarget]['RemoteData']) &&
            preg_match("~(\n" . preg_quote($ThisTarget) . ":?)(\n [^\n]*)*\n~i", $NewMeta, $NewMetaMatches) &&
            ($NewMetaMatches = $NewMetaMatches[0]) &&
            (!$CIDRAM['FE']['CronMode'] || empty(
                $CIDRAM['Components']['Meta'][$ThisTarget]['Tests']
            ) || $CIDRAM['AppendTests']($CIDRAM['Components']['Meta'][$ThisTarget], true))
        ) {
            $Congruents[$ThisReannotate] = $NewMeta;
            $CIDRAM['Arrayify']($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']);
            $CIDRAM['Arrayify']($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['From']);
            $CIDRAM['Arrayify']($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To']);
            if (!empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'])) {
                $CIDRAM['Arrayify']($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum']);
            }
            $NewMeta = str_replace($OldMetaMatches, $NewMetaMatches, $OldMeta);
            $Count = count($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['From']);
            $CIDRAM['RemoteFiles'] = [];
            $CIDRAM['IgnoredFiles'] = [];
            $Rollback = false;
            /** Write new and updated files and directories. */
            for ($Iterate = 0; $Iterate < $Count; $Iterate++) {
                if (empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To'][$Iterate])) {
                    continue;
                }
                $ThisFileName = $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To'][$Iterate];
                /** Rolls back to previous version or uninstalls if an update/install fails. */
                if ($Rollback) {
                    if (
                        isset($CIDRAM['RemoteFiles'][$ThisFileName]) &&
                        !isset($CIDRAM['IgnoredFiles'][$ThisFileName]) &&
                        is_readable($CIDRAM['Vault'] . $ThisFileName)
                    ) {
                        $CIDRAM['Components']['BytesAdded'] -= filesize($CIDRAM['Vault'] . $ThisFileName);
                        unlink($CIDRAM['Vault'] . $ThisFileName);
                        if (is_readable($CIDRAM['Vault'] . $ThisFileName . '.rollback')) {
                            $CIDRAM['Components']['BytesRemoved'] -= filesize($CIDRAM['Vault'] . $ThisFileName . '.rollback');
                            rename($CIDRAM['Vault'] . $ThisFileName . '.rollback', $CIDRAM['Vault'] . $ThisFileName);
                        }
                    }
                    continue;
                }
                if (
                    !empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate]) &&
                    !empty($CIDRAM['Components']['Meta'][$ThisTarget]['Files']['Checksum'][$Iterate]) && (
                        $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate] ===
                        $CIDRAM['Components']['Meta'][$ThisTarget]['Files']['Checksum'][$Iterate]
                    )
                ) {
                    $CIDRAM['IgnoredFiles'][$ThisFileName] = true;
                    continue;
                }
                if (
                    empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['From'][$Iterate]) ||
                    !($ThisFile = $CIDRAM['Request'](
                        $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['From'][$Iterate]
                    ))
                ) {
                    $Iterate = 0;
                    $Rollback = true;
                    continue;
                }
                if (
                    strtolower(substr(
                        $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['From'][$Iterate], -2
                    )) === 'gz' &&
                    strtolower(substr($ThisFileName, -2)) !== 'gz' &&
                    substr($ThisFile, 0, 2) === "\x1f\x8b"
                ) {
                    $ThisFile = gzdecode($ThisFile);
                }
                if (!empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate])) {
                    $ThisChecksum = $CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate];
                    $ThisLen = strlen($ThisFile);
                    if (
                        (md5($ThisFile) . ':' . $ThisLen) !== $ThisChecksum &&
                        (sha1($ThisFile) . ':' . $ThisLen) !== $ThisChecksum &&
                        (hash('sha256', $ThisFile) . ':' . $ThisLen) !== $ThisChecksum
                    ) {
                        $CIDRAM['FE']['state_msg'] .=
                            '<code>' . $ThisTarget . '</code> ? ' .
                            '<code>' . $ThisFileName . '</code> ? ' .
                            $CIDRAM['L10N']->getString('response_checksum_error') . '<br />';
                        if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['On Checksum Error'])) {
                            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ThisTarget]['On Checksum Error'], true);
                        }
                        $Iterate = 0;
                        $Rollback = true;
                        continue;
                    }
                }
                if (
                    preg_match('~\.(?:css|dat|gif|inc|jpe?g|php|png|ya?ml|[a-z]{0,2}db)$~i', $ThisFileName) &&
                    !$CIDRAM['SanityCheck']($ThisFileName, $ThisFile)
                ) {
                    $CIDRAM['FE']['state_msg'] .= sprintf(
                        '<code>%s</code> ? <code>%s</code> ? %s<br />',
                        $ThisTarget,
                        $ThisFileName,
                        $CIDRAM['L10N']->getString('response_sanity_1')
                    );
                    if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['On Sanity Error'])) {
                        $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ThisTarget]['On Sanity Error'], true);
                    }
                    $Iterate = 0;
                    $Rollback = true;
                    continue;
                }
                $ThisName = $ThisFileName;
                $ThisPath = $CIDRAM['Vault'];
                while (strpos($ThisName, '/') !== false || strpos($ThisName, "\\") !== false) {
                    $CIDRAM['Separator'] = (strpos($ThisName, '/') !== false) ? '/' : "\\";
                    $CIDRAM['ThisDir'] = substr($ThisName, 0, strpos($ThisName, $CIDRAM['Separator']));
                    $ThisPath .= $CIDRAM['ThisDir'] . '/';
                    $ThisName = substr($ThisName, strlen($CIDRAM['ThisDir']) + 1);
                    if (!file_exists($ThisPath) || !is_dir($ThisPath)) {
                        mkdir($ThisPath);
                    }
                }
                if (is_readable($CIDRAM['Vault'] . $ThisFileName)) {
                    $CIDRAM['Components']['BytesRemoved'] += filesize($CIDRAM['Vault'] . $ThisFileName);
                    if (file_exists($CIDRAM['Vault'] . $ThisFileName . '.rollback')) {
                        unlink($CIDRAM['Vault'] . $ThisFileName . '.rollback');
                    }
                    rename($CIDRAM['Vault'] . $ThisFileName, $CIDRAM['Vault'] . $ThisFileName . '.rollback');
                }
                $CIDRAM['Components']['BytesAdded'] += strlen($ThisFile);
                $Handle = fopen($CIDRAM['Vault'] . $ThisFileName, 'wb');
                $CIDRAM['RemoteFiles'][$ThisFileName] = fwrite($Handle, $ThisFile);
                $CIDRAM['RemoteFiles'][$ThisFileName] = ($CIDRAM['RemoteFiles'][$ThisFileName] !== false);
                fclose($Handle);
                $ThisFile = '';
            }
            if ($Rollback) {
                /** Prune unwanted empty directories (update/install failure+rollback). */
                if (
                    !empty($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To']) &&
                    is_array($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To'])
                ) {
                    array_walk($CIDRAM['Components']['RemoteMeta'][$ThisTarget]['Files']['To'], function ($ThisFile) use (&$CIDRAM) {
                        if (!empty($ThisFile) && $CIDRAM['Traverse']($ThisFile)) {
                            $CIDRAM['DeleteDirectory']($ThisFile);
                        }
                    });
                }
                $CIDRAM['UpdateFailed'] = true;
            } else {
                /** Prune unwanted files and directories (update/install success). */
                if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['Files']['To'])) {
                    $ThisArr = $CIDRAM['Components']['Meta'][$ThisTarget]['Files']['To'];
                    $CIDRAM['Arrayify']($ThisArr);
                    array_walk($ThisArr, function ($ThisFile) use (&$CIDRAM) {
                        if (!empty($ThisFile) && $CIDRAM['Traverse']($ThisFile)) {
                            if (file_exists($CIDRAM['Vault'] . $ThisFile . '.rollback')) {
                                unlink($CIDRAM['Vault'] . $ThisFile . '.rollback');
                            }
                            if (
                                !isset($CIDRAM['RemoteFiles'][$ThisFile]) &&
                                !isset($CIDRAM['IgnoredFiles'][$ThisFile]) &&
                                file_exists($CIDRAM['Vault'] . $ThisFile)
                            ) {
                                $CIDRAM['Components']['BytesRemoved'] += filesize($CIDRAM['Vault'] . $ThisFile);
                                unlink($CIDRAM['Vault'] . $ThisFile);
                                $CIDRAM['DeleteDirectory']($ThisFile);
                            }
                        }
                    });
                    unset($ThisArr);
                }

                /** Assign updated component annotation. */
                $CIDRAM['Updater-IO']->writeFile($CIDRAM['Vault'] . $ThisReannotate, $NewMeta);

                $CIDRAM['FE']['state_msg'] .= '<code>' . $ThisTarget . '</code> ? ';
                if (
                    empty($CIDRAM['Components']['Meta'][$ThisTarget]['Version']) &&
                    empty($CIDRAM['Components']['Meta'][$ThisTarget]['Files'])
                ) {
                    $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_component_successfully_installed');
                    if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['When Install Succeeds'])) {
                        $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ThisTarget]['When Install Succeeds'], true);
                    }
                } else {
                    $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_component_successfully_updated');
                    if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['When Update Succeeds'])) {
                        $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ThisTarget]['When Update Succeeds'], true);
                    }
                }

                /** Replace downstream meta with upstream meta. */
                $CIDRAM['Components']['Meta'][$ThisTarget] = $CIDRAM['Components']['RemoteMeta'][$ThisTarget];
            }
        } else {
            $CIDRAM['UpdateFailed'] = true;
        }
        if ($CIDRAM['UpdateFailed']) {
            $CIDRAM['FE']['state_msg'] .= '<code>' . $ThisTarget . '</code> ? ';
            if (
                empty($CIDRAM['Components']['Meta'][$ThisTarget]['Version']) &&
                empty($CIDRAM['Components']['Meta'][$ThisTarget]['Files'])
            ) {
                $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_failed_to_install');
                if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['When Install Fails'])) {
                    $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ThisTarget]['When Install Fails'], true);
                }
            } else {
                $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_failed_to_update');
                if (!empty($CIDRAM['Components']['Meta'][$ThisTarget]['When Update Fails'])) {
                    $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ThisTarget]['When Update Fails'], true);
                }
            }
        }
        $CIDRAM['FormatFilesize']($CIDRAM['Components']['BytesAdded']);
        $CIDRAM['FormatFilesize']($CIDRAM['Components']['BytesRemoved']);
        $CIDRAM['FE']['state_msg'] .= sprintf(
            $CIDRAM['FE']['CronMode'] ? "  +%s | -%s | %s \n" : ' <code><span class="txtGn">+%s</span> | <span class="txtRd">-%s</span> | <span class="txtOe">%s</span></code><br />',
            $CIDRAM['Components']['BytesAdded'],
            $CIDRAM['Components']['BytesRemoved'],
            $CIDRAM['NumberFormatter']->format(microtime(true) - $CIDRAM['Components']['TimeRequired'], 3)
        );
        if ($Reactivate) {
            $CIDRAM['UpdatesHandler-Activate']($ThisTarget);
        }
    }

    /** Remove superfluous metadata. */
    foreach ($Congruents as $File => $Upstream) {
        $Downstream = $CIDRAM['Congruency']($CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . $File), $Upstream);
        $CIDRAM['Updater-IO']->writeFile($CIDRAM['Vault'] . $File, $Downstream);
    }

    /** Cleanup. */
    unset($CIDRAM['RemoteFiles'], $CIDRAM['IgnoredFiles']);
};

/**
 * Updates handler: Uninstall a component.
 *
 * @param string $ID The ID of the component to uninstall.
 */
$CIDRAM['UpdatesHandler-Uninstall'] = function (string $ID) use (&$CIDRAM) {
    $InUse = $CIDRAM['ComponentFunctionUpdatePrep']($ID);
    $CIDRAM['Components']['BytesRemoved'] = 0;
    $CIDRAM['Components']['TimeRequired'] = microtime(true);
    $CIDRAM['FE']['state_msg'] .= '<code>' . $ID . '</code> ? ';
    if (
        empty($InUse) &&
        !empty($CIDRAM['Components']['Meta'][$ID]['Files']['To']) &&
        !empty($CIDRAM['Components']['Meta'][$ID]['Reannotate']) &&
        (!empty($CIDRAM['Components']['Meta'][$ID]['Uninstallable']) || !empty($CIDRAM['RestrictUninstallBypass'])) &&
        ($OldMeta = $CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . $CIDRAM['Components']['Meta'][$ID]['Reannotate'])) &&
        preg_match("~(\n" . preg_quote($ID) . ":?)(\n [^\n]*)*\n~i", $OldMeta, $OldMetaMatches) &&
        ($OldMetaMatches = $OldMetaMatches[0])
    ) {
        $NewMeta = str_replace($OldMetaMatches, preg_replace(
            ["/\n Files:(\n  [^\n]*)*\n/i", "/\n Version: [^\n]*\n/i"],
            "\n",
            $OldMetaMatches
        ), $OldMeta);
        array_walk($CIDRAM['Components']['Meta'][$ID]['Files']['To'], function ($ThisFile) use (&$CIDRAM) {
            if (!empty($ThisFile) && $CIDRAM['Traverse']($ThisFile)) {
                if (file_exists($CIDRAM['Vault'] . $ThisFile)) {
                    $CIDRAM['Components']['BytesRemoved'] += filesize($CIDRAM['Vault'] . $ThisFile);
                    unlink($CIDRAM['Vault'] . $ThisFile);
                }
                if (file_exists($CIDRAM['Vault'] . $ThisFile . '.rollback')) {
                    $CIDRAM['Components']['BytesRemoved'] += filesize($CIDRAM['Vault'] . $ThisFile . '.rollback');
                    unlink($CIDRAM['Vault'] . $ThisFile . '.rollback');
                }
                $CIDRAM['DeleteDirectory']($ThisFile);
            }
        });
        $CIDRAM['Updater-IO']->writeFile($CIDRAM['Vault'] . $CIDRAM['Components']['Meta'][$ID]['Reannotate'], $NewMeta);
        $CIDRAM['Components']['Meta'][$ID]['Version'] = false;
        $CIDRAM['Components']['Meta'][$ID]['Files'] = false;
        $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_component_successfully_uninstalled');
        if (!empty($CIDRAM['Components']['Meta'][$ID]['When Uninstall Succeeds'])) {
            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ID]['When Uninstall Succeeds'], true);
        }
    } else {
        $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_component_uninstall_error');
        if (!empty($CIDRAM['Components']['Meta'][$ID]['When Uninstall Fails'])) {
            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ID]['When Uninstall Fails'], true);
        }
    }
    $CIDRAM['FormatFilesize']($CIDRAM['Components']['BytesRemoved']);
    $CIDRAM['FE']['state_msg'] .= sprintf(
        $CIDRAM['FE']['CronMode'] ? "  -%s | %s \n" : ' <code><span class="txtRd">-%s</span> | <span class="txtOe">%s</span></code><br />',
        $CIDRAM['Components']['BytesRemoved'],
        $CIDRAM['NumberFormatter']->format(microtime(true) - $CIDRAM['Components']['TimeRequired'], 3)
    );
};

/**
 * Updates handler: Activate a component.
 *
 * @param string $ID The ID of the component to activate.
 */
$CIDRAM['UpdatesHandler-Activate'] = function (string $ID) use (&$CIDRAM) {
    $CIDRAM['Activation'] = [
        'Config' => $CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . $CIDRAM['FE']['ActiveConfigFile']),
        'ipv4' => $CIDRAM['Config']['signatures']['ipv4'],
        'ipv6' => $CIDRAM['Config']['signatures']['ipv6'],
        'modules' => $CIDRAM['Config']['signatures']['modules'],
        'Modified' => false
    ];
    $InUse = $CIDRAM['ComponentFunctionUpdatePrep']($ID);
    $CIDRAM['FE']['state_msg'] .= '<code>' . $ID . '</code> ? ';
    if (
        empty($InUse) &&
        !empty($CIDRAM['Components']['Meta'][$ID]['Files']['To']) && (
            !empty($CIDRAM['Components']['Meta'][$ID]['Used with']) || !empty($CIDRAM['Components']['Meta'][$ID]['Extended Description'])
    )) {
        $UsedWith = empty($CIDRAM['Components']['Meta'][$ID]['Used with']) ? '' : $CIDRAM['Components']['Meta'][$ID]['Used with'];
        $Description = empty($CIDRAM['Components']['Meta'][$ID]['Extended Description']) ? '' : $CIDRAM['Components']['Meta'][$ID]['Extended Description'];
        if ($UsedWith === 'ipv4' || strpos($Description, 'signatures-&gt;ipv4') !== false) {
            $CIDRAM['ActivateComponent']('ipv4', $ID);
        }
        if ($UsedWith === 'ipv6' || strpos($Description, 'signatures-&gt;ipv6') !== false) {
            $CIDRAM['ActivateComponent']('ipv6', $ID);
        }
        if ($UsedWith === 'modules' || strpos($Description, 'signatures-&gt;modules') !== false) {
            $CIDRAM['ActivateComponent']('modules', $ID);
        }
    }
    if (!$CIDRAM['Activation']['Modified'] || !$CIDRAM['Activation']['Config']) {
        $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_activation_failed') . '<br />';
        if (!empty($CIDRAM['Components']['Meta'][$ID]['When Activation Fails'])) {
            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ID]['When Activation Fails'], true);
        }
    } else {
        $EOL = (strpos($CIDRAM['Activation']['Config'], "\r\n") !== false) ? "\r\n" : "\n";
        $CIDRAM['Activation']['Config'] = str_replace([
            $EOL . "ipv4='" . $CIDRAM['Config']['signatures']['ipv4'] . "'" . $EOL,
            $EOL . "ipv6='" . $CIDRAM['Config']['signatures']['ipv6'] . "'" . $EOL,
            $EOL . "modules='" . $CIDRAM['Config']['signatures']['modules'] . "'" . $EOL
        ], [
            $EOL . "ipv4='" . $CIDRAM['Activation']['ipv4'] . "'" . $EOL,
            $EOL . "ipv6='" . $CIDRAM['Activation']['ipv6'] . "'" . $EOL,
            $EOL . "modules='" . $CIDRAM['Activation']['modules'] . "'" . $EOL
        ], $CIDRAM['Activation']['Config']);
        $CIDRAM['Config']['signatures']['ipv4'] = $CIDRAM['Activation']['ipv4'];
        $CIDRAM['Config']['signatures']['ipv6'] = $CIDRAM['Activation']['ipv6'];
        $CIDRAM['Config']['signatures']['modules'] = $CIDRAM['Activation']['modules'];
        $CIDRAM['Updater-IO']->writeFile($CIDRAM['Vault'] . $CIDRAM['FE']['ActiveConfigFile'], $CIDRAM['Activation']['Config']);
        $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_activated') . '<br />';
        if (!empty($CIDRAM['Components']['Meta'][$ID]['When Activation Succeeds'])) {
            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ID]['When Activation Succeeds'], true);
        }
    }
    /** Cleanup. */
    unset($CIDRAM['Activation']);
};

/**
 * Updates handler: Deactivate a component.
 *
 * @param string $ID The ID of the component to deactivate.
 */
$CIDRAM['UpdatesHandler-Deactivate'] = function (string $ID) use (&$CIDRAM) {
    $CIDRAM['Deactivation'] = [
        'Config' => $CIDRAM['Updater-IO']->readFile($CIDRAM['Vault'] . $CIDRAM['FE']['ActiveConfigFile']),
        'ipv4' => $CIDRAM['Config']['signatures']['ipv4'],
        'ipv6' => $CIDRAM['Config']['signatures']['ipv6'],
        'modules' => $CIDRAM['Config']['signatures']['modules'],
        'Modified' => false
    ];
    $InUse = false;
    $CIDRAM['FE']['state_msg'] .= '<code>' . $ID . '</code> ? ';
    if (!empty($CIDRAM['Components']['Meta'][$ID]['Files'])) {
        $CIDRAM['Arrayify']($CIDRAM['Components']['Meta'][$ID]['Files']);
        $CIDRAM['Arrayify']($CIDRAM['Components']['Meta'][$ID]['Files']['To']);
        $ThisComponent = $CIDRAM['Components']['Meta'][$ID];
        $CIDRAM['PrepareExtendedDescription']($ThisComponent);
        $InUse = $CIDRAM['IsInUse']($ThisComponent);
        unset($ThisComponent);
    }
    if (!empty($InUse) && !empty($CIDRAM['Components']['Meta'][$ID]['Files']['To'])) {
        $CIDRAM['DeactivateComponent']('ipv4', $ID);
        $CIDRAM['DeactivateComponent']('ipv6', $ID);
        $CIDRAM['DeactivateComponent']('modules', $ID);
    }
    if (!$CIDRAM['Deactivation']['Modified'] || !$CIDRAM['Deactivation']['Config']) {
        $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_deactivation_failed') . '<br />';
        if (!empty($CIDRAM['Components']['Meta'][$ID]['When Deactivation Fails'])) {
            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ID]['When Deactivation Fails'], true);
        }
    } else {
        $EOL = (strpos($CIDRAM['Deactivation']['Config'], "\r\n") !== false) ? "\r\n" : "\n";
        $CIDRAM['Deactivation']['Config'] = str_replace([
            $EOL . "ipv4='" . $CIDRAM['Config']['signatures']['ipv4'] . "'" . $EOL,
            $EOL . "ipv6='" . $CIDRAM['Config']['signatures']['ipv6'] . "'" . $EOL,
            $EOL . "modules='" . $CIDRAM['Config']['signatures']['modules'] . "'" . $EOL
        ], [
            $EOL . "ipv4='" . $CIDRAM['Deactivation']['ipv4'] . "'" . $EOL,
            $EOL . "ipv6='" . $CIDRAM['Deactivation']['ipv6'] . "'" . $EOL,
            $EOL . "modules='" . $CIDRAM['Deactivation']['modules'] . "'" . $EOL
        ], $CIDRAM['Deactivation']['Config']);
        $CIDRAM['Config']['signatures']['ipv4'] = $CIDRAM['Deactivation']['ipv4'];
        $CIDRAM['Config']['signatures']['ipv6'] = $CIDRAM['Deactivation']['ipv6'];
        $CIDRAM['Config']['signatures']['modules'] = $CIDRAM['Deactivation']['modules'];
        $CIDRAM['Updater-IO']->writeFile($CIDRAM['Vault'] . $CIDRAM['FE']['ActiveConfigFile'], $CIDRAM['Deactivation']['Config']);
        $CIDRAM['FE']['state_msg'] .= $CIDRAM['L10N']->getString('response_deactivated') . '<br />';
        if (!empty($CIDRAM['Components']['Meta'][$ID]['When Deactivation Succeeds'])) {
            $CIDRAM['FE_Executor']($CIDRAM['Components']['Meta'][$ID]['When Deactivation Succeeds'], true);
        }
    }
    /** Cleanup. */
    unset($CIDRAM['Deactivation']);
};

/**
 * Updates handler: Verify a component.
 *
 * @param string|array $ID The ID (or array of IDs) of the component(/s) to verify.
 */
$CIDRAM['UpdatesHandler-Verify'] = function ($ID) use (&$CIDRAM) {
    $CIDRAM['Arrayify']($ID);
    foreach ($ID as $ThisID) {
        $Table = '<blockquote class="ng1 comSub">';
        if (empty($CIDRAM['Components']['Meta'][$ThisID]['Files'])) {
            continue;
        }
        $TheseFiles = $CIDRAM['Components']['Meta'][$ThisID]['Files'];
        if (!empty($TheseFiles['To'])) {
            $CIDRAM['Arrayify']($TheseFiles['To']);
        }
        $Count = count($TheseFiles['To']);
        if (!empty($TheseFiles['Checksum'])) {
            $CIDRAM['Arrayify']($TheseFiles['Checksum']);
        }
        $Passed = true;
        for ($Iterate = 0; $Iterate < $Count; $Iterate++) {
            $ThisFile = $TheseFiles['To'][$Iterate];
            $ThisFileData = $CIDRAM['ReadFile']($CIDRAM['Vault'] . $ThisFile);

            /** Sanity check. */
            if (
                preg_match('~\.(?:css|dat|gif|inc|jpe?g|php|png|ya?ml|[a-z]{0,2}db)$~i', $ThisFile)
            ) {
                $Class = $CIDRAM['SanityCheck']($ThisFile, $ThisFileData) ? 'txtGn' : 'txtRd';
                $Sanity = sprintf('<span class="%s">%s</span>', $Class, $CIDRAM['L10N']->getString(
                    $Class === 'txtGn' ? 'response_passed' : 'response_failed'
                ));
                if ($Class === 'txtRd') {
                    $Passed = false;
                }
            } else {
                $Sanity = sprintf('<span class="txtOe">%s</span>', $CIDRAM['L10N']->getString('response_skipped'));
            }

            $Checksum = empty($TheseFiles['Checksum'][$Iterate]) ? '' : $TheseFiles['Checksum'][$Iterate];
            $Len = strlen($ThisFileData);
            $HashPartLen = strpos($Checksum, ':') ?: 64;
            if ($HashPartLen === 32) {
                $Actual = md5($ThisFileData) . ':' . $Len;
            } else {
                $Actual = (($HashPartLen === 40) ? sha1($ThisFileData) : hash('sha256', $ThisFileData)) . ':' . $Len;
            }

            /** Integrity check. */
            if ($Checksum) {
                if ($Actual !== $Checksum) {
                    $Class = 'txtRd';
                    $Passed = false;
                } else {
                    $Class = 'txtGn';
                }
                $Integrity = sprintf('<span class="%s">%s</span>', $Class, $CIDRAM['L10N']->getString(
                    $Class === 'txtGn' ? 'response_passed' : 'response_failed'
                ));
            } else {
                $Class = 's';
                $Integrity = sprintf('<span class="txtOe">%s</span>', $CIDRAM['L10N']->getString('response_skipped'));
            }

            /** Append results. */
            $Table .= sprintf(
                '<code>%1$s</code> ? %7$s%8$s ? %9$s%10$s<br />%2$s ? <code class="%6$s">%3$s</code><br />%4$s ? <code class="%6$s">%5$s</code><hr />',
                $ThisFile,
                $CIDRAM['L10N']->getString('label_actual'),
                $Actual ?: '?',
                $CIDRAM['L10N']->getString('label_expected'),
                $Checksum ?: '?',
                $Class,
                $CIDRAM['L10N']->getString('label_integrity_check'),
                $Integrity,
                $CIDRAM['L10N']->getString('label_sanity_check'),
                $Sanity
            );
        }
        $Table .= '</blockquote>';
        $CIDRAM['FE']['state_msg'] .= sprintf(
            '<div><span class="comCat" style="cursor:pointer"><code>%s</code> ? <span class="%s">%s</span></span>%s</div>',
            $ThisID,
            ($Passed ? 's' : 'txtRd'),
            $CIDRAM['L10N']->getString($Passed ? 'response_verification_success' : 'response_verification_failed'),
            $Table
        );
    }
};

/**
 * Normalise linebreaks.
 *
 * @param string $Data The data to normalise.
 */
$CIDRAM['NormaliseLinebreaks'] = function (string &$Data) {
    if (strpos($Data, "\r")) {
        $Data = (strpos($Data, "\r\n") !== false) ? str_replace("\r", '', $Data) : str_replace("\r", "\n", $Data);
    }
};

/**
 * Signature files handler for sections list.
 *
 * @param array $Files The signature files to process.
 * @return string Generated sections list data.
 */
$CIDRAM['SectionsHandler'] = function (array $Files) use (&$CIDRAM): string {
    if (!isset($CIDRAM['Ignore'])) {
        $CIDRAM['Ignore'] = $CIDRAM['FetchIgnores']();
    }
    $CIDRAM['FE']['SL_Signatures'] = 0;
    $CIDRAM['FE']['SL_Sections'] = 0;
    $CIDRAM['FE']['SL_Files'] = count($Files);
    $CIDRAM['FE']['SL_Unique'] = 0;
    $Out = '';
    $SectionsForIgnore = [];
    $SignaturesCount = [];
    $SectionMeta = [];
    $BaseSectionMeta = ['Deny' => 0, 'Bogon' => 0, 'Cloud' => 0, 'Generic' => 0, 'Legal' => 0, 'Malware' => 0, 'Proxy' => 0, 'Spam' => 0, 'Run' => 0, 'Greylist' => 0, 'Whitelist' => 0];
    $ThisSectionMeta = $BaseSectionMeta;
    foreach ($Files as $File) {
        $Data = $File && is_readable($CIDRAM['Vault'] . $File) ? $CIDRAM['ReadFile']($CIDRAM['Vault'] . $File) : '';
        if (!$Data) {
            continue;
        }
        $CIDRAM['NormaliseLinebreaks']($Data);
        $Data = "\n" . $Data . "\n";
        $PosB = -1;
        $ThisCount = 0;
        $OriginCount = 0;
        while (true) {
            $PosA = strpos($Data, "\n", $PosB + 1);
            if ($PosA === false) {
                break;
            }
            $PosA++;
            if (!$PosB = strpos($Data, "\n", $PosA)) {
                break;
            }
            $Line = substr($Data, $PosA, $PosB - $PosA);
            $PosB--;
            if (substr($Line, -1) === "\n") {
                $Line = substr($Line, 0, -1);
            }
            if (substr($Line, 0, 5) === 'Tag: ') {
                $Tag = substr($Line, 5);
                $CIDRAM['FE']['SL_Sections']++;
                if (!isset($SectionsForIgnore[$Tag])) {
                    $SectionsForIgnore[$Tag] = empty($CIDRAM['Ignore'][$Tag]);
                }
                if (!isset($SignaturesCount[$Tag])) {
                    $SignaturesCount[$Tag] = 0;
                }
                $SignaturesCount[$Tag] += $ThisCount;
                $ThisCount = 0;
                if (!isset($SectionMeta[$Tag])) {
                    $SectionMeta[$Tag] = $BaseSectionMeta;
                }
                foreach ($ThisSectionMeta as $MetaKey => $MetaValue) {
                    if (!isset($SectionMeta[$Tag][$MetaKey])) {
                        $SectionMeta[$Tag][$MetaKey] = 0;
                    }
                    $SectionMeta[$Tag][$MetaKey] += $MetaValue;
                }
                $ThisSectionMeta = $BaseSectionMeta;
                continue;
            }
            if (substr($Line, 0, 8) === 'Origin: ') {
                $Origin = substr($Line, 8);
                if ($CIDRAM['FE']['Flags']) {
                    $Origin = '<span class="flag ' . $Origin . '"></span>';
                }
                $ThisSectionMeta[$Origin] = $OriginCount;
                $OriginCount = 0;
                continue;
            }
            if (!$Line || preg_match('~^([\n#]|Expires|Defers to)~', $Line) || strpos($Line, '/') === false) {
                continue;
            }
            $ThisCount++;
            $OriginCount++;
            $CIDRAM['FE']['SL_Signatures']++;
            if (($XPos = strpos($Line, 'Deny ')) !== false && ($Speculate = substr($Line, $XPos + 5))) {
                if (!preg_match('~^(?:Bogon|Cloud|Generic|Legal|Malware|Proxy|Spam)$~', $Speculate)) {
                    $Speculate = 'Deny';
                }
                $ThisSectionMeta[$Speculate]++;
            } elseif (($XPos = strpos($Line, 'Run ')) !== false && substr($Line, $XPos + 4)) {
                $ThisSectionMeta['Run']++;
            } elseif (strpos($Line, 'Greylist') !== false) {
                $ThisSectionMeta['Greylist']++;
            } elseif (strpos($Line, 'Whitelist') !== false) {
                $ThisSectionMeta['Whitelist']++;
            }
        }
    }
    $Class = 'ng2';
    ksort($SectionsForIgnore);
    $CIDRAM['FE']['SL_Unique'] = count($SectionsForIgnore);
    foreach ($SectionsForIgnore as $Section => $State) {
        $ThisCount = $CIDRAM['NumberFormatter']->format(isset($SignaturesCount[$Section]) ? $SignaturesCount[$Section] : 0);
        $Class = (isset($Class) && $Class === 'ng2') ? 'ng1' : 'ng2';
        $SectionSafe = preg_replace('~[^\da-z]~i', '', $Section);
        $SectionLabel = $Section . ' (<span class="txtRd">' . $ThisCount . '</span>)';
        $SectionBreakdown = '';
        $Next = '';
        $HasOrigin = false;
        foreach ($SectionMeta[$Section] as $BreakdownItem => $Quantity) {
            if ($Next === 'Origin') {
                $SectionBreakdown .= sprintf(
                    '<span class="%1$s">' . ($SectionBreakdown ? ' ? ' : '') . '<a href="javascript:void()" onclick="javascript:hide(\'%1$s\');show(\'%2$s\')">%3$s</a></span><span class="%2$s" style="display:none">',
                    'originLink' . $SectionSafe,
                    'originContent' . $SectionSafe,
                    $CIDRAM['L10N']->getString('label_show_by_origin')
                );
                $HasOrigin = true;
            }
            $Next = ($BreakdownItem === 'Whitelist') ? 'Origin' : '';
            if ($Quantity) {
                $Quantity = $CIDRAM['NumberFormatter']->format($Quantity);
                $SectionBreakdown .= ($SectionBreakdown ? ' ? ' : '') . $BreakdownItem . ': ' . $Quantity;
            }
        }
        if ($HasOrigin) {
            $SectionBreakdown .= "</span>";
        }
        $Out .= sprintf(
            '<div class="%1$s sectionControlNotIgnored%2$s"><strong>%3$s%4$s</strong><br /><em>%5$s</em></div>',
            $Class,
            $State ? $SectionSafe : $SectionSafe . '" style="display:none',
            $SectionLabel,
            ' ? <a href="javascript:void()" onclick="javascript:slx(\'' . $Section . '\',\'ignore\',\'sectionControlNotIgnored' . $SectionSafe . '\',\'sectionControlIgnored' . $SectionSafe . '\')">' . $CIDRAM['L10N']->getString('label_ignore') . '</a>',
            $SectionBreakdown
        ) . sprintf(
            '<div class="%1$s sectionControlIgnored%2$s"><strong>%3$s%4$s</strong><br /><em>%5$s</em></div>',
            $Class,
            $SectionSafe . '" style="filter:grayscale(50%) contrast(50%)' . ($State ? ';display:none' : ''),
            $SectionLabel . ' ? ' . $CIDRAM['L10N']->getString('state_ignored'),
            ' ? <a href="javascript:void()" onclick="javascript:slx(\'' . $Section . '\',\'unignore\',\'sectionControlIgnored' . $SectionSafe . '\',\'sectionControlNotIgnored' . $SectionSafe . '\')">' . $CIDRAM['L10N']->getString('label_unignore') . '</a>',
            $SectionBreakdown
        );
    }
    return $Out;
};

/**
 * Tally IPv6 count.
 *
 * @param array $Arr
 * @param int $Range (1-128)
 */
$CIDRAM['RangeTablesTallyIPv6'] = function (array &$Arr, int $Range) {
    $Order = ceil($Range / 16) - 1;
    $Arr[$Order] += pow(2, (128 - $Range) % 16);
};

/**
 * Finalise IPv6 count.
 *
 * @param array $Arr Values of the IPv6 octets.
 * @return array A base value (first parameter), and the power it would need to
 *      be raised by (second parameter) to accurately reflect the total amount.
 */
$CIDRAM['RangeTablesFinaliseIPv6'] = function (array $Arr): array {
    for ($Iter = 7; $Iter > 0; $Iter--) {
        if (!empty($Arr[$Iter + 1])) {
            $Arr[$Iter] += (floor($Arr[$Iter + 1] / 655.36) / 100);
        }
        while ($Arr[$Iter] >= 65536) {
            $Arr[$Iter] -= 65536;
            $Arr[$Iter - 1] += 1;
        }
    }
    $Power = 0;
    foreach ($Arr as $Order => $Value) {
        if ($Value) {
            $Power = (7 - $Order) * 16;
            break;
        }
    }
    return [$Value, $Power];
};

/**
 * Fetch some data about CIDRs.
 *
 * @param string $Data The signature file data.
 * @param int $Offset The current offset position.
 * @param string $Needle A needle to identify the parts of the data we're looking for.
 * @param bool $HasOrigin Whether the data has an origin tag.
 * @return array|bool The CIDR's parameter and origin (when available), or false on failure.
 */
$CIDRAM['RangeTablesFetchLine'] = function (string &$Data, int &$Offset, string &$Needle, bool &$HasOrigin) {
    $Check = strpos($Data, $Needle, $Offset);
    if ($Check !== false) {
        $NeedleLen = strlen($Needle);
        $LFPos = strpos($Data, "\n", $Check + $NeedleLen);
        if ($LFPos !== false) {
            if ($Deduct = $LFPos - $Check - $NeedleLen) {
                $Param = trim(substr($Data, $Check + $NeedleLen + 1, $Deduct - 1));
                $Offset = $Check + $NeedleLen + strlen($Param) + 1;
            } else {
                $Param = '';
                $Offset = $Check + $NeedleLen;
            }
        } else {
            $Param = trim(substr($Data, $Check + $NeedleLen));
            $Offset = false;
        }
        $From = $Check > 128 ? $Check - 128 : 0;
        $CPos = strrpos(substr($Data, $From, $Check - $From), "\n");
        if (substr($Data, $CPos + 1, 1) === '#') {
            return false;
        }
        $Origin = '??';
        if ($Offset !== false && $HasOrigin) {
            $CPos = strpos($Data, "\n\n", $Offset);
            $OPos = strpos($Data, "\nOrigin: ", $Offset);
            if ($OPos !== false && ($CPos === false || $CPos > $OPos)) {
                $Origin = substr($Data, $OPos + 9, 2);
            }
        }
        $Param = trim($Param) ?: '';
        return ['Param' => $Param, 'Origin' => $Origin];
    }
    $Offset = false;
    return false;
};

/**
 * Iterate range tables files.
 *
 * @param array $Arr Where we're populating it all.
 * @param array $Files The currently active (IPv4 or IPv6) signature files.
 * @param array $SigTypes The various types of signatures supported.
 * @param int $MaxRange (32 or 128).
 * @param string $IPType (IPv4 or IPv6).
 */
$CIDRAM['RangeTablesIterateFiles'] = function (array &$Arr, array $Files, array $SigTypes, int $MaxRange, string $IPType) use (&$CIDRAM) {
    foreach ($Files as $File) {
        $File = (strpos($File, ':') === false) ? $File : substr($File, strpos($File, ':') + 1);
        $Data = $File && is_readable($CIDRAM['Vault'] . $File) ? $CIDRAM['ReadFile']($CIDRAM['Vault'] . $File) : '';
        if (!$Data) {
            continue;
        }
        $CIDRAM['NormaliseLinebreaks']($Data);
        $HasOrigin = (strpos($Data, "\nOrigin: ") !== false);
        foreach ($SigTypes as $SigType) {
            for ($Range = 1; $Range <= $MaxRange; $Range++) {
                if ($MaxRange === 32) {
                    $Order = pow(2, $MaxRange - $Range);
                }
                $Offset = 0;
                $Needle = '/' . $Range . ' ' . $SigType;
                while ($Offset !== false) {
                    if ($Entry = $CIDRAM['RangeTablesFetchLine']($Data, $Offset, $Needle, $HasOrigin)) {
                        if (empty($Arr[$IPType][$SigType][$Range][$Entry['Param']])) {
                            $Arr[$IPType][$SigType][$Range][$Entry['Param']] = 0;
                        }
                        $Arr[$IPType][$SigType][$Range][$Entry['Param']]++;
                        if ($MaxRange === 32) {
                            if (empty($Arr[$IPType][$SigType]['Total'][$Entry['Param']])) {
                                $Arr[$IPType][$SigType]['Total'][$Entry['Param']] = 0;
                            }
                            $Arr[$IPType][$SigType]['Total'][$Entry['Param']] += $Order;
                        } elseif ($MaxRange === 128) {
                            if (empty($Arr[$IPType][$SigType]['Total'][$Entry['Param']])) {
                                $Arr[$IPType][$SigType]['Total'][$Entry['Param']] = [0, 0, 0, 0, 0, 0, 0, 0];
                            }
                            $CIDRAM['RangeTablesTallyIPv6']($Arr[$IPType][$SigType]['Total'][$Entry['Param']], $Range);
                        }
                        if ($Entry['Origin']) {
                            if (empty($Arr[$IPType . '-Origin'][$SigType][$Range][$Entry['Origin']])) {
                                $Arr[$IPType . '-Origin'][$SigType][$Range][$Entry['Origin']] = 0;
                            }
                            $Arr[$IPType . '-Origin'][$SigType][$Range][$Entry['Origin']]++;
                            if ($MaxRange === 32) {
                                if (empty($Arr[$IPType . '-Origin'][$SigType]['Total'][$Entry['Origin']])) {
                                    $Arr[$IPType . '-Origin'][$SigType]['Total'][$Entry['Origin']] = 0;
                                }
                                $Arr[$IPType . '-Origin'][$SigType]['Total'][$Entry['Origin']] += $Order;
                            } elseif ($MaxRange === 128) {
                                if (empty($Arr[$IPType . '-Origin'][$SigType]['Total'][$Entry['Origin']])) {
                                    $Arr[$IPType . '-Origin'][$SigType]['Total'][$Entry['Origin']] = [0, 0, 0, 0, 0, 0, 0, 0];
                                }
                                $CIDRAM['RangeTablesTallyIPv6']($Arr[$IPType . '-Origin'][$SigType]['Total'][$Entry['Origin']], $Range);
                            }
                        }
                    }
                }
            }
        }
    }
};

/**
 * Iterate range tables data.
 *
 * @param array $Arr
 * @param array $Out
 * @param string $JS
 * @param string $SigType
 * @param int $MaxRange (32 or 128).
 * @param string $IPType (IPv4 or IPv6).
 * @param string $ZeroPlus (txtGn, txtRd or txtOe, depending on the type of signature we're working with).
 * @param string $Class The table entry class that our JavaScript will need to work with.
 */
$CIDRAM['RangeTablesIterateData'] = function (
    array &$Arr,
    array &$Out,
    string &$JS,
    string $SigType,
    int $MaxRange,
    string $IPType,
    string $ZeroPlus,
    string $Class
) use (&$CIDRAM) {
    for ($Range = 1; $Range <= $MaxRange; $Range++) {
        $Size = '*Math.pow(2,' . ($MaxRange - $Range) . ')';
        if (count($Arr[$IPType][$SigType][$Range])) {
            $StatClass = $ZeroPlus;
            arsort($Arr[$IPType][$SigType][$Range]);
            foreach ($Arr[$IPType][$SigType][$Range] as $Param => &$Count) {
                if ($IPType === 'IPv4') {
                    $ThisID = $IPType . preg_replace('~[^\da-z]~i', '_', $SigType . $Range . $Param);
                    $Total = '<span id="' . $ThisID . '"></span>';
                    $JS .= 'w(\'' . $ThisID . '\',nft((' . $Count . $Size . ').toString()));';
                    $Count = $CIDRAM['NumberFormatter']->format($Count) . ' (' . $Total . ')';
                } else {
                    $Count = $CIDRAM['NumberFormatter']->format($Count);
                }
                if ($Param) {
                    $Count = $Param . ' ? ' . $Count;
                }
            }
            $Arr[$IPType][$SigType][$Range] = implode('<br />', $Arr[$IPType][$SigType][$Range]);
            if (count($Arr[$IPType . '-Origin'][$SigType][$Range])) {
                arsort($Arr[$IPType . '-Origin'][$SigType][$Range]);
                foreach ($Arr[$IPType . '-Origin'][$SigType][$Range] as $Origin => &$Count) {
                    if ($IPType === 'IPv4') {
                        $ThisID = $IPType . preg_replace('~[^\da-z]~i', '_', $SigType . $Range . $Origin);
                        $Total = '<span id="' . $ThisID . '"></span>';
                        $JS .= 'w(\'' . $ThisID . '\',nft((' . $Count . $Size . ').toString()));';
                        $Count = $CIDRAM['NumberFormatter']->format($Count) . ' (' . $Total . ')';
                    } else {
                        $Count = $CIDRAM['NumberFormatter']->format($Count);
                    }
                    $Count = '<code class="hB">' . $Origin . '</code> ? ' . (
                        $CIDRAM['FE']['Flags'] && $Origin !== '??' ? '<span class="flag ' . $Origin . '"></span> ? ' : ''
                    ) . $Count;
                }
                $Arr[$IPType . '-Origin'][$SigType][$Range] = implode('<br />', $Arr[$IPType . '-Origin'][$SigType][$Range]);
                $Arr[$IPType][$SigType][$Range] .= '<hr />' . $Arr[$IPType . '-Origin'][$SigType][$Range];
            }
        } else {
            $StatClass = 's';
            $Arr[$IPType][$SigType][$Range] = '';
        }
        if ($Arr[$IPType][$SigType][$Range]) {
            if (!isset($Out[$IPType . '/' . $Range])) {
                $Out[$IPType . '/' . $Range] = '';
            }
            $Out[$IPType . '/' . $Range] .= '<span style="display:none" class="' . $Class . ' ' . $StatClass . '">' . $Arr[$IPType][$SigType][$Range] . '</span>';
        }
    }
    if (count($Arr[$IPType][$SigType]['Total'])) {
        $StatClass = $ZeroPlus;
        if ($MaxRange === 32) {
            arsort($Arr[$IPType][$SigType]['Total']);
        } elseif ($MaxRange === 128) {
            uasort($Arr[$IPType][$SigType]['Total'], function ($A, $B) {
                for ($i = 0; $i < 8; $i++) {
                    if ($A[$i] !== $B[$i]) {
                        return $A[$i] > $B[$i] ? -1 : 1;
                    }
                }
                return 0;
            });
        }
        foreach ($Arr[$IPType][$SigType]['Total'] as $Param => &$Count) {
            if ($MaxRange === 32) {
                $ThisID = $IPType . preg_replace('~[^\da-z]~i', '_', $SigType . 'Total' . $Param);
                $JS .= 'w(\'' . $ThisID . '\',nft((' . $Count . ').toString()));';
            } elseif ($MaxRange === 128) {
                $Count = $CIDRAM['RangeTablesFinaliseIPv6']($Count);
                $Count[1] = $Count[1] ? '+\'  \'+nft((2).toString())+\'<sup>^\'+nft((' . $Count[1] . ').toString())+\'</sup>\'' : '';
                $ThisID = $IPType . preg_replace('~[^\da-z]~i', '_', $SigType . 'Total' . $Param);
                $JS .= 'w(\'' . $ThisID . '\',' . ($Count[1] ? '\'~\'+' : '') . 'nft((' . $Count[0] . ').toString())' . $Count[1] . ');';
            }
            $Count = '<span id="' . $ThisID . '"></span>';
            if ($Param) {
                $Count = $Param . ' ? ' . $Count;
            }
        }
        $Arr[$IPType][$SigType]['Total'] = implode('<br />', $Arr[$IPType][$SigType]['Total']);
        if (count($Arr[$IPType . '-Origin'][$SigType]['Total'])) {
            arsort($Arr[$IPType . '-Origin'][$SigType]['Total']);
            foreach ($Arr[$IPType . '-Origin'][$SigType]['Total'] as $Origin => &$Count) {
                if ($MaxRange === 32) {
                    $ThisID = $IPType . preg_replace('~[^\da-z]~i', '_', $SigType . 'Total' . $Origin);
                    $JS .= 'w(\'' . $ThisID . '\',nft((' . $Count . ').toString()));';
                } elseif ($MaxRange === 128) {
                    $Count = $CIDRAM['RangeTablesFinaliseIPv6']($Count);
                    $Count[1] = $Count[1] ? '+\'  \'+nft((2).toString())+\'<sup>^\'+nft((' . $Count[1] . ').toString())+\'</sup>\'' : '';
                    $ThisID = $IPType . preg_replace('~[^\da-z]~i', '_', $SigType . 'Total' . $Origin);
                    $JS .= 'w(\'' . $ThisID . '\',' . ($Count[1] ? '\'~\'+' : '') . 'nft((' . $Count[0] . ').toString())' . $Count[1] . ');';
                }
                $Count = '<code class="hB">' . $Origin . '</code> ? ' . (
                    $CIDRAM['FE']['Flags'] && $Origin !== '??' ? '<span class="flag ' . $Origin . '"></span> ? ' : ''
                ) . '<span id="' . $ThisID . '"></span>';
            }
            $Arr[$IPType . '-Origin'][$SigType]['Total'] = implode('<br />', $Arr[$IPType . '-Origin'][$SigType]['Total']);
            $Arr[$IPType][$SigType]['Total'] .= '<hr />' . $Arr[$IPType . '-Origin'][$SigType]['Total'];
        }
    } else {
        $StatClass = 's';
        $Arr[$IPType][$SigType]['Total'] = '';
    }
    if ($Arr[$IPType][$SigType]['Total']) {
        if (!isset($Out[$IPType . '/Total'])) {
            $Out[$IPType . '/Total'] = '';
        }
        $Out[$IPType . '/Total'] .= '<span style="display:none" class="' . $Class . ' ' . $StatClass . '">' . $Arr[$IPType][$SigType]['Total'] . '</span>';
    }
};

/**
 * Range tables handler.
 *
 * @param array $IPv4 The currently active IPv4 signature files.
 * @param array $IPv6 The currently active IPv6 signature files.
 * @return string Some JavaScript generated to populate the range tables data.
 */
$CIDRAM['RangeTablesHandler'] = function (array $IPv4, array $IPv6) use (&$CIDRAM): string {
    $CIDRAM['FE']['rangeCatOptions'] = '';
    $Arr = ['IPv4' => [], 'IPv4-Origin' => [], 'IPv6' => [], 'IPv6-Origin' => []];
    $SigTypes = ['Run', 'Whitelist', 'Greylist', 'Deny'];
    foreach ($SigTypes as $SigType) {
        $Arr['IPv4'][$SigType] = ['Total' => []];
        $Arr['IPv4-Origin'][$SigType] = ['Total' => []];
        $Arr['IPv6'][$SigType] = ['Total' => []];
        $Arr['IPv6-Origin'][$SigType] = ['Total' => []];
        for ($Range = 1; $Range <= 32; $Range++) {
            $Arr['IPv4'][$SigType][$Range] = [];
            $Arr['IPv4-Origin'][$SigType][$Range] = [];
        }
        for ($Range = 1; $Range <= 128; $Range++) {
            $Arr['IPv6'][$SigType][$Range] = [];
            $Arr['IPv6-Origin'][$SigType][$Range] = [];
        }
    }
    $Out = [];
    $CIDRAM['RangeTablesIterateFiles']($Arr, $IPv4, $SigTypes, 32, 'IPv4');
    $CIDRAM['RangeTablesIterateFiles']($Arr, $IPv6, $SigTypes, 128, 'IPv6');
    $CIDRAM['FE']['Labels'] = '';
    $JS = '';
    foreach ($SigTypes as $SigType) {
        $Class = 'sigtype_' . strtolower($SigType);
        $CIDRAM['FE']['rangeCatOptions'] .= "\n      <option value=\"" . $Class . '">' . $SigType . '</option>';
        $CIDRAM['FE']['Labels'] .= '<span style="display:none" class="s ' . $Class . '">' . $CIDRAM['L10N']->getString('label_signature_type') . ' ' . $SigType . '</span>';
        if ($SigType === 'Run') {
            $ZeroPlus = 'txtOe';
        } else {
            $ZeroPlus = ($SigType === 'Whitelist' || $SigType === 'Greylist') ? 'txtGn' : 'txtRd';
        }
        $CIDRAM['RangeTablesIterateData']($Arr, $Out, $JS, $SigType, 32, 'IPv4', $ZeroPlus, $Class);
        $CIDRAM['RangeTablesIterateData']($Arr, $Out, $JS, $SigType, 128, 'IPv6', $ZeroPlus, $Class);
    }
    $CIDRAM['FE']['RangeRows'] = '';
    foreach ([['IPv4', 32], ['IPv6', 128]] as $Build) {
        for ($Range = 1; $Range <= $Build[1]; $Range++) {
            $Label = $Build[0] . '/' . $Range;
            if (!empty($Out[$Label])) {
                foreach ($SigTypes as $SigType) {
                    $Class = 'sigtype_' . strtolower($SigType);
                    if (strpos($Out[$Label], $Class) === false) {
                        $Out[$Label] .= '<span style="display:none" class="' . $Class . ' s">-</span>';
                    }
                }
                $ThisArr = ['RangeType' => $Label, 'NumOfCIDRs' => $Out[$Label], 'state_loading' => $CIDRAM['L10N']->getString('state_loading')];
                $CIDRAM['FE']['RangeRows'] .= $CIDRAM['ParseVars']($ThisArr, $CIDRAM['FE']['RangeRow']);
            }
        }
    }
    foreach (['IPv4', 'IPv6'] as $IPType) {
        $Label = $IPType . '/' . $CIDRAM['L10N']->getString('label_total');
        $Internal = $IPType . '/Total';
        if (!empty($Out[$Internal])) {
            foreach ($SigTypes as $SigType) {
                $Class = 'sigtype_' . strtolower($SigType);
                if (strpos($Out[$Internal], $Class) === false) {
                    $Out[$Internal] .= '<span style="display:none" class="' . $Class . ' s">-</span>';
                }
            }
            $ThisArr = ['RangeType' => $Label, 'NumOfCIDRs' => $Out[$Internal], 'state_loading' => $CIDRAM['L10N']->getString('state_loading')];
            $CIDRAM['FE']['RangeRows'] .= $CIDRAM['ParseVars']($ThisArr, $CIDRAM['FE']['RangeRow']);
        }
    }
    return $JS;
};

/**
 * Assign some basic variables (initial prepwork for most front-end pages).
 *
 * @param string $Title The page title.
 * @param string $Tips The page "tip" to include ("Hello username! Here you can...").
 * @param bool $JS Whether to include the standard front-end JavaScript boilerplate.
 */
$CIDRAM['InitialPrepwork'] = function (string $Title = '', string $Tips = '', bool $JS = true) use (&$CIDRAM) {

    /** Set page title. */
    $CIDRAM['FE']['FE_Title'] = 'CIDRAM ? ' . $Title;

    /** Fetch and prepare username. */
    if ($Username = (empty($CIDRAM['FE']['UserRaw']) ? '' : $CIDRAM['FE']['UserRaw'])) {
        $Username = preg_replace('~^([^<>]+)<[^<>]+>$~', '\1', $Username);
        if (($AtChar = strpos($Username, '@')) !== false) {
            $Username = substr($Username, 0, $AtChar);
        }
    }

    /** Prepare page tooltip/description. */
    $CIDRAM['FE']['FE_Tip'] = $CIDRAM['ParseVars'](['username' => $Username], $Tips);

    /** Load main front-end JavaScript data. */
    $CIDRAM['FE']['JS'] = $JS ? $CIDRAM['ReadFile']($CIDRAM['GetAssetPath']('scripts.js')) : '';

};

/**
 * Send page output for front-end pages (plus some other final prepwork).
 *
 * @return string Page output.
 */
$CIDRAM['SendOutput'] = function () use (&$CIDRAM): string {
    if ($CIDRAM['FE']['JS']) {
        $CIDRAM['FE']['JS'] = "\n<script type=\"text/javascript\">" . $CIDRAM['FE']['JS'] . '</script>';
    }
    return $CIDRAM['ParseVars']($CIDRAM['L10N']->Data + $CIDRAM['FE'], $CIDRAM['FE']['Template']);
};

/**
 * Confirm whether a file is a logfile (used by the file manager and logs viewer).
 *
 * @param string $File The path/name of the file to be confirmed.
 * @return bool True if it's a logfile; False if it isn't.
 */
$CIDRAM['FileManager-IsLogFile'] = function (string $File) use (&$CIDRAM): bool {
    static $Pattern_logfile = false;
    if (!$Pattern_logfile && $CIDRAM['Config']['general']['logfile']) {
        $Pattern_logfile = $CIDRAM['BuildLogPattern']($CIDRAM['Config']['general']['logfile'], true);
    }
    static $Pattern_logfile_apache = false;
    if (!$Pattern_logfile_apache && $CIDRAM['Config']['general']['logfile_apache']) {
        $Pattern_logfile_apache = $CIDRAM['BuildLogPattern']($CIDRAM['Config']['general']['logfile_apache'], true);
    }
    static $Pattern_logfile_serialized = false;
    if (!$Pattern_logfile_serialized && $CIDRAM['Config']['general']['logfile_serialized']) {
        $Pattern_logfile_serialized = $CIDRAM['BuildLogPattern']($CIDRAM['Config']['general']['logfile_serialized'], true);
    }
    static $Pattern_frontend_log = false;
    if (!$Pattern_frontend_log && $CIDRAM['Config']['general']['frontend_log']) {
        $Pattern_frontend_log = $CIDRAM['BuildLogPattern']($CIDRAM['Config']['general']['frontend_log'], true);
    }
    static $Pattern_reCAPTCHA_logfile = false;
    if (!$Pattern_reCAPTCHA_logfile && $CIDRAM['Config']['recaptcha']['logfile']) {
        $Pattern_reCAPTCHA_logfile = $CIDRAM['BuildLogPattern']($CIDRAM['Config']['recaptcha']['logfile'], true);
    }
    static $Pattern_PHPMailer_EventLog = false;
    if (!$Pattern_PHPMailer_EventLog && $CIDRAM['Config']['PHPMailer']['event_log']) {
        $Pattern_PHPMailer_EventLog = $CIDRAM['BuildLogPattern']($CIDRAM['Config']['PHPMailer']['event_log'], true);
    }
    return preg_match('~\.log(?:\.gz)?$~', strtolower($File)) || (
        $CIDRAM['Config']['general']['logfile'] && preg_match($Pattern_logfile, $File)
    ) || (
        $CIDRAM['Config']['general']['logfile_apache'] && preg_match($Pattern_logfile_apache, $File)
    ) || (
        $CIDRAM['Config']['general']['logfile_serialized'] && preg_match($Pattern_logfile_serialized, $File)
    ) || (
        $CIDRAM['Config']['general']['frontend_log'] && preg_match($Pattern_frontend_log, $File)
    ) || (
        $CIDRAM['Config']['recaptcha']['logfile'] && preg_match($Pattern_reCAPTCHA_logfile, $File)
    ) || (
        $CIDRAM['Config']['PHPMailer']['event_log'] && preg_match($Pattern_PHPMailer_EventLog, $File)
    );
};

/**
 * Generates JavaScript snippets for confirmation prompts for front-end actions.
 *
 * @param string $Action The action being taken to be confirmed.
 * @param string $Form The ID of the form to be submitted when the action is confirmed.
 * @return string The JavaScript snippet.
 */
$CIDRAM['GenerateConfirm'] = function (string $Action, string $Form) use (&$CIDRAM): string {
    $Confirm = str_replace(["'", '"'], ["\'", '\x22'], sprintf($CIDRAM['L10N']->getString('confirm_action'), $Action));
    return 'javascript:confirm(\'' . $Confirm . '\')&&document.getElementById(\'' . $Form . '\').submit()';
};

/**
 * A quicker way to add entries to the front-end logfile.
 *
 * @param string $IPAddr The IP address triggering the log event.
 * @param string $User The user triggering the log event.
 * @param string $Message The message to be logged.
 */
$CIDRAM['FELogger'] = function (string $IPAddr, string $User, string $Message) use (&$CIDRAM) {
    if (!$CIDRAM['Config']['general']['frontend_log'] || empty($CIDRAM['FE']['DateTime'])) {
        return;
    }
    $File = (strpos($CIDRAM['Config']['general']['frontend_log'], '{') !== false) ? $CIDRAM['TimeFormat'](
        $CIDRAM['Now'],
        $CIDRAM['Config']['general']['frontend_log']
    ) : $CIDRAM['Config']['general']['frontend_log'];
    $Data = $CIDRAM['Config']['legal']['pseudonymise_ip_addresses'] ? $CIDRAM['Pseudonymise-IP']($IPAddr) : $IPAddr;
    $Data .= ' - ' . $CIDRAM['FE']['DateTime'] . ' - "' . $User . '" - ' . $Message . "\n";
    $WriteMode = (!file_exists($CIDRAM['Vault'] . $File) || (
        $CIDRAM['Config']['general']['truncate'] > 0 &&
        filesize($CIDRAM['Vault'] . $File) >= $CIDRAM['ReadBytes']($CIDRAM['Config']['general']['truncate'])
    )) ? 'w' : 'a';
    if ($CIDRAM['BuildLogPath']($File)) {
        $Handle = fopen($CIDRAM['Vault'] . $File, $WriteMode);
        fwrite($Handle, $Data);
        fclose($Handle);
        if ($WriteMode === 'w') {
            $CIDRAM['LogRotation']($CIDRAM['Config']['general']['frontend_log']);
        }
    }
};

/**
 * Writes to the PHPMailer event log.
 *
 * @param string $Data What to write.
 * @return bool True on success; False on failure.
 */
$CIDRAM['Events']->addHandler('writeToPHPMailerEventLog', function (string $Data) use (&$CIDRAM): bool {
    if (!$CIDRAM['Config']['PHPMailer']['event_log']) {
        return false;
    }
    $EventLog = (strpos($CIDRAM['Config']['PHPMailer']['event_log'], '{') !== false) ? $CIDRAM['TimeFormat'](
        $CIDRAM['Now'],
        $CIDRAM['Config']['PHPMailer']['event_log']
    ) : $CIDRAM['Config']['PHPMailer']['event_log'];
    $WriteMode = (!file_exists($CIDRAM['Vault'] . $EventLog) || (
        $CIDRAM['Config']['general']['truncate'] > 0 &&
        filesize($CIDRAM['Vault'] . $EventLog) >= $CIDRAM['ReadBytes']($CIDRAM['Config']['general']['truncate'])
    )) ? 'w' : 'a';
    $Handle = fopen($CIDRAM['Vault'] . $EventLog, $WriteMode);
    fwrite($Handle, $Data);
    fclose($Handle);
    if ($WriteMode === 'w') {
        $CIDRAM['LogRotation']($CIDRAM['Config']['PHPMailer']['event_log']);
    }
    return true;
});

/**
 * Wrapper for PHPMailer functionality.
 *
 * @param array $Recipients An array of recipients to send to.
 * @param string $Subject The subject line of the email.
 * @param string $Body The HTML content of the email.
 * @param string $AltBody The alternative plain-text content of the email.
 * @param array $Attachments An optional array of attachments.
 * @return bool Operation failed (false) or succeeded (true).
 */
$CIDRAM['SendEmail'] = function (array $Recipients = [], string $Subject = '', string $Body = '', string $AltBody = '', array $Attachments = []) use (&$CIDRAM): bool {

    /** Prepare event logging. */
    $EventLogData = sprintf(
        '%s - %s - ',
        $CIDRAM['Config']['legal']['pseudonymise_ip_addresses'] ? $CIDRAM['Pseudonymise-IP']($_SERVER[$CIDRAM['IPAddr']]) : $_SERVER[$CIDRAM['IPAddr']],
        isset($CIDRAM['FE']['DateTime']) ? $CIDRAM['FE']['DateTime'] : $CIDRAM['TimeFormat'](
            $CIDRAM['Now'],
            $CIDRAM['Config']['general']['time_format']
        )
    );

    /** Operation success state. */
    $State = false;

    /** Check whether class exists to either load it and continue or fail the operation. */
    if (!class_exists('\PHPMailer\PHPMailer\PHPMailer')) {
        $EventLogData .= $CIDRAM['L10N']->getString('state_failed_missing') . "\n";
    } else {
        try {

            /** Create a new PHPMailer instance. */
            $Mail = new \PHPMailer\PHPMailer\PHPMailer();

            /** Tell PHPMailer to use SMTP. */
            $Mail->isSMTP();

            /** Disable debugging. */
            $Mail->SMTPDebug = 0;

            /** Skip authorisation process for some extreme problematic cases. */
            if ($CIDRAM['Config']['PHPMailer']['skip_auth_process']) {
                $Mail->SMTPOptions = ['ssl' => [
                    'verify_peer' => false,
                    'verify_peer_name' => false,
                    'allow_self_signed' => true
                ]];
            }

            /** Set mail server hostname. */
            $Mail->Host = $CIDRAM['Config']['PHPMailer']['host'];

            /** Set the SMTP port. */
            $Mail->Port = $CIDRAM['Config']['PHPMailer']['port'];

            /** Set the encryption system to use. */
            if (
                !empty($CIDRAM['Config']['PHPMailer']['smtp_secure']) &&
                $CIDRAM['Config']['PHPMailer']['smtp_secure'] !== '-'
            ) {
                $Mail->SMTPSecure = $CIDRAM['Config']['PHPMailer']['smtp_secure'];
            }

            /** Set whether to use SMTP authentication. */
            $Mail->SMTPAuth = $CIDRAM['Config']['PHPMailer']['smtp_auth'];

            /** Set the username to use for SMTP authentication. */
            $Mail->Username = $CIDRAM['Config']['PHPMailer']['username'];

            /** Set the password to use for SMTP authentication. */
            $Mail->Password = $CIDRAM['Config']['PHPMailer']['password'];

            /** Set the email sender address and name. */
            $Mail->setFrom(
                $CIDRAM['Config']['PHPMailer']['set_from_address'],
                $CIDRAM['Config']['PHPMailer']['set_from_name']
            );

            /** Set the optional "reply to" address and name. */
            if (
                !empty($CIDRAM['Config']['PHPMailer']['add_reply_to_address']) &&
                !empty($CIDRAM['Config']['PHPMailer']['add_reply_to_name'])
            ) {
                $Mail->addReplyTo(
                    $CIDRAM['Config']['PHPMailer']['add_reply_to_address'],
                    $CIDRAM['Config']['PHPMailer']['add_reply_to_name']
                );
            }

            /** Used by logging when send succeeds. */
            $SuccessDetails = '';

            /** Set the recipient address and name. */
            foreach ($Recipients as $Recipient) {
                if (empty($Recipient['Address']) || empty($Recipient['Name'])) {
                    continue;
                }
                $Mail->addAddress($Recipient['Address'], $Recipient['Name']);
                $SuccessDetails .= (($SuccessDetails) ? ', ' : '') . $Recipient['Name'] . ' <' . $Recipient['Address'] . '>';
            }

            /** Set the subject line of the email. */
            $Mail->Subject = $Subject;

            /** Tell PHPMailer that the email is written using HTML. */
            $Mail->isHTML = true;

            /** Set the HTML body of the email. */
            $Mail->Body = $Body;

            /** Set the alternative, plain-text body of the email. */
            $Mail->AltBody = $AltBody;

            /** Process attachments. */
            foreach ($Attachments as $Attachment) {
                $Mail->addAttachment($Attachment);
            }

            /** Send it! */
            $State = $Mail->send();

            /** Log the results of the send attempt. */
            $EventLogData .= ($State ? sprintf(
                $CIDRAM['L10N']->getString('state_email_sent'),
                $SuccessDetails
            ) : $CIDRAM['L10N']->getString('response_error') . ' - ' . $Mail->ErrorInfo) . "\n";

        } catch (\Exception $e) {

            /** An exeption occurred. Log the information. */
            $EventLogData .= $CIDRAM['L10N']->getString('response_error') . ' - ' . $e->getMessage() . "\n";

        }
    }

    /** Write to the event log. */
    $CIDRAM['Events']->fireEvent('writeToPHPMailerEventLog', $EventLogData);

    /** Exit. */
    return $State;
};

/**
 * Generates very simple 8-digit numbers used for 2FA.
 *
 * @return int An 8-digit number.
 */
$CIDRAM['2FA-Number'] = function (): int {
    static $MinInt = 10000000;
    static $MaxInt = 99999999;
    if (function_exists('random_int')) {
        try {
            $Key = random_int($MinInt, $MaxInt);
        } catch (\Exception $e) {
            $Key = rand($MinInt, $MaxInt);
        }
    }
    return isset($Key) ? $Key : rand($MinInt, $MaxInt);
};

/**
 * Generates the rules data displayed on the auxiliary rules page.
 */
$CIDRAM['AuxGenerateFEData'] = function () use (&$CIDRAM) {

    /** Populate output here. */
    $Output = '';

    /** Potential sources. */
    $Sources = preg_replace('~(?:| )?(?:?|:) ?$~', '', $CIDRAM['SourcesL10N']);

    /** Potential modes. */
    static $Modes = ['Whitelist', 'Greylist', 'Block', 'Bypass'];

    /** Attempt to parse the auxiliary rules file. */
    if (!isset($CIDRAM['AuxData'])) {
        $CIDRAM['AuxData'] = (new \Maikuolan\Common\YAML($CIDRAM['ReadFile']($CIDRAM['Vault'] . 'auxiliary.yaml')))->Data;
    }

    /** Count entries (needed for offering first and last move options). */
    $Count = count($CIDRAM['AuxData']);
    $Current = 1;

    /** Iterate through the auxiliary rules. */
    foreach ($CIDRAM['AuxData'] as $Name => $Data) {

        /** Rule row ID. */
        $RuleClass = preg_replace('~^0+~', '', bin2hex($Name));

        /** Figure out what options are available for the rule. */
        $Options = ['<input type="button" onclick="javascript:%1$s(\'%2$s\',\'%3$s\')" value="%4$s" class="auto" />'];
        $Options['delRule'] = sprintf($Options[0], 'delRule', $Name, $RuleClass, $CIDRAM['L10N']->getString('field_delete'));
        if ($Count > 1) {
            if ($Current !== 1) {
                $Options['moveToTop'] = sprintf($Options[0], 'moveToTop', $Name, $RuleClass, $CIDRAM['L10N']->getString('label_aux_move_top'));
            }
            if ($Current !== $Count) {
                $Options['moveToBottom'] = sprintf($Options[0], 'moveToBottom', $Name, $RuleClass, $CIDRAM['L10N']->getString('label_aux_move_bottom'));
            }
            $Current++;
        }
        $Options[0] = '';
        $Options = implode('', $Options);

        /** Begin generating rule output. */
        $Output .= sprintf(
            '        <tr class="%2$s">%1$s  <td class="h4"><div class="s">%3$s</div></td>%1$s  <td class="h4f">%4$s</td>%1$s</tr>' .
            '%1$s<tr class="%2$s">%1$s  <td class="h3f" colspan="2">',
            "\n        ",
            $RuleClass,
            $Name,
            $Options
        );

        /** Detailed reason. */
        if (!empty($Data['Reason'])) {
            $Output .= '<span class="s">' . $CIDRAM['L10N']->getString('label_aux_reason') . '</span><br />';
            $Output .= '<ul><li>' . $Data['Reason'] . '</li></ul>';
        }

        /** Iterate through actions. */
        foreach ([
            ['Whitelist', 'optActWhl'],
            ['Greylist', 'optActGrl'],
            ['Block', 'optActBlk'],
            ['Bypass', 'optActByp'],
            ['Don\'t log', 'optActLog']
        ] as $Action) {

            /** Skip action if the current rule doesn't use this action. */
            if (empty($Data[$Action[0]])) {
                continue;
            }

            /** Show the appropriate label for this action. */
            $Output .= '<span class="s">' . $CIDRAM['FE'][$Action[1]] . '</span><br />';

            /** Show the method to be used. */
            $Output .= '<span class="s">' . (isset($Data['Method']) ? (
                $Data['Method'] === 'RegEx' ? $CIDRAM['FE']['optMtdReg'] : (
                    $Data['Method'] === 'WinEx' ? $CIDRAM['FE']['optMtdWin'] : $CIDRAM['FE']['optMtdStr']
                )
            ) : $CIDRAM['FE']['optMtdStr']) . '</span><br />';

            /** Begin writing conditions list. */
            $Output .= '<ul>';

            /** List all "not equals" conditions . */
            if (!empty($Data[$Action[0]]['But not if matches'])) {

                /** Iterate through sources. */
                foreach ($Data[$Action[0]]['But not if matches'] as $Source => $Values) {
                    $ThisSource = $Sources[$Source] ?? $Source;
                    if (!is_array($Values)) {
                        $Values = [$Values];
                    }
                    foreach ($Values as $Value) {
                        $Output .= "\n            <li>" . $ThisSource . ' ? <code>' . $Value . '</code></li>';
                    }
                }

            }

            /** List all "equals" conditions . */
            if (!empty($Data[$Action[0]]['If matches'])) {

                /** Iterate through sources. */
                foreach ($Data[$Action[0]]['If matches'] as $Source => $Values) {
                    $ThisSource = $Sources[$Source] ?? $Source;
                    if (!is_array($Values)) {
                        $Values = [$Values];
                    }
                    foreach ($Values as $Value) {
                        $Output .= "\n            <li>" . $ThisSource . ' = <code>' . $Value . '</code></li>';
                    }
                }

            }

            /** Finish writing conditions list. */
            $Output .= "\n          </ul><br />";

        }

        /** Describe matching logic used. */
        if (!empty($Data['Logic']) && $Data['Logic'] !== 'Any') {
            $Output .= '<em>' . $CIDRAM['L10N']->getString('label_aux_logic_all') . '</em>';
        } else {
            $Output .= '<em>' . $CIDRAM['L10N']->getString('label_aux_logic_any') . '</em>';
        }

        /** Finish writing new rule. */
        $Output .= "</td>\n        </tr>\n";

    }

    /** Exit with generated output. */
    return $Output;

};

/**
 * Generate select options from an associative array.
 *
 * @param array $Options An associative array of the options to generate.
 * @param string $Trim An optional regex of data to remove from labels.
 * @return string The generated options.
 */
$CIDRAM['GenerateOptions'] = function (array $Options, $Trim = '') {
    $Output = '';
    foreach ($Options as $Value => $Label) {
        if ($Trim) {
            $Label = preg_replace($Trim, '', $Label);
        }
        $Output .= '<option value="' . $Value . '">' . $Label . '</option>';
    }
    return $Output;
};

/**
 * Generate a clickable list from an array.
 *
 * @param array $Arr The array to convert from.
 * @param string $DeleteKey The key to use for async calls to delete a cache entry.
 * @param int $Depth Current cache entry list depth.
 * @param string $ParentKey An optional key of the parent data source.
 * @return string The generated clickable list.
 */
$CIDRAM['ArrayToClickableList'] = function (array $Arr = [], string $DeleteKey = '', int $Depth = 0, string $ParentKey = '') use (&$CIDRAM): string {
    $Output = '';
    $Count = count($Arr);
    $Prefix = substr($DeleteKey, 0, 2) === 'fe' ? 'FE' : '';
    foreach ($Arr as $Key => $Value) {
        $Delete = ($Depth === 0) ? ' ? (<span style="cursor:pointer" onclick="javascript:' . $DeleteKey . '(\'' . addslashes($Key) . '\')"><code class="s">' . $CIDRAM['L10N']->getString('field_delete') . '</code></span>)' : '';
        $Output .= ($Depth === 0 ? '<span id="' . $Key . $Prefix . 'Container">' : '') . '<li>';
        if (!is_array($Value)) {
            if (substr($Value, 0, 2) === '{"' && substr($Value, -2) === '"}') {
                $Try = json_decode($Value, true);
                if ($Try !== null) {
                    $Value = $Try;
                }
            } elseif (
                preg_match('~\.ya?ml$~i', $Key) ||
                (preg_match('~^(?:Data|\d+)$~', $Key) && preg_match('~\.ya?ml$~i', $ParentKey)) ||
                substr($Value, 0, 4) === "---\n"
            ) {
                $Try = new \Maikuolan\Common\YAML();
                if ($Try->process($Value, $Try->Data) && !empty($Try->Data)) {
                    $Value = $Try->Data;
                }
            } elseif (substr($Value, 0, 2) === '["' && substr($Value, -2) === '"]' && strpos($Value, '","') !== false) {
                $Value = explode('","', substr($Value, 2, -2));
            }
        }
        if (is_array($Value)) {
            if ($Depth === 0) {
                $SizeField = $CIDRAM['L10N']->getString('field_size') ?: 'Size';
                $Size = isset($Value['Data']) && is_string($Value['Data']) ? strlen($Value['Data']) : (
                    isset($Value[0]) && is_string($Value[0]) ? strlen($Value[0]) : false
                );
                if ($Size !== false) {
                    $CIDRAM['FormatFilesize']($Size);
                    $Value[$SizeField] = $Size;
                }
            }
            $Output .= '<span class="comCat" style="cursor:pointer"><code class="s">' . str_replace(['<', '>'], ['&lt;', '&gt;'], $Key) . '</code></span>' . $Delete . '<ul class="comSub">';
            $Output .= $CIDRAM['ArrayToClickableList']($Value, $DeleteKey, $Depth + 1, $Key);
            $Output .= '</ul>';
        } else {
            if ($Key === 'Time' && preg_match('~^\d+$~', $Value)) {
                $Key = $CIDRAM['L10N']->getString('label_expires');
                $Value = $CIDRAM['TimeFormat']($Value, $CIDRAM['Config']['general']['time_format']);
            }
            $Class = ($Key === $CIDRAM['L10N']->getString('field_size') || $Key === $CIDRAM['L10N']->getString('label_expires')) ? 'txtRd' : 's';
            $Text = ($Count === 1 && $Key === 0) ? $Value : $Key . ($Class === 's' ? ' => ' : '') . $Value;
            $Output .= '<code class="' . $Class . '" style="word-wrap:break-word;word-break:break-all">' . str_replace(['<', '>'], ['&lt;', '&gt;'], $Text) . '</code>' . $Delete;
        }
        $Output .= '</li>' . ($Depth === 0 ? '<br /></span>' : '');
    }
    return $Output;
};

/**
 * Append to the current state message.
 *
 * @param string $Message What to append.
 */
$CIDRAM['Message'] = function (string $Message) use (&$CIDRAM) {
    if (isset($CIDRAM['FE']['state_msg'])) {
        if ($CIDRAM['FE']['state_msg'] || substr($CIDRAM['FE']['state_msg'], -6) !== '<br />') {
            $CIDRAM['FE']['state_msg'] .= '<br />';
        }
        $CIDRAM['FE']['state_msg'] .= $Message . '<br />';
    }
};

/**
 * Supplied string is used to generate arbitrary values used as RGB information
 * for CSS styling.
 *
 * @param string $String The supplied string to use.
 * @param int $Mode Whether to return the values as an array of integers,
 *      a hash-like string, or both.
 * @return string|array an array of integers, a hash-like string, or both.
 */
$CIDRAM['RGB'] = function (string $String = '', int $Mode = 0) {
    $Diff = [247, 127, 31];
    if (is_string($String) && !empty($String)) {
        $String = str_split($String);
        foreach ($String as $Char) {
            $Char = ord($Char);
            $Diff[0] = ($Diff[0] >> 1) + (($Diff[2] & 1) === 1 ? 128 : 0);
            $Diff[1] = ($Diff[1] >> 1) + (($Diff[0] & 1) === 1 ? 128 : 0);
            $Diff[2] = ($Diff[2] >> 1) + (($Diff[1] & 1) === 1 ? 128 : 0);
            $Diff[0] ^= $Char;
        }
    }
    if ($Mode === 1) {
        return $Diff;
    }
    for ($Hash = '', $Index = 0; $Index < 3; $Index++) {
        $Hash .= str_pad(bin2hex(chr($Diff[$Index])), 2, '0', STR_PAD_LEFT);
    }
    if ($Mode === 2) {
        return $Hash;
    }
    return ['Values' => $Diff, 'Hash' => $Hash];
};
For more information send a message to info at phpclasses dot org.