PHP Classes

File: vault/frontend_functions.php

Recommend this page to a friend!
  Classes of Caleb  >  PHP Mussel  >  vault/frontend_functions.php  >  Download  
File: vault/frontend_functions.php
Role: Example script
Content type: text/plain
Description: Example script
Class: PHP Mussel
PHP file virus scanner to detect malware
Author: By
Last change:
Date: 3 years ago
Size: 105,635 bytes
 

Contents

Class file image Download
<?php
/**
 * This file is a part of the phpMussel package.
 * Homepage: https://phpmussel.github.io/
 *
 * PHPMUSSEL COPYRIGHT 2013 AND BEYOND BY THE PHPMUSSEL TEAM.
 *
 * Authors:
 * @see PEOPLE.md
 *
 * License: GNU/GPLv2
 * @see LICENSE.txt
 *
 * This file: Front-end functions file (last modified: 2019.04.29).
 */

/**
 * Validates or ensures that two different sets of component metadata share the
 * same base elements (or components). One set acts as a model for which base
 * elements are expected, and if additional/superfluous entries are found in
 * the other set (the base), they'll be removed. Installed components are
 * ignored as to future-proof legacy support (just removes non-installed
 * components).
 *
 * @param string $Base The base set (generally, the local copy).
 * @param string $Model The model set (generally, the remote copy).
 * @param bool $Validate Validate (true) or ensure congruency (false; default).
 * @return string|bool If $Validate is true, returns true|false according to
 *      whether the sets are congruent. If $Validate is false, returns the
 *      corrected $Base set.
 */
$phpMussel['Congruency'] = function ($Base, $Model, $Validate = false) use (&$phpMussel) {
    if (empty($Base) || empty($Model)) {
        return $Validate ? false : '';
    }
    $BaseArr = (new \Maikuolan\Common\YAML($Base))->Data;
    $ModelArr = (new \Maikuolan\Common\YAML($Model))->Data;
    foreach ($BaseArr as $Element => $Data) {
        if (!isset($Data['Version']) && !isset($Data['Files']) && !isset($ModelArr[$Element])) {
            if ($Validate) {
                return false;
            }
            $Base = preg_replace("~\n" . preg_quote($Element) . ":?(\n [^\n]*)*\n~i", "\n", $Base);
        }
    }
    return $Validate ? true : $Base;
};

/**
 * Can be used to delete some files via the front-end.
 *
 * @param string $File The file to delete.
 * @return bool Success or failure.
 */
