diff options
author | Simon Rettberg | 2022-05-02 18:49:09 +0200 |
---|---|---|
committer | Simon Rettberg | 2022-05-02 18:49:09 +0200 |
commit | 5eb8df7432a708284862e4b126e418265d36b4ab (patch) | |
tree | d8812cc2bd6245e0b02ca6866a4c14e977e1bb62 /inc | |
parent | [rebootcontrol] Show time of execution for WOL/reboot/shutdown (diff) | |
download | slx-admin-5eb8df7432a708284862e4b126e418265d36b4ab.tar.gz slx-admin-5eb8df7432a708284862e4b126e418265d36b4ab.tar.xz slx-admin-5eb8df7432a708284862e4b126e418265d36b4ab.zip |
[inc/Util] Add types, move error printing functions to their own class
Diffstat (limited to 'inc')
-rw-r--r-- | inc/arrayutil.inc.php | 18 | ||||
-rw-r--r-- | inc/crypto.inc.php | 2 | ||||
-rw-r--r-- | inc/database.inc.php | 12 | ||||
-rw-r--r-- | inc/download.inc.php | 6 | ||||
-rw-r--r-- | inc/errorhandler.inc.php | 148 | ||||
-rw-r--r-- | inc/module.inc.php | 2 | ||||
-rw-r--r-- | inc/paginate.inc.php | 12 | ||||
-rw-r--r-- | inc/permission.inc.php | 2 | ||||
-rw-r--r-- | inc/render.inc.php | 6 | ||||
-rw-r--r-- | inc/session.inc.php | 10 | ||||
-rw-r--r-- | inc/taskmanager.inc.php | 2 | ||||
-rw-r--r-- | inc/util.inc.php | 221 |
12 files changed, 231 insertions, 210 deletions
diff --git a/inc/arrayutil.inc.php b/inc/arrayutil.inc.php index 3beceb41..d82cdbeb 100644 --- a/inc/arrayutil.inc.php +++ b/inc/arrayutil.inc.php @@ -48,4 +48,22 @@ class ArrayUtil array_multisort($sorter, $sortFlags, $array); } + /** + * Check whether $array 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 $array, array $keyList): bool + { + if (!is_array($array)) + return false; + foreach ($keyList as $key) { + if (!isset($array[$key])) + return false; + } + return true; + } + }
\ No newline at end of file diff --git a/inc/crypto.inc.php b/inc/crypto.inc.php index d3dd60dc..eb0d344f 100644 --- a/inc/crypto.inc.php +++ b/inc/crypto.inc.php @@ -13,7 +13,7 @@ class Crypto $salt = substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16); $hash = crypt($password, '$6$' . $salt); - if (strlen($hash) < 60) Util::traceError('Error hashing password using SHA-512'); + if (strlen($hash) < 60) ErrorHandler::traceError('Error hashing password using SHA-512'); return $hash; } diff --git a/inc/database.inc.php b/inc/database.inc.php index 8d50a02d..8f2ba6d1 100644 --- a/inc/database.inc.php +++ b/inc/database.inc.php @@ -34,7 +34,7 @@ class Database } catch (PDOException $e) { if (self::$returnErrors) return false; - Util::traceError('Connecting to the local database failed: ' . $e->getMessage()); + ErrorHandler::traceError('Connecting to the local database failed: ' . $e->getMessage()); } if (CONFIG_DEBUG) { Database::exec("SET SESSION sql_mode='STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ERROR_FOR_DIVISION_BY_ZERO'"); @@ -203,7 +203,7 @@ class Database self::$lastError = implode("\n", $stmt->errorInfo()); if ($ignoreError === true || ($ignoreError === null && self::$returnErrors)) return false; - Util::traceError("Database Error: \n" . self::$lastError); + ErrorHandler::traceError("Database Error: \n" . self::$lastError); } if (CONFIG_DEBUG) { $duration = microtime(true) - $start; @@ -221,7 +221,7 @@ class Database self::$lastError = '(' . $e->getCode() . ') ' . $e->getMessage(); if ($ignoreError === true || ($ignoreError === null && self::$returnErrors)) return false; - Util::traceError("Database Error: \n" . self::$lastError); + ErrorHandler::traceError("Database Error: \n" . self::$lastError); } return false; } @@ -383,10 +383,10 @@ class Database { // Sanity checks if (array_key_exists($aiKey, $uniqueValues)) { - Util::traceError("$aiKey must not be in \$uniqueValues"); + ErrorHandler::traceError("$aiKey must not be in \$uniqueValues"); } if (is_array($additionalValues) && array_key_exists($aiKey, $additionalValues)) { - Util::traceError("$aiKey must not be in \$additionalValues"); + ErrorHandler::traceError("$aiKey must not be in \$additionalValues"); } // Simple SELECT first $selectSql = 'SELECT ' . $aiKey . ' FROM ' . $table . ' WHERE 1'; @@ -444,7 +444,7 @@ class Database // Insert done, retrieve key again $res = self::queryFirst($selectSql, $uniqueValues); if ($res === false) { - Util::traceError('Could not find value in table ' . $table . ' that was just inserted'); + ErrorHandler::traceError('Could not find value in table ' . $table . ' that was just inserted'); } return $res[$aiKey]; } diff --git a/inc/download.inc.php b/inc/download.inc.php index 39f8e2e2..5a71014e 100644 --- a/inc/download.inc.php +++ b/inc/download.inc.php @@ -14,7 +14,7 @@ class Download if (self::$curlHandle === false) { self::$curlHandle = curl_init(); if (self::$curlHandle === false) { - Util::traceError('Could not initialize cURL'); + ErrorHandler::traceError('Could not initialize cURL'); } curl_setopt(self::$curlHandle, CURLOPT_CONNECTTIMEOUT, ceil($timeout / 2)); curl_setopt(self::$curlHandle, CURLOPT_TIMEOUT, $timeout); @@ -30,7 +30,7 @@ class Download $head = fopen($tmpfile, 'w+b'); unlink($tmpfile); if ($head === false) - Util::traceError("Could not open temporary head file $tmpfile for writing."); + ErrorHandler::traceError("Could not open temporary head file $tmpfile for writing."); curl_setopt(self::$curlHandle, CURLOPT_WRITEHEADER, $head); return self::$curlHandle; } @@ -111,7 +111,7 @@ class Download { $fh = fopen($target, 'wb'); if ($fh === false) - Util::traceError("Could not open $target for writing."); + ErrorHandler::traceError("Could not open $target for writing."); $ch = self::initCurl($url, $timeout, $head); curl_setopt($ch, CURLOPT_FILE, $fh); $res = curl_exec($ch); diff --git a/inc/errorhandler.inc.php b/inc/errorhandler.inc.php new file mode 100644 index 00000000..c7a32b02 --- /dev/null +++ b/inc/errorhandler.inc.php @@ -0,0 +1,148 @@ +<?php + +class ErrorHandler +{ + + + /** + * 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 sensitive 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); + } + + public static function formatBacktraceHtml($trace): string + { + $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): string + { + $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; + } + + 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; + } +}
\ No newline at end of file diff --git a/inc/module.inc.php b/inc/module.inc.php index 55713cd0..ea3af1ba 100644 --- a/inc/module.inc.php +++ b/inc/module.inc.php @@ -189,7 +189,7 @@ class Module { $modulePath = 'modules/' . $this->name . '/page.inc.php'; if (!file_exists($modulePath)) { - Util::traceError("Module doesn't have a page: " . $modulePath); + ErrorHandler::traceError("Module doesn't have a page: " . $modulePath); } require_once $modulePath; $class = 'Page_' . $this->name; diff --git a/inc/paginate.inc.php b/inc/paginate.inc.php index b212e252..3187261c 100644 --- a/inc/paginate.inc.php +++ b/inc/paginate.inc.php @@ -19,30 +19,30 @@ class Paginate $this->currentPage = (isset($_GET['page']) ? (int)$_GET['page'] : 0); $this->perPage = (int)$perPage; if ($this->currentPage < 0) { - Util::traceError('Current page < 0'); + ErrorHandler::traceError('Current page < 0'); } if ($this->perPage < 1) { - Util::traceError('Per page < 1'); + ErrorHandler::traceError('Per page < 1'); } // Query if (!preg_match('/\s*SELECT\s/is', $query)) { - Util::traceError('Query has to start with SELECT!'); + ErrorHandler::traceError('Query has to start with SELECT!'); } // XXX: MySQL only if (preg_match('/^mysql/i', CONFIG_SQL_DSN)) { // Sanity: Check for LIMIT specification at the end if (preg_match('/LIMIT\s+(\d+|\:\w+|\?)\s*,\s*(\d+|\:\w+|\?)(\s|;)*(\-\-.*)?$/is', $query)) { - Util::traceError("You cannot pass a query containing a LIMIT to the Paginator class!"); + ErrorHandler::traceError("You cannot pass a query containing a LIMIT to the Paginator class!"); } // Sanity: no comment or semi-colon at end (sloppy, might lead to false negatives) if (preg_match('/(\-\-|;)(\s|[^\'"`])*$/is', $query)) { - Util::traceError("Your query must not end in a comment or semi-colon!"); + ErrorHandler::traceError("Your query must not end in a comment or semi-colon!"); } // Don't use SQL_CALC_FOUND_ROWS as it leads to filesort frequently thus being slower than two queries // See https://www.percona.com/blog/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/ } else { - Util::traceError('Unsupported database engine'); + ErrorHandler::traceError('Unsupported database engine'); } // Mangle URL if ($url === false) $url = $_SERVER['REQUEST_URI']; diff --git a/inc/permission.inc.php b/inc/permission.inc.php index cd9cc43c..abeabb1d 100644 --- a/inc/permission.inc.php +++ b/inc/permission.inc.php @@ -11,7 +11,7 @@ class Permission public static function get($permission) { - if (!isset(self::$permissions[$permission])) Util::traceError('Invalid permission: ' . $permission); + if (!isset(self::$permissions[$permission])) ErrorHandler::traceError('Invalid permission: ' . $permission); return self::$permissions[$permission]; } diff --git a/inc/render.inc.php b/inc/render.inc.php index 2c3a1da7..697646c5 100644 --- a/inc/render.inc.php +++ b/inc/render.inc.php @@ -28,7 +28,7 @@ class Render public static function init() { if (self::$mustache !== false) - Util::traceError('Called Render::init() twice!'); + ErrorHandler::traceError('Called Render::init() twice!'); $options = array(); $tmp = '/tmp/bwlp-cache'; $dir = is_dir($tmp); @@ -262,10 +262,10 @@ class Render public static function closeTag($tag) { if (empty(self::$tags)) - Util::traceError('Tried to close tag ' . $tag . ' when no open tags exist.'); + ErrorHandler::traceError('Tried to close tag ' . $tag . ' when no open tags exist.'); $last = array_pop(self::$tags); if ($last !== $tag) - Util::traceError('Tried to close tag ' . $tag . ' when last opened tag was ' . $last); + ErrorHandler::traceError('Tried to close tag ' . $tag . ' when last opened tag was ' . $last); self::$body .= '</' . $tag . '>'; } diff --git a/inc/session.inc.php b/inc/session.inc.php index fc875669..3afbc4ce 100644 --- a/inc/session.inc.php +++ b/inc/session.inc.php @@ -13,7 +13,7 @@ class Session private static function generateSessionId(string $salt) { if (self::$sid !== false) - Util::traceError('Error: Asked to generate session id when already set.'); + ErrorHandler::traceError('Error: Asked to generate session id when already set.'); self::$sid = sha1($salt . ',' . mt_rand(0, 65535) . $_SERVER['REMOTE_ADDR'] @@ -74,7 +74,7 @@ class Session public static function set(string $key, $value, $validMinutes = 60) { if (self::$data === false) - Util::traceError('Tried to set session data with no active session'); + ErrorHandler::traceError('Tried to set session data with no active session'); if ($value === false) { unset(self::$data[$key]); } else { @@ -86,7 +86,7 @@ class Session private static function loadSessionId(): bool { if (self::$sid !== false) - Util::traceError('Error: Asked to load session id when already set.'); + ErrorHandler::traceError('Error: Asked to load session id when already set.'); if (empty($_COOKIE['sid'])) return false; $id = preg_replace('/[^a-zA-Z0-9]/', '', $_COOKIE['sid']); @@ -115,7 +115,7 @@ class Session private static function readSessionData(): bool { if (self::$data !== false) - Util::traceError('Tried to call read session data twice'); + ErrorHandler::traceError('Tried to call read session data twice'); $row = Database::queryFirst("SELECT userid, dateline, lastip, fixedip, data FROM session WHERE sid = :sid", ['sid' => self::$sid]); $now = time(); @@ -149,7 +149,7 @@ class Session $ret = setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); if (!$ret) - Util::traceError('Error: Could not set Cookie for Client (headers already sent)'); + ErrorHandler::traceError('Error: Could not set Cookie for Client (headers already sent)'); } register_shutdown_function(function () { Session::saveInternal(); diff --git a/inc/taskmanager.inc.php b/inc/taskmanager.inc.php index f7c72e04..a21dc5f7 100644 --- a/inc/taskmanager.inc.php +++ b/inc/taskmanager.inc.php @@ -288,7 +288,7 @@ class Taskmanager if (count($parts) !== 2) { error_log('TM: Invalid reply, no "," in payload'); } elseif ($parts[0] === 'ERROR') { - Util::traceError('Taskmanager remote error: ' . $parts[1]); + ErrorHandler::traceError('Taskmanager remote error: ' . $parts[1]); } elseif ($parts[0] === 'WARNING') { Message::addWarning('main.taskmanager-warning', $parts[1]); } else { diff --git a/inc/util.inc.php b/inc/util.inc.php index c3e70f89..d95265f4 100644 --- a/inc/util.inc.php +++ b/inc/util.inc.php @@ -5,158 +5,17 @@ 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 sensitive 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) + public static function redirect($location = false, bool $preferRedirectPost = false) { if ($location === false) { - $location = preg_replace('/([&?])message\[\]\=[^&]*/', '\1', $_SERVER['REQUEST_URI']); + $location = preg_replace('/([&?])message\[\]=[^&]*/', '\1', $_SERVER['REQUEST_URI']); } $messages = Message::toRequest(); if ($preferRedirectPost @@ -189,7 +48,7 @@ SADFACE; exit(0); } - public static function addRedirectParam($key, $value) + public static function addRedirectParam(string $key, string $value) { self::$redirectParams[] = $key . '=' . urlencode($value); } @@ -201,7 +60,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; @@ -218,12 +77,12 @@ 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); } @@ -236,7 +95,7 @@ SADFACE; * @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 */ - public static function readableFileSize($bytes, $decimals = -1, $shift = 0) + public static function readableFileSize(float $bytes, int $decimals = -1, int $shift = 0): string { // round doesn't reliably work for large floats, pick workaround depending on OS if (PHP_INT_SIZE === 4) { @@ -257,17 +116,26 @@ SADFACE; return sprintf("%.{$decimals}f", $bytes) . "\xe2\x80\x89" . $sz[$factor + $shift]; } - public static function sanitizeFilename($name) + public static function sanitizeFilename(string $name) { 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 + * @return false|string + */ + public static function safePath(string $path, string $prefix = '') { if (empty($path)) return false; $path = trim($path); - if ($path[0] == '/' || preg_match('/[\x00-\x19\?\*]/', $path)) + if ($path[0] == '/' || preg_match('/[\x00-\x19?*]/', $path)) return false; if (strpos($path, '..') !== false) return false; @@ -288,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: @@ -326,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; @@ -348,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 @@ -374,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(); @@ -404,7 +255,7 @@ SADFACE; * @param bool $secure true = only use strong random sources * @return string|bool string of requested length, false on error */ - public static function randomBytes($length, $secure = true) + public static function randomBytes(int $length, bool $secure = true) { if (function_exists('random_bytes')) { try { @@ -453,7 +304,7 @@ SADFACE; /** * @return string a random UUID, v4. */ - public static function randomUuid() + public static function randomUuid(): string { $b = unpack('h8a/h4b/h12c', self::randomBytes(12)); return sprintf('%08s-%04s-%04x-%04x-%012s', @@ -481,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 */ - public static function prettyTime($ts) + public static function prettyTime(int $ts): string { settype($ts, 'int'); if ($ts === 0) @@ -507,10 +359,11 @@ 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); @@ -519,11 +372,12 @@ SADFACE; /** * 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 + * @param bool $showSecs whether to show seconds, or rather cut after minutes * @return string */ - 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]; @@ -550,9 +404,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) { $parts = explode('/', $_SERVER['SCRIPT_NAME']); $path = ''; @@ -567,7 +422,7 @@ SADFACE; /** * Remove any non-utf8 sequences from string. */ - public static function cleanUtf8(string $string) : string + public static function cleanUtf8(string $string): string { // https://stackoverflow.com/a/1401716/2043481 $regex = '/ @@ -586,7 +441,7 @@ SADFACE; /** * Remove non-printable < 0x20 chars from ANSI string, then convert to UTF-8 */ - public static function ansiToUtf8(string $string) : string + public static function ansiToUtf8(string $string): string { $regex = '/ ( |