File: vault/functions.php

Recommend this page to a friend!
  Classes of Caleb  >  CIDRAM  >  vault/functions.php  >  Download  
File: vault/functions.php
Role: Example script
Content type: text/plain
Description: Example script
Class: CIDRAM
Check if an IP address is a bad source of traffic
Author: By
Last change: Add an error handler event.

Changelog excerpt:

- Added an error handler event to be able to catch and report errors more
effectively.
Add an events orchestrator and refactor.
Date: 2 years ago
Size: 89,289 bytes
 

Contents

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

/** Autoloader for CIDRAM classes. */
spl_autoload_register(function ($Class) {
    $Vendor = (($Pos = strpos($Class, "\\", 1)) === false) ? '' : substr($Class, 0, $Pos);
    $File = __DIR__ . '/classes/' . ((!$Vendor || $Vendor === 'CIDRAM') ? '' : $Vendor . '/') . (
        (($Pos = strrpos($Class, "\\")) === false) ? $Class : substr($Class, $Pos + 1)
    ) . '.php';
    if (is_readable($File)) {
        require $File;
    }
});

/** Instantiate YAML object for accessing data reconstruction and processing various YAML files. */
$CIDRAM['YAML'] = new \Maikuolan\Common\YAML();

/** Instantiate events orchestrator in order to allow malleable logging and etc. */
$CIDRAM['Events'] = new \Maikuolan\Common\Events();

/**
 * Reads and returns the contents of files.
 *
 * @param string $File Path and filename of the file to read.
 * @return string The file's contents (an empty string on failure).
 */
$CIDRAM['ReadFile'] = function (string $File): string {
    if (!is_file($File) || !is_readable($File)) {
        return '';
    }
    /** Default blocksize (128KB). */
    static $Blocksize = 131072;
    $Filesize = filesize($File);
    $Size = ($Filesize && $Blocksize) ? ceil($Filesize / $Blocksize) : 0;
    $Data = '';
    if ($Size > 0) {
        $Handle = fopen($File, 'rb');
        $r = 0;
        while ($r < $Size) {
            $Data .= fread($Handle, $Blocksize);
            $r++;
        }
        fclose($Handle);
    }
    return $Data;
};

/**
 * Replaces encapsulated substrings within a string using the values of the
 * corresponding elements within an array.
 *
 * @param array $Needle An array containing replacement values.
 * @param string $Haystack The string to work with.
 * @return string The string with its encapsulated substrings replaced.
 */
$CIDRAM['ParseVars'] = function (array $Needle, string $Haystack): string {
    if (!is_array($Needle) || empty($Haystack)) {
        return '';
    }
    array_walk($Needle, function ($Value, $Key) use (&$Haystack) {
        if (!is_array($Value)) {
            $Haystack = str_replace('{' . $Key . '}', $Value, $Haystack);
        }
    });
    return $Haystack;
};

/**
 * Fetches instructions from the `ignore.dat` file.
 *
 * @return array An array listing the sections that CIDRAM should ignore.
 */
$CIDRAM['FetchIgnores'] = function () use (&$CIDRAM): array {
    $IgnoreMe = [];
    $IgnoreFile = $CIDRAM['ReadFile']($CIDRAM['Vault'] . 'ignore.dat');
    if (strpos($IgnoreFile, "\r")) {
        $IgnoreFile = (
            strpos($IgnoreFile, "\r\n") !== false
        ) ? str_replace("\r", '', $IgnoreFile) : str_replace("\r", "\n", $IgnoreFile);
    }
    $IgnoreFile = "\n" . $IgnoreFile . "\n";
    $PosB = -1;
    while (true) {
        $PosA = strpos($IgnoreFile, "\nIgnore ", ($PosB + 1));
        if ($PosA === false) {
            break;
        }
        $PosA += 8;
        if (!$PosB = strpos($IgnoreFile, "\n", $PosA)) {
            break;
        }
        $Tag = substr($IgnoreFile, $PosA, ($PosB - $PosA));
        if (strlen($Tag)) {
            $IgnoreMe[$Tag] = true;
        }
        $PosB--;
    }
    return $IgnoreMe;
};

/**
 * Tests whether $Addr is an IPv4 address, and if it is, expands its potential
 * factors (i.e., constructs an array containing the CIDRs that contain $Addr).
 * Returns false if $Addr is *not* an IPv4 address, and otherwise, returns the
 * contructed array.
 *
 * @param string $Addr Refer to the description above.
 * @param bool $ValidateOnly If true, just checks if the IP is valid only.
 * @param int $FactorLimit Maximum number of CIDRs to return (default: 32).
 * @return bool|array Refer to the description above.
 */
$CIDRAM['ExpandIPv4'] = function (string $Addr, bool $ValidateOnly = false, int $FactorLimit = 32) {
    if (!preg_match(
        '/^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$/i',
        $Addr,
        $Octets
    )) {
        return false;
    }
    if ($ValidateOnly) {
        return true;
    }
    $CIDRs = [];
    $Base = [0, 0, 0, 0];
    for ($Cycle = 0; $Cycle < 4; $Cycle++) {
        for ($Size = 128, $Step = 0; $Step < 8; $Step++, $Size /= 2) {
            $CIDR = $Step + ($Cycle * 8);
            $Base[$Cycle] = floor($Octets[$Cycle + 1] / $Size) * $Size;
            $CIDRs[$CIDR] = $Base[0] . '.' . $Base[1] . '.' . $Base[2] . '.' . $Base[3] . '/' . ($CIDR + 1);
            if ($CIDR >= $FactorLimit) {
                break 2;
            }
        }
    }
    return $CIDRs;
};

/**
 * Tests whether $Addr is an IPv6 address, and if it is, expands its potential
 * factors (i.e., constructs an array containing the CIDRs that contain $Addr).
 * Returns false if $Addr is *not* an IPv6 address, and otherwise, returns the
 * contructed array.
 *
 * @param string $Addr Refer to the description above.
 * @param bool $ValidateOnly If true, just checks if the IP is valid only.
 * @param int $FactorLimit Maximum number of CIDRs to return (default: 128).
 * @return bool|array Refer to the description above.
 */
$CIDRAM['ExpandIPv6'] = function (string $Addr, bool $ValidateOnly = false, int $FactorLimit = 128) {
    /**
     * The REGEX pattern used by this `preg_match` call was adapted from the
     * IPv6 REGEX pattern that can be found at
     * https://sroze.io/regex-ip-v4-et-ipv6-6cc005cabe8c
     */
    if (!preg_match(
        '/^(([\da-f]{1,4}\:){7}[\da-f]{1,4})|(([\da-f]{1,4}\:){6}\:[\da-f]{1' .
        ',4})|(([\da-f]{1,4}\:){5}\:([\da-f]{1,4}\:)?[\da-f]{1,4})|(([\da-f]' .
        '{1,4}\:){4}\:([\da-f]{1,4}\:){0,2}[\da-f]{1,4})|(([\da-f]{1,4}\:){3' .
        '}\:([\da-f]{1,4}\:){0,3}[\da-f]{1,4})|(([\da-f]{1,4}\:){2}\:([\da-f' .
        ']{1,4}\:){0,4}[\da-f]{1,4})|(([\da-f]{1,4}\:){6}((\b((25[0-5])|(1\d' .
        '{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)' .
        '|(\d{1,2}))\b))|(([\da-f]{1,4}\:){0,5}\:((\b((25[0-5])|(1\d{2})|(2[' .
        '0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2' .
        '}))\b))|(\:\:([\da-f]{1,4}\:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d' .
        ')|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)' .
        ')|([\da-f]{1,4}\:\:([\da-f]{1,4}\:){0,5}[\da-f]{1,4})|(\:\:([\da-f]' .
        '{1,4}\:){0,6}[\da-f]{1,4})|(([\da-f]{1,4}\:){1,7}\:)$/i',
    $Addr)) {
        return false;
    }
    if ($ValidateOnly) {
        return true;
    }
    $NAddr = $Addr;
    if (preg_match('/^\:\:/i', $NAddr)) {
        $NAddr = '0' . $NAddr;
    }
    if (preg_match('/\:\:$/i', $NAddr)) {
        $NAddr .= '0';
    }
    if (strpos($NAddr, '::') !== false) {
        $Key = 7 - substr_count($Addr, ':');
        $Arr = [':0:', ':0:0:', ':0:0:0:', ':0:0:0:0:', ':0:0:0:0:0:', ':0:0:0:0:0:0:'];
        if (!isset($Arr[$Key])) {
            return false;
        }
        $NAddr = str_replace('::', $Arr[$Key], $Addr);
        unset($Arr, $Key);
    }
    $NAddr = explode(':', $NAddr);
    if (count($NAddr) !== 8) {
        return false;
    }
    for ($i = 0; $i < 8; $i++) {
        $NAddr[$i] = hexdec($NAddr[$i]);
    }
    $CIDRs = [];
    $Base = [0, 0, 0, 0, 0, 0, 0, 0];
    for ($Cycle = 0; $Cycle < 8; $Cycle++) {
        for ($Size = 32768, $Step = 0; $Step < 16; $Step++, $Size /= 2) {
            $CIDR = $Step + ($Cycle * 16);
            $Base[$Cycle] = dechex(floor($NAddr[$Cycle] / $Size) * $Size);
            $CIDRs[$CIDR] = $Base[0] . ':' . $Base[1] . ':' . $Base[2] . ':' . $Base[3] . ':' . $Base[4] . ':' . $Base[5] . ':' . $Base[6] . ':' . $Base[7] . '/' . ($CIDR + 1);
            if ($CIDR >= $FactorLimit) {
                break 2;
            }
        }
    }
    if ($FactorLimit > 128) {
        $FactorLimit = 128;
    }
    for ($CIDR = 0; $CIDR < $FactorLimit; $CIDR++) {
        if (strpos($CIDRs[$CIDR], '::') !== false) {
            $CIDRs[$CIDR] = preg_replace('/(\:0)*\:\:(0\:)*/i', '::', $CIDRs[$CIDR], 1);
            $CIDRs[$CIDR] = str_replace('::0/', '::/', $CIDRs[$CIDR]);
            continue;
        }
        if (strpos($CIDRs[$CIDR], ':0:0/') !== false) {
            $CIDRs[$CIDR] = preg_replace('/(\:0){2,}\//i', '::/', $CIDRs[$CIDR], 1);
            continue;
        }
        if (strpos($CIDRs[$CIDR], ':0:0:') !== false) {
            $CIDRs[$CIDR] = preg_replace('/(\:0)+\:(0\:)+/i', '::', $CIDRs[$CIDR], 1);
            $CIDRs[$CIDR] = str_replace('::0/', '::/', $CIDRs[$CIDR]);
            continue;
        }
    }
    return $CIDRs;
};

/**
 * Gets tags from signature files.
 *
 * @param string $Haystack The haystack to search within for the target tag.
 * @param int $Offset The position to start searching from within the haystack.
 * @param string $Tag The tag we're trying to get.
 * @param string $DefTag The value to use when the target tag isn't found.
 * @return string The value of the tag we're trying to get, or of DefTag.
 */
$CIDRAM['Getter'] = function (string $Haystack, int $Offset, string $Tag, string $DefTag): string {
    $Key = "\n" . $Tag . ': ';
    $KeyLen = strlen($Key);
    return (
        ($PosX = strpos($Haystack, $Key, $Offset)) &&
        ($PosY = strpos($Haystack, "\n", $PosX + 1)) &&
        !substr_count($Haystack, "\n\n", $Offset, $PosX - $Offset + 1)
    ) ? substr($Haystack, $PosX + $KeyLen, $PosY - $PosX - $KeyLen) : $DefTag;
};

/**
 * Checks CIDRs (generally, potential factors expanded from IP addresses)
 * against the IPv4/IPv6 signature files, and if any matches are found,
 * increments `$CIDRAM['BlockInfo']['SignatureCount']`, and
 * appends to `$CIDRAM['BlockInfo']['ReasonMessage']`.
 *
 * @param array $Files Which IPv4/IPv6 signature files to check against.
 * @param array $Factors Which CIDRs/factors to check against.
 * @throws Exception if a triggered signature indicates a non-existent file to run.
 * @return bool Returns true.
 */
