* @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 (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 = ''; // 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', 'admin_password', 'password', 'new_customer_password', 'privileged_password', 'email_password', 'directory_password', 'ftp_password', 'mysql_password', ]; 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(" $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; } }