Files
Froxlor/lib/Froxlor/PhpHelper.php
Michael Kaufmann 1679675aa1 introduce http-request rate-limit; smaller fixes
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-02 10:19:53 +02:00

553 lines
15 KiB
PHP

<?php
/**
* This file is part of the Froxlor project.
* Copyright (c) 2010 the Froxlor Team (see authors).
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you can also view it online at
* https://files.froxlor.org/misc/COPYING.txt
*
* @copyright the authors
* @author Froxlor team <team@froxlor.org>
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
*/
namespace Froxlor;
use Exception;
use Froxlor\UI\Panel\UI;
use Net_DNS2_Exception;
use Net_DNS2_Resolver;
use Throwable;
use voku\helper\AntiXSS;
class PhpHelper
{
private static $sort_key = 'id';
private static $sort_type = SORT_STRING;
/**
* sort an array by either natural or string sort and a given index where the value for comparison is found
*
* @param array $list
* @param string $key
*
* @return bool
*/
public static function sortListBy(array &$list, string $key = 'id'): bool
{
self::$sort_type = Settings::Get('panel.natsorting') == 1 ? SORT_NATURAL : SORT_STRING;
self::$sort_key = $key;
return usort($list, [
'self',
'sortListByGivenKey'
]);
}
/**
* Wrapper around htmlentities to handle arrays, with the advantage that you
* can select which fields should be handled by htmlentities
*
* @param array|string $subject The subject array
* @param array|string $fields The fields which should be checked for, separated by spaces
* @param int $quote_style See php documentation about this
* @param string $charset See php documentation about this
*
* @return array|string The string or an array with htmlentities converted strings
* @author Florian Lippert <flo@syscp.org> (2003-2009)
*/
public static function htmlentitiesArray($subject, $fields = '', $quote_style = ENT_QUOTES, $charset = 'UTF-8')
{
if (is_array($subject)) {
if (!is_array($fields)) {
$fields = self::arrayTrim(explode(' ', $fields));
}
foreach ($subject as $field => $value) {
if ((!is_array($fields) || empty($fields)) || (in_array($field, $fields))) {
// Just call ourselve to manage multi-dimensional arrays
$subject[$field] = self::htmlentitiesArray($value, $fields, $quote_style, $charset);
}
}
} else {
$subject = empty($subject) ? "" : htmlentities($subject, $quote_style, $charset);
}
return $subject;
}
/**
* Returns array with all empty-values removed
*
* @param array $source The array to trim
* @return array The trim'med array
*/
public static function arrayTrim(array $source): array
{
$source = array_map('trim', $source);
return array_filter($source, function ($value) {
return $value !== '';
});
}
/**
* Replaces Strings in an array, with the advantage that you
* can select which fields should be str_replace'd
*
* @param string|array $search String or array of strings to search for
* @param string|array $replace String or array to replace with
* @param string|array $subject String or array The subject array
* @param string|array $fields string The fields which should be checked for, separated by spaces
*
* @return string|array The str_replace'd array
*/
public static function strReplaceArray($search, $replace, $subject, $fields = '')
{
if (is_array($subject)) {
if (!is_array($fields)) {
$fields = self::arrayTrim(explode(' ', $fields));
}
foreach ($subject as $field => $value) {
if ((!is_array($fields) || empty($fields)) || (in_array($field, $fields))) {
$subject[$field] = str_replace($search, $replace, $value);
}
}
} else {
$subject = str_replace($search, $replace, $subject);
}
return $subject;
}
/**
* froxlor php error handler
*
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
*
* @return void|boolean
*/
public static function phpErrHandler($errno, $errstr, $errfile, $errline)
{
if (!(error_reporting() & $errno)) {
// This error code is not included in error_reporting
return;
}
if (!isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) {
// prevent possible file-path-disclosure
$errfile = str_replace(Froxlor::getInstallDir(), "", $errfile);
// build alert
$type = 'danger';
if ($errno == E_NOTICE || $errno == E_DEPRECATED || $errno == E_STRICT) {
$type = 'info';
} elseif ($errno = E_WARNING) {
$type = 'warning';
}
$err_display = '<div class="alert alert-' . $type . ' my-1" role="alert">';
$err_display .= '<strong>#' . $errno . ' ' . $errstr . '</strong><br>';
$err_display .= $errfile . ':' . $errline;
// later depended on whether to show or now
$err_display .= '<br><p><pre>';
$debug = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
foreach ($debug as $dline) {
$err_display .= $dline['function'] . '() called at [' . str_replace(Froxlor::getInstallDir(), '',
($dline['file'] ?? 'unknown')) . ':' . ($dline['line'] ?? 0) . ']<br>';
}
$err_display .= '</pre></p>';
// end later
$err_display .= '</div>';
// check for more existing errors
$errors = isset(UI::twig()->getGlobals()['global_errors']) ? UI::twig()->getGlobals()['global_errors'] : "";
UI::twig()->addGlobal('global_errors', $errors . $err_display);
// return true to ignore php standard error-handler
return true;
}
// of on shell, use the php standard error-handler
return false;
}
/**
* @param Throwable $exception
* @return void
*/
public static function phpExceptionHandler(Throwable $exception)
{
if (!isset($_SERVER['SHELL']) || $_SERVER['SHELL'] == '') {
// show
UI::initTwig(true);
UI::twig()->addGlobal('install_mode', '1');
UI::view('misc/alert_nosession.html.twig', [
'page_title' => 'Uncaught exception',
'heading' => 'Uncaught exception',
'type' => 'danger',
'alert_msg' => $exception->getCode() . ' ' . $exception->getMessage(),
'alert_info' => $exception->getTraceAsString()
]);
die();
}
}
/**
* @param ...$configdirs
* @return array|null
*/
public static function loadConfigArrayDir(...$configdirs)
{
if (count($configdirs) <= 0) {
return null;
}
$data = [];
$data_files = [];
$has_data = false;
foreach ($configdirs as $data_dirname) {
if (is_dir($data_dirname)) {
$data_dirhandle = opendir($data_dirname);
while (false !== ($data_filename = readdir($data_dirhandle))) {
if ($data_filename != '.' && $data_filename != '..' && $data_filename != '' && substr($data_filename,
-4) == '.php') {
$data_files[] = $data_dirname . $data_filename;
}
}
$has_data = true;
}
}
if ($has_data) {
sort($data_files);
foreach ($data_files as $data_filename) {
$data = array_merge_recursive($data, include $data_filename);
}
}
return $data;
}
/**
* ipv6 aware gethostbynamel function
*
* @param string $host
* @param boolean $try_a default true
* @param string|null $nameserver set additional resolver nameserver to use (e.g. 1.1.1.1)
* @return bool|array
*/
public static function gethostbynamel6(string $host, bool $try_a = true, string $nameserver = null)
{
$ips = [];
try {
// set the default nameservers to use, use the system default if none are provided
$resolver = new Net_DNS2_Resolver($nameserver ? ['nameservers' => [$nameserver]] : []);
// get all ip addresses from the A record and normalize them
if ($try_a) {
try {
$answer = $resolver->query($host, 'A')->answer;
foreach ($answer as $rr) {
if ($rr instanceof \Net_DNS2_RR_A) {
$ips[] = inet_ntop(inet_pton($rr->address));
}
}
} catch (Net_DNS2_Exception $e) {
// we can't do anything here, just continue
}
}
// get all ip addresses from the AAAA record and normalize them
try {
$answer = $resolver->query($host, 'AAAA')->answer;
foreach ($answer as $rr) {
if ($rr instanceof \Net_DNS2_RR_AAAA) {
$ips[] = inet_ntop(inet_pton($rr->address));
}
}
} catch (Net_DNS2_Exception $e) {
// we can't do anything here, just continue
}
} catch (Net_DNS2_Exception $e) {
// fallback to php's dns_get_record if Net_DNS2 has no resolver available, but this may cause
// problems if the system's dns is not configured correctly; for example, the acme pre-check
// will fail because some providers put a local ip in /etc/hosts
// get all ip addresses from the A record and normalize them
if ($try_a) {
$answer = @dns_get_record($host, DNS_A);
foreach ($answer as $rr) {
$ips[] = inet_ntop(inet_pton($rr['ip']));
}
}
// get all ip addresses from the AAAA record and normalize them
$answer = @dns_get_record($host, DNS_AAAA);
foreach ($answer as $rr) {
$ips[] = inet_ntop(inet_pton($rr['ipv6']));
}
}
return count($ips) > 0 ? $ips : false;
}
/**
* Function randomStr
*
* generate a pseudo-random string of bytes
*
* @param int $length
* @return string
* @throws Exception
*/
public static function randomStr(int $length): string
{
if (function_exists('openssl_random_pseudo_bytes')) {
return openssl_random_pseudo_bytes($length);
}
return random_bytes($length);
}
/**
* Return human-readable sizes
*
* @param int $size size in bytes
* @param ?string $max maximum unit
* @param string $system 'si' for SI, 'bi' for binary prefixes
* @param string $retstring string-format
*
* @return string
*/
public static function sizeReadable(
$size,
?string $max = '',
string $system = 'si',
string $retstring = '%01.2f %s'
): string {
// Pick units
$systems = [
'si' => [
'prefix' => [
'B',
'KB',
'MB',
'GB',
'TB',
'PB'
],
'size' => 1000
],
'bi' => [
'prefix' => [
'B',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB'
],
'size' => 1024
]
];
$sys = $systems[$system] ?? $systems['si'];
// Max unit to display
$depth = count($sys['prefix']) - 1;
if ($max && false !== $d = array_search($max, $sys['prefix'])) {
$depth = $d;
}
// Loop
$i = 0;
while ($size >= $sys['size'] && $i < $depth) {
$size /= $sys['size'];
$i++;
}
return sprintf($retstring, $size, $sys['prefix'][$i]);
}
/**
* Replaces all occurrences of variables defined in the second argument
* in the first argument with their values.
*
* @param string $text The string that should be searched for variables
* @param array $vars The array containing the variables with their values
*
* @return string The submitted string with the variables replaced.
*/
public static function replaceVariables(string $text, array $vars): string
{
$pattern = "/\{([a-zA-Z0-9\-_]+)\}/";
$matches = [];
if (count($vars) > 0 && preg_match_all($pattern, $text, $matches)) {
for ($i = 0; $i < count($matches[1]); $i++) {
$current = $matches[1][$i];
if (isset($vars[$current])) {
$var = $vars[$current];
$text = str_replace("{" . $current . "}", $var, $text);
}
}
}
return str_replace('\n', "\n", $text);
}
/**
* @param string $needle
* @param array $haystack
* @param array $keys
* @param string $currentKey
* @return true
*/
public static function recursive_array_search(
string $needle,
array $haystack,
array &$keys = [],
string $currentKey = ''
): bool {
foreach ($haystack as $key => $value) {
$pathkey = empty($currentKey) ? $key : $currentKey . '.' . $key;
if (is_array($value)) {
self::recursive_array_search($needle, $value, $keys, $pathkey);
} else {
if (stripos($value, $needle) !== false) {
$keys[] = $pathkey;
}
}
}
return true;
}
/**
* function to check a super-global passed by reference,
* so it gets automatically updated
*
* @param array $global
* @param AntiXSS $antiXss
*/
public static function cleanGlobal(array &$global, AntiXSS &$antiXss)
{
$ignored_fields = [
'system_default_vhostconf',
'system_default_sslvhostconf',
'system_apache_globaldiropt',
'specialsettings',
'ssl_specialsettings',
'default_vhostconf_domain',
'ssl_default_vhostconf_domain',
'filecontent'
];
if (!empty($global)) {
$tmp = $global;
foreach ($tmp as $index => $value) {
if (!in_array($index, $ignored_fields)) {
$global[$index] = $antiXss->xss_clean($value);
}
}
}
}
/**
* @param array $a
* @param array $b
* @return int
*/
private static function sortListByGivenKey(array $a, array $b): int
{
if (self::$sort_type == SORT_NATURAL) {
return strnatcasecmp($a[self::$sort_key], $b[self::$sort_key]);
}
return strcasecmp($a[self::$sort_key], $b[self::$sort_key]);
}
/**
* Generate php file from array.
*
* @param array $array
* @param string|null $comment
* @param bool $asReturn
* @return string
*/
public static function parseArrayToPhpFile(array $array, string $comment = null, bool $asReturn = false): string
{
$str = sprintf("<?php\n// %s\n\n", $comment ?? 'autogenerated froxlor file');
if ($asReturn) {
return $str . sprintf("return %s;\n", rtrim(self::parseArrayToString($array), "\n,"));
}
foreach ($array as $var => $arr) {
$str .= sprintf("\$%s = %s;\n", $var, rtrim(self::parseArrayToString($arr), "\n,"));
}
return $str;
}
/**
* Parse array to array string.
*
* @param array $array
* @param ?string $key
* @param int $depth
* @return string
*/
public static function parseArrayToString(array $array, string $key = null, int $depth = 1): string
{
$str = '';
if (!is_null($key)) {
$str .= self::tabPrefix(($depth - 1), "'{$key}' => [\n");
} else {
$str .= self::tabPrefix(($depth - 1), "[\n");
}
foreach ($array as $key => $value) {
if (!is_array($value)) {
if (is_bool($value)) {
$str .= self::tabPrefix($depth, sprintf("'%s' => %s,\n", $key, $value ? 'true' : 'false'));
} elseif (is_int($value)) {
$str .= self::tabPrefix($depth, "'{$key}' => $value,\n");
} else {
if ($key == 'password') {
// special case for passwords (nowdoc)
$str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n");
} else {
$str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n");
}
}
} else {
$str .= self::parseArrayToString($value, $key, ($depth + 1));
}
}
$str .= self::tabPrefix(($depth - 1), "],\n");
return $str;
}
/**
* Apply tabs with given depth to string.
*
* @param int $depth
* @param string $str
* @return string
*/
private static function tabPrefix(int $depth, string $str = ''): string
{
$tab = '';
for ($i = 1; $i <= $depth; $i++) {
$tab .= "\t";
}
return $tab . $str;
}
}