$CIDRAM['CheckFactors'] = function (array $Files, array $Factors) use (&$CIDRAM): bool {
    $Counts = [
        'Files' => count($Files),
        'Factors' => count($Factors)
    ];
    if (!isset($CIDRAM['FileCache'])) {
        $CIDRAM['FileCache'] = [];
    }
    for ($FileIndex = 0; $FileIndex < $Counts['Files']; $FileIndex++) {
        $Files[$FileIndex] = (
            strpos($Files[$FileIndex], ':') === false
        ) ? $Files[$FileIndex] : substr($Files[$FileIndex], strpos($Files[$FileIndex], ':') + 1);
        if (!$Files[$FileIndex]) {
            continue;
        }
        if ($Counts['Factors'] === 32) {
            $DefTag = $Files[$FileIndex] . '-IPv4';
        } elseif ($Counts['Factors'] === 128) {
            $DefTag = $Files[$FileIndex] . '-IPv6';
        } else {
            $DefTag = $Files[$FileIndex] . '-Unknown';
        }
        $FileExtension = strtolower(substr($Files[$FileIndex], -4));
        if (!isset($CIDRAM['FileCache'][$Files[$FileIndex]])) {
            $CIDRAM['FileCache'][$Files[$FileIndex]] = $CIDRAM['ReadFile']($CIDRAM['Vault'] . $Files[$FileIndex]);
        }
        if (!$Files[$FileIndex] = $CIDRAM['FileCache'][$Files[$FileIndex]]) {
            continue;
        }
        if (
            $FileExtension === '.csv' &&
            strpos($Files[$FileIndex], "\n") === false &&
            strpos($Files[$FileIndex], "\r") === false &&
            strpos($Files[$FileIndex], ",") !== false
        ) {
            $Files[$FileIndex] = ',' . $Files[$FileIndex] . ',';
            $SigFormat = 'CSV';
        } else {
            $SigFormat = 'DAT';
        }
        if ($Counts['Factors'] === 32) {
            if ($SigFormat === 'CSV') {
                $NoCIDR = ',' . substr($Factors[31], 0, -3) . ',';
                $LastCIDR = ',' . $Factors[31] . ',';
            } else {
                $NoCIDR = "\n" . substr($Factors[31], 0, -3) . ' ';
                $LastCIDR = "\n" . $Factors[31] . ' ';
            }
        } elseif ($Counts['Factors'] === 128) {
            if ($SigFormat === 'CSV') {
                $NoCIDR = ',' . substr($Factors[127], 0, -4) . ',';
                $LastCIDR = ',' . $Factors[127] . ',';
            } else {
                $NoCIDR = "\n" . substr($Factors[127], 0, -4) . ' ';
                $LastCIDR = "\n" . $Factors[127] . ' ';
            }
        }
        if (strpos($Files[$FileIndex], $NoCIDR) !== false) {
            $Files[$FileIndex] = str_replace($NoCIDR, $LastCIDR, $Files[$FileIndex]);
        }
        if ($SigFormat === 'CSV') {
            $LN = ' ("' . $DefTag . '", L0:F' . $FileIndex . ')';
            for ($FactorIndex = 0; $FactorIndex < $Counts['Factors']; $FactorIndex++) {
                if ($Infractions = substr_count($Files[$FileIndex], ',' . $Factors[$FactorIndex] . ',')) {
                    $CIDRAM['BlockInfo']['ReasonMessage'] = $CIDRAM['L10N']->getString('ReasonMessage_Generic');
                    if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
                        $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
                    }
                    $CIDRAM['BlockInfo']['WhyReason'] .= $CIDRAM['L10N']->getString('Short_Generic') . $LN;
                    if (!empty($CIDRAM['BlockInfo']['Signatures'])) {
                        $CIDRAM['BlockInfo']['Signatures'] .= ', ';
                    }
                    $CIDRAM['BlockInfo']['Signatures'] .= $Factors[$FactorIndex];
                    $CIDRAM['BlockInfo']['SignatureCount'] += $Infractions;
                }
            }
            continue;
        }
        if (strpos($Files[$FileIndex], "\r") !== false) {
            $Files[$FileIndex] =
                (strpos($Files[$FileIndex], "\r\n")) ?
                str_replace("\r", '', $Files[$FileIndex]) :
                str_replace("\r", "\n", $Files[$FileIndex]);
        }
        $Files[$FileIndex] = "\n" . $Files[$FileIndex] . "\n";
        for ($FactorIndex = 0; $FactorIndex < $Counts['Factors']; $FactorIndex++) {
            $PosB = -1;
            while (true) {
                $PosA = strpos($Files[$FileIndex], "\n" . $Factors[$FactorIndex] . ' ', ($PosB + 1));
                if ($PosA === false) {
                    break;
                }
                $PosA += strlen($Factors[$FactorIndex]) + 2;
                if (!$PosB = strpos($Files[$FileIndex], "\n", $PosA)) {
                    break;
                }
                if ($DefersTo = $CIDRAM['Getter']($Files[$FileIndex], $PosA, 'Defers to', '')) {
                    $DefersTo = preg_quote($DefersTo);
                    if (
                        preg_match('~(?:^|,)' . $DefersTo . '(?:$|,)~i', $CIDRAM['Config']['signatures']['ipv4']) ||
                        preg_match('~(?:^|,)' . $DefersTo . '(?:$|,)~i', $CIDRAM['Config']['signatures']['ipv6'])
                    ) {
                        continue;
                    }
                }
                if (
                    ($Expires = $CIDRAM['Getter']($Files[$FileIndex], $PosA, 'Expires', '')) &&
                    ($Expires = $CIDRAM['FetchExpires']($Expires)) &&
                    $Expires < $CIDRAM['Now']
                ) {
                    continue;
                }
                $Tag = $CIDRAM['Getter']($Files[$FileIndex], $PosA, 'Tag', $DefTag);
                if (!empty($CIDRAM['Ignore'][$Tag])) {
                    continue;
                }
                $Origin = (
                    $Origin = $CIDRAM['Getter']($Files[$FileIndex], $PosA, 'Origin', '')
                ) ? ', [' . $Origin . ']' : '';
                if (
                    ($PosX = strpos($Files[$FileIndex], "\n---\n", $PosA)) &&
                    ($PosY = strpos($Files[$FileIndex], "\n\n", ($PosX + 1))) &&
                    !substr_count($Files[$FileIndex], "\n\n", $PosA, ($PosX - $PosA + 1))
                ) {
                    if (!isset($YAML)) {
                        $YAML = new \Maikuolan\Common\YAML();
                    }
                    $YAML->process(substr($Files[$FileIndex], ($PosX + 5), ($PosY - $PosX - 5)), $CIDRAM['Config']);
                }
                $LN = ' ("' . $Tag . '", L' . substr_count($Files[$FileIndex], "\n", 0, $PosA) . ':F' . $FileIndex . $Origin . ')';
                $Signature = substr($Files[$FileIndex], $PosA, ($PosB - $PosA));
                if (!$Category = substr($Signature, 0, strpos($Signature, ' '))) {
                    $Category = $Signature;
                } else {
                    $Signature = substr($Signature, strpos($Signature, ' ') + 1);
                }
                $RunExitCode = 0;
                if ($Category === 'Run') {
                    if (!isset($CIDRAM['RunParamResCache'])) {
                        $CIDRAM['RunParamResCache'] = [];
                    }
                    if (isset($CIDRAM['RunParamResCache'][$Signature]) && is_object($CIDRAM['RunParamResCache'][$Signature])) {
                        $RunExitCode = $CIDRAM['RunParamResCache'][$Signature]($Factors, $FactorIndex, $LN, $Tag);
                    } else {
                        if (file_exists($CIDRAM['Vault'] . $Signature)) {
                            require_once $CIDRAM['Vault'] . $Signature;
                        } else {
                            throw new \Exception($CIDRAM['ParseVars'](
                                ['FileName' => $Signature],
                                '[CIDRAM] ' . $CIDRAM['L10N']->getString('Error_MissingRequire')
                            ));
                        }
                    }
                }
                if ($Category === 'Whitelist' || $RunExitCode === 3) {
                    $CIDRAM['ZeroOutBlockInfo'](true);
                    break 3;
                }
                if ($Category === 'Greylist' || $RunExitCode === 2) {
                    $CIDRAM['ZeroOutBlockInfo']();
                    break 2;
                }
                if ($Category === 'Deny') {
                    $DenyMatched = false;
                    foreach ([
                        ['Type' => 'Bogon', 'Config' => 'block_bogons', 'ReasonLong' => 'ReasonMessage_Bogon', 'ReasonShort' => 'Short_Bogon'],
                        ['Type' => 'Cloud', 'Config' => 'block_cloud', 'ReasonLong' => 'ReasonMessage_Cloud', 'ReasonShort' => 'Short_Cloud'],
                        ['Type' => 'Generic', 'Config' => 'block_generic', 'ReasonLong' => 'ReasonMessage_Generic', 'ReasonShort' => 'Short_Generic'],
                        ['Type' => 'Legal', 'Config' => 'block_legal', 'ReasonLong' => 'ReasonMessage_Legal', 'ReasonShort' => 'Short_Legal'],
                        ['Type' => 'Malware', 'Config' => 'block_malware', 'ReasonLong' => 'ReasonMessage_Malware', 'ReasonShort' => 'Short_Malware'],
                        ['Type' => 'Proxy', 'Config' => 'block_proxies', 'ReasonLong' => 'ReasonMessage_Proxy', 'ReasonShort' => 'Short_Proxy'],
                        ['Type' => 'Spam', 'Config' => 'block_spam', 'ReasonLong' => 'ReasonMessage_Spam', 'ReasonShort' => 'Short_Spam']
                    ] as $Params) {
                        if ($Signature === $Params['Type']) {
                            if (empty($CIDRAM['Config']['signatures'][$Params['Config']])) {
                                continue 2;
                            }
                            $CIDRAM['BlockInfo']['ReasonMessage'] = $CIDRAM['L10N']->getString($Params['ReasonLong']);
                            if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
                                $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
                            }
                            $CIDRAM['BlockInfo']['WhyReason'] .= $CIDRAM['L10N']->getString($Params['ReasonShort']) . $LN;
                            $DenyMatched = true;
                            break;
                        }
                    }
                    if (!$DenyMatched) {
                        $CIDRAM['BlockInfo']['ReasonMessage'] = $Signature;
                        if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
                            $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
                        }
                        $CIDRAM['BlockInfo']['WhyReason'] .= $Signature . $LN;
                    }
                    if (!empty($CIDRAM['BlockInfo']['Signatures'])) {
                        $CIDRAM['BlockInfo']['Signatures'] .= ', ';
                    }
                    $CIDRAM['BlockInfo']['Signatures'] .= $Factors[$FactorIndex];
                    $CIDRAM['BlockInfo']['SignatureCount']++;
                }
            }
        }
    }
    return true;
};

/**
 * Initialises all IPv4/IPv6 tests.
 *
 * @param string $Addr The IP address to check.
 * @param bool $Retain Specifies whether we need to retain factors for later.
 * @throws Exception if CheckFactors throws an exception.
 * @return bool Returns false if all tests fail, or true otherwise.
 */