$phpMussel['Delete'] = function ($File) use (&$phpMussel) {
    if (preg_match('~^(\'.*\'|".*")$~', $File)) {
        $File = substr($File, 1, -1);
    }
    if (!empty($File) && file_exists($phpMussel['Vault'] . $File) && $phpMussel['Traverse']($File)) {
        if (!unlink($phpMussel['Vault'] . $File)) {
            return false;
        }
        $phpMussel['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.
 */
$phpMussel['In'] = function ($Query) use (&$phpMussel) {
    if (!$Delimiter = substr($Query, 0, 1)) {
        return false;
    }
    $QueryParts = explode($Delimiter, $Query);
    $CountParts = count($QueryParts);
    if (!($CountParts % 2)) {
        $QueryParts = preg_split('~ +~', $Query, -1, PREG_SPLIT_NO_EMPTY);
        return;
    }
    $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($phpMussel['Vault'] . $QueryParts[0]) || !is_readable($phpMussel['Vault'] . $QueryParts[0])) {
        return false;
    }

    /** Fetch file content. */
    if (!isset($phpMussel['FE_Executor_Files'][$QueryParts[0]])) {
        $phpMussel['FE_Executor_Files'][$QueryParts[0]] = ['Old' => $phpMussel['ReadFile']($phpMussel['Vault'] . $QueryParts[0])];
        $phpMussel['FE_Executor_Files'][$QueryParts[0]]['New'] = $phpMussel['FE_Executor_Files'][$QueryParts[0]]['Old'];
    }

    /** For clean, easy referencing. */
    $Data = &$phpMussel['FE_Executor_Files'][$QueryParts[0]]['New'];

    /** 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], (isset($QueryParts[4]) ? $QueryParts[4] : ''), $Data);
        return true;
    }

    /** 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.
 */
$phpMussel['ZeroMin'] = function () {
    $Sum = 0;
    foreach (func_get_args() as $Value) {
        $IntValue = (int)$Value;
        if ($IntValue !== $Value) {
            return 0;
        }
        $Sum += $IntValue;
    }
    return $Sum < 0 ? 0 : $Sum;
};

/** Format filesize information. */
$phpMussel['FormatFilesize'] = function (&$Filesize) use (&$phpMussel) {
    $Scale = ['field_size_bytes', 'field_size_KB', 'field_size_MB', 'field_size_GB', 'field_size_TB'];
    $Iterate = 0;
    $Filesize = (int)$Filesize;
    while ($Filesize > 1024) {
        $Filesize = $Filesize / 1024;
        $Iterate++;
        if ($Iterate > 4) {
            break;
        }
    }
    $Filesize = $phpMussel['Number_L10N']($Filesize, ($Iterate === 0) ? 0 : 2) . ' ' . $phpMussel['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.
 */
$phpMussel['FECacheRemove'] = function (&$Source, &$Rebuild, $Entry) use (&$phpMussel) {

    /** Override if using a different preferred caching mechanism. */
    if (isset($phpMussel['Cache']) && $phpMussel['Cache']->Using) {
        $phpMussel['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).
 */
$phpMussel['FECacheAdd'] = function (&$Source, &$Rebuild, $Entry, $Data, $Expires) use (&$phpMussel) {

    /** Override if using a different preferred caching mechanism. */
    if (isset($phpMussel['Cache']) && $phpMussel['Cache']->Using) {
        $phpMussel['Cache']->setEntry($Entry, $Data, $Expires - $phpMussel['Time']);
        return;
    }

    /** Default process. */
    $phpMussel['FECacheRemove']($Source, $Rebuild, $Entry);
    $Expires = (int)$Expires;
    $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).
 */
$phpMussel['FECacheGet'] = function (&$Source, $Entry) use (&$phpMussel) {

    /** Override if using a different preferred caching mechanism. */
    if (isset($phpMussel['Cache']) && $phpMussel['Cache']->Using) {
        return $phpMussel['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 phpMussel, or two different versions of a
 * component for phpMussel, 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).
 */
$phpMussel['VersionCompare'] = function ($A, $B) {
    $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.
 */
$phpMussel['ArrayFlatten'] = function ($Arr) {
    return array_filter($Arr, function () {
        return (!is_array(func_get_args()[0]));
    });
};

/** Isolate a L10N array down to a single relevant L10N string. */
$phpMussel['IsolateL10N'] = function (&$Arr, $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).
 */
$phpMussel['AppendToString'] = function (&$String, $Delimit = '', $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.
 */
$phpMussel['SanityCheck'] = function ($FileName, $FileData) {

    /** 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.
 */
$phpMussel['FileManager-RecursiveList'] = function ($Base) use (&$phpMussel) {
    $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'] = $phpMussel['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($phpMussel['FE']['TotalSize'])) {
                $phpMussel['FE']['TotalSize'] += $Arr[$Key]['Filesize'];
            }
            if (isset($phpMussel['Components']['Components'])) {
                $Component = $phpMussel['L10N']->getString('field_filetype_unknown');
                $ThisNameFixed = str_replace("\\", '/', $ThisName);
                if (isset($phpMussel['Components']['Files'][$ThisNameFixed])) {
                    if (!empty($phpMussel['Components']['Names'][$phpMussel['Components']['Files'][$ThisNameFixed]])) {
                        $Component = $phpMussel['Components']['Names'][$phpMussel['Components']['Files'][$ThisNameFixed]];
                    } else {
                        $Component = $phpMussel['Components']['Files'][$ThisNameFixed];
                    }
                    if ($Component === 'phpMussel') {
                        $Component .= ' (' . $phpMussel['L10N']->getString('field_component') . ')';
                    }
                } elseif (preg_match('~(?:[^|/]\.ht|\.safety$|^salt\.dat$)~i', $ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('label_fmgr_safety');
                } elseif (preg_match('/^config\.ini$/i', $ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('link_config');
                } elseif ($phpMussel['FileManager-IsLogFile']($ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('link_logs');
                } elseif (preg_match('~^(?:greylist\.csv|signatures/.*\.(?:csv|db))$~i', $ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('label_fmgr_other_sig');
                } elseif (preg_match('~(?:\.tmp|\.rollback|^(?:cache/index|fe_assets/frontend)\.dat)$~i', $ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('label_fmgr_cache_data');
                } elseif (preg_match('/\.qfu$/i', $ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('label_quarantined');
                } elseif (preg_match('/\.(?:dat|ya?ml)$/i', $ThisNameFixed)) {
                    $Component = $phpMussel['L10N']->getString('label_fmgr_updates_metadata');
                }
                if (!isset($phpMussel['Components']['Components'][$Component])) {
                    $phpMussel['Components']['Components'][$Component] = 0;
                }
                $phpMussel['Components']['Components'][$Component] += $Arr[$Key]['Filesize'];
                if (!isset($phpMussel['Components']['ComponentFiles'][$Component])) {
                    $phpMussel['Components']['ComponentFiles'][$Component] = [];
                }
                $phpMussel['Components']['ComponentFiles'][$Component][$ThisNameFixed] = $Arr[$Key]['Filesize'];
            }
            if (($ExtDel = strrpos($Item, '.')) !== false) {
                $Ext = strtoupper(substr($Item, $ExtDel + 1));
                if (!$Ext) {
                    $Arr[$Key]['Filetype'] = $phpMussel['L10N']->getString('field_filetype_unknown');
                    $Arr[$Key]['Icon'] = 'icon=unknown';
                    $phpMussel['FormatFilesize']($Arr[$Key]['Filesize']);
                    continue;
                }
                $Arr[$Key]['Filetype'] = $phpMussel['ParseVars'](['EXT' => $Ext], $phpMussel['L10N']->getString('field_filetype_info'));
                if ($Ext === 'ICO') {
                    $Arr[$Key]['Icon'] = 'file=' . urlencode($Prepend . $Item);
                    $phpMussel['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'] = $phpMussel['L10N']->getString('field_filetype_unknown');
            }
        }
        if (empty($Arr[$Key]['Icon'])) {
            $Arr[$Key]['Icon'] = 'icon=unknown';
        }
        if ($Arr[$Key]['Filesize']) {
            $phpMussel['FormatFilesize']($Arr[$Key]['Filesize']);
        } 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.
 */
$phpMussel['FetchComponentsLists'] = function ($Base, &$Arr) use (&$phpMussel) {
    $Files = new DirectoryIterator($Base);
    foreach ($Files as $ThisFile) {
        if (!empty($ThisFile) && preg_match('/\.(?:dat|inc|ya?ml)$/i', $ThisFile)) {
            $Data = $phpMussel['ReadFile']($Base . $ThisFile);
            if (substr($Data, 0, 4) === "---\n" && ($EoYAML = strpos($Data, "\n\n")) !== false) {
                $phpMussel['YAML-Object']->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.
 */
$phpMussel['FileManager-PathSecurityCheck'] = function ($Path) {
    $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.
 */
$phpMussel['Logs-RecursiveList'] = function ($Base) use (&$phpMussel) {
    $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) || !$phpMussel['FileManager-IsLogFile']($ThisName)) {
            continue;
        }
        $Arr[$ThisName] = ['Filename' => $ThisName, 'Filesize' => filesize($Item)];
        $phpMussel['FormatFilesize']($Arr[$ThisName]['Filesize']);
    }
    ksort($Arr);
    return $Arr;
};

/** Checks whether a component is in use (front-end closure). */
$phpMussel['IsInUse'] = function (&$Component) use (&$phpMussel) {
    $Files = empty($Component['Files']['To']) ? [] : $Component['Files']['To'];
    foreach ($Files as $File) {
        if (substr($File, 0, 11) === 'signatures/' && preg_match(
            '~,(?:[\w\d]+:)?' . preg_quote(substr($File, 11)) . ',~',
            ',' . $phpMussel['Config']['signatures']['Active'] . ','
        )) {
            return true;
        }
    }
    return false;
};

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

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

/** Check whether component is activable. */
$phpMussel['IsActivable'] = function (&$Component) {
    return (!empty($Component['Files']['To'][0]) && substr($Component['Files']['To'][0], 0, 11) === 'signatures/');
};

/** Prepares component extended description (front-end updates page). */
$phpMussel['PrepareExtendedDescription'] = function (&$Arr, $Key = '') use (&$phpMussel) {
    $Key = 'Extended Description ' . $Key;
    if (isset($phpMussel['lang'][$Key])) {
        $Arr['Extended Description'] = $phpMussel['L10N']->getString($Key);
    } elseif (empty($Arr['Extended Description'])) {
        $Arr['Extended Description'] = '';
    }
    if (is_array($Arr['Extended Description'])) {
        $phpMussel['IsolateL10N']($Arr['Extended Description'], $phpMussel['Config']['general']['lang']);
    }
};

/** Prepares component name (front-end updates page). */
$phpMussel['PrepareName'] = function (&$Arr, $Key = '') use (&$phpMussel) {
    $Key = 'Name ' . $Key;
    if (isset($phpMussel['lang'][$Key])) {
        $Arr['Name'] = $phpMussel['L10N']->getString($Key);
    } elseif (empty($Arr['Name'])) {
        $Arr['Name'] = '';
    }
    if (is_array($Arr['Name'])) {
        $phpMussel['IsolateL10N']($Arr['Name'], $phpMussel['Config']['general']['lang']);
    }
};

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

/**
 * 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.
 */
$phpMussel['FilterLang'] = function ($ChoiceKey) use (&$phpMussel) {
    $Path = $phpMussel['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.
 */
$phpMussel['FilterAlgo'] = function ($ChoiceKey) use (&$phpMussel) {
    return ($ChoiceKey === 'PASSWORD_ARGON2I') ? !$phpMussel['VersionCompare'](PHP_VERSION, '7.2.0RC1') : 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.
 */
$phpMussel['FilterTheme'] = function ($ChoiceKey) use (&$phpMussel) {
    if ($ChoiceKey === 'default') {
        return true;
    }
    $Path = $phpMussel['Vault'] . 'fe_assets/' . $ChoiceKey . '/';
    return (file_exists($Path . 'frontend.css') || file_exists($phpMussel['Vault'] . 'template_' . $ChoiceKey . '.html'));
};

/**
 * 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)
 * @return string The asset path.
 */
$phpMussel['GetAssetPath'] = function ($Asset, $CanFail = false) use (&$phpMussel) {
    if (
        $phpMussel['Config']['template_data']['theme'] !== 'default' &&
        file_exists($phpMussel['Vault'] . 'fe_assets/' . $phpMussel['Config']['template_data']['theme'] . '/' . $Asset)
    ) {
        return $phpMussel['Vault'] . 'fe_assets/' . $phpMussel['Config']['template_data']['theme'] . '/' . $Asset;
    }
    if (file_exists($phpMussel['Vault'] . 'fe_assets/' . $Asset)) {
        return $phpMussel['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.
 */
$phpMussel['VersionWarning'] = function ($Version = PHP_VERSION) use (&$phpMussel) {
    $Level = 0;
    $Minor = (float)$Version;
    if (!empty($phpMussel['ForceVersionWarning']) || $Minor < 7.1 || (
        $Minor === 7.1 && $phpMussel['VersionCompare']($Version, '7.1.28')
    ) || (
        $Minor === 7.2 && $phpMussel['VersionCompare']($Version, '7.2.16')
    ) || (
        $Minor === 7.3 && $phpMussel['VersionCompare']($Version, '7.3.3')
    )) {
        $Level += 2;
    }
    if ($Minor < 7.2) {
        $Level += 1;
    }
    $phpMussel['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.
 */
$phpMussel['FE_Executor'] = function ($Closures = false, $Queue = false) use (&$phpMussel) {
    if ($Queue && $Closures !== false) {
        if (empty($phpMussel['FE_Executor_Queue'])) {
            $phpMussel['FE_Executor_Queue'] = [];
        }
        $phpMussel['FE_Executor_Queue'][] = $Closures;
        return;
    }
    if ($Closures === false && !empty($phpMussel['FE_Executor_Queue'])) {
        foreach ($phpMussel['FE_Executor_Queue'] as $QueueItem) {
            $phpMussel['FE_Executor']($QueueItem);
        }
        $phpMussel['FE_Executor_Queue'] = [];
        return;
    }
    $phpMussel['Arrayify']($Closures);
    $phpMussel['FE_Executor_Files'] = [];
    foreach ($Closures as $Closure) {
        if (isset($phpMussel[$Closure]) && is_object($phpMussel[$Closure])) {
            $phpMussel[$Closure]();
        } elseif (($Pos = strpos($Closure, ' ')) !== false) {
            $Params = substr($Closure, $Pos + 1);
            $Closure = substr($Closure, 0, $Pos);
            if (isset($phpMussel[$Closure]) && is_object($phpMussel[$Closure])) {
                $phpMussel[$Closure]($Params);
            }
        }
    }
    foreach ($phpMussel['FE_Executor_Files'] as $Name => $Data) {
        if (isset($Data['New']) && isset($Data['Old']) && $Data['New'] !== $Data['Old'] && is_file($phpMussel['Vault'] . $Name) && is_writable($phpMussel['Vault'] . $Name)) {
            $Handle = fopen($phpMussel['Vault'] . $Name, 'w');
            fwrite($Handle, $Data['New']);
            fclose($Handle);
        }
    }
    unset($phpMussel['FE_Executor_Files']);
};

/**
 * Localises a number according to configuration specification.
 *
 * @param int $Number The number to localise.
 * @param int $Decimals Decimal places (optional).
 */
$phpMussel['Number_L10N'] = function ($Number, $Decimals = 0) use (&$phpMussel) {
    $Number = (float)$Number;
    $Sets = [
        'NoSep-1' => ['.', '', 3, false, 0],
        'NoSep-2' => [',', '', 3, false, 0],
        'Latin-1' => ['.', ',', 3, false, 0],
        'Latin-2' => ['.', '?', 3, false, 0],
        'Latin-3' => [',', '.', 3, false, 0],
        'Latin-4' => [',', '?', 3, false, 0],
        'Latin-5' => ['', ',', 3, false, 0],
        'China-1' => ['.', ',', 4, false, 0],
        'India-1' => ['.', ',', 2, false, -1],
        'India-2' => ['.', ',', 2, ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?'], -1],
        'Bengali-1' => ['.', ',', 2, ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?'], -1],
        'Arabic-1' => ['?', '', 3, ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?'], 0],
        'Arabic-2' => ['?', '?', 3, ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?'], 0],
        'Thai-1' => ['.', ',', 3, ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?'], 0]
    ];
    $Set = empty($Sets[$phpMussel['Config']['general']['numbers']]) ? 'Latin-1' : $Sets[$phpMussel['Config']['general']['numbers']];
    $DecPos = strpos($Number, '.') ?: strlen($Number);
    if ($Decimals && $Set[0]) {
        $Fraction = substr($Number, $DecPos + 1, $Decimals);
        $Fraction .= str_repeat('0', $Decimals - strlen($Fraction));
    }
    for ($Formatted = '', $ThouPos = $Set[4], $Pos = 1; $Pos <= $DecPos; $Pos++) {
        if ($ThouPos >= $Set[2]) {
            $ThouPos = 1;
            $Formatted = $Set[1] . $Formatted;
        } else {
            $ThouPos++;
        }
        $NegPos = $DecPos - $Pos;
        $ThisChar = substr($Number, $NegPos, 1);
        $Formatted = empty($Set[3][$ThisChar]) ? $ThisChar . $Formatted : $Set[3][$ThisChar] . $Formatted;
    }
    if ($Decimals && $Set[0]) {
        $Formatted .= $Set[0];
        for ($FracLen = strlen($Fraction), $Pos = 0; $Pos < $FracLen; $Pos++) {
            $Formatted .= empty($Set[3][$Fraction[$Pos]]) ? $Fraction[$Pos] : $Set[3][$Fraction[$Pos]];
        }
    }
    return $Formatted;
};

/**
 * Generates JavaScript code for localising numbers according to configuration
 * specification.
 */
$phpMussel['Number_L10N_JS'] = function () use (&$phpMussel) {
    $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}';
    $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],
        'Bengali-1' => ['.', ',', 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],
        'Thai-1' => ['.', ',', 3, 'var nls=[\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\',\'?\'];return nls[l10nd]||l10nd', 1],
    ];
    if (!empty($phpMussel['Config']['general']['numbers']) && isset($Sets[$phpMussel['Config']['general']['numbers']])) {
        $Set = $Sets[$phpMussel['Config']['general']['numbers']];
        return sprintf($Base, $Set[0], $Set[1], $Set[2], $Set[3], $Set[4]);
    }
    return sprintf($Base, $Sets['Latin-1'][0], $Sets['Latin-1'][1], $Sets['Latin-1'][2], $Sets['Latin-1'][3], $Sets['Latin-1'][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.
 */
$phpMussel['FilterSwitch'] = function ($Switches, $Selector, &$StateModified, &$Redirect, &$Options) use (&$phpMussel) {
    foreach ($Switches as $Switch) {
        $State = (!empty($Selector) && $Selector === $Switch);
        $phpMussel['FE'][$Switch] = empty($phpMussel['QueryVars'][$Switch]) ? false : (
            ($phpMussel['QueryVars'][$Switch] === 'true' && !$State) ||
            ($phpMussel['QueryVars'][$Switch] !== 'true' && $State)
        );
        if ($State) {
            $StateModified = true;
        }
        if ($phpMussel['FE'][$Switch]) {
            $Redirect .= '&' . $Switch . '=true';
            $LangItem = 'switch-' . $Switch . '-set-false';
        } else {
            $Redirect .= '&' . $Switch . '=false';
            $LangItem = 'switch-' . $Switch . '-set-true';
        }
        $Label = $phpMussel['L10N']->getString($LangItem) ?: $LangItem;
        $Options .= '<option value="' . $Switch . '">' . $Label . '</option>';
    }
};

/** Quarantine file list generator (returns an array of quarantined files). */
$phpMussel['Quarantine-RecursiveList'] = function ($DeleteMode = false) use (&$phpMussel) {
    $Arr = [];
    $Key = -1;
    $Offset = strlen($phpMussel['qfuPath']);
    $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($phpMussel['qfuPath']), RecursiveIteratorIterator::SELF_FIRST);
    foreach ($List as $Item => $List) {
        /** Skips if not a quarantined file. */
        if (!preg_match('~\.qfu$~i', $Item) || is_dir($Item) || !is_file($Item) || !is_readable($Item)) {
            continue;
        }
        /** Deletes all files in quarantine. */
        if ($DeleteMode) {
            $DeleteMe = substr($Item, $Offset);
            $phpMussel['FE']['state_msg'] .= '<code>' . $DeleteMe . '</code> ' . $phpMussel['L10N']->getString(
                unlink($phpMussel['qfuPath'] . $DeleteMe) ? 'response_file_deleted' : 'response_delete_error'
            ) . '<br />';
            continue;
        }
        $Key++;
        $Arr[$Key] = [
            'QFU-Name' => substr($Item, $Offset),
            'QFU-JS-ID' => substr($Item, $Offset, -4),
            'QFU-Size' => filesize($Item)
        ];
        $phpMussel['FormatFilesize']($Arr[$Key]['QFU-Size']);
        $Head = $phpMussel['ReadFile']($Item, 256);
        /** Upload date/time. */
        $Arr[$Key]['Upload-Date'] = (
            ($DatePos = strpos($Head, 'Time/Date Uploaded: ')) !== false
        ) ? $phpMussel['TimeFormat'](
            (int)substr($Head, $DatePos + 20, 16),
            $phpMussel['Config']['general']['timeFormat']
        ) : $phpMussel['L10N']->getString('field_filetype_unknown');
        /** Upload origin. */
        $Arr[$Key]['Upload-Origin'] = (
            ($OriginStartPos = strpos($Head, 'Uploaded From: ')) !== false &&
            ($OriginEndPos = strpos($Head, ' ', $OriginStartPos + 15)) !== false
        ) ? substr($Head, $OriginStartPos + 15, $OriginEndPos - $OriginStartPos - 15) : $phpMussel['L10N']->getString('field_filetype_unknown');
        /** If the phpMussel QFU (Quarantined File Upload) header isn't found, it probably isn't a quarantined file. */
        if (($HeadPos = strpos($Head, "\xa1phpMussel\x21")) !== false && (substr($Head, $HeadPos + 31, 1) === "\x01")) {
            $Head = substr($Head, $HeadPos);
            $Arr[$Key]['Upload-MD5'] = bin2hex(substr($Head, 11, 16));
            $Arr[$Key]['Upload-Size'] = $phpMussel['UnpackSafe']('l*', substr($Head, 27, 4));
            $Arr[$Key]['Upload-Size'] = isset($Arr[$Key]['Upload-Size'][1]) ? (int)$Arr[$Key]['Upload-Size'][1] : 0;
            $phpMussel['FormatFilesize']($Arr[$Key]['Upload-Size']);
        } else {
            $Arr[$Key]['Upload-MD5'] = $phpMussel['L10N']->getString('field_filetype_unknown');
            $Arr[$Key]['Upload-Size'] = $phpMussel['L10N']->getString('field_filetype_unknown');
        }
        /** Appends Virus Total search URL for this hash onto the hash. */
        if (strlen($Arr[$Key]['Upload-MD5']) === 32) {
            $Arr[$Key]['Upload-MD5'] = sprintf(
                '<a href="https://www.virustotal.com/#/file/%1$s">%1$s</a>',
                $Arr[$Key]['Upload-MD5']
            );
        }
    }
    return $Arr;
};

/** Restore a quarantined file (returns the restored file data or false on failure). */
$phpMussel['Quarantine-Restore'] = function ($File, $Key) use (&$phpMussel) {
    $phpMussel['RestoreStatus'] = 1;
    if (!$File || !$Key) {
        return false;
    }
    $Data = $phpMussel['ReadFile']($File);
    /** Fetch headers. */
    if (($HeadPos = strpos($Data, "\xa1phpMussel\x21")) === false || (substr($Data, $HeadPos + 31, 1) !== "\x01")) {
        $phpMussel['RestoreStatus'] = 2;
        return false;
    }
    $Data = substr($Data, $HeadPos);
    $UploadMD5 = bin2hex(substr($Data, 11, 16));
    $UploadSize = $phpMussel['UnpackSafe']('l*', substr($Data, 27, 4));
    $UploadSize = isset($UploadSize[1]) ? (int)$UploadSize[1] : 0;
    $Data = substr($Data, 32);
    $DataLen = strlen($Data);
    if ($Key < 128) {
        $Key = $phpMussel['HexSafe'](hash('sha512', $Key) . hash('whirlpool', $Key));
    }
    $KeyLen = strlen($Key);
    $Output = '';
    $Cycle = 0;
    while ($Cycle < $DataLen) {
        for ($Inner = 0; $Inner < $KeyLen; $Inner++, $Cycle++) {
            if (strlen($Output) >= $UploadSize) {
                break 2;
            }
            $L = substr($Data, $Cycle, 1);
            $R = substr($Key, $Inner, 1);
            $Output .= ($L === false ? "\x00" : $L) ^ ($R === false ? "\x00" : $R);
        }
    }
    $Output = gzinflate($Output);
    if (empty($Output) || md5($Output) !== $UploadMD5) {
        $phpMussel['RestoreStatus'] = 3;
        return false;
    }
    $phpMussel['RestoreStatus'] = 0;
    return $Output;
};

/** Duplication avoidance (front-end updates page). */
$phpMussel['AppendTests'] = function (&$Component, $ReturnState = false) use (&$phpMussel) {
    $TestData = $phpMussel['FECacheGet'](
        $phpMussel['FE']['Cache'],
        $phpMussel['Components']['RemoteMeta'][$Component['ID']]['Tests']
    );
    if (!$TestData) {
        $TestData = $phpMussel['Request'](
            $phpMussel['Components']['RemoteMeta'][$Component['ID']]['Tests']
        ) ?: '-';
        $phpMussel['FECacheAdd'](
            $phpMussel['FE']['Cache'],
            $phpMussel['FE']['Rebuild'],
            $phpMussel['Components']['RemoteMeta'][$Component['ID']]['Tests'],
            $TestData,
            $phpMussel['Time'] + 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) {
                $phpMussel['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 === '?' ? '?' : $phpMussel['Number_L10N']($TestsPassed)),
                $phpMussel['Number_L10N']($TestsTotal),
                $Component['ID'],
                $TestDetails
            );
            $phpMussel['AppendToString'](
                $Component['StatusOptions'],
                '<hr />',
                '<div class="s">' . $phpMussel['L10N']->getString('label_tests') . ' ' . $TestsTotal
            );
        }
    }
    if ($ReturnState) {
        return true;
    }
};

/** Traversal detection. */
$phpMussel['Traverse'] = function ($Path) {
    return !preg_match('~(?:[\./]{2}|[\x01-\x1f\[-^`?*$])~i', str_replace("\\", '/', $Path));
};

/** Sort function used by the front-end updates page. */
$phpMussel['UpdatesSortFunc'] = function ($A, $B) {
    $CheckA = preg_match('/^l10n/i', $A);
    $CheckB = preg_match('/^l10n/i', $B);
    if (($CheckA && !$CheckB) || ($A === 'phpMussel' && $B !== 'phpMussel')) {
        return -1;
    }
    if (($CheckB && !$CheckA) || ($B === 'phpMussel' && $A !== 'phpMussel')) {
        return 1;
    }
    if ($A < $B) {
        return -1;
    }
    if ($A > $B) {
        return 1;
    }
    return 0;
};

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

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

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

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

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

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

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

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

};

/** Updates handler: Update a component. */
$phpMussel['UpdatesHandler-Update'] = function ($ID) use (&$phpMussel) {
    $phpMussel['Arrayify']($ID);
    $FileData = [];
    $Annotations = [];
    foreach ($ID as $ThisTarget) {
        if (!isset(
            $phpMussel['Components']['Meta'][$ThisTarget]['Remote'],
            $phpMussel['Components']['Meta'][$ThisTarget]['Reannotate']
        )) {
            continue;
        }
        $phpMussel['Components']['BytesAdded'] = 0;
        $phpMussel['Components']['BytesRemoved'] = 0;
        $phpMussel['Components']['TimeRequired'] = microtime(true);
        $phpMussel['Components']['RemoteMeta'] = [];
        $phpMussel['Components']['Meta'][$ThisTarget]['RemoteData'] = '';
        $phpMussel['FetchRemote-ContextFree'](
            $phpMussel['Components']['Meta'][$ThisTarget]['RemoteData'],
            $phpMussel['Components']['Meta'][$ThisTarget]['Remote']
        );
        $phpMussel['UpdateFailed'] = false;
        if (
            substr($phpMussel['Components']['Meta'][$ThisTarget]['RemoteData'], 0, 4) === "---\n" &&
            ($phpMussel['Components']['EoYAML'] = strpos(
                $phpMussel['Components']['Meta'][$ThisTarget]['RemoteData'], "\n\n"
            )) !== false &&
            $phpMussel['YAML-Object']->process(
                substr($phpMussel['Components']['Meta'][$ThisTarget]['RemoteData'], 4, $phpMussel['Components']['EoYAML'] - 4),
                $phpMussel['Components']['RemoteMeta']
            ) &&
            !empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Minimum Required']) &&
            !$phpMussel['VersionCompare'](
                $phpMussel['ScriptVersion'],
                $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Minimum Required']
            ) &&
            (
                empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Minimum Required PHP']) ||
                !$phpMussel['VersionCompare'](PHP_VERSION, $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Minimum Required PHP'])
            ) &&
            !empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['From']) &&
            !empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To']) &&
            !empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Reannotate']) &&
            $phpMussel['Traverse']($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Reannotate']) &&
            ($ThisReannotate = $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Reannotate']) &&
            file_exists($phpMussel['Vault'] . $ThisReannotate) &&
            ((
                !empty($FileData[$ThisReannotate]) &&
                $phpMussel['Components']['OldMeta'] = $FileData[$ThisReannotate]
            ) || (
                $FileData[$ThisReannotate] = $phpMussel['Components']['OldMeta'] = $phpMussel['ReadFile'](
                    $phpMussel['Vault'] . $ThisReannotate
                )
            )) &&
            preg_match(
                "~(\n" . preg_quote($ThisTarget) . ":?)(\n [^\n]*)*\n~i",
                $phpMussel['Components']['OldMeta'],
                $phpMussel['Components']['OldMetaMatches']
            ) &&
            ($phpMussel['Components']['OldMetaMatches'] = $phpMussel['Components']['OldMetaMatches'][0]) &&
            ($phpMussel['Components']['NewMeta'] = $phpMussel['Components']['Meta'][$ThisTarget]['RemoteData']) &&
            preg_match(
                "~(\n" . preg_quote($ThisTarget) . ":?)(\n [^\n]*)*\n~i",
                $phpMussel['Components']['NewMeta'],
                $phpMussel['Components']['NewMetaMatches']
            ) &&
            ($phpMussel['Components']['NewMetaMatches'] = $phpMussel['Components']['NewMetaMatches'][0]) &&
            (!$phpMussel['FE']['CronMode'] || empty(
                $phpMussel['Components']['Meta'][$ThisTarget]['Tests']
            ) || $phpMussel['AppendTests']($phpMussel['Components']['Meta'][$ThisTarget], true))
        ) {
            $phpMussel['Arrayify']($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']);
            $phpMussel['Arrayify']($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['From']);
            $phpMussel['Arrayify']($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To']);
            if (!empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'])) {
                $phpMussel['Arrayify']($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum']);
            }
            $phpMussel['Components']['NewMeta'] = str_replace(
                $phpMussel['Components']['OldMetaMatches'],
                $phpMussel['Components']['NewMetaMatches'],
                $phpMussel['Components']['OldMeta']
            );
            $Count = count($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['From']);
            $phpMussel['RemoteFiles'] = [];
            $phpMussel['IgnoredFiles'] = [];
            $Rollback = false;
            /** Write new and updated files and directories. */
            for ($Iterate = 0; $Iterate < $Count; $Iterate++) {
                if (empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To'][$Iterate])) {
                    continue;
                }
                $ThisFileName = $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To'][$Iterate];
                /** Rolls back to previous version or uninstalls if an update/install fails. */
                if ($Rollback) {
                    if (
                        isset($phpMussel['RemoteFiles'][$ThisFileName]) &&
                        !isset($phpMussel['IgnoredFiles'][$ThisFileName]) &&
                        is_readable($phpMussel['Vault'] . $ThisFileName)
                    ) {
                        $phpMussel['Components']['BytesAdded'] -= filesize($phpMussel['Vault'] . $ThisFileName);
                        unlink($phpMussel['Vault'] . $ThisFileName);
                        if (is_readable($phpMussel['Vault'] . $ThisFileName . '.rollback')) {
                            $phpMussel['Components']['BytesRemoved'] -= filesize($phpMussel['Vault'] . $ThisFileName . '.rollback');
                            rename($phpMussel['Vault'] . $ThisFileName . '.rollback', $phpMussel['Vault'] . $ThisFileName);
                        }
                    }
                    continue;
                }
                if (
                    !empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate]) &&
                    !empty($phpMussel['Components']['Meta'][$ThisTarget]['Files']['Checksum'][$Iterate]) && (
                        $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate] ===
                        $phpMussel['Components']['Meta'][$ThisTarget]['Files']['Checksum'][$Iterate]
                    )
                ) {
                    $phpMussel['IgnoredFiles'][$ThisFileName] = true;
                    continue;
                }
                if (
                    empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['From'][$Iterate]) ||
                    !($ThisFile = $phpMussel['Request'](
                        $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['From'][$Iterate]
                    ))
                ) {
                    $Iterate = 0;
                    $Rollback = true;
                    continue;
                }
                if (
                    strtolower(substr(
                        $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['From'][$Iterate], -2
                    )) === 'gz' &&
                    strtolower(substr($ThisFileName, -2)) !== 'gz' &&
                    substr($ThisFile, 0, 2) === "\x1f\x8b"
                ) {
                    $ThisFile = gzdecode($ThisFile);
                }
                if (!empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate])) {
                    $ThisChecksum = $phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['Checksum'][$Iterate];
                    $ThisLen = strlen($ThisFile);
                    if (
                        (md5($ThisFile) . ':' . $ThisLen) !== $ThisChecksum &&
                        (sha1($ThisFile) . ':' . $ThisLen) !== $ThisChecksum &&
                        (hash('sha256', $ThisFile) . ':' . $ThisLen) !== $ThisChecksum
                    ) {
                        $phpMussel['FE']['state_msg'] .=
                            '<code>' . $ThisTarget . '</code> ? ' .
                            '<code>' . $ThisFileName . '</code> ? ' .
                            $phpMussel['L10N']->getString('response_checksum_error') . '<br />';
                        if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['On Checksum Error'])) {
                            $phpMussel['FE_Executor']($phpMussel['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) &&
                    !$phpMussel['SanityCheck']($ThisFileName, $ThisFile)
                ) {
                    $phpMussel['FE']['state_msg'] .= sprintf(
                        '<code>%s</code> ? <code>%s</code> ? %s<br />',
                        $ThisTarget,
                        $ThisFileName,
                        $phpMussel['L10N']->getString('response_sanity_1')
                    );
                    if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['On Sanity Error'])) {
                        $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ThisTarget]['On Sanity Error'], true);
                    }
                    $Iterate = 0;
                    $Rollback = true;
                    continue;
                }
                $ThisName = $ThisFileName;
                $ThisPath = $phpMussel['Vault'];
                while (strpos($ThisName, '/') !== false || strpos($ThisName, "\\") !== false) {
                    $phpMussel['Separator'] = (strpos($ThisName, '/') !== false) ? '/' : "\\";
                    $phpMussel['ThisDir'] = substr($ThisName, 0, strpos($ThisName, $phpMussel['Separator']));
                    $ThisPath .= $phpMussel['ThisDir'] . '/';
                    $ThisName = substr($ThisName, strlen($phpMussel['ThisDir']) + 1);
                    if (!file_exists($ThisPath) || !is_dir($ThisPath)) {
                        mkdir($ThisPath);
                    }
                }
                if (is_readable($phpMussel['Vault'] . $ThisFileName)) {
                    $phpMussel['Components']['BytesRemoved'] += filesize($phpMussel['Vault'] . $ThisFileName);
                    if (file_exists($phpMussel['Vault'] . $ThisFileName . '.rollback')) {
                        unlink($phpMussel['Vault'] . $ThisFileName . '.rollback');
                    }
                    rename($phpMussel['Vault'] . $ThisFileName, $phpMussel['Vault'] . $ThisFileName . '.rollback');
                }
                $phpMussel['Components']['BytesAdded'] += strlen($ThisFile);
                $Handle = fopen($phpMussel['Vault'] . $ThisFileName, 'w');
                $phpMussel['RemoteFiles'][$ThisFileName] = fwrite($Handle, $ThisFile);
                $phpMussel['RemoteFiles'][$ThisFileName] = ($phpMussel['RemoteFiles'][$ThisFileName] !== false);
                fclose($Handle);
                $ThisFile = '';
            }
            if ($Rollback) {
                /** Prune unwanted empty directories (update/install failure+rollback). */
                if (
                    !empty($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To']) &&
                    is_array($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To'])
                ) {
                    array_walk($phpMussel['Components']['RemoteMeta'][$ThisTarget]['Files']['To'], function ($ThisFile) use (&$phpMussel) {
                        if (!empty($ThisFile) && $phpMussel['Traverse']($ThisFile)) {
                            $phpMussel['DeleteDirectory']($ThisFile);
                        }
                    });
                }
                $phpMussel['UpdateFailed'] = true;
            } else {
                /** Prune unwanted files and directories (update/install success). */
                if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['Files']['To'])) {
                    $ThisArr = $phpMussel['Components']['Meta'][$ThisTarget]['Files']['To'];
                    $phpMussel['Arrayify']($ThisArr);
                    array_walk($ThisArr, function ($ThisFile) use (&$phpMussel) {
                        if (!empty($ThisFile) && $phpMussel['Traverse']($ThisFile)) {
                            if (file_exists($phpMussel['Vault'] . $ThisFile . '.rollback')) {
                                unlink($phpMussel['Vault'] . $ThisFile . '.rollback');
                            }
                            if (
                                !isset($phpMussel['RemoteFiles'][$ThisFile]) &&
                                !isset($phpMussel['IgnoredFiles'][$ThisFile]) &&
                                file_exists($phpMussel['Vault'] . $ThisFile)
                            ) {
                                $phpMussel['Components']['BytesRemoved'] += filesize($phpMussel['Vault'] . $ThisFile);
                                unlink($phpMussel['Vault'] . $ThisFile);
                                $phpMussel['DeleteDirectory']($ThisFile);
                            }
                        }
                    });
                    unset($ThisArr);
                }
                /** Assign updated component annotation. */
                $FileData[$ThisReannotate] = $phpMussel['Components']['NewMeta'];
                if (!isset($Annotations[$ThisReannotate])) {
                    $Annotations[$ThisReannotate] = $phpMussel['Components']['Meta'][$ThisTarget]['RemoteData'];
                }
                $phpMussel['FE']['state_msg'] .= '<code>' . $ThisTarget . '</code> ? ';
                if (
                    empty($phpMussel['Components']['Meta'][$ThisTarget]['Version']) &&
                    empty($phpMussel['Components']['Meta'][$ThisTarget]['Files'])
                ) {
                    $phpMussel['FE']['state_msg'] .= $phpMussel['L10N']->getString('response_component_successfully_installed');
                    if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['When Install Succeeds'])) {
                        $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ThisTarget]['When Install Succeeds'], true);
                    }
                } else {
                    $phpMussel['FE']['state_msg'] .= $phpMussel['L10N']->getString('response_component_successfully_updated');
                    if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['When Update Succeeds'])) {
                        $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ThisTarget]['When Update Succeeds'], true);
                    }
                }
                $phpMussel['Components']['Meta'][$ThisTarget] = $phpMussel['Components']['RemoteMeta'][$ThisTarget];
            }
        } else {
            $phpMussel['UpdateFailed'] = true;
        }
        if ($phpMussel['UpdateFailed']) {
            $phpMussel['FE']['state_msg'] .= '<code>' . $ThisTarget . '</code> ? ';
            if (
                empty($phpMussel['Components']['Meta'][$ThisTarget]['Version']) &&
                empty($phpMussel['Components']['Meta'][$ThisTarget]['Files'])
            ) {
                $phpMussel['FE']['state_msg'] .= $phpMussel['L10N']->getString('response_failed_to_install');
                if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['When Install Fails'])) {
                    $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ThisTarget]['When Install Fails'], true);
                }
            } else {
                $phpMussel['FE']['state_msg'] .= $phpMussel['L10N']->getString('response_failed_to_update');
                if (!empty($phpMussel['Components']['Meta'][$ThisTarget]['When Update Fails'])) {
                    $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ThisTarget]['When Update Fails'], true);
                }
            }
        }
        $phpMussel['FormatFilesize']($phpMussel['Components']['BytesAdded']);
        $phpMussel['FormatFilesize']($phpMussel['Components']['BytesRemoved']);
        $phpMussel['FE']['state_msg'] .= sprintf(
            $phpMussel['FE']['CronMode'] ? "  +%s | -%s | %s \n" : ' <code><span class="txtGn">+%s</span> | <span class="txtRd">-%s</span> | <span class="txtOe">%s</span></code><br />',
            $phpMussel['Components']['BytesAdded'],
            $phpMussel['Components']['BytesRemoved'],
            $phpMussel['Number_L10N'](microtime(true) - $phpMussel['Components']['TimeRequired'], 3)
        );
    }
    /** Update annotations. */
    foreach ($FileData as $ThisKey => $ThisFile) {
        /** Remove superfluous metadata. */
        if (!empty($Annotations[$ThisKey])) {
            $ThisFile = $phpMussel['Congruency']($ThisFile, $Annotations[$ThisKey]);
        }
        $Handle = fopen($phpMussel['Vault'] . $ThisKey, 'w');
        fwrite($Handle, $ThisFile);
        fclose($Handle);
    }
    /** Cleanup. */
    unset($phpMussel['RemoteFiles'], $phpMussel['IgnoredFiles']);
};

/** Updates handler: Uninstall a component. */
$phpMussel['UpdatesHandler-Uninstall'] = function ($ID) use (&$phpMussel) {
    $InUse = $phpMussel['ComponentFunctionUpdatePrep']($ID);
    $phpMussel['Components']['BytesRemoved'] = 0;
    $phpMussel['Components']['TimeRequired'] = microtime(true);
    if (
        empty($InUse) &&
        !empty($phpMussel['Components']['Meta'][$ID]['Files']['To']) &&
        ($ID !== 'l10n/' . $phpMussel['Config']['general']['lang']) &&
        ($ID !== 'theme/' . $phpMussel['Config']['template_data']['theme']) &&
        ($ID !== 'phpMussel') &&
        !empty($phpMussel['Components']['Meta'][$ID]['Reannotate']) &&
        !empty($phpMussel['Components']['Meta'][$ID]['Uninstallable']) &&
        ($phpMussel['Components']['OldMeta'] = $phpMussel['ReadFile'](
            $phpMussel['Vault'] . $phpMussel['Components']['Meta'][$ID]['Reannotate']
        )) &&
        preg_match(
            "~(\n" . preg_quote($ID) . ":?)(\n [^\n]*)*\n~i",
            $phpMussel['Components']['OldMeta'],
            $phpMussel['Components']['OldMetaMatches']
        ) &&
        ($phpMussel['Components']['OldMetaMatches'] = $phpMussel['Components']['OldMetaMatches'][0])
    ) {
        $phpMussel['Components']['NewMeta'] = str_replace(
            $phpMussel['Components']['OldMetaMatches'],
            preg_replace(
                ["/\n Files:(\n  [^\n]*)*\n/i", "/\n Version: [^\n]*\n/i"],
                "\n",
                $phpMussel['Components']['OldMetaMatches']
            ),
            $phpMussel['Components']['OldMeta']
        );
        array_walk($phpMussel['Components']['Meta'][$ID]['Files']['To'], function ($ThisFile) use (&$phpMussel) {
            if (!empty($ThisFile) && $phpMussel['Traverse']($ThisFile)) {
                if (file_exists($phpMussel['Vault'] . $ThisFile)) {
                    $phpMussel['Components']['BytesRemoved'] += filesize($phpMussel['Vault'] . $ThisFile);
                    unlink($phpMussel['Vault'] . $ThisFile);
                }
                if (file_exists($phpMussel['Vault'] . $ThisFile . '.rollback')) {
                    $phpMussel['Components']['BytesRemoved'] += filesize($phpMussel['Vault'] . $ThisFile . '.rollback');
                    unlink($phpMussel['Vault'] . $ThisFile . '.rollback');
                }
                $phpMussel['DeleteDirectory']($ThisFile);
            }
        });
        $Handle = fopen($phpMussel['Vault'] . $phpMussel['Components']['Meta'][$ID]['Reannotate'], 'w');
        fwrite($Handle, $phpMussel['Components']['NewMeta']);
        fclose($Handle);
        $phpMussel['Components']['Meta'][$ID]['Version'] = false;
        $phpMussel['Components']['Meta'][$ID]['Files'] = false;
        $phpMussel['FE']['state_msg'] = $phpMussel['L10N']->getString('response_component_successfully_uninstalled');
        if (!empty($phpMussel['Components']['Meta'][$ID]['When Uninstall Succeeds'])) {
            $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ID]['When Uninstall Succeeds'], true);
        }
    } else {
        $phpMussel['FE']['state_msg'] = $phpMussel['L10N']->getString('response_component_uninstall_error');
        if (!empty($phpMussel['Components']['Meta'][$ID]['When Uninstall Fails'])) {
            $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ID]['When Uninstall Fails'], true);
        }
    }
    $phpMussel['FormatFilesize']($phpMussel['Components']['BytesRemoved']);
    $phpMussel['FE']['state_msg'] .= sprintf(
        $phpMussel['FE']['CronMode'] ? "  -%s | %s \n" : ' <code><span class="txtRd">-%s</span> | <span class="txtOe">%s</span></code>',
        $phpMussel['Components']['BytesRemoved'],
        $phpMussel['Number_L10N'](microtime(true) - $phpMussel['Components']['TimeRequired'], 3)
    );
};

/** Updates handler: Activate a component. */
$phpMussel['UpdatesHandler-Activate'] = function ($ID) use (&$phpMussel) {
    $phpMussel['Activation'] = [
        'Config' => $phpMussel['ReadFile']($phpMussel['Vault'] . $phpMussel['FE']['ActiveConfigFile']),
        'Active' => $phpMussel['Config']['signatures']['Active'],
        'Modified' => false
    ];
    $InUse = $phpMussel['ComponentFunctionUpdatePrep']($ID);
    if (empty($InUse) && !empty($phpMussel['Components']['Meta'][$ID]['Files']['To'])) {
        $phpMussel['Activation']['Active'] = array_unique(array_filter(
            explode(',', $phpMussel['Activation']['Active']),
            function ($Component) use (&$phpMussel) {
                $Component = (strpos($Component, ':') === false) ? $Component : substr($Component, strpos($Component, ':') + 1);
                return ($Component && file_exists($phpMussel['sigPath'] . $Component));
            }
        ));
        foreach ($phpMussel['Components']['Meta'][$ID]['Files']['To'] as $phpMussel['Activation']['ThisFile']) {
            if (
                !empty($phpMussel['Activation']['ThisFile']) &&
                file_exists($phpMussel['Vault'] . $phpMussel['Activation']['ThisFile']) &&
                substr($phpMussel['Activation']['ThisFile'], 0, 11) === 'signatures/' &&
                $phpMussel['Traverse']($phpMussel['Activation']['ThisFile'])
            ) {
                $phpMussel['Activation']['Active'][] = substr($phpMussel['Activation']['ThisFile'], 11);
            }
        }
        if (count($phpMussel['Activation']['Active'])) {
            sort($phpMussel['Activation']['Active']);
        }
        $phpMussel['Activation']['Active'] = implode(',', $phpMussel['Activation']['Active']);
        if ($phpMussel['Activation']['Active'] !== $phpMussel['Config']['signatures']['Active']) {
            $phpMussel['Activation']['Modified'] = true;
        }
    }
    if (!$phpMussel['Activation']['Modified'] || !$phpMussel['Activation']['Config']) {
        $phpMussel['FE']['state_msg'] = $phpMussel['L10N']->getString('response_activation_failed');
        if (!empty($phpMussel['Components']['Meta'][$ID]['When Activation Fails'])) {
            $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ID]['When Activation Fails'], true);
        }
    } else {
        $EOL = (strpos($phpMussel['Activation']['Config'], "\r\nActive=") !== false) ? "\r\n" : "\n";
        $phpMussel['Activation']['Config'] = str_replace(
            $EOL . "Active='" . $phpMussel['Config']['signatures']['Active'] . "'" . $EOL,
            $EOL . "Active='" . $phpMussel['Activation']['Active'] . "'" . $EOL,
            $phpMussel['Activation']['Config']
        );
        $phpMussel['Config']['signatures']['Active'] = $phpMussel['Activation']['Active'];
        $Handle = fopen($phpMussel['Vault'] . $phpMussel['FE']['ActiveConfigFile'], 'w');
        fwrite($Handle, $phpMussel['Activation']['Config']);
        fclose($Handle);
        $phpMussel['FE']['state_msg'] = $phpMussel['L10N']->getString('response_activated');
        if (!empty($phpMussel['Components']['Meta'][$ID]['When Activation Succeeds'])) {
            $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ID]['When Activation Succeeds'], true);
        }
    }
    /** Cleanup. */
    unset($phpMussel['Activation']);
};

/** Updates handler: Deactivate a component. */
$phpMussel['UpdatesHandler-Deactivate'] = function ($ID) use (&$phpMussel) {
    $phpMussel['Deactivation'] = [
        'Config' => $phpMussel['ReadFile']($phpMussel['Vault'] . $phpMussel['FE']['ActiveConfigFile']),
        'Active' => $phpMussel['Config']['signatures']['Active'],
        'Modified' => false
    ];
    $InUse = $phpMussel['ComponentFunctionUpdatePrep']($ID);
    if (!empty($InUse) && !empty($phpMussel['Components']['Meta'][$ID]['Files']['To'])) {
        $phpMussel['Deactivation']['Active'] = array_unique(array_filter(
            explode(',', $phpMussel['Deactivation']['Active']),
            function ($Component) use (&$phpMussel) {
                $Component = (strpos($Component, ':') === false) ? $Component : substr($Component, strpos($Component, ':') + 1);
                return ($Component && file_exists($phpMussel['sigPath'] . $Component));
            }
        ));
        if (count($phpMussel['Deactivation']['Active'])) {
            sort($phpMussel['Deactivation']['Active']);
        }
        $phpMussel['Deactivation']['Active'] = ',' . implode(',', $phpMussel['Deactivation']['Active']) . ',';
        foreach ($phpMussel['Components']['Meta'][$ID]['Files']['To'] as $phpMussel['Deactivation']['ThisFile']) {
            if (substr($phpMussel['Deactivation']['ThisFile'], 0, 11) === 'signatures/') {
                $phpMussel['Deactivation']['Active'] = preg_replace(
                    '~,(?:[\w\d]+:)?' . preg_quote(substr($phpMussel['Deactivation']['ThisFile'], 11)) . ',~',
                    ',',
                    $phpMussel['Deactivation']['Active']
                );
            }
        }
        $phpMussel['Deactivation']['Active'] = substr($phpMussel['Deactivation']['Active'], 1, -1);
        if ($phpMussel['Deactivation']['Active'] !== $phpMussel['Config']['signatures']['Active']) {
            $phpMussel['Deactivation']['Modified'] = true;
        }
    }
    if (!$phpMussel['Deactivation']['Modified'] || !$phpMussel['Deactivation']['Config']) {
        $phpMussel['FE']['state_msg'] = $phpMussel['L10N']->getString('response_deactivation_failed');
        if (!empty($phpMussel['Components']['Meta'][$ID]['When Deactivation Fails'])) {
            $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ID]['When Deactivation Fails'], true);
        }
    } else {
        $EOL = (strpos($phpMussel['Deactivation']['Config'], "\r\nActive=") !== false) ? "\r\n" : "\n";
        $phpMussel['Deactivation']['Config'] = str_replace(
            $EOL . "Active='" . $phpMussel['Config']['signatures']['Active'] . "'" . $EOL,
            $EOL . "Active='" . $phpMussel['Deactivation']['Active'] . "'" . $EOL,
            $phpMussel['Deactivation']['Config']
        );
        $phpMussel['Config']['signatures']['Active'] = $phpMussel['Deactivation']['Active'];
        $Handle = fopen($phpMussel['Vault'] . $phpMussel['FE']['ActiveConfigFile'], 'w');
        fwrite($Handle, $phpMussel['Deactivation']['Config']);
        fclose($Handle);
        $phpMussel['FE']['state_msg'] = $phpMussel['L10N']->getString('response_deactivated');
        if (!empty($phpMussel['Components']['Meta'][$ID]['When Deactivation Succeeds'])) {
            $phpMussel['FE_Executor']($phpMussel['Components']['Meta'][$ID]['When Deactivation Succeeds'], true);
        }
    }
    /** Cleanup. */
    unset($phpMussel['Deactivation']);
};

/** Updates handler: Verify a component. */
$phpMussel['UpdatesHandler-Verify'] = function ($ID) use (&$phpMussel) {
    $phpMussel['Arrayify']($ID);
    foreach ($ID as $ThisID) {
        $Table = '<blockquote class="ng1 comSub">';
        if (empty($phpMussel['Components']['Meta'][$ThisID]['Files'])) {
            continue;
        }
        $TheseFiles = $phpMussel['Components']['Meta'][$ThisID]['Files'];
        if (!empty($TheseFiles['To'])) {
            $phpMussel['Arrayify']($TheseFiles['To']);
        }
        $Count = count($TheseFiles['To']);
        if (!empty($TheseFiles['Checksum'])) {
            $phpMussel['Arrayify']($TheseFiles['Checksum']);
        }
        $Passed = true;
        for ($Iterate = 0; $Iterate < $Count; $Iterate++) {
            $ThisFile = $TheseFiles['To'][$Iterate];
            $ThisFileData = $phpMussel['ReadFile']($phpMussel['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 = $phpMussel['SanityCheck']($ThisFile, $ThisFileData) ? 'txtGn' : 'txtRd';
                $Sanity = sprintf('<span class="%s">%s</span>', $Class, $phpMussel['L10N']->getString(
                    $Class === 'txtGn' ? 'response_passed' : 'response_failed'
                ));
                if ($Class === 'txtRd') {
                    $Passed = false;
                }
            } else {
                $Sanity = sprintf('<span class="txtOe">%s</span>', $phpMussel['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, $phpMussel['L10N']->getString(
                    $Class === 'txtGn' ? 'response_passed' : 'response_failed'
                ));
            } else {
                $Class = 's';
                $Integrity = sprintf('<span class="txtOe">%s</span>', $phpMussel['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,
                $phpMussel['L10N']->getString('label_actual'),
                $Actual ?: '?',
                $phpMussel['L10N']->getString('label_expected'),
                $Checksum ?: '?',
                $Class,
                $phpMussel['L10N']->getString('label_integrity_check'),
                $Integrity,
                $phpMussel['L10N']->getString('label_sanity_check'),
                $Sanity
            );
        }
        $Table .= '</blockquote>';
        $phpMussel['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'),
            $phpMussel['L10N']->getString($Passed ? 'response_verification_success' : 'response_verification_failed'),
            $Table
        );
    }
};

/** Normalise linebreaks. */
$phpMussel['NormaliseLinebreaks'] = function (&$Data) {
    if (strpos($Data, "\r")) {
        $Data = (strpos($Data, "\r\n") !== false) ? str_replace("\r", '', $Data) : str_replace("\r", "\n", $Data);
    }
};

/** Signature information handler. */
$phpMussel['SigInfoHandler'] = function ($Active) use (&$phpMussel) {

    /** Check whether shorthand data has been fetched. If it hasn't, fetch it. */
    if (!isset($phpMussel['shorthand.yaml'])) {
        if (!file_exists($phpMussel['Vault'] . 'shorthand.yaml') || !is_readable($phpMussel['Vault'] . 'shorthand.yaml')) {
            return '<span class="s">' . $phpMussel['L10N']->getString('response_error') . '</span>';
        }
        $phpMussel['shorthand.yaml'] = (new \Maikuolan\Common\YAML($phpMussel['ReadFile']($phpMussel['Vault'] . 'shorthand.yaml')))->Data;
    }

    /** Get list of vendor search patterns and metadata search pattern partials. */
    $Arr = [
        'Vendors' => $phpMussel['shorthand.yaml']['Vendor Search Patterns'],
        'SigTypes' => $phpMussel['shorthand.yaml']['Metadata Search Pattern Partials']
    ];

    /** Expand patterns for signature metadata. */
    foreach ($Arr['SigTypes'] as &$Type) {
        $Type = sprintf(
            '\x1a(?![\x80-\x8f])[\x0%1$s\x1%1$s\x2%1$s\x3%1$s\x4%1$s\x5%1$s\x6%1$s\x7%1$s\x8%1$s\x9%1$s\xa%1$s\xb%1$s\xc%1$s\xd%1$s\xe%1$s\ef%1$s]',
            $Type
        );
    }

    /** Get list of vector search patterns. */
    $Arr['Targets'] = $phpMussel['shorthand.yaml']['Vector Search Patterns'];

    /** Get list of malware type search patterns. */
    $Arr['MalwareTypes'] = $phpMussel['shorthand.yaml']['Malware Type Search Patterns'];

    /** To be populated by totals. */
    $Totals = ['Classes' => [], 'Files' => [], 'Vendors' => [], 'SigTypes' => [], 'Targets' => [], 'MalwareTypes' => []];

    /** Signature file classes. */
    $Classes = [
        ['General_Command_Detections', ''],
        ['Filename', '\n(?!>)'],
        ['Hash', '\n[\dA-Fa-f]{32,}:\d+:'],
        ['Standard', '\n(?!>)'],
        ['Standard_RegEx', '\n(?!>)'],
        ['Normalised', '\n(?!>)'],
        ['Normalised_RegEx', '\n(?!>)'],
        ['HTML', '\n(?!>)'],
        ['HTML_RegEx', '\n(?!>)'],
        ['PE_Extended', '\n\$PE\w+:[\dA-Fa-f]{32,}:\d+:'],
        ['PE_Sectional', '\n\d+:[\dA-Fa-f]{32,}:'],
        ['Complex_Extended', '\n\$\S+;'],
        ['URL_Scanner', '\n(?:TLD|(?:DOMAIN|URL)(?:-NOLOOKUP)?|QUERY)\S+:']
    ];

    /** We cycle through this several times in this closure. */
    $Subs = ['Classes', 'Files', 'Vendors', 'SigTypes', 'Targets', 'MalwareTypes'];

    /** Iterate through active signature files and append totals. */
    foreach ($Active as $File) {
        $File = (strpos($File, ':') === false) ? $File : substr($File, strpos($File, ':') + 1);
        $Data = $File && is_readable($phpMussel['sigPath'] . $File) ? $phpMussel['ReadFile']($phpMussel['sigPath'] . $File) : '';
        if (substr($Data, 0, 9) !== 'phpMussel') {
            continue;
        }
        $Class = substr($Data, 9, 1);
        $Nibbles = $phpMussel['split_nibble']($Class);
        $Class = !isset($Classes[$Nibbles[0]]) ? '?' : $Classes[$Nibbles[0]];
        $Totals['Files'][$File] = empty($Class[1]) ? substr_count($Data, ',') + 1 : preg_match_all('/' . $Class[1] . '\S+/', $Data);
        if (!empty($Class[0])) {
            $Totals['Classes'][$Class[0]] = isset($Totals['Classes'][$Class[0]]) ? $Totals['Classes'][$Class[0]] + $Totals['Files'][$File] : $Totals['Files'][$File];
        }
        foreach ($Subs as $Sub) {
            $Totals[$Sub]['Total'] = isset($Totals[$Sub]['Total']) ? $Totals[$Sub]['Total'] + $Totals['Files'][$File] : $Totals['Files'][$File];
        }
        $phpMussel['NormaliseLinebreaks']($Data);
        if (!empty($Class[1])) {
            foreach (['Vendors', 'SigTypes', 'Targets', 'MalwareTypes'] as $Sub) {
                foreach ($Arr[$Sub] as $Key => $Pattern) {
                    $Counts = preg_match_all('/' . $Class[1] . $Pattern . '\S+/', $Data);
                    $Totals[$Sub][$Key] = isset($Totals[$Sub][$Key]) ? $Totals[$Sub][$Key] + $Counts : $Counts;
                }
            }
        }
    }

    /** Build "other" totals. */
    foreach ($Subs as $Sub) {
        $Other = isset($Totals[$Sub]['Total']) ? $Totals[$Sub]['Total'] : 0;
        foreach ($Totals[$Sub] as $Key => $SubTotal) {
            if ($Key === 'Total') {
                continue;
            }
            $Other -= $SubTotal;
        }
        $Totals[$Sub]['Other'] = $Other;
    }

    /** Cleanup. */
    unset($SubTotal, $Other, $Data, $File, $Counts, $Arr);

    /** To be populated by category menus. */
    $phpMussel['FE']['infoCatOptions'] = '';

    /** To be populated by output tables. */
    $Out = '';

    /** Process totals. */
    foreach ($Subs as $Sub) {
        $Label = $phpMussel['L10N']->getString('siginfo_sub_' . $Sub) ?: $Sub;
        $Class = 'sigtype_' . strtolower($Sub);
        $phpMussel['FE']['infoCatOptions'] .= "\n      <option value=\"" . $Class . '">' . $Label . '</option>';
        $ThisTable = '<span style="display:none" class="' . $Class . '"><table><tr><td class="center h4f" colspan="2"><span class="s">' . $Label . '</span></td></tr>' . "\n";
        arsort($Totals[$Sub]);
        foreach ($Totals[$Sub] as $Key => &$Total) {
            if (!$Total) {
                continue;
            }
            $Total = $phpMussel['Number_L10N']($Total);
            $Label = $phpMussel['L10N']->getString(
                ($Key === 'Other' && $Sub === 'SigTypes') ? 'siginfo_key_Other_Metadata' : 'siginfo_key_' . $Key
            );
            if ($Key !== 'Total' && $Key !== 'Other') {
                if (!$Label) {
                    $Label = sprintf($phpMussel['L10N']->getString('siginfo_xkey'), $Key);
                }
                $CellClass = 'h3';
            } else {
                $CellClass = 'r';
            }
            $ThisTable .= $phpMussel['ParseVars'](['x' => $CellClass, 'InfoType' => $Label, 'InfoNum' => $Total], $phpMussel['FE']['InfoRow']);
        }
        $Out .= $ThisTable . '</table></span>' . "\n";
    }

    /** Return totals and exit closure. */
    return $Out;
};

/** Assign some basic variables (initial prepwork for most front-end pages). */
$phpMussel['InitialPrepwork'] = function ($Title = '', $Tips = '', $JS = true) use (&$phpMussel) {

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

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

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

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

};

/** Send page output for front-end pages (plus some other final prepwork). */
$phpMussel['SendOutput'] = function () use (&$phpMussel) {
    if ($phpMussel['FE']['JS']) {
        $phpMussel['FE']['JS'] = "\n<script type=\"text/javascript\">" . $phpMussel['FE']['JS'] . '</script>';
    }
    return $phpMussel['ParseVars']($phpMussel['lang'] + $phpMussel['FE'], $phpMussel['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.
 */
$phpMussel['FileManager-IsLogFile'] = function ($File) use (&$phpMussel) {
    static $Pattern_scan_log = false;
    if (!$Pattern_scan_log && $phpMussel['Config']['general']['scan_log']) {
        $Pattern_scan_log = $phpMussel['BuildLogPattern']($phpMussel['Config']['general']['scan_log'], true);
    }
    static $Pattern_scan_log_serialized = false;
    if (!$Pattern_scan_log_serialized && $phpMussel['Config']['general']['scan_log_serialized']) {
        $Pattern_scan_log_serialized = $phpMussel['BuildLogPattern']($phpMussel['Config']['general']['scan_log_serialized'], true);
    }
    static $Pattern_scan_kills = false;
    if (!$Pattern_scan_kills && $phpMussel['Config']['general']['scan_kills']) {
        $Pattern_scan_kills = $phpMussel['BuildLogPattern']($phpMussel['Config']['general']['scan_kills'], true);
    }
    static $Pattern_FrontEndLog = false;
    if (!$Pattern_FrontEndLog && $phpMussel['Config']['general']['FrontEndLog']) {
        $Pattern_FrontEndLog = $phpMussel['BuildLogPattern']($phpMussel['Config']['general']['FrontEndLog'], true);
    }
    static $Pattern_PHPMailer_EventLog = false;
    if (!$Pattern_PHPMailer_EventLog && $phpMussel['Config']['PHPMailer']['EventLog']) {
        $Pattern_PHPMailer_EventLog = $phpMussel['BuildLogPattern']($phpMussel['Config']['PHPMailer']['EventLog'], true);
    }
    return preg_match('~\.log(?:\.gz)?$~', strtolower($File)) || (
        $phpMussel['Config']['general']['scan_log'] && preg_match($Pattern_scan_log, $File)
    ) || (
        $phpMussel['Config']['general']['scan_log_serialized'] && preg_match($Pattern_scan_log_serialized, $File)
    ) || (
        $phpMussel['Config']['general']['scan_kills'] && preg_match($Pattern_scan_kills, $File)
    ) || (
        $phpMussel['Config']['general']['FrontEndLog'] && preg_match($Pattern_FrontEndLog, $File)
    ) || (
        $phpMussel['Config']['PHPMailer']['EventLog'] && 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.
 */
$phpMussel['GenerateConfirm'] = function ($Action, $Form) use (&$phpMussel) {
    $Confirm = str_replace(["'", '"'], ["\'", '\x22'], sprintf($phpMussel['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.
 */
$phpMussel['FELogger'] = function ($IPAddr, $User, $Message) use (&$phpMussel) {
    if (!$phpMussel['Config']['general']['FrontEndLog'] || empty($phpMussel['FE']['DateTime'])) {
        return;
    }
    $File = (strpos($phpMussel['Config']['general']['FrontEndLog'], '{') !== false) ? $phpMussel['TimeFormat'](
        $phpMussel['Time'],
        $phpMussel['Config']['general']['FrontEndLog']
    ) : $phpMussel['Config']['general']['FrontEndLog'];
    $Data = $phpMussel['Config']['legal']['pseudonymise_ip_addresses'] ? $phpMussel['Pseudonymise-IP']($IPAddr) : $IPAddr;
    $Data .= ' - ' . $phpMussel['FE']['DateTime'] . ' - "' . $User . '" - ' . $Message . "\n";
    $WriteMode = (!file_exists($phpMussel['Vault'] . $File) || (
        $phpMussel['Config']['general']['truncate'] > 0 &&
        filesize($phpMussel['Vault'] . $File) >= $phpMussel['ReadBytes']($phpMussel['Config']['general']['truncate'])
    )) ? 'w' : 'a';
    if ($phpMussel['BuildLogPath']($File)) {
        $Handle = fopen($phpMussel['Vault'] . $File, $WriteMode);
        fwrite($Handle, $Data);
        fclose($Handle);
        if ($WriteMode === 'w') {
            $phpMussel['LogRotation']($phpMussel['Config']['general']['FrontEndLog']);
        }
    }
};

/**
 * 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).
 */
$phpMussel['SendEmail'] = function ($Recipients = [], $Subject = '', $Body = '', $AltBody = '', $Attachments = []) use (&$phpMussel) {
    $EventLog = '';
    $EventLogData = '';

    /** Prepare event logging. */
    if ($phpMussel['Config']['PHPMailer']['EventLog']) {
        $EventLog = (strpos($phpMussel['Config']['PHPMailer']['EventLog'], '{') !== false) ? $phpMussel['TimeFormat'](
            $phpMussel['Time'],
            $phpMussel['Config']['PHPMailer']['EventLog']
        ) : $phpMussel['Config']['PHPMailer']['EventLog'];
        $EventLogData = ((
            $phpMussel['Config']['legal']['pseudonymise_ip_addresses']
        ) ? $phpMussel['Pseudonymise-IP']($_SERVER[$phpMussel['IPAddr']]) : $_SERVER[$phpMussel['IPAddr']]) . ' - ' . (
            isset($phpMussel['FE']['DateTime']) ? $phpMussel['FE']['DateTime'] : $phpMussel['TimeFormat'](
                $phpMussel['Time'],
                $phpMussel['Config']['general']['timeFormat']
            )
        ) . ' - ';
        $WriteMode = (!file_exists($phpMussel['Vault'] . $EventLog) || (
            $phpMussel['Config']['general']['truncate'] > 0 &&
            filesize($phpMussel['Vault'] . $EventLog) >= $phpMussel['ReadBytes']($phpMussel['Config']['general']['truncate'])
        )) ? 'w' : 'a';
    }

    /** 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')) {
        if ($EventLog) {
            $EventLogData .= $phpMussel['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 ($phpMussel['Config']['PHPMailer']['SkipAuthProcess']) {
                $Mail->SMTPOptions = ['ssl' => [
                    'verify_peer' => false,
                    'verify_peer_name' => false,
                    'allow_self_signed' => true
                ]];
            }

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

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

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

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

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

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

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

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

            /** 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. */
            if ($EventLog) {
                $EventLogData .= ($State ? sprintf(
                    $phpMussel['L10N']->getString('state_email_sent'),
                    $SuccessDetails
                ) : $phpMussel['L10N']->getString('response_error') . ' - ' . $Mail->ErrorInfo) . "\n";
            }

        } catch (\Exception $e) {

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

        }
    }

    /** Write to the event log. */
    if ($EventLog) {
        $Handle = fopen($phpMussel['Vault'] . $EventLog, $WriteMode);
        fwrite($Handle, $EventLogData);
        fclose($Handle);
        if ($WriteMode === 'w') {
            $phpMussel['LogRotation']($phpMussel['Config']['PHPMailer']['EventLog']);
        }
    }

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

/**
 * Generates very simple 8-digit numbers used for 2FA.
 *
 * @return int An 8-digit number.
 */
$phpMussel['2FA-Number'] = function () {
    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);
};

/**
 * 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.
 */
$phpMussel['ArrayToClickableList'] = function ($Arr = [], $DeleteKey = '', $Depth = 0, $ParentKey = '') use (&$phpMussel) {
    $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">' . $phpMussel['L10N']->getString('field_delete_file') . '</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 = $phpMussel['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) {
                    $phpMussel['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 .= $phpMussel['ArrayToClickableList']($Value, $DeleteKey, $Depth + 1, $Key);
            $Output .= '</ul>';
        } else {
            if ($Key === 'Time' && preg_match('~^\d+$~', $Value)) {
                $Key = $phpMussel['L10N']->getString('label_expires');
                $Value = $phpMussel['TimeFormat']($Value, $phpMussel['Config']['general']['timeFormat']);
            }
            $Class = ($Key === $phpMussel['L10N']->getString('field_size') || $Key === $phpMussel['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. */
$phpMussel['Message'] = function ($Message) use (&$phpMussel) {
    if (isset($phpMussel['FE']['state_msg'])) {
        if ($phpMussel['FE']['state_msg'] || substr($phpMussel['FE']['state_msg'], -6) !== '<br />') {
            $phpMussel['FE']['state_msg'] .= '<br />';
        }
        $phpMussel['FE']['state_msg'] .= $Message . '<br />';
    }
};

/**
 * Attempt to perform some simple formatting for the log data.
 *
 * @param string $In The log data to be formatted.
 */
$phpMussel['Formatter'] = function (&$In) {
    if (strpos($In, "<br />\n") === false) {
        $In = '<div class="fW">' . $In . '</div>';
        return;
    }
    if (strpos($In, "<br />\n<br />\n") !== false) {
        $Data = array_filter(explode("<br />\n<br />\n", $In));
        $SeparatorType = 0;
    } elseif (strpos($In, "\n&gt;") !== false) {
        $Data = preg_split("~(<br />\n(?!-|&gt;)[^\n]+)\n(?!-|&gt;)~i", $In, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        $SeparatorType = 1;
    } else {
        $Data = array_filter(explode("<br />\n", $In));
        $SeparatorType = 2;
    }
    $In = '';
    if ($SeparatorType === 1) {
        $Blocks = count($Data);
        for ($Block = 0; $Block < $Blocks; $Block += 2) {
            $Darken = empty($Darken);
            $In .= '<div class="h' . ($Darken ? 'B' : 'W') . ' hFd fW">' . $Data[$Block] . $Data[$Block + 1] . "\n</div>";
        }
        $In = '<div style="filter:saturate(60%)"><span class="s">' . $In . '</span></div>';
        return;
    }
    foreach ($Data as &$Block) {
        $Darken = empty($Darken);
        $Block = '<div class="h' . ($Darken ? 'B' : 'W') . ' hFd fW">' . $Block;
        $Block .= $SeparatorType === 0 ? "<br />\n<br />\n</div>" : "<br />\n</div>";
        if ($SeparatorType === 2) {
            $Block = preg_replace([
                '~(a\:\d+\:)\{~',
                '~("|\d);\}~',
                '~\:(\d+)~',
                '~\:"([^"]+)"~'
            ], [
                '\1<span class="txtRd">{</span>',
                '\1;<span class="txtRd">}</span>',
                ':<span class="txtGn">\1</span>',
                ':"<span class="txtBl">\1</span>"'
            ], $Block);
        }
    }
    $In = implode('', $Data);
    if ($SeparatorType === 2) {
        $In = '<div style="filter:saturate(60%)"><span class="txtOe">' . $In . '</span></div>';
    } else {
        $In = '<div style="filter:saturate(60%)"><span class="s">' . $In . '</span></div>';
    }
};
For more information send a message to info at phpclasses dot org.