<commit_before><?php

use Astrotomic\Twemoji\Twemoji;
use Symfony\Component\HttpFoundation\IpUtils;

/**
 * Contains misc utility methods.
 *
 * @package NamelessMC\Core
 * @author Samerton
 * @author Aberdeener
 * @author Partydragen
 * @version 2.0.0-pr13
 * @license MIT
 */
class Util {

    private static array $_enabled_modules = [];
    private static ?array $_cached_settings = null;

    /**
     * Convert Cyrillic to Latin letters.
     * https://en.wikipedia.org/wiki/ISO_9.
     *
     * @param string $string String to convert.
     *
     * @return string Converted string.
     */
    public static function cyrillicToLatin(string $string): string {
        $cyrillic = [
            'а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',
            'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я',
            'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П',
            'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я'
        ];
        $latin = [
            'a', 'b', 'v', 'g', 'd', 'e', 'io', 'zh', 'z', 'i', 'y', 'k', 'l', 'm', 'n', 'o', 'p',
            'r', 's', 't', 'u', 'f', 'h', 'ts', 'ch', 'sh', 'sht', 'a', 'i', 'y', 'e', 'yu', 'ya',
            'A', 'B', 'V', 'G', 'D', 'E', 'Io', 'Zh', 'Z', 'I', 'Y', 'K', 'L', 'M', 'N', 'O', 'P',
            'R', 'S', 'T', 'U', 'F', 'H', 'Ts', 'Ch', 'Sh', 'Sht', 'A', 'I', 'Y', 'e', 'Yu', 'Ya'
        ];

        return str_replace($cyrillic, $latin, $string);
    }

    /**
     * Recursively remove a directory.
     *
     * @param string $directory Path to directory to remove.
     *
     * @return bool Whether the action succeeded or not.
     */
    public static function recursiveRemoveDirectory(string $directory): bool {
        // safety precaution, only allow deleting files in "custom" directory
        if (!str_contains($directory, 'custom')) {
            return false;
        }

        foreach (glob($directory . '/*') as $file) {
            if (is_dir($file)) {
                if (!self::recursiveRemoveDirectory($file)) {
                    return false;
                }
            } else {
                if (!unlink($file)) {
                    return false;
                }
            }
        }

        rmdir($directory);

        return true;
    }

    /**
     * Get an array containing all timezone lists.
     *
     * @return array All timezones.
     */
    public static function listTimezones(): array {
        // Array to contain timezones
        $timezones = [];

        // Array to contain offsets
        $offsets = [];

        // Get all PHP timezones
        $all_timezones = DateTimeZone::listIdentifiers();

        // Get current UTC time to calculate offset
        $current = new DateTime('now', new DateTimeZone('UTC'));

        foreach ($all_timezones as $timezone) {
            // Get timezone offset
            $current->setTimezone(new DateTimeZone($timezone));

            // Add offset to offset array
            $offsets[] = $current->getOffset();

            // Format timezone offset
            $offset = 'GMT ' . (int)($current->getOffset() / 3600) . ':' . str_pad(abs((int)($current->getOffset() % 3600 / 60)), 2, 0);

            // Prettify timezone name
            $name = Output::getClean(str_replace(['/', '_'], [', ', ' '], $timezone));

            // Add to timezones array
            $timezones[$timezone] = ['offset' => $offset, 'name' => $name, 'time' => $current->format('H:i')];
        }

        array_multisort($offsets, $timezones);

        return $timezones;
    }

    /**
     * Is a URL internal or external? Accepts full URL and also just a path.
     *
     * @param string $url URL/path to check.
     *
     * @return bool Whether URL is external or not.
     */
    public static function isExternalURL(string $url): bool {
        if ($url[0] == '/' && $url[1] != '/') {
            return false;
        }

        $parsed = parse_url($url);

        return !(str_replace('www.', '', rtrim(self::getSelfURL(false), '/')) == str_replace('www.', '', $parsed['host']));
    }