$CIDRAM['RunTests'] = function (string $Addr, bool $Retain = false) use (&$CIDRAM): bool {
    if (!isset($CIDRAM['BlockInfo'])) {
        return false;
    }
    if (!isset($CIDRAM['Ignore'])) {
        $CIDRAM['Ignore'] = $CIDRAM['FetchIgnores']();
    }
    $CIDRAM['Whitelisted'] = false;
    $CIDRAM['LastTestIP'] = 0;
    if ($IPv4Factors = $CIDRAM['ExpandIPv4']($Addr)) {
        $IPv4Files = empty(
            $CIDRAM['Config']['signatures']['ipv4']
        ) ? [] : explode(',', $CIDRAM['Config']['signatures']['ipv4']);
        try {
            $IPv4Test = $CIDRAM['CheckFactors']($IPv4Files, $IPv4Factors);
        } catch (\Exception $e) {
            throw new \Exception($e->getMessage());
        }
        if ($IPv4Test) {
            $CIDRAM['LastTestIP'] = 4;
            if ($Retain) {
                $CIDRAM['Factors'] = $IPv4Factors;
            }
        }
    } else {
        $IPv4Test = false;
    }
    if ($IPv6Factors = $CIDRAM['ExpandIPv6']($Addr)) {
        $IPv6Files = empty(
            $CIDRAM['Config']['signatures']['ipv6']
        ) ? [] : explode(',', $CIDRAM['Config']['signatures']['ipv6']);
        try {
            $IPv6Test = $CIDRAM['CheckFactors']($IPv6Files, $IPv6Factors);
        } catch (\Exception $e) {
            throw new \Exception($e->getMessage());
        }
        if ($IPv6Test) {
            $CIDRAM['LastTestIP'] = 6;
            if ($Retain) {
                $CIDRAM['Factors'] = $IPv6Factors;
            }
        }
    } else {
        $IPv6Test = false;
    }
    return ($IPv4Test || $IPv6Test);
};

/**
 * Zeros out blockinfo and optionally sets the whitelisted flag.
 *
 * @param bool $Whitelist Whether to set the whitelisted flag.
 */
$CIDRAM['ZeroOutBlockInfo'] = function (bool $Whitelist = false) use (&$CIDRAM) {
    $CIDRAM['BlockInfo']['Signatures'] = '';
    $CIDRAM['BlockInfo']['ReasonMessage'] = '';
    $CIDRAM['BlockInfo']['WhyReason'] = '';
    $CIDRAM['BlockInfo']['SignatureCount'] = 0;
    if ($Whitelist) {
        $CIDRAM['Whitelisted'] = true;
    }
};

/**
 * Reduces code duplicity (the contained code used by multiple parts of the
 * script for dealing with expiry tags).
 *
 * @param string $in Expiry tag.
 * @return int|bool A unix timestamp representing the expiry tag, or false if
 *      the expiry tag doesn't contain a valid ISO 8601 date/time.
 */
$CIDRAM['FetchExpires'] = function (string $in) {
    static $CommonPart = '([12]\d{3})(?:\xe2\x88\x92|[\x2d-\x2f\x5c])?(0[1-9]|1[0-2])(?:\xe2\x88\x92|[\x2d-\x2f\x5c])?(0[1-9]|[1-2]\d|3[01])';
    if (
        preg_match('/^' . $CommonPart . '\x20?T?([01]\d|2[0-3])[\x2d\x2e\x3a]?([0-5]\d)[\x2d\x2e\x3a]?([0-5]\d)$/i', $in, $Arr) ||
        preg_match('/^' . $CommonPart . '\x20?T?([01]\d|2[0-3])[\x2d\x2e\x3a]?([0-5]\d)$/i', $in, $Arr) ||
        preg_match('/^' . $CommonPart . '\x20?T?([01]\d|2[0-3])$/i', $in, $Arr) ||
        preg_match('/^' . $CommonPart . '$/i', $in, $Arr) ||
        preg_match('/^([12]\d{3})(?:\xe2\x88\x92|[\x2d-\x2f\x5c])?(0[1-9]|1[0-2])$/i', $in, $Arr) ||
        preg_match('/^([12]\d{3})$/i', $in, $Arr)
    ) {
        $Arr = [
            (int)$Arr[1],
            isset($Arr[2]) ? (int)$Arr[2] : 1,
            isset($Arr[3]) ? (int)$Arr[3] : 1,
            isset($Arr[4]) ? (int)$Arr[4] : 0,
            isset($Arr[5]) ? (int)$Arr[5] : 0,
            isset($Arr[6]) ? (int)$Arr[6] : 0
        ];
        $Expires = mktime($Arr[3], $Arr[4], $Arr[5], $Arr[1], $Arr[2], $Arr[0]);
        return $Expires ?: false;
    }
    return false;
};

/**
 * A simple closure for replacing date/time placeholders with corresponding
 * date/time information. Used by the logfiles and some timestamps.
 *
 * @param int $Time A unix timestamp.
 * @param string|array $In An input or an array of inputs to manipulate.
 * @return string|array The adjusted input(/s).
 */
$CIDRAM['TimeFormat'] = function (int $Time, $In) use (&$CIDRAM) {
    $Time = date('dmYHisDMP', $Time);
    $values = [
        'dd' => substr($Time, 0, 2),
        'mm' => substr($Time, 2, 2),
        'yyyy' => substr($Time, 4, 4),
        'yy' => substr($Time, 6, 2),
        'hh' => substr($Time, 8, 2),
        'ii' => substr($Time, 10, 2),
        'ss' => substr($Time, 12, 2),
        'Day' => substr($Time, 14, 3),
        'Mon' => substr($Time, 17, 3),
        'tz' => substr($Time, 20, 3) . substr($Time, 24, 2),
        't:z' => substr($Time, 20, 6)
    ];
    $values['d'] = (int)$values['dd'];
    $values['m'] = (int)$values['mm'];
    return is_array($In) ? array_map(function ($Item) use (&$values, &$CIDRAM) {
        return $CIDRAM['ParseVars']($values, $Item);
    }, $In) : $CIDRAM['ParseVars']($values, $In);
};

/**
 * Fix incorrect typecasting for some for some variables that sometimes default
 * to strings instead of booleans or integers.
 *
 * @param mixed $Var The variable to fix (passed by reference).
 * @param string $Type The type (or pseudo-type) to cast the variable to.
 */
$CIDRAM['AutoType'] = function (&$Var, string $Type = '') use (&$CIDRAM) {
    if (in_array($Type, ['string', 'timezone', 'checkbox', 'url', 'email'], true)) {
        $Var = (string)$Var;
    } elseif ($Type === 'int') {
        $Var = (int)$Var;
    } elseif ($Type === 'float') {
        $Var = (float)$Var;
    } elseif ($Type === 'bool') {
        $Var = (strtolower($Var) !== 'false' && $Var);
    } elseif ($Type === 'kb') {
        $Var = $CIDRAM['ReadBytes']((string)$Var, 1);
    } else {
        $LVar = strtolower($Var);
        if ($LVar === 'true') {
            $Var = true;
        } elseif ($LVar === 'false') {
            $Var = false;
        } elseif ($Var !== true && $Var !== false) {
            $Var = (int)$Var;
        }
    }
};

/**
 * Performs fallbacks and autotyping for missing configuration directives.
 *
 * @param array $Fallbacks Fallback source.
 * @param array $Config Configuration source.
 */
$CIDRAM['Fallback'] = function (array $Fallbacks, array &$Config) use (&$CIDRAM) {
    foreach ($Fallbacks as $KeyCat => $DCat) {
        if (!isset($Config[$KeyCat])) {
            $Config[$KeyCat] = [];
        }
        if (isset($Cat)) {
            unset($Cat);
        }
        $Cat = &$Config[$KeyCat];
        if (!is_array($DCat)) {
            continue;
        }
        foreach ($DCat as $DKey => $DData) {
            if (!isset($Cat[$DKey]) && isset($DData['default'])) {
                $Cat[$DKey] = $DData['default'];
            }
            if (isset($Dir)) {
                unset($Dir);
            }
            $Dir = &$Cat[$DKey];
            if (isset($DData['type'])) {
                $CIDRAM['AutoType']($Dir, $DData['type']);
            }
        }
    }
};

/**
 * Check for supplementary configuration.
 *
 * @param string $Source The directive or CSV that we're checking from.
 * @return array An array of valid supplementary configuration sources.
 */
$CIDRAM['Supplementary'] = function (string $Source) use (&$CIDRAM): array {
    $Out = [];
    $Source = explode(',', $Source);
    foreach ($Source as $File) {
        if (($DecPos = strpos($File, '.')) === false) {
            continue;
        }
        $File = substr($File, 0, $DecPos) . '.yaml';
        if (file_exists($CIDRAM['Vault'] . $File)) {
            $Out[] = $File;
        }
    }
    return $Out;
};

/**
 * Used to send cURL requests.
 *
 * @param string $URI The resource to request.
 * @param array $Params An optional associative array of key-value pairs to
 *      send with the request.
 * @param int $Timeout An optional timeout limit.
 * @param array $Headers An optional array of headers to send with the request.
 * @param int $Depth Recursion depth of the current closure instance.
 * @return string The results of the request, or an empty string upon failure.
 */
