diff options
Diffstat (limited to 'inc/util.inc.php')
-rw-r--r-- | inc/util.inc.php | 332 |
1 files changed, 124 insertions, 208 deletions
diff --git a/inc/util.inc.php b/inc/util.inc.php index 9c9d4e58..267a3971 100644 --- a/inc/util.inc.php +++ b/inc/util.inc.php @@ -1,168 +1,31 @@ <?php +declare(strict_types=1); + +use JetBrains\PhpStorm\NoReturn; + class Util { private static $redirectParams = array(); /** - * Displays an error message and stops script execution. - * If CONFIG_DEBUG is true, it will also dump a stack trace - * and all globally defined variables. - * (As this might reveal sensistive data you should never enable it in production) - */ - public static function traceError($message) - { - if ((defined('API') && API) || (defined('AJAX') && AJAX) || php_sapi_name() === 'cli') { - error_log('API ERROR: ' . $message); - error_log(self::formatBacktracePlain(debug_backtrace())); - } - if (php_sapi_name() === 'cli') { - // Don't spam HTML when invoked via cli, above error_log should have gone to stdout/stderr - exit(1); - } - Header('HTTP/1.1 500 Internal Server Error'); - if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'html') === false ) { - Header('Content-Type: text/plain; charset=utf-8'); - echo 'API ERROR: ', $message, "\n", self::formatBacktracePlain(debug_backtrace()); - exit(0); - } - Header('Content-Type: text/html; charset=utf-8'); - echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><style>', "\n", - ".arg { color: red; background: white; }\n", - "h1 a { color: inherit; text-decoration: inherit; font-weight: inherit; }\n", - '</style><title>Fatal Error</title></head><body>'; - echo '<h1>Flagrant <a href="https://www.youtube.com/watch?v=7rrZ-sA4FQc&t=2m2s" target="_blank">S</a>ystem error</h1>'; - echo "<h2>Message</h2><pre>$message</pre>"; - if (strpos($message, 'Database') !== false) { - echo '<div><a href="install.php">Try running database setup</a></div>'; - } - echo "<br><br>"; - if (defined('CONFIG_DEBUG') && CONFIG_DEBUG) { - global $SLX_ERRORS; - if (!empty($SLX_ERRORS)) { - echo '<h2>PHP Errors</h2><pre>'; - foreach ($SLX_ERRORS as $error) { - echo htmlspecialchars("{$error['errstr']} ({$error['errfile']}:{$error['errline']}\n"); - } - echo '</pre>'; - } - echo "<h2>Stack Trace</h2>"; - echo '<pre>', self::formatBacktraceHtml(debug_backtrace()), '</pre>'; - echo "<h2>Globals</h2><pre>"; - echo htmlspecialchars(print_r($GLOBALS, true)); - echo '</pre>'; - } else { - echo <<<SADFACE -<pre> -________________________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶________ -____________________¶¶¶___________________¶¶¶¶_____ -________________¶¶¶_________________________¶¶¶¶___ -______________¶¶______________________________¶¶¶__ -___________¶¶¶_________________________________¶¶¶_ -_________¶¶_____________________________________¶¶¶ -________¶¶_________¶¶¶¶¶___________¶¶¶¶¶_________¶¶ -______¶¶__________¶¶¶¶¶¶__________¶¶¶¶¶¶_________¶¶ -_____¶¶___________¶¶¶¶____________¶¶¶¶___________¶¶ -____¶¶___________________________________________¶¶ -___¶¶___________________________________________¶¶_ -__¶¶____________________¶¶¶¶____________________¶¶_ -_¶¶_______________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶______________¶¶__ -_¶¶____________¶¶¶¶___________¶¶¶¶¶___________¶¶___ -¶¶¶_________¶¶¶__________________¶¶__________¶¶____ -¶¶_________¶______________________¶¶________¶¶_____ -¶¶¶______¶________________________¶¶_______¶¶______ -¶¶¶_____¶_________________________¶¶_____¶¶________ -_¶¶¶___________________________________¶¶__________ -__¶¶¶________________________________¶¶____________ -___¶¶¶____________________________¶¶_______________ -____¶¶¶¶______________________¶¶¶__________________ -_______¶¶¶¶¶_____________¶¶¶¶¶_____________________ -</pre> -SADFACE; - } - echo '</body></html>'; - exit(0); - } - - private static function formatArgument($arg, $expandArray = true) - { - if (is_string($arg)) { - $arg = "'$arg'"; - } elseif (is_object($arg)) { - $arg = 'instanceof ' . get_class($arg); - } elseif (is_array($arg)) { - if ($expandArray && count($arg) < 20) { - $expanded = ''; - foreach ($arg as $key => $value) { - if (!empty($expanded)) { - $expanded .= ', '; - } - $expanded .= $key . ': ' . self::formatArgument($value, false); - if (strlen($expanded) > 200) - break; - } - if (strlen($expanded) <= 200) - return '[' . $expanded . ']'; - } - $arg = 'Array(' . count($arg) . ')'; - } - return $arg; - } - - public static function formatBacktraceHtml($trace) - { - $output = ''; - foreach ($trace as $idx => $line) { - $args = array(); - foreach ($line['args'] as $arg) { - $arg = self::formatArgument($arg); - $args[] = '<span class="arg">' . htmlspecialchars($arg) . '</span>'; - } - $frame = str_pad('#' . $idx, 3, ' ', STR_PAD_LEFT); - $function = htmlspecialchars($line['function']); - $args = implode(', ', $args); - $file = preg_replace('~(/[^/]+)$~', '<b>$1</b>', htmlspecialchars($line['file'])); - // Add line - $output .= $frame . ' ' . $function . '<b>(</b>' - . $args . '<b>)</b>' . ' @ <i>' . $file . '</i>:' . $line['line'] . "\n"; - } - return $output; - } - - public static function formatBacktracePlain($trace) - { - $output = ''; - foreach ($trace as $idx => $line) { - $args = array(); - foreach ($line['args'] as $arg) { - $args[] = self::formatArgument($arg); - } - $frame = str_pad('#' . $idx, 3, ' ', STR_PAD_LEFT); - $args = implode(', ', $args); - // Add line - $output .= "\n" . $frame . ' ' . $line['function'] . '(' - . $args . ')' . ' @ ' . $line['file'] . ':' . $line['line']; - } - return $output; - } - - /** * Redirects the user via a '302 Moved' header. * An active session will be saved, any messages that haven't * been displayed yet will be appended to the redirect. + * * @param string|false $location Location to redirect to. "false" to redirect to same URL (useful after POSTs) * @param bool $preferRedirectPost if true, use the value from $_POST['redirect'] instead of $location */ - public static function redirect($location = false, $preferRedirectPost = false) + #[NoReturn] + public static function redirect($location = false, bool $preferRedirectPost = false): void { if ($location === false) { - $location = preg_replace('/(&|\?)message\[\]\=[^&]*/', '\1', $_SERVER['REQUEST_URI']); + $location = preg_replace('/([&?])message\[\]=[^&]*/', '\1', $_SERVER['REQUEST_URI']); } - Session::save(); $messages = Message::toRequest(); if ($preferRedirectPost && ($redirect = Request::post('redirect', false, 'string')) !== false - && !preg_match(',^(\w+\:|//),', $redirect) /* no uri scheme, no server */) { + && !preg_match(',^([0-9a-zA-Z_+\-]+:|//),', $redirect) /* no uri scheme, no server */) { $location = $redirect; } if (!empty($messages)) { @@ -190,7 +53,7 @@ SADFACE; exit(0); } - public static function addRedirectParam($key, $value) + public static function addRedirectParam(string $key, string $value): void { self::$redirectParams[] = $key . '=' . urlencode($value); } @@ -202,7 +65,7 @@ SADFACE; * token, this function will return false and display an error. * If the token matches, or the user is not logged in, it will return true. */ - public static function verifyToken() + public static function verifyToken(): bool { if (!User::isLoggedIn() && Session::get('token') === false) return true; @@ -219,62 +82,71 @@ SADFACE; * _word_ is underlined * \n is line break */ - public static function markup($string) + public static function markup(string $string): string { $string = htmlspecialchars($string); - $string = preg_replace('#(^|[\n \-_/\.])\*(.+?)\*($|[ \-_/\.\!\?,:])#is', '$1<b>$2</b>$3', $string); - $string = preg_replace('#(^|[\n \-\*/\.])_(.+?)_($|[ \-\*/\.\!\?,:])#is', '$1<u>$2</u>$3', $string); - $string = preg_replace('#(^|[\n \-_\*\.])/(.+?)/($|[ \-_\*\.\!\?,:])#is', '$1<i>$2</i>$3', $string); + $string = preg_replace('#(^|[\n \-_/.])\*(.+?)\*($|[ \-_/.!?,:])#is', '$1<b>$2</b>$3', $string); + $string = preg_replace('#(^|[\n \-*/.])_(.+?)_($|[ \-*/.!?,:])#is', '$1<u>$2</u>$3', $string); + $string = preg_replace('#(^|[\n \-_*.])/(.+?)/($|[ \-_*.!?,:])#is', '$1<i>$2</i>$3', $string); return nl2br($string); } /** - * Convert given number to human readable file size string. + * Convert given number to human-readable file size string. * Will append Bytes, KiB, etc. depending on magnitude of number. * * @param float|int $bytes numeric value of the filesize to make readable * @param int $decimals number of decimals to show, -1 for automatic * @param int $shift how many units to skip, i.e. if you pass in KiB or MiB - * @return string human readable string representing the given file size + * @return string human-readable string representing the given file size */ - public static function readableFileSize($bytes, $decimals = -1, $shift = 0) + public static function readableFileSize($bytes, int $decimals = -1, int $shift = 0): string { - $bytes = round($bytes); + // round doesn't reliably work for large floats, pick workaround depending on OS + $bytes = sprintf('%u', $bytes); static $sz = array('Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'); $factor = (int)floor((strlen($bytes) - 1) / 3); if ($factor === 0) { $decimals = 0; } else { - $bytes = $bytes / pow(1024, $factor); + $bytes /= 1024 ** $factor; if ($decimals === -1) { - $decimals = 2 - floor(strlen((int)$bytes) - 1); + $decimals = 2 - strlen((string)floor($bytes)) - 1; } } - return sprintf("%.{$decimals}f", $bytes) . "\xe2\x80\x89" . $sz[$factor + $shift]; + return Dictionary::number((float)$bytes, $decimals) . "\xe2\x80\x89" . ($sz[$factor + $shift] ?? '#>PiB#'); } - public static function sanitizeFilename($name) + public static function sanitizeFilename(string $name): string { return preg_replace('/[^a-zA-Z0-9_\-]+/', '_', $name); } - public static function safePath($path, $prefix = '') + /** + * Make sure given path is not absolute, and does not contain '..', or weird characters. + * Returns sanitized path, or false if invalid. If prefix is given, also make sure + * $path starts with it. + * + * @param string $path path to check for safety + * @param string $prefix required prefix of $path + */ + public static function safePath(string $path, string $prefix = ''): ?string { if (empty($path)) - return false; + return null; $path = trim($path); - if ($path{0} == '/' || preg_match('/[\x00-\x19\?\*]/', $path)) - return false; + if ($path[0] == '/' || preg_match('/[\x00-\x19?*]/', $path)) + return null; if (strpos($path, '..') !== false) - return false; + return null; if (substr($path, 0, 2) !== './') $path = "./$path"; - if (empty($prefix)) - return $path; - if (substr($prefix, 0, 2) !== './') - $prefix = "./$prefix"; - if (substr($path, 0, strlen($prefix)) !== $prefix) - return false; + if (!empty($prefix)) { + if (substr($prefix, 0, 2) !== './') + $prefix = "./$prefix"; + if (substr($path, 0, strlen($prefix)) !== $prefix) + return null; + } return $path; } @@ -284,7 +156,7 @@ SADFACE; * @param int $code the code to turn into an error description * @return string the error description */ - public static function uploadErrorString($code) + public static function uploadErrorString(int $code): string { switch ($code) { case UPLOAD_ERR_INI_SIZE: @@ -322,7 +194,7 @@ SADFACE; * @param string $ip_addr input to check * @return boolean true iff $ip_addr is a valid public ipv4 address */ - public static function isPublicIpv4($ip_addr) + public static function isPublicIpv4(string $ip_addr): bool { if (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $ip_addr)) return false; @@ -344,23 +216,6 @@ SADFACE; } /** - * Check whether $arrax contains all keys given in $keyList - * @param array $array An array - * @param array $keyList A list of strings which must all be valid keys in $array - * @return boolean - */ - public static function hasAllKeys($array, $keyList) - { - if (!is_array($array)) - return false; - foreach ($keyList as $key) { - if (!isset($array[$key])) - return false; - } - return true; - } - - /** * Send a file to user for download. * * @param string $file path of local file @@ -370,7 +225,7 @@ SADFACE; * true: error while reading the file * - on success, the function does not return */ - public static function sendFile($file, $name, $delete = false) + public static function sendFile(string $file, string $name, bool $delete = false): bool { while ((@ob_get_level()) > 0) @ob_end_clean(); @@ -398,9 +253,9 @@ SADFACE; * * @param int $length number of bytes to return * @param bool $secure true = only use strong random sources - * @return string|bool string of requested length, false on error + * @return ?string string of requested length, false on error */ - public static function randomBytes($length, $secure = true) + public static function randomBytes(int $length, bool $secure = true): ?string { if (function_exists('random_bytes')) { try { @@ -437,7 +292,7 @@ SADFACE; } } if ($secure) { - return false; + return null; } $bytes = ''; while ($length > 0) { @@ -449,9 +304,9 @@ SADFACE; /** * @return string a random UUID, v4. */ - public static function randomUuid() + public static function randomUuid(): string { - $b = unpack('h8a/h4b/h12c', self::randomBytes(12)); + $b = unpack('h8a/h4b/h12c', self::randomBytes(12, false)); return sprintf('%08s-%04s-%04x-%04x-%012s', // 32 bits for "time_low" @@ -477,10 +332,11 @@ SADFACE; /** * Transform timestamp to easily readable string. * The format depends on how far the timestamp lies in the past. + * * @param int $ts unix timestamp - * @return string human readable representation + * @return string human-readable representation */ - public static function prettyTime($ts) + public static function prettyTime(int $ts): string { settype($ts, 'int'); if ($ts === 0) @@ -503,35 +359,42 @@ SADFACE; /** * Return localized strings for yes or no depending on $bool + * * @param bool $bool Input to evaluate * @return string Yes or No, in user's selected language */ - public static function boolToString($bool) + public static function boolToString(bool $bool): string { if ($bool) - return Dictionary::translate('lang_yes', true); - return Dictionary::translate('lang_no', true); + return Dictionary::translate('lang_yes'); + return Dictionary::translate('lang_no'); } /** * Format a duration, in seconds, into a readable string. + * * @param int $seconds The number to format - * @param int $showSecs whether to show seconds, or rather cut after minutes - * @return string + * @param bool $showSecs whether to show seconds, or rather cut after minutes */ - public static function formatDuration($seconds, $showSecs = true) + public static function formatDuration(int $seconds, bool $showSecs = true): string { - settype($seconds, 'int'); static $UNITS = ['y' => 31536000, 'mon' => 2592000, 'd' => 86400]; $parts = []; + $prev = false; foreach ($UNITS as $k => $v) { if ($seconds < $v) continue; $n = floor($seconds / $v); $seconds -= $n * $v; - $parts[] = $n. $k; + if ($prev) { + $parts[] = sprintf('%02d', $n) . $k; + } else { + $parts[] = $n . $k; + $prev = true; + } } - return implode(' ', $parts) . ' ' . gmdate($showSecs ? 'H:i:s' : 'H:i', $seconds); + $parts[] = gmdate($showSecs ? 'H:i:s' : 'H:i', (int)$seconds); + return implode(' ', $parts); } /** @@ -540,9 +403,10 @@ SADFACE; * was a weird problem where firefox would keep sending a cookie with * path /slx-admin/ but trying to delete it from /slx-admin, which php's * setcookie automatically sends by default, did not clear it. + * * @param string $name cookie name */ - public static function clearCookie($name) + public static function clearCookie(string $name): void { $parts = explode('/', $_SERVER['SCRIPT_NAME']); $path = ''; @@ -554,4 +418,56 @@ SADFACE; } } + /** + * Remove any non-utf8 sequences from string. + */ + public static function cleanUtf8(string $string): string + { + // https://stackoverflow.com/a/1401716/2043481 + $regex = '/ + ( + (?: [\x00-\x7F] # single-byte sequences 0xxxxxxx + | [\xC0-\xDF][\x80-\xBF] # double-byte sequences 110xxxxx 10xxxxxx + | [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences 1110xxxx 10xxxxxx * 2 + | [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3 + ){1,100} # ...one or more times + ) + | . # anything else + /x'; + return preg_replace($regex, '$1', $string); + } + + /** + * Remove non-printable < 0x20 chars from ANSI string, then convert to UTF-8 + */ + public static function ansiToUtf8(string $string): string + { + $regex = '/ + ( + [\x20-\xFF]{1,100} # ignore lower non-printable range + ) + | . # anything else + /x'; + return iconv('MS-ANSI', 'UTF-8', preg_replace($regex, '$1', $string)); + } + + /** + * Clamp given value into [min, max] range. + * @param mixed $value value to clamp. This should be a number. + * @param int $min lower bound + * @param int $max upper bound + * @param bool $toInt if true, variable type of $value will be set to int in addition to clamping + */ + public static function clamp(&$value, int $min, int $max, bool $toInt = true): void + { + if (!is_numeric($value) || $value < $min) { + $value = $min; + } elseif ($value > $max) { + $value = $max; + } + if ($toInt) { + settype($value, 'int'); + } + } + } |