    /**
     * @return array List of trusted proxy networks according to config file and environment
     */
    public static function getTrustedProxies(): array {
        $trustedProxies = [];

        // Add trusted proxies from config file
        $configProxies = Config::get('core/trustedProxies');
        if ($configProxies !== false) {
            if (!is_array($configProxies)) {
                die('Trusted proxies should be an array');
            }
            $trustedProxies = array_merge($trustedProxies, $configProxies);
        }

        // Add trusted proxies from environment variable (comma-separated string)
        $envProxies = getenv('NAMELESS_TRUSTED_PROXIES');
        if ($envProxies !== false) {
            $envProxiesArray = explode(',', $envProxies);
            $trustedProxies = array_merge($trustedProxies, $envProxiesArray);
        }

        return $trustedProxies;
    }

    /**
     * Checks whether the client making the request is a trusted proxy. If not,
     * abruptly aborts the request using die().
     */
    private static function ensureTrustedProxy(): void {
        $trustedProxies = self::getTrustedProxies();

        if (count($trustedProxies) === 0) {
            die('Received proxy header but no trusted proxies are configured. Please see <a href="https://docs.namelessmc.com/trusted-proxies">https://docs.namelessmc.com/trusted-proxies</a> for more information.');
        }

        $trusted = false;

        foreach ($trustedProxies as $trustedProxy) {
            if (IpUtils::checkIp($_SERVER['REMOTE_ADDR'], $trustedProxy)) {
                $trusted = true;
                break;
            }
        }

        if (!$trusted) {
            die('Received proxy header from untrusted remote address: ' . $_SERVER['REMOTE_ADDR']);
        }
    }