$CIDRAM['Request'] = function (string $URI, array $Params = [], int $Timeout = -1, array $Headers = [], int $Depth = 0) use (&$CIDRAM): string {

    /** Fetch channel information. */
    if (!isset($CIDRAM['Channels'])) {
        $CIDRAM['Channels'] = (
            $Channels = $CIDRAM['ReadFile']($CIDRAM['Vault'] . 'channels.yaml')
        ) ? (new \Maikuolan\Common\YAML($Channels))->Data : [];
        if (!isset($CIDRAM['Channels']['Triggers'])) {
            $CIDRAM['Channels']['Triggers'] = [];
        }
    }

    /** Test channel triggers. */
    foreach ($CIDRAM['Channels']['Triggers'] as $TriggerName => $TriggerURI) {
        if (
            !isset($CIDRAM['Channels'][$TriggerName]) ||
            !is_array($CIDRAM['Channels'][$TriggerName]) ||
            substr($URI, 0, strlen($TriggerURI)) !== $TriggerURI
        ) {
            continue;
        }
        foreach ($CIDRAM['Channels'][$TriggerName] as $Channel => $Options) {
            if (!is_array($Options) || !isset($Options[$TriggerName])) {
                continue;
            }
            $Len = strlen($Options[$TriggerName]);
            if (substr($URI, 0, $Len) !== $Options[$TriggerName]) {
                continue;
            }
            unset($Options[$TriggerName]);
            if (empty($Options) || $CIDRAM['in_csv'](key($Options), $CIDRAM['Config']['general']['disabled_channels'])) {
                continue;
            }
            $AlternateURI = current($Options) . substr($URI, $Len);
            break;
        }
        if ($CIDRAM['in_csv']($TriggerName, $CIDRAM['Config']['general']['disabled_channels'])) {
            if (isset($AlternateURI)) {
                return $CIDRAM['Request']($AlternateURI, $Params, $Timeout, $Headers, $Depth);
            }
            return '';
        }
        break;
    }

    /** Initialise the cURL session. */
    $Request = curl_init($URI);

    $LCURI = strtolower($URI);
    $SSL = (substr($LCURI, 0, 6) === 'https:');

    curl_setopt($Request, CURLOPT_FRESH_CONNECT, true);
    curl_setopt($Request, CURLOPT_HEADER, false);
    if (empty($Params)) {
        curl_setopt($Request, CURLOPT_POST, false);
    } else {
        curl_setopt($Request, CURLOPT_POST, true);
        curl_setopt($Request, CURLOPT_POSTFIELDS, $Params);
    }
    if ($SSL) {
        curl_setopt($Request, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
        curl_setopt($Request, CURLOPT_SSL_VERIFYPEER, false);
    }
    curl_setopt($Request, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($Request, CURLOPT_MAXREDIRS, 1);
    curl_setopt($Request, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($Request, CURLOPT_TIMEOUT, ($Timeout > 0 ? $Timeout : $CIDRAM['Timeout']));
    curl_setopt($Request, CURLOPT_USERAGENT, $CIDRAM['ScriptUA']);
    curl_setopt($Request, CURLOPT_HTTPHEADER, $Headers ?: []);

    /** Execute and get the response. */
    $Response = curl_exec($Request);

    /** Check for problems (e.g., resource not found, server errors, etc). */
    if (($Info = curl_getinfo($Request)) && is_array($Info) && isset($Info['http_code'])) {

        /** Most recent HTTP code flag. */
        $CIDRAM['Most-Recent-HTTP-Code'] = $Info['http_code'];

        /** Request failed. Try again using an alternative address. */
        if ($Info['http_code'] >= 400 && isset($AlternateURI) && $Depth < 3) {
            curl_close($Request);
            return $CIDRAM['Request']($AlternateURI, $Params, $Timeout, $Headers, $Depth + 1);
        }

    } else {
        /** Most recent HTTP code flag. */
        $CIDRAM['Most-Recent-HTTP-Code'] = 200;
    }

    /** Close the cURL session. */
    curl_close($Request);

    /** Return the results of the request. */
    return $Response;

};

/**
 * Performs reverse DNS lookups for IP addresses, to resolve their hostnames.
 * This is functionally equivalent to the in-built "gethostbyaddr" PHP
 * function, but with the added benefit of being able to specify which DNS
 * servers to use for lookups, and of being able to enforce timeouts, which
 * should help to avoid some of the problems normally encountered by using
 * "gethostbyaddr".
 *
 * @param string $Addr The IP address to look up.
 * @param string $DNS An optional, comma delimited list of DNS servers to use.
 * @param int $Timeout The timeout limit (optional; defaults to 5 seconds).
 * @return string The hostname on success, or the IP address on failure.
 */
$CIDRAM['DNS-Reverse'] = function (string $Addr, string $DNS = '', int $Timeout = 5) use (&$CIDRAM): string {

    /** Shouldn't try to reverse localhost addresses; There'll be problems. */
    if ($Addr === '127.0.0.1' || $Addr === '::1') {
        return 'localhost';
    }

    /** We've already got it cached. We can return the results early. */
    if (isset($CIDRAM['DNS-Reverses'][$Addr]['Host'])) {
        return $CIDRAM['DNS-Reverses'][$Addr]['Host'];
    }

    /** The IP address is IPv4. */
    if (strpos($Addr, '.') !== false && strpos($Addr, ':') === false && preg_match(
        '/^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])' .
        '\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$/i',
    $Addr, $Octets)) {
        $Lookup =
            chr(strlen($Octets[4])) . $Octets[4] .
            chr(strlen($Octets[3])) . $Octets[3] .
            chr(strlen($Octets[2])) . $Octets[2] .
            chr(strlen($Octets[1])) . $Octets[1] .
            "\7in-addr\4arpa\0\0\x0c\0\1";
    }

    /** The IP address is IPv6. */
    elseif (strpos($Addr, '.') === false && strpos($Addr, ':') !== false && $CIDRAM['ExpandIPv6']($Addr, true)) {
        $Lookup = $Addr;
        if (strpos($Addr, '::') !== false) {
            $Repeat = 8 - substr_count($Addr, ':');
            $Lookup = str_replace('::', str_repeat(':0', ($Repeat < 1 ? 0 : $Repeat)) . ':', $Lookup);
        }
        while (strlen($Lookup) < 39) {
            $Lookup = preg_replace(
                ['/^:/', '/:$/', '/^([\da-f]{1,3}):/i', '/:([\da-f]{1,3})$/i', '/:([\da-f]{1,3}):/i'],
                ['0:', ':0', '0\1:', ':0\1', ':0\1:'],
                $Lookup
            );
        }
        $Lookup = strrev(preg_replace(['/\:/', '/(.)/'], ['', "\\1\1"], $Lookup)) . "\3ip6\4arpa\0\0\x0c\0\1";
    }

    /** The IP address is.. wrong. Let's exit the closure. */
    else {
        return $Addr;
    }

    /** Sending UDP is usually pointless if we're not on root. */
    if (!isset($CIDRAM['Root'])) {
        $CIDRAM['Root'] = (!function_exists('posix_getuid') || posix_getuid() === 0);
    }

    /** Use gethostbyaddr if enabled and if we anticipate UDP failing. */
    if (!$CIDRAM['Root'] && $CIDRAM['Config']['general']['allow_gethostbyaddr_lookup']) {
        return $CIDRAM['DNS-Reverse-Fallback']($Addr);
    }

    /** Some safety mechanisms. */
    if (!isset($CIDRAM['_allow_url_fopen'])) {
        $CIDRAM['_allow_url_fopen'] = ini_get('allow_url_fopen');
        $CIDRAM['_allow_url_fopen'] = !(!$CIDRAM['_allow_url_fopen'] || $CIDRAM['_allow_url_fopen'] == 'Off');
    }
    if (!$CIDRAM['Root'] || empty($Lookup) || !function_exists('fsockopen') || !$CIDRAM['_allow_url_fopen']) {
        return $Addr;
    }

    $CIDRAM['DNS-Reverses'][$Addr] = ['Host' => $Addr, 'Time' => $CIDRAM['Now'] + 21600];
    $CIDRAM['DNS-Reverses-Modified'] = true;

    /** DNS is disabled. Let's exit the closure. */
    if (!$DNS && !$DNS = $CIDRAM['Config']['general']['default_dns']) {
        return ($CIDRAM['Config']['general']['allow_gethostbyaddr_lookup']) ? $CIDRAM['DNS-Reverse-Fallback']($Addr) : $Addr;
    }

    /** Expand list of lookup servers. */
    $DNS = explode(',', $DNS);

    /** UDP padding. */
    $LeftPad = str_pad(rand(0, 99), 2, '0', STR_PAD_LEFT) . "\1\0\0\1\0\0\0\0\0\0";

    /** Perform the lookup. */
    foreach ($DNS as $Server) {
        if (!empty($Response) || !$Server) {
            break;
        }

        $Handle = fsockopen('udp://' . $Server, 53);
        if ($Handle !== false) {
            fwrite($Handle, $LeftPad . $Lookup);
            stream_set_timeout($Handle, $Timeout);
            stream_set_blocking($Handle, true);
            $Response = fread($Handle, 1024);
            fclose($Handle);
        }
    }

    /** No response, or failed lookup. Let's exit the closure. */
    if (empty($Response)) {
        return (
            $CIDRAM['Config']['general']['allow_gethostbyaddr_lookup']
        ) ? $CIDRAM['DNS-Reverse-Fallback']($Addr) : ($CIDRAM['DNS-Reverses'][$Addr]['Host'] = $Addr);
    }

    /** We got a response! Now let's process it accordingly. */
    $Host = '';
    if (($Pos = strpos($Response, $Lookup)) !== false) {
        $Pos += strlen($Lookup) + 12;
        while (($Byte = substr($Response, $Pos, 1)) && $Byte !== "\0") {
            if ($Host) {
                $Host .= '.';
            }
            $Len = hexdec(bin2hex($Byte));
            $Host .= substr($Response, $Pos + 1, $Len);
            $Pos += 1 + $Len;
        }
    }

    /** Return results. */
    return $CIDRAM['DNS-Reverses'][$Addr]['Host'] = preg_replace('/[^:\da-z._~-]/i', '', $Host) ?: $Addr;

};

/** Aliases for "DNS-Reverse". */
$CIDRAM['DNS-Reverse-IPv4'] = $CIDRAM['DNS-Reverse-IPv6'] = $CIDRAM['DNS-Reverse'];

/**
 * Fallback for failed lookups.
 *
 * @param string $Addr The IP address to look up.
 * @return string The results of gethostbyaddr(), or the IP address verbatim.
 */
$CIDRAM['DNS-Reverse-Fallback'] = function (string $Addr) use (&$CIDRAM): string {
    $CIDRAM['DNS-Reverses'][$Addr] = ['Host' => $Addr, 'Time' => $CIDRAM['Now'] + 21600];
    $CIDRAM['DNS-Reverses-Modified'] = true;

    /** Return results. */
    return $CIDRAM['DNS-Reverses'][$Addr]['Host'] = preg_replace('/[^:\da-z._~-]/i', '', gethostbyaddr($Addr)) ?: $Addr;

};

/**
 * Performs forward DNS lookups for hostnames, to resolve their IP address.
 * This is functionally equivalent to the in-built PHP function
 * "gethostbyname", but with the added benefits of having IPv6 support and of
 * being able to enforce timeout limits, which should help to avoid some of the
 * problems normally associated with using "gethostbyname").
 *
 * @param string $Host The hostname to look up.
 * @param int $Timeout The timeout limit (optional; defaults to 5 seconds).
 * @return string The IP address on success, or an empty string on failure.
 */
$CIDRAM['DNS-Resolve'] = function (string $Host, int $Timeout = 5) use (&$CIDRAM) {
    if (isset($CIDRAM['DNS-Forwards'][$Host]['IPAddr'])) {
        return $CIDRAM['DNS-Forwards'][$Host]['IPAddr'];
    }
    $Host = urlencode($Host);
    if (($HostLen = strlen($Host)) > 253) {
        return '';
    }
    static $Valid = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~';

    $CIDRAM['DNS-Forwards'][$Host] = ['IPAddr' => '', 'Time' => $CIDRAM['Now'] + 21600];
    $CIDRAM['DNS-Forwards-Modified'] = true;

    $URI = 'https://dns.google.com/resolve?name=' . urlencode($Host) . '&random_padding=';
    $PadLen = 204 - $HostLen;
    if ($PadLen < 1) {
        $PadLen = 972 - $HostLen;
    }
    while ($PadLen > 0) {
        $PadLen--;
        $URI .= str_shuffle($Valid)[0];
    }

    if (!$Results = json_decode($CIDRAM['Request']($URI, [], $Timeout), true)) {
        return '';
    }
    return $CIDRAM['DNS-Forwards'][$Host]['IPAddr'] = empty(
        $Results['Answer'][0]['data']
    ) ? '' : preg_replace('/[^\da-f.:]/i', '', $Results['Answer'][0]['data']);
};

/**
 * Used to identify when bots ghost/masquerade as popular search engines and
 * social media tools. Tracking is disabled for legitimate requests, while
 * ghosted/faked requests are blocked. If DNS is unresolvable and/or if a bot's
 * identity can't be verified, no action is taken (i.e., tracking isn't messed
 * with and the request isn't blocked).
 *
 * @param string|array $Domains Accepted domain/hostname partials.
 * @param string $Friendly A friendly name to use in logfiles.
 * @param array $Options Various options that can be passed to the closure.
 * @return bool Returns true when a determination is successfully made, and
 *      false when a determination isn't able to be made.
 */
$CIDRAM['DNS-Reverse-Forward'] = function ($Domains, string $Friendly, array $Options = []) use (&$CIDRAM): bool {

    /** Fetch the hostname. */
    if (empty($CIDRAM['Hostname'])) {
        $CIDRAM['Hostname'] = $CIDRAM['DNS-Reverse']($CIDRAM['BlockInfo']['IPAddr']);
    }

    /** Do nothing more if we weren't able to resolve the DNS hostname. */
    if (!$CIDRAM['Hostname'] || $CIDRAM['Hostname'] === $CIDRAM['BlockInfo']['IPAddr']) {
        return false;
    }

    /** Flag for whether our checks pass or fail. */
    $Pass = false;

    /** Force domains to be an array. */
    $CIDRAM['Arrayify']($Domains);

    /** Compare the hostname against the accepted domain/hostname partials. */
    foreach ($Domains as $Domain) {
        $Len = strlen($Domain) * -1;
        if (substr($CIDRAM['Hostname'], $Len) === $Domain) {
            $Pass = true;
            break;
        }
    }

    /** Successfully passed. */
    if ($Pass) {

        /** We're only reversing; Don't forward resolve. Disable tracking and return. */
        if (!empty($Options['ReverseOnly'])) {
            if (!empty($Options['CanModTrackable'])) {
                $CIDRAM['Trackable'] = false;
            }
            return true;
        }

        /** Attempt to resolve. */
        if (!$Resolved = $CIDRAM['DNS-Resolve']($CIDRAM['Hostname'])) {
            /** Failed to resolve. Do nothing and return. */
            return false;
        }

        /** It's the real deal; Disable tracking and return. */
        if ($Resolved === $CIDRAM['BlockInfo']['IPAddr']) {
            if (!empty($Options['CanModTrackable'])) {
                $CIDRAM['Trackable'] = false;
            }
            return true;
        }

    }

    /** It's a fake; Block it. */
    $Reason = $CIDRAM['ParseVars'](['ua' => $Friendly], $CIDRAM['L10N']->getString('fake_ua'));
    $CIDRAM['BlockInfo']['ReasonMessage'] = $Reason;
    if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
        $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
    }
    $CIDRAM['BlockInfo']['WhyReason'] .= $Reason;
    if (!empty($CIDRAM['BlockInfo']['Signatures'])) {
        $CIDRAM['BlockInfo']['Signatures'] .= ', ';
    }
    $Debug = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
    $CIDRAM['BlockInfo']['Signatures'] .= basename($Debug['file']) . ':L' . $Debug['line'];
    $CIDRAM['BlockInfo']['SignatureCount']++;

    /** Reporting. */
    $CIDRAM['Reporter']->report([19], ['Caught masquerading as ' . $Friendly . '.'], $CIDRAM['BlockInfo']['IPAddr']);

    /** Exit. */
    return true;

};

/**
 * Checks whether an IP is expected. If so, tracking is disabled for the IP
 * being checked, and if not, the request is blocked. Has no return value.
 *
 * @param string|array $Expected Accepted/Expected IPs.
 * @param string $Friendly A friendly name to use in logfiles.
 * @param array $Options Various options that can be passed to the closure.
 */
$CIDRAM['UA-IP-Match'] = function ($Expected, string $Friendly, array $Options = []) use (&$CIDRAM) {

    /** Convert expected IPs to an array. */
    $CIDRAM['Arrayify']($Expected);

    /** Compare the actual IP of the request against the expected IPs. */
    if (in_array($CIDRAM['BlockInfo']['IPAddr'], $Expected)) {
        /** Disable tracking (if there are matches, and if relevant). */
        if (!empty($Options['CanModTrackable'])) {
            $CIDRAM['Trackable'] = false;
        }
        return;
    }

    /** Nothing matched. Block it. */
    $Reason = $CIDRAM['ParseVars'](['ua' => $Friendly], $CIDRAM['L10N']->getString('fake_ua'));
    $CIDRAM['BlockInfo']['ReasonMessage'] = $Reason;
    if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
        $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
    }
    $CIDRAM['BlockInfo']['WhyReason'] .= $Reason;
    if (!empty($CIDRAM['BlockInfo']['Signatures'])) {
        $CIDRAM['BlockInfo']['Signatures'] .= ', ';
    }
    $Debug = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
    $CIDRAM['BlockInfo']['Signatures'] .= basename($Debug['file']) . ':L' . $Debug['line'];
    $CIDRAM['BlockInfo']['SignatureCount']++;

    /** Reporting. */
    $CIDRAM['Reporter']->report([19], ['Caught masquerading as ' . $Friendly . '.'], $CIDRAM['BlockInfo']['IPAddr']);

};

/**
 * A default closure for handling signature triggers within module files.
 *
 * @param bool $Condition Include any variable or PHP code which can be
 *      evaluated for truthiness. Truthiness is evaluated, and if true, the
 *      signature is "triggered". If false, the signature is *not* "triggered".
 * @param string $ReasonShort Cited in the "Why Blocked" field when the
 *      signature is triggered and thus included within logfile entries.
 * @param string $ReasonLong Message displayed to the user/client when blocked,
 *      to explain why they've been blocked. Optional. Defaults to the standard
 *      "Access Denied!" message.
 * @param array $DefineOptions An optional array containing key/value pairs,
 *      used to define configuration options specific to the request instance.
 *      Configuration options will be applied when the signature is triggered.
 * @return bool Returns true if the signature was triggered, and false if it
 *      wasn't. Should correspond to the truthiness of $Condition.
 */
$CIDRAM['Trigger'] = function ($Condition, $ReasonShort, $ReasonLong = '', array $DefineOptions = []) use (&$CIDRAM) {
    if (!$Condition) {
        return false;
    }
    if (!$ReasonLong) {
        $ReasonLong = $CIDRAM['L10N']->getString('denied');
    }
    if (is_array($DefineOptions) && !empty($DefineOptions)) {
        foreach ($DefineOptions as $CatKey => $CatValue) {
            if (is_array($CatValue) && !empty($CatValue)) {
                foreach ($CatValue as $OptionKey => $OptionValue) {
                    $CIDRAM['Config'][$CatKey][$OptionKey] = $OptionValue;
                }
            }
        }
    }
    $CIDRAM['BlockInfo']['ReasonMessage'] = $ReasonLong;
    if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
        $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
    }
    $CIDRAM['BlockInfo']['WhyReason'] .= $ReasonShort;
    if (!empty($CIDRAM['BlockInfo']['Signatures'])) {
        $CIDRAM['BlockInfo']['Signatures'] .= ', ';
    }
    $Debug = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
    $CIDRAM['BlockInfo']['Signatures'] .= basename($Debug['file']) . ':L' . $Debug['line'];
    $CIDRAM['BlockInfo']['SignatureCount']++;
    return true;
};

/**
 * A default closure for handling signature bypasses within module files.
 *
 * @param bool $Condition Include any variable or PHP code which can be
 *      evaluated for truthiness. Truthiness is evaluated, and if true, the
 *      bypass is "triggered". If false, the bypass is *not* "triggered".
 * @param string $ReasonShort Cited in the "Why Blocked" field when the
 *      bypass is triggered (included within logfile entries if there are still
 *      other preexisting signatures which have otherwise been triggered).
 * @param array $DefineOptions An optional array containing key/value pairs,
 *      used to define configuration options specific to the request instance.
 *      Configuration options will be applied when the bypass is triggered.
 * @return bool Returns true if the bypass was triggered, and false if it
 *      wasn't. Should correspond to the truthiness of $Condition.
 */
$CIDRAM['Bypass'] = function ($Condition, $ReasonShort, array $DefineOptions = []) use (&$CIDRAM) {
    if (!$Condition) {
        return false;
    }
    if (is_array($DefineOptions) && !empty($DefineOptions)) {
        foreach ($DefineOptions as $CatKey => $CatValue) {
            if (is_array($CatValue) && !empty($CatValue)) {
                foreach ($CatValue as $OptionKey => $OptionValue) {
                    $CIDRAM['Config'][$CatKey][$OptionKey] = $OptionValue;
                }
            }
        }
    }
    if (!empty($CIDRAM['BlockInfo']['WhyReason'])) {
        $CIDRAM['BlockInfo']['WhyReason'] .= ', ';
    }
    $CIDRAM['BlockInfo']['WhyReason'] .= $ReasonShort;
    if (!empty($CIDRAM['BlockInfo']['Signatures'])) {
        $CIDRAM['BlockInfo']['Signatures'] .= ', ';
    }
    $Debug = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
    $CIDRAM['BlockInfo']['Signatures'] .= basename($Debug['file']) . ':L' . $Debug['line'];
    $CIDRAM['BlockInfo']['SignatureCount']--;
    return true;
};

/**
 * Used to generate new salts when necessary, which may be occasionally used by
 * some specific optional peripheral features (note: should not be considered
 * cryptographically secure; especially so for versions of PHP < 7).
 *
 * @return string Salt.
 */
$CIDRAM['GenerateSalt'] = function (): string {
    static $MinLen = 32;
    static $MaxLen = 72;
    static $MinChr = 1;
    static $MaxChr = 255;
    $Salt = '';
    if (function_exists('random_int')) {
        try {
            $Length = random_int($MinLen, $MaxLen);
        } catch (\Exception $e) {
            $Length = rand($MinLen, $MaxLen);
        }
    } else {
        $Length = rand($MinLen, $MaxLen);
    }
    if (function_exists('random_bytes')) {
        try {
            $Salt = random_bytes($Length);
        } catch (\Exception $e) {
            $Salt = '';
        }
    }
    if (empty($Salt)) {
        if (function_exists('random_int')) {
            try {
                for ($Index = 0; $Index < $Length; $Index++) {
                    $Salt .= chr(random_int($MinChr, $MaxChr));
                }
            } catch (\Exception $e) {
                $Salt = '';
                for ($Index = 0; $Index < $Length; $Index++) {
                    $Salt .= chr(rand($MinChr, $MaxChr));
                }
            }
        } else {
            for ($Index = 0; $Index < $Length; $Index++) {
                $Salt .= chr(rand($MinChr, $MaxChr));
            }
        }
    }
    return $Salt;
};

/**
 * Meld together two or more strings by padding to equal length and
 * bitshifting each by each other.
 *
 * @return string The melded string.
 */
$CIDRAM['Meld'] = function (string ...$Strings): string {
    $StrLens = array_map('strlen', $Strings);
    $WalkLen = max($StrLens);
    $Count = count($Strings);
    for ($Index = 0; $Index < $Count; $Index++) {
        if ($StrLens[$Index] < $WalkLen) {
            $Strings[$Index] = str_pad($Strings[$Index], $WalkLen, "\xff");
        }
    }
    for ($Lt = $Strings[0], $Index = 1, $Meld = ''; $Index < $Count; $Index++, $Meld = '') {
        $Rt = $Strings[$Index];
        for ($Caret = 0; $Caret < $WalkLen; $Caret++) {
            $Meld .= $Lt[$Caret] ^ $Rt[$Caret];
        }
        $Lt = $Meld;
    }
    $Meld = $Lt;
    return $Meld;
};

/**
 * Clears expired entries from a list.
 *
 * @param string $List The list to clear from.
 * @param bool $Check A flag indicating when changes have occurred.
 */
$CIDRAM['ClearExpired'] = function (string &$List, bool &$Check) use (&$CIDRAM) {
    if ($List) {
        $End = 0;
        while (true) {
            $Begin = $End;
            if (!$End = strpos($List, "\n", $Begin + 1)) {
                break;
            }
            $Line = substr($List, $Begin, $End - $Begin);
            if ($Split = strrpos($Line, ',')) {
                $Expiry = (int)substr($Line, $Split + 1);
                if ($Expiry < $CIDRAM['Now']) {
                    $List = str_replace($Line, '', $List);
                    $End = 0;
                    $Check = true;
                }
            }
        }
    }
};

/**
 * If input isn't an array, make it so. Remove empty elements.
 *
 * @param mixed $Input
 */
$CIDRAM['Arrayify'] = function (&$Input) {
    if (!is_array($Input)) {
        $Input = [$Input];
    }
    $Input = array_filter($Input);
};

/**
 * Read byte value configuration directives as byte values.
 *
 * @param string $In Input.
 * @param int $Mode Operating mode. 0 for true byte values, 1 for validating.
 *      Default is 0.
 * @return string|int Output (return type depends on operating mode).
 */
$CIDRAM['ReadBytes'] = function (string $In, int $Mode = 0) {
    if (preg_match('/[KMGT][oB]$/i', $In)) {
        $Unit = substr($In, -2, 1);
    } elseif (preg_match('/[KMGToB]$/i', $In)) {
        $Unit = substr($In, -1);
    }
    $Unit = isset($Unit) ? strtoupper($Unit) : 'K';
    $In = (float)$In;
    if ($Mode === 1) {
        return $Unit === 'B' || $Unit === 'o' ? $In . 'B' : $In . $Unit . 'B';
    }
    $Multiply = ['K' => 1024, 'M' => 1048576, 'G' => 1073741824, 'T' => 1099511627776];
    return (int)floor($In * (isset($Multiply[$Unit]) ? $Multiply[$Unit] : 1));
};

/**
 * Add to page output and block event logfile fields.
 *
 * @param string $FieldName Name of the field for internal use (e.g., logging).
 * @param string $ClientFieldName Name of the field for external use (e.g., for
 *      showing to the client when they see the Access Denied page).
 * @param string $FieldData Data for the field.
 * @param bool $Sanitise Whether the data needs to be sanitised against XSS
 *      attacks.
 */
$CIDRAM['AddField'] = function (string $FieldName, string $ClientFieldName, string $FieldData, bool $Sanitise = false) use (&$CIDRAM) {
    $Prepared = $Sanitise ? str_replace(
        ['<', '>', "\r", "\n"],
        ['&lt;', '&gt;', '&#13;', '&#10;'],
        $FieldData
    ) : $FieldData;
    $Logged = $CIDRAM['Config']['general']['log_sanitisation'] ? $Prepared : $FieldData;
    $CIDRAM['FieldTemplates']['Logs'] .= $FieldName . $Logged . "\n";
    $CIDRAM['FieldTemplates']['Output'][] = sprintf(
        '<span class="textLabel"%s>%s</span>%s<br />',
        $CIDRAM['L10N-Lang-Attache'],
        $ClientFieldName,
        $Prepared
    );
};

/**
 * Resolves 6to4, Teredo, ISATAP addresses and etc to their IPv4 counterparts.
 *
 * @param string $In An IPv6 address.
 * @return string An IPv4 address, or an empty string upon failure to resolve.
 */