    /**
     * Extract trustworthy address from a list of addresses provided by the Forwarded or X-Forwarded-For header.
     * @return string Address that may be used for security purposes
     */
    private static function firstNonProxyAddress(array $addresses): string {
        if (count($addresses) === 0) {
            throw new InvalidArgumentException('Addresses must not be empty');
        }

        $trusted_proxies = self::getTrustedProxies();

        /*
        https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#parsing<commit_msg>Log warning instead of stopping script if no proxies configured<commit_after><?php

use Astrotomic\Twemoji\Twemoji;
use Symfony\Component\HttpFoundation\IpUtils;

/**
 * Contains misc utility methods.
 *
 * @package NamelessMC\Core
 * @author Samerton
 * @author Aberdeener
 * @author Partydragen
 * @version 2.0.0-pr13
 * @license MIT
 */
class Util {

    private static array $_enabled_modules = [];
    private static ?array $_cached_settings = null;

    /**
     * Convert Cyrillic to Latin letters.
     * https://en.wikipedia.org/wiki/ISO_9.
     *
     * @param string $string String to convert.
     *
     * @return string Converted string.
     */
    public static function cyrillicToLatin(string $string): string {
        $cyrillic = [
            'а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',
            'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я',
            'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П',
            'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я'
        ];
        $latin = [
            'a', 'b', 'v', 'g', 'd', 'e', 'io', 'zh', 'z', 'i', 'y', 'k', 'l', 'm', 'n', 'o', 'p',
            'r', 's', 't', 'u', 'f', 'h', 'ts', 'ch', 'sh', 'sht', 'a', 'i', 'y', 'e', 'yu', 'ya',
            'A', 'B', 'V', 'G', 'D', 'E', 'Io', 'Zh', 'Z', 'I', 'Y', 'K', 'L', 'M', 'N', 'O', 'P',
            'R', 'S', 'T', 'U', 'F', 'H', 'Ts', 'Ch', 'Sh', 'Sht', 'A', 'I', 'Y', 'e', 'Yu', 'Ya'
        ];

        return str_replace($cyrillic, $latin, $string);
    }

    /**
     * Recursively remove a directory.
     *
     * @param string $directory Path to directory to remove.
     *
     * @return bool Whether the action succeeded or not.
     */
    public static function recursiveRemoveDirectory(string $directory): bool {
        // safety precaution, only allow deleting files in "custom" directory
        if (!str_contains($directory, 'custom')) {
            return false;
        }

        foreach (glob($directory . '/*') as $file) {
            if (is_dir($file)) {
                if (!self::recursiveRemoveDirectory($file)) {
                    return false;
                }
            } else {
                if (!unlink($file)) {
                    return false;
                }
            }
        }

        rmdir($directory);

        return true;
    }

    /**
     * Get an array containing all timezone lists.
     *
     * @return array All timezones.
     */
    public static function listTimezones(): array {
        // Array to contain timezones
        $timezones = [];

        // Array to contain offsets
        $offsets = [];

        // Get all PHP timezones
        $all_timezones = DateTimeZone::listIdentifiers();

        // Get current UTC time to calculate offset
        $current = new DateTime('now', new DateTimeZone('UTC'));

        foreach ($all_timezones as $timezone) {
            // Get timezone offset
            $current->setTimezone(new DateTimeZone($timezone));

            // Add offset to offset array
            $offsets[] = $current->getOffset();

            // Format timezone offset
            $offset = 'GMT ' . (int)($current->getOffset() / 3600) . ':' . str_pad(abs((int)($current->getOffset() % 3600 / 60)), 2, 0);

            // Prettify timezone name
            $name = Output::getClean(str_replace(['/', '_'], [', ', ' '], $timezone));

            // Add to timezones array
            $timezones[$timezone] = ['offset' => $offset, 'name' => $name, 'time' => $current->format('H:i')];
        }

        array_multisort($offsets, $timezones);

        return $timezones;
    }

    /**
     * Is a URL internal or external? Accepts full URL and also just a path.
     *
     * @param string $url URL/path to check.
     *
     * @return bool Whether URL is external or not.
     */
    public static function isExternalURL(string $url): bool {
        if ($url[0] == '/' && $url[1] != '/') {
            return false;
        }

        $parsed = parse_url($url);

        return !(str_replace('www.', '', rtrim(self::getSelfURL(false), '/')) == str_replace('www.', '', $parsed['host']));
    }

    /**
     * @return array List of trusted proxy networks according to config file and environment
     */
    public static function getTrustedProxies(): array {
        $trustedProxies = [];

        // Add trusted proxies from config file
        $configProxies = Config::get('core/trustedProxies');
        if ($configProxies !== false) {
            if (!is_array($configProxies)) {
                die('Trusted proxies should be an array');
            }
            $trustedProxies = array_merge($trustedProxies, $configProxies);
        }

        // Add trusted proxies from environment variable (comma-separated string)
        $envProxies = getenv('NAMELESS_TRUSTED_PROXIES');
        if ($envProxies !== false) {
            $envProxiesArray = explode(',', $envProxies);
            $trustedProxies = array_merge($trustedProxies, $envProxiesArray);
        }

        return $trustedProxies;
    }

    /**
     * Checks whether the client making the request is a trusted proxy. If not,
     * abruptly aborts the request using die().
     */
    private static function ensureTrustedProxy(): void {
        $trustedProxies = self::getTrustedProxies();

        if (count($trustedProxies) === 0) {
            ErrorHandler::logWarning('Received proxy header but no trusted proxies are configured. Please see https://docs.namelessmc.com/trusted-proxies for more information.');
            return;
        }

        $trusted = false;

        foreach ($trustedProxies as $trustedProxy) {
            if (IpUtils::checkIp($_SERVER['REMOTE_ADDR'], $trustedProxy)) {
                $trusted = true;
                break;
            }
        }

        if (!$trusted) {
            die('Received proxy header from untrusted remote address: ' . $_SERVER['REMOTE_ADDR']);
        }
    }

    /**
     * Extract trustworthy address from a list of addresses provided by the Forwarded or X-Forwarded-For header.
     * @return string Address that may be used for security purposes
     */
    private static function firstNonProxyAddress(array $addresses): string {
        if (count($addresses) === 0) {
            throw new InvalidArgumentException('Addresses must not be empty');
        }

        $trusted_proxies = self::getTrustedProxies();

        /*
        https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#parsing
{'fraction_non_alphanumeric': 0.16943496397421312, 'fraction_numerical': 0.003337125521425863, 'mean_word_length': 2.129836221220033, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 21, 'https://': 7, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1750164', 'n_tokens_mistral': 4218, 'n_tokens_neox': 4088, 'n_words': 1132}