$CIDRAM['Resolve6to4'] = function (string $In): string {
    if (!preg_match('~^(?:200[12]|fe80)\:~i', $In)) {
        return '';
    }
    $Parts = explode(':', $In, 8);

    /** 6to4. */
    if ($Parts[0] === '2002') {
        if (empty($Parts[1]) || empty($Parts[2]) || preg_match('~[^\da-f]~i', $Parts[1]) || preg_match('~[^\da-f]~i', $Parts[2])) {
            return '';
        }
        $Parts[1] = hexdec($Parts[1]) ?: 0;
        $Parts[2] = hexdec($Parts[2]) ?: 0;
        $Octets = [0 => floor($Parts[1] / 256), 1 => $Parts[1] % 256, 2 => floor($Parts[2] / 256), 3 => $Parts[2] % 256];
        return implode('.', $Octets);
    }

    /** Teredo. */
    if ($Parts[0] === '2001' && empty($Parts[1])) {
        $Parts = array_reverse($Parts);
        $Bits = ($Parts[1] ?: '') . str_pad(($Parts[0] ?: ''), 4, '0', STR_PAD_LEFT);
        if (preg_match('~[^\da-f]~i', $Bits)) {
            return '';
        }
        $Bits = hexdec($Bits) ^ 0xffffffff;
        $Octets = [0 => 0, 1 => 0, 2 => 0, 3 => $Bits % 256];
        $Octets[2] = ($Bits = floor($Bits / 256)) % 256;
        $Octets[1] = ($Bits = floor($Bits / 256)) % 256;
        $Octets[0] = floor($Bits / 256);
        return implode('.', $Octets);
    }

    /** ISATAP. */
    if (preg_match('~^fe80\:\:(?:0200\:)?5efe\:([\da-f]{1,4})\:([\da-f]{1,4})$~i', $In, $Parts)) {
        $Parts[1] = hexdec($Parts[1]) ?: 0;
        $Parts[2] = hexdec($Parts[2]) ?: 0;
        $Octets = [0 => floor($Parts[1] / 256), 1 => $Parts[1] % 256, 2 => floor($Parts[2] / 256), 3 => $Parts[2] % 256];
        return implode('.', $Octets);
    }

    return '';
};

/** Initialise cache. */
$CIDRAM['InitialiseCache'] = function () use (&$CIDRAM) {

    /** Create new cache object. */
    $CIDRAM['Cache'] = new \Maikuolan\Common\Cache();
    $CIDRAM['Cache']->EnableAPCu = $CIDRAM['Config']['supplementary_cache_options']['enable_apcu'];
    $CIDRAM['Cache']->EnableMemcached = $CIDRAM['Config']['supplementary_cache_options']['enable_memcached'];
    $CIDRAM['Cache']->EnableRedis = $CIDRAM['Config']['supplementary_cache_options']['enable_redis'];
    $CIDRAM['Cache']->EnablePDO = $CIDRAM['Config']['supplementary_cache_options']['enable_pdo'];
    $CIDRAM['Cache']->MemcachedHost = $CIDRAM['Config']['supplementary_cache_options']['memcached_host'];
    $CIDRAM['Cache']->MemcachedPort = $CIDRAM['Config']['supplementary_cache_options']['memcached_port'];
    $CIDRAM['Cache']->RedisHost = $CIDRAM['Config']['supplementary_cache_options']['redis_host'];
    $CIDRAM['Cache']->RedisPort = $CIDRAM['Config']['supplementary_cache_options']['redis_port'];
    $CIDRAM['Cache']->RedisTimeout = $CIDRAM['Config']['supplementary_cache_options']['redis_timeout'];
    $CIDRAM['Cache']->PDOdsn = $CIDRAM['Config']['supplementary_cache_options']['pdo_dsn'];
    $CIDRAM['Cache']->PDOusername = $CIDRAM['Config']['supplementary_cache_options']['pdo_username'];
    $CIDRAM['Cache']->PDOpassword = $CIDRAM['Config']['supplementary_cache_options']['pdo_password'];
    $CIDRAM['Cache']->FFDefault = $CIDRAM['Vault'] . 'cache.dat';

    if (!$CIDRAM['Cache']->connect()) {
        if ($CIDRAM['Cache']->Using === 'FF') {
            header('Content-Type: text/plain');
            die('[CIDRAM] ' . $CIDRAM['L10N']->getString('Error_WriteCache'));
        } else {
            $Status = $CIDRAM['GetStatusHTTP'](503);
            header('HTTP/1.0 503 ' . $Status);
            header('HTTP/1.1 503 ' . $Status);
            header('Status: 503 ' . $Status);
            header('Retry-After: 3600');
            die;
        }
    }

    $CIDRAM['AtCacheDestroyUnset'] = [];
    $CIDRAM['InitialiseCacheSection']('Tracking');
    $CIDRAM['InitialiseCacheSection']('DNS-Forwards');
    $CIDRAM['InitialiseCacheSection']('DNS-Reverses');

};

/**
 * Initialise a cache section.
 *
 * @param string $SectionName The name of the cache section.
 */
$CIDRAM['InitialiseCacheSection'] = function (string $SectionName) use (&$CIDRAM) {

    /** Safety. */
    if (empty($SectionName) || !is_string($SectionName) || isset($CIDRAM[$SectionName], $CIDRAM[$SectionName . '-Modified'])) {
        return;
    }

    /** Mark for unsetting at cache destruction. */
    $CIDRAM['AtCacheDestroyUnset'][] = $SectionName;

    /** Section modified flag. */
    $CIDRAM[$SectionName . '-Modified'] = false;

    /** Fetch currently stored and clear expired entries. */
    if ($CIDRAM[$SectionName] = $CIDRAM['Cache']->getEntry($SectionName)) {
        if ($CIDRAM['Cache']->clearExpired($CIDRAM[$SectionName])) {
            $CIDRAM[$SectionName . '-Modified'] = true;
        }
    }

    /** Set default empty array. */
    if ($CIDRAM[$SectionName] === false) {
        $CIDRAM[$SectionName] = [];
        $CIDRAM[$SectionName . '-Modified'] = true;
    }

};

/** Destroy cache object and some related values. */
$CIDRAM['DestroyCacheObject'] = function () use (&$CIDRAM) {
    foreach ($CIDRAM['AtCacheDestroyUnset'] as $DestroyThis) {
        if ($CIDRAM[$DestroyThis . '-Modified']) {
            $CIDRAM['Cache']->setEntry($DestroyThis, $CIDRAM[$DestroyThis], 0);
        }
        unset($CIDRAM[$DestroyThis . '-Modified'], $CIDRAM[$DestroyThis]);
    }
    unset($CIDRAM['AtCacheDestroyUnset'], $CIDRAM['Cache']);
};

/**
 * Block bots masquerading as popular search engines and disable tracking for
 * for verified requests.
 */
$CIDRAM['SearchEngineVerification'] = function () use (&$CIDRAM) {
    if (
        empty($CIDRAM['TestResults']) ||
        $CIDRAM['Config']['general']['maintenance_mode'] ||
        !$CIDRAM['Config']['general']['search_engine_verification'] ||
        !empty($CIDRAM['SkipVerification']) ||
        empty($CIDRAM['BlockInfo']['UALC'])
    ) {
        return;
    }
    if (!isset($CIDRAM['VerificationData'])) {
        if (!$Raw = $CIDRAM['ReadFile']($CIDRAM['Vault'] . 'verification.yaml')) {
            $CIDRAM['SkipVerification'] = true;
            return;
        }
        $CIDRAM['VerificationData'] = (new \Maikuolan\Common\YAML($Raw))->Data;
    }
    if (empty($CIDRAM['VerificationData']['Search Engine Verification'])) {
        return;
    }
    foreach ($CIDRAM['VerificationData']['Search Engine Verification'] as $Name => $Values) {
        if (empty($CIDRAM[$Values['Bypass Flag']]) && (
            (!empty($Values['User Agent']) && strpos($CIDRAM['BlockInfo']['UALC'], $Values['User Agent']) !== false) ||
            (!empty($Values['User Agent Pattern']) && preg_match($Values['User Agent Pattern'], $CIDRAM['BlockInfo']['UALC']))
        )) {
            $Options = [
                'ReverseOnly' => isset($Values['Reverse Only']) ? $Values['Reverse Only'] : false,
                'CanModTrackable' => isset($Values['Can Modify Trackable']) ? $Values['Can Modify Trackable'] : true
            ];
            $CIDRAM[$Values['Closure']]($Values['Valid Domains'], $Name, $Options);
        }
    }
};

/** Reset bypass flags. */
$CIDRAM['ResetBypassFlags'] = function () use (&$CIDRAM) {
    if (isset($CIDRAM['VerificationData']['Search Engine Verification'])) {
        foreach ($CIDRAM['VerificationData']['Search Engine Verification'] as $Values) {
            if (!empty($Values['Bypass Flag'])) {
                $CIDRAM[$Values['Bypass Flag']] = false;
            }
        }
    }
};

/**
 * Block bots masquerading as popular social media tools.
 */
$CIDRAM['SocialMediaVerification'] = function () use (&$CIDRAM) {
    if (
        empty($CIDRAM['TestResults']) ||
        $CIDRAM['Config']['general']['maintenance_mode'] ||
        !$CIDRAM['Config']['general']['social_media_verification'] ||
        !empty($CIDRAM['SkipVerification']) ||
        empty($CIDRAM['BlockInfo']['UALC'])
    ) {
        return;
    }
    if (!isset($CIDRAM['VerificationData'])) {
        if (!$Raw = $CIDRAM['ReadFile']($CIDRAM['Vault'] . 'verification.yaml')) {
            $CIDRAM['SkipVerification'] = true;
            return;
        }
        $CIDRAM['VerificationData'] = (new \Maikuolan\Common\YAML($Raw))->Data;
    }
    if (empty($CIDRAM['VerificationData']['Social Media Verification'])) {
        return;
    }
    foreach ($CIDRAM['VerificationData']['Social Media Verification'] as $Name => $Values) {
        if (
            (!empty($Values['User Agent']) && strpos($CIDRAM['BlockInfo']['UALC'], $Values['User Agent']) !== false) ||
            (!empty($Values['User Agent Pattern']) && preg_match($Values['User Agent Pattern'], $CIDRAM['BlockInfo']['UALC']))
        ) {
            $Options = [
                'ReverseOnly' => isset($Values['Reverse Only']) ? $Values['Reverse Only'] : false,
                'CanModTrackable' => isset($Values['Can Modify Trackable']) ? $Values['Can Modify Trackable'] : true
            ];
            $CIDRAM[$Values['Closure']]($Values['Valid Domains'], $Name, $Options);
        }
    }
};

/**
 * Build directory path for logfiles.
 *
 * @param string $File The file we're building for.
 * @return bool True on success; False on failure.
 */
$CIDRAM['BuildLogPath'] = function (string $File) use (&$CIDRAM): bool {
    $ThisPath = $CIDRAM['Vault'];
    $File = str_replace("\\", '/', $File);
    while (strpos($File, '/') !== false) {
        $Dir = substr($File, 0, strpos($File, '/'));
        $ThisPath .= $Dir . '/';
        $File = substr($File, strlen($Dir) + 1);
        if (!file_exists($ThisPath) || !is_dir($ThisPath)) {
            if (!mkdir($ThisPath)) {
                return false;
            }
        }
    }
    return true;
};

/**
 * Checks whether the specified directory is empty.
 *
 * @param string $Directory The directory to check.
 * @return bool True if empty; False if not empty.
 */
$CIDRAM['IsDirEmpty'] = function (string $Directory): bool {
    return !((new \FilesystemIterator($Directory))->valid());
};

/**
 * Deletes empty directories (used by some front-end functions and log rotation).
 *
 * @param string $Dir The directory to delete.
 */
$CIDRAM['DeleteDirectory'] = function (string $Dir) use (&$CIDRAM) {
    while (strrpos($Dir, '/') !== false || strrpos($Dir, "\\") !== false) {
        $Separator = (strrpos($Dir, '/') !== false) ? '/' : "\\";
        $Dir = substr($Dir, 0, strrpos($Dir, $Separator));
        if (!is_dir($CIDRAM['Vault'] . $Dir) || !$CIDRAM['IsDirEmpty']($CIDRAM['Vault'] . $Dir)) {
            break;
        }
        rmdir($CIDRAM['Vault'] . $Dir);
    }
};

/**
 * Convert log file configuration directives to regular expressions.
 *
 * @param string $Str The log file configuration directive to work with.
 * @param bool $GZ Whether to include GZ files in the resulting expression.
 * @return string A corresponding regular expression.
 */
$CIDRAM['BuildLogPattern'] = function (string $Str, bool $GZ = false): string {
    return '~^' . preg_replace(
        ['~\\\{(?:dd|mm|yy|hh|ii|ss)\\\}~i', '~\\\{yyyy\\\}~i', '~\\\{(?:Day|Mon)\\\}~i', '~\\\{tz\\\}~i', '~\\\{t\\\:z\\\}~i'],
        ['\d{2}', '\d{4}', '\w{3}', '.{1,2}\d{4}', '.{1,2}\d{2}\:\d{2}'],
        preg_quote(str_replace("\\", '/', $Str))
    ) . ($GZ ? '(?:\.gz)?' : '') . '$~i';
};

/**
 * GZ-compress a file (used by log rotation).
 *
 * @param string $File The file to GZ-compress.
 * @return bool True if the file exists and is readable; False otherwise.
 */
$CIDRAM['GZCompressFile'] = function (string $File): bool {
    if (!is_file($File) || !is_readable($File)) {
        return false;
    }
    static $Blocksize = 131072;
    $Filesize = filesize($File);
    $Size = ($Filesize && $Blocksize) ? ceil($Filesize / $Blocksize) : 0;
    if ($Size > 0) {
        $Handle = fopen($File, 'rb');
        $HandleGZ = gzopen($File . '.gz', 'wb');
        $Block = 0;
        while ($Block < $Size) {
            $Data = fread($Handle, $Blocksize);
            gzwrite($HandleGZ, $Data);
            $Block++;
        }
        gzclose($HandleGZ);
        fclose($Handle);
    }
    return true;
};

/**
 * Log rotation.
 *
 * @param string $Pattern What to identify logfiles by (should be supplied via the relevant logging directive).
 * @return bool False when log rotation is disabled or errors occur; True otherwise.
 */
$CIDRAM['LogRotation'] = function (string $Pattern) use (&$CIDRAM): bool {
    $Action = empty($CIDRAM['Config']['general']['log_rotation_action']) ? '' : $CIDRAM['Config']['general']['log_rotation_action'];
    $Limit = empty($CIDRAM['Config']['general']['log_rotation_limit']) ? 0 : $CIDRAM['Config']['general']['log_rotation_limit'];
    if (!$Limit || ($Action !== 'Delete' && $Action !== 'Archive')) {
        return false;
    }
    $Pattern = $CIDRAM['BuildLogPattern']($Pattern);
    $Arr = [];
    $Offset = strlen($CIDRAM['Vault']);
    $List = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($CIDRAM['Vault']), RecursiveIteratorIterator::SELF_FIRST);
    foreach ($List as $Item => $List) {
        $ItemFixed = str_replace("\\", '/', substr($Item, $Offset));
        if ($ItemFixed && preg_match($Pattern, $ItemFixed) && is_readable($Item)) {
            $Arr[$ItemFixed] = filemtime($Item);
        }
    }
    unset($ItemFixed, $List, $Offset);
    $Count = count($Arr);
    $Err = 0;
    if ($Count > $Limit) {
        asort($Arr, SORT_NUMERIC);
        foreach ($Arr as $Item => $Modified) {
            if ($Action === 'Archive') {
                $Err += !$CIDRAM['GZCompressFile']($CIDRAM['Vault'] . $Item);
            }
            $Err += !unlink($CIDRAM['Vault'] . $Item);
            if (strpos($Item, '/') !== false) {
                $CIDRAM['DeleteDirectory']($Item);
            }
            $Count--;
            if (!($Count > $Limit)) {
                break;
            }
        }
    }
    return $Err ? false : true;
};

/**
 * Pseudonymise an IP address (reduce IPv4s to /24s and IPv6s to /32s).
 *
 * @param string $IP An IP address.
 * @return string A pseudonymised IP address.
 */
$CIDRAM['Pseudonymise-IP'] = function (string $IP): string {
    if (($CPos = strpos($IP, ':')) !== false) {
        $Parts = [(substr($IP, 0, $CPos) ?: ''), (substr($IP, $CPos +1) ?: '')];
        if (($CPos = strpos($Parts[1], ':')) !== false) {
            $Parts[1] = substr($Parts[1], 0, $CPos) ?: '';
        }
        $Parts = $Parts[0] . ':' . $Parts[1] . '::x';
        return str_replace(':::', '::', $Parts);
    }
    return preg_replace(
        '/^([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])\.([01]?\d{1,2}|2[0-4]\d|25[0-5])$/i',
        '\1.\2.\3.x',
        $IP
    );
};

/**
 * Fetch a status message from a HTTP status code for blocked requests.
 *
 * @param int $Status HTTP status code.
 * @return string HTTP status message (empty when using non-supported codes).
 */
$CIDRAM['GetStatusHTTP'] = function (int $Status): string {
    $Message = [
        301 => 'Moved Permanently',
        403 => 'Forbidden',
        410 => 'Gone',
        418 => 'I\'m a teapot',
        429 => 'Too Many Requests',
        451 => 'Unavailable For Legal Reasons',
        503 => 'Service Unavailable'
    ];
    return isset($Message[$Status]) ? $Message[$Status] : '';
};

/**
 * Used for matching auxiliary rule criteria.
 *
 * @param string|array $Criteria The criteria to accept for the match.
 * @param string $Actual The actual value we're trying to match.
 * @param string $Method The method for handling data when matching.
 * @return bool Match succeeded (true) or failed (false).
 */
$CIDRAM['AuxMatch'] = function ($Criteria, $Actual, $Method = '') use (&$CIDRAM) {

    /** Normalise criteria to an array. */
    if (!is_array($Criteria)) {
        $Criteria = [$Criteria];
    }

    /** Perform a match using regular expressions. */
    if ($Method === 'RegEx') {
        foreach ($Criteria as $TestCase) {
            if (preg_match($TestCase, $Actual)) {
                return true;
            }
        }
        return false;
    }

    /** Perform a match using Windows-style wildcards. */
    if ($Method === 'WinEx') {
        foreach ($Criteria as $TestCase) {
            if (preg_match('~^' . str_replace('\*', '.*', preg_quote($TestCase, '~')) . '$~', $Actual)) {
                return true;
            }
        }
        return false;
    }

    /** Perform a match using direct string comparison. */
    foreach ($Criteria as $TestCase) {
        if ($TestCase === $Actual) {
            return true;
        }
    }

    /** Failed to match anything. */
    return false;

};

/**
 * Used for performing actions when an auxiliary rule matches.
 *
 * @param string $Action The type of action to perform.
 * @param string $Name The name of the rule.
 * @param string $Reason The reason for performing the action.
 * @return bool Whether the calling parent should return immediately.
 */
$CIDRAM['AuxAction'] = function ($Action, $Name, $Reason = '') use (&$CIDRAM) {

    /** Whitelist. */
    if ($Action === 'Whitelist') {
        $CIDRAM['ZeroOutBlockInfo'](true);
        return true;
    }

    /** Greylist. */
    if ($Action === 'Greylist') {
        $CIDRAM['ZeroOutBlockInfo']();
    }

    /** Block. */
    elseif ($Action === 'Block') {
        $CIDRAM['Trigger'](true, $Name, $Reason);
    }

    /** Bypass. */
    elseif ($Action === 'Bypass') {
        $CIDRAM['Bypass'](true, $Name);
    }

    /** Don't log the request instance. */
    elseif ($Action === 'Don\'t log') {
        $CIDRAM['Flag Don\'t Log'] = true;
    }

    /** Exit. */
    return false;

};

/** Procedure for parsing and processing auxiliary rules. */
$CIDRAM['Aux'] = function () use (&$CIDRAM) {

    /** Exit procedure early if the rules don't exist. */
    if (!file_exists($CIDRAM['Vault'] . 'auxiliary.yaml')) {
        return;
    }

    /** Possibly used by some rules, but not used elsewhere. */
    if (!isset($CIDRAM['Request_Method'])) {
        $CIDRAM['Request_Method'] = $_SERVER['REQUEST_METHOD'] ?? '';
    }

    /** Potential sources. */
    static $Sources = [
        'Hostname',
        'Request_Method',
        'BlockInfo' => [
            'IPAddr',
            'IPAddrResolved',
            'Query',
            'Referrer',
            'UA',
            'UALC',
            'ReasonMessage',
            'SignatureCount',
            'Signatures',
            'WhyReason',
            'rURI'
        ]
    ];

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

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

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

        /** Matching logic. */
        $Logic = empty($Data['Logic']) ? 'Any' : $Data['Logic'];

        /** Detailed reason. */
        $Reason = empty($Data['Reason']) ? $Name : $Data['Reason'];

        /** The matching method to use. */
        $Method = empty($Data['Method']) ? '' : $Data['Method'];

        /** Iterate through modes. */
        foreach ($Modes as $Mode) {

            /** Skip mode if not used by this rule. */
            if (empty($Data[$Mode])) {
                continue;
            }

            /** Flag for successful matches. */
            $Matched = false;

            /** Match exceptions. */
            if (!empty($Data[$Mode]['But not if matches'])) {

                /** Iterate through sources. */
                foreach ($Sources as $SourceKey => $SourceArr) {
                    if (is_array($SourceArr)) {
                        foreach ($SourceArr as $Source) {
                            if (isset(
                                $Data[$Mode]['But not if matches'][$Source],
                                $CIDRAM[$SourceKey][$Source]
                            )) {
                                if (!is_array($Data[$Mode]['But not if matches'][$Source])) {
                                    $Data[$Mode]['But not if matches'][$Source] = [$Data[$Mode]['But not if matches'][$Source]];
                                }
                                foreach ($Data[$Mode]['But not if matches'][$Source] as $Value) {
                                    /** Perform match. */
                                    if ($CIDRAM['AuxMatch']($Value, $CIDRAM[$SourceKey][$Source], $Method)) {
                                        continue 4;
                                    }
                                }
                            }
                        }
                        continue;
                    }
                    if (isset($Data[$Mode]['But not if matches'][$SourceArr], $CIDRAM[$SourceArr])) {
                        if (!is_array($Data[$Mode]['But not if matches'][$SourceArr])) {
                            $Data[$Mode]['But not if matches'][$SourceArr] = [$Data[$Mode]['But not if matches'][$SourceArr]];
                        }
                        foreach ($Data[$Mode]['But not if matches'][$SourceArr] as $Value) {
                            /** Perform match. */
                            if ($CIDRAM['AuxMatch']($Value, $CIDRAM[$SourceArr], $Method)) {
                                continue 3;
                            }
                        }
                    }
                }

            }

            /** Matches. */
            if (!empty($Data[$Mode]['If matches'])) {

                /** Iterate through sources. */
                foreach ($Sources as $SourceKey => $SourceArr) {
                    if (is_array($SourceArr)) {
                        foreach ($SourceArr as $Source) {
                            if (isset(
                                $Data[$Mode]['If matches'][$Source],
                                $CIDRAM[$SourceKey][$Source]
                            )) {
                                if (!is_array($Data[$Mode]['If matches'][$Source])) {
                                    $Data[$Mode]['If matches'][$Source] = [$Data[$Mode]['If matches'][$Source]];
                                }
                                foreach ($Data[$Mode]['If matches'][$Source] as $Value) {
                                    /** Perform match. */
                                    if ($CIDRAM['AuxMatch']($Value, $CIDRAM[$SourceKey][$Source], $Method)) {
                                        $Matched = true;
                                        if ($Logic === 'All') {
                                            continue;
                                        }
                                        if ($CIDRAM['AuxAction']($Mode, $Name, $Reason)) {
                                            return;
                                        }
                                        continue 4;
                                    } elseif ($Logic === 'All') {
                                        continue 4;
                                    }
                                }
                            }
                        }
                        continue;
                    }
                    if (isset($Data[$Mode]['If matches'][$SourceArr], $CIDRAM[$SourceArr])) {
                        if (!is_array($Data[$Mode]['If matches'][$SourceArr])) {
                            $Data[$Mode]['If matches'][$SourceArr] = [$Data[$Mode]['If matches'][$SourceArr]];
                        }
                        foreach ($Data[$Mode]['If matches'][$SourceArr] as $Value) {
                            /** Perform match. */
                            if ($CIDRAM['AuxMatch']($Value, $CIDRAM[$SourceArr], $Method)) {
                                $Matched = true;
                                if ($Logic === 'All') {
                                    continue;
                                }
                                if ($CIDRAM['AuxAction']($Mode, $Name, $Reason)) {
                                    return;
                                }
                                continue 3;
                            } elseif ($Logic === 'All') {
                                continue 3;
                            }
                        }
                    }
                }

            }

            /** Perform action for matching rules requiring all conditions to be met. */
            if ($Logic === 'All' && $Matched && $CIDRAM['AuxAction']($Mode, $Name, $Reason)) {
                return;
            }

        }

    }

};

/**
 * Write an access event to the rate limiting cache.
 *
 * @param string $RL_Capture What we've captured to identify the requesting entity.
 * @param int $RL_Size The size of the output served to the requesting entity.
 */
$CIDRAM['RL_WriteEvent'] = function ($RL_Capture, $RL_Size) use (&$CIDRAM) {
    $TimePacked = pack('l*', $CIDRAM['Now']);
    $SizePacked = pack('l*', $RL_Size);
    $Data = $TimePacked . $SizePacked . $RL_Capture;
    $Handle = fopen($CIDRAM['Vault'] . 'rl.dat', 'ab');
    fwrite($Handle, $Data);
    fclose($Handle);
};

/** Remove outdated access events from the rate limiting cache. */
$CIDRAM['RL_Clean'] = function () use (&$CIDRAM) {
    $Pos = 0;
    $EoS = strlen($CIDRAM['RL_Data']);
    while ($Pos < $EoS) {
        $Time = substr($CIDRAM['RL_Data'], $Pos, 4);
        if (strlen($Time) !== 4) {
            break;
        }
        $Time = unpack('l*', $Time);
        if ($Time[1] > $CIDRAM['RL_Expired']) {
            break;
        }
        $Pos += 8;
        $Block = substr($CIDRAM['RL_Data'], $Pos, 4);
        if (strlen($Block) !== 4) {
            $CIDRAM['RL_Data'] = '';
            break;
        }
        $Block = unpack('l*', $Block);
        $Pos += 4 + $Block[1];
    }
    if ($Pos) {
        if ($CIDRAM['RL_Data']) {
            $CIDRAM['RL_Data'] = substr($CIDRAM['RL_Data'], $Pos);
        }
        $Handle = fopen($CIDRAM['Vault'] . 'rl.dat', 'wb');
        fwrite($Handle, $CIDRAM['RL_Data']);
        fclose($Handle);
    }
};

/**
 * Count the requesting entity's requests and bandwidth usage for this period.
 *
 * @return int The requesting entity's requests and bandwidth usage for this period.
 */
$CIDRAM['RL_Get_Usage'] = function () use (&$CIDRAM) {
    $Pos = 0;
    $Bytes = 0;
    $Requests = 0;
    while (strlen($CIDRAM['RL_Data']) > $Pos && $Pos = strpos($CIDRAM['RL_Data'], $CIDRAM['RL_Capture'], $Pos + 1)) {
        if ($Pos === false) {
            break;
        }
        $Size = substr($CIDRAM['RL_Data'], $Pos - 4, 4);
        if (strlen($Size) !== 4) {
            break;
        }
        $Size = unpack('l*', $Size);
        $Bytes += $Size[1];
        $Requests++;
    }
    return ['Bytes' => $Bytes, 'Requests' => $Requests];
};

/**
 * Checks for a value within CSV.
 *
 * @param string $Value The value to look for.
 * @param string $CSV The CSV to look in.
 * @return bool True when found; False when not found.
 */
$CIDRAM['in_csv'] = function (string $Value, string $CSV): bool {
    if (!$Value || !$CSV) {
        return false;
    }
    $Arr = explode(',', $CSV);
    if (strpos($CSV, '"') !== false) {
        foreach ($Arr as &$Item) {
            if (substr($Item, 0, 1) === '"' && substr($Item, -1) === '"') {
                $Item = substr($Item, 1, -1);
            }
        }
    }
    return in_array($Value, $Arr, true);
};

/**
 * Initialises an error handler to catch any errors generated by CIDRAM when
 * needed.
 */
$CIDRAM['InitialiseErrorHandler'] = function () use (&$CIDRAM) {

    /** Stores any errors generated by the error handler. */
    $CIDRAM['Errors'] = [];

    /**
     * For a full description of all supported parameters, please see:
     * @link https://php.net/set_error_handler
     *
     * @param int $errno
     * @param string $errstr
     * @param string $errfile
     * @param int $errline
     * @return bool True to end further processing; False to defer processing.
     */
    $CIDRAM['PreviousErrorHandler'] = set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$CIDRAM) {
        $VaultLen = strlen($CIDRAM['Vault']);
        if (
            strlen($errfile) > $VaultLen &&
            str_replace("\\", '/', substr($errfile, 0, $VaultLen)) === str_replace("\\", '/', $CIDRAM['Vault'])
        ) {
            $errfile = substr($errfile, $VaultLen);
        }
        $CIDRAM['Errors'][] = [$errno, $errstr, $errfile, $errline];
        if ($CIDRAM['Events']->assigned('error')) {
            $CIDRAM['Events']->fireEvent('error', serialize([$CIDRAM['Stage'] ?? '', $errno, $errstr, $errfile, $errline]));
        }
        return true;
    });
};

/**
 * Restores previous error handler after having initialised an error handler.
 */
$CIDRAM['RestoreErrorHandler'] = function () use (&$CIDRAM) {

    /** Reset errors array. */
    $CIDRAM['Errors'] = [];

    /** Restore previous error handler. */
    restore_error_handler();
};

/**
 * Generates unique IDs for block events.
 *
 * @return string A unique ID to use for block events.
 */
$CIDRAM['GenerateID'] = function (): string {
    $Time = explode(' ', microtime(), 2);
    $Time[0] = (string)($Time[0] * 1000000);
    while (strlen($Time[0]) < 6) {
        $Time[0] = '0' . $Time[0];
    }
    if (function_exists('hrtime')) {
        try {
            $HRTime = (string)hrtime(true);
            if (strlen($HRTime) > 10) {
                $HRTime = substr($HRTime, -10);
            }
            while (strlen($HRTime) < 10) {
                $HRTime = '0' . $HRTime;
            }
        } catch (\Exception $Exception) {
            $HRTime = '';
        }
    } else {
        $HRTime = '';
    }
    $HRLen = strlen($HRTime);
    $Time = $Time[1] . '-' . $Time[0] . '-' . $HRTime;
    if ($HRLen < 10) {
        $Low = pow(10, (9 - strlen($HRTime)));
        $High = ($Low * 10) - 1;
        if (function_exists('random_int')) {
            try {
                $Pad = random_int($Low, $High);
            } catch (\Exception $Exception) {
                $Pad = rand($Low, $High);
            }
        } else {
            $Pad = rand($Low, $High);
        }
        $Time .= $Pad;
    }
    return $Time;
};

/**
 * Writes to the standard logfile.
 *
 * @return bool True on success; False on failure.
 */
$CIDRAM['Events']->addHandler('writeToLog', function () use (&$CIDRAM): bool {
    if (!$CIDRAM['Config']['general']['logfile'] || empty($CIDRAM['LogFileNames']['logfile'])) {
        return false;
    }
    $Data = !file_exists($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile']) || (
        $CIDRAM['Config']['general']['truncate'] > 0 &&
        filesize($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile']) >= $CIDRAM['ReadBytes']($CIDRAM['Config']['general']['truncate'])
    ) ? "\x3c\x3fphp die; \x3f\x3e\n\n" : '';
    $WriteMode = !empty($Data) ? 'w' : 'a';
    $Data .= $CIDRAM['ParseVars']($CIDRAM['Parsables'], $CIDRAM['FieldTemplates']['Logs'] . "\n");
    if ($CIDRAM['BuildLogPath']($CIDRAM['LogFileNames']['logfile'])) {
        $File = fopen($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile'], $WriteMode);
        fwrite($File, $Data);
        fclose($File);
        if ($WriteMode === 'w') {
            $CIDRAM['LogRotation']($CIDRAM['Config']['general']['logfile']);
        }
        return true;
    }
    return false;
});

/**
 * Writes to the Apache-style logfile.
 *
 * @return bool True on success; False on failure.
 */
$CIDRAM['Events']->addHandler('writeToLog', function () use (&$CIDRAM): bool {
    if (
        !$CIDRAM['Config']['general']['logfile_apache'] ||
        empty($CIDRAM['LogFileNames']['logfile_apache']) ||
        empty($CIDRAM['BlockInfo'])
    ) {
        return false;
    }
    $Data = sprintf(
        "%s - - [%s] \"%s %s %s\" %s %s \"%s\" \"%s\"\n",
        $CIDRAM['BlockInfo']['IPAddr'],
        $CIDRAM['BlockInfo']['DateTime'],
        $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
        $_SERVER['REQUEST_URI'] ?? '/',
        $_SERVER['SERVER_PROTOCOL'] ?? 'UNKNOWN/x.x',
        $CIDRAM['errCode'],
        strlen($CIDRAM['HTML']),
        $CIDRAM['BlockInfo']['Referrer'] ?? '-',
        $CIDRAM['BlockInfo']['UA'] ?? '-'
    );
    $WriteMode = !file_exists($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile_apache']) || (
        $CIDRAM['Config']['general']['truncate'] > 0 &&
        filesize($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile_apache']) >= $CIDRAM['ReadBytes']($CIDRAM['Config']['general']['truncate'])
    ) ? 'w' : 'a';
    if ($CIDRAM['BuildLogPath']($CIDRAM['LogFileNames']['logfile_apache'])) {
        $File = fopen($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile_apache'], $WriteMode);
        fwrite($File, $Data);
        fclose($File);
        if ($WriteMode === 'w') {
            $CIDRAM['LogRotation']($CIDRAM['Config']['general']['logfile_apache']);
        }
        return true;
    }
    return false;
});

/**
 * Writes to the serialised logfile.
 *
 * @return bool True on success; False on failure.
 */
$CIDRAM['Events']->addHandler('writeToLog', function () use (&$CIDRAM): bool {
    if (
        !$CIDRAM['Config']['general']['logfile_serialized'] ||
        empty($CIDRAM['LogFileNames']['logfile_serialized']) ||
        empty($CIDRAM['BlockInfo'])
    ) {
        return false;
    }
    $BlockInfo = $CIDRAM['BlockInfo'];
    unset($BlockInfo['EmailAddr'], $BlockInfo['UALC'], $BlockInfo['favicon']);
    /** Remove empty entries prior to serialising. */
    $BlockInfo = array_filter($BlockInfo, function ($Value) {
        return !(is_string($Value) && empty($Value));
    });
    $WriteMode = !file_exists($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile_serialized']) || (
        $CIDRAM['Config']['general']['truncate'] > 0 &&
        filesize($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile_serialized']) >= $CIDRAM['ReadBytes']($CIDRAM['Config']['general']['truncate'])
    ) ? 'w' : 'a';
    if ($CIDRAM['BuildLogPath']($CIDRAM['LogFileNames']['logfile_serialized'])) {
        $File = fopen($CIDRAM['Vault'] . $CIDRAM['LogFileNames']['logfile_serialized'], $WriteMode);
        fwrite($File, serialize($BlockInfo) . "\n");
        fclose($File);
        if ($WriteMode === 'w') {
            $CIDRAM['LogRotation']($CIDRAM['Config']['general']['logfile_serialized']);
        }
        return true;
    }
    return false;
});
For more information send a message to info at phpclasses dot org.