summaryrefslogtreecommitdiffstats
path: root/inc
diff options
context:
space:
mode:
Diffstat (limited to 'inc')
-rw-r--r--inc/arrayutil.inc.php66
-rw-r--r--inc/crypto.inc.php18
-rw-r--r--inc/dashboard.inc.php35
-rw-r--r--inc/database.inc.php155
-rw-r--r--inc/dictionary.inc.php101
-rw-r--r--inc/download.inc.php76
-rw-r--r--inc/errorhandler.inc.php153
-rw-r--r--inc/event.inc.php16
-rw-r--r--inc/eventlog.inc.php61
-rw-r--r--inc/fileutil.inc.php18
-rw-r--r--inc/hook.inc.php19
-rw-r--r--inc/iputil.inc.php41
-rw-r--r--inc/mailer.inc.php186
-rw-r--r--inc/message.inc.php47
-rw-r--r--inc/module.inc.php69
-rw-r--r--inc/paginate.inc.php39
-rw-r--r--inc/permission.inc.php20
-rw-r--r--inc/property.inc.php159
-rw-r--r--inc/render.inc.php63
-rw-r--r--inc/request.inc.php16
-rw-r--r--inc/session.inc.php178
-rw-r--r--inc/taskmanager.inc.php38
-rw-r--r--inc/taskmanagercallback.inc.php61
-rw-r--r--inc/trigger.inc.php56
-rw-r--r--inc/user.inc.php68
-rw-r--r--inc/util.inc.php298
26 files changed, 1284 insertions, 773 deletions
diff --git a/inc/arrayutil.inc.php b/inc/arrayutil.inc.php
index 490b5a4f..3d93d7d5 100644
--- a/inc/arrayutil.inc.php
+++ b/inc/arrayutil.inc.php
@@ -1,33 +1,24 @@
<?php
+declare(strict_types=1);
+
class ArrayUtil
{
/**
* Take an array of arrays, take given key from each sub-array and return
* new array with just those corresponding values.
- * @param array $list
- * @param string $key
- * @return array
*/
- public static function flattenByKey(array $list, string $key)
+ public static function flattenByKey(array $list, string $key): array
{
- $ret = [];
- foreach ($list as $item) {
- if (array_key_exists($key, $item)) {
- $ret[] = $item[$key];
- }
- }
- return $ret;
+ return array_column($list, $key);
}
/**
* Pass an array of arrays you want to merge. The keys of the outer array will become
* the inner keys of the resulting array, and vice versa.
- * @param array $arrays
- * @return array
*/
- public static function mergeByKey(array $arrays)
+ public static function mergeByKey(array $arrays): array
{
$empty = array_combine(array_keys($arrays), array_fill(0, count($arrays), false));
$out = [];
@@ -42,4 +33,51 @@ class ArrayUtil
return $out;
}
+ /**
+ * Sort array by given column.
+ */
+ public static function sortByColumn(array &$array, string $column, int $sortOrder = SORT_ASC, int $sortFlags = SORT_REGULAR): void
+ {
+ $sorter = array_column($array, $column);
+ array_multisort($sorter, $sortOrder, $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
+ */
+ public static function hasAllKeys(array $array, array $keyList): bool
+ {
+ foreach ($keyList as $key) {
+ if (!isset($array[$key]))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if all elements in given array are primitive types,
+ * i.e. not object, array or resource.
+ */
+ public static function isOnlyPrimitiveTypes(array $array): bool
+ {
+ foreach ($array as $item) {
+ if (is_array($item) || is_object($item) || is_resource($item))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Force each element of given array to be of type $type.
+ */
+ public static function forceType(array &$array, string $type): void
+ {
+ foreach ($array as &$elem) {
+ settype($elem, $type);
+ }
+ }
+
} \ No newline at end of file
diff --git a/inc/crypto.inc.php b/inc/crypto.inc.php
index 56f5073c..acefcf67 100644
--- a/inc/crypto.inc.php
+++ b/inc/crypto.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class Crypto
{
@@ -8,19 +10,25 @@ class Crypto
* which translates to ~130 bit salt
* and 5000 rounds of hashing with SHA-512.
*/
- public static function hash6($password)
+ public static function hash6(string $password): string
{
- $salt = substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16);
+ $bytes = Util::randomBytes(16);
+ if ($bytes === null)
+ ErrorHandler::traceError('Could not get random bytes');
+ $salt = substr(str_replace('+', '.',
+ base64_encode($bytes)), 0, 16);
$hash = crypt($password, '$6$' . $salt);
- if (strlen($hash) < 60) Util::traceError('Error hashing password using SHA-512');
+ if ($hash === null || strlen($hash) < 60) {
+ ErrorHandler::traceError('Error hashing password using SHA-512');
+ }
return $hash;
}
/**
- * Check if the given password matches the given cryp hash.
+ * Check if the given password matches the given crypt hash.
* Useful for checking a hashed password.
*/
- public static function verify($password, $hash)
+ public static function verify(string $password, string $hash): bool
{
return crypt($password, $hash) === $hash;
}
diff --git a/inc/dashboard.inc.php b/inc/dashboard.inc.php
index 0b0f69e3..449fe7c0 100644
--- a/inc/dashboard.inc.php
+++ b/inc/dashboard.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class Dashboard
{
@@ -7,12 +9,12 @@ class Dashboard
private static $subMenu = array();
private static $disabled = false;
- public static function disable()
+ public static function disable(): void
{
self::$disabled = true;
}
- public static function createMenu()
+ public static function createMenu(): void
{
if (self::$disabled)
return;
@@ -22,7 +24,7 @@ class Dashboard
if (isset($MENU_CAT_OVERRIDE)) {
foreach ($MENU_CAT_OVERRIDE as $cat => $list) {
foreach ($list as $mod) {
- $modByCategory[$cat][$mod] = false;
+ $modByCategory[$cat][$mod] = null;
$modById[$mod] =& $modByCategory[$cat][$mod];
}
}
@@ -30,10 +32,10 @@ class Dashboard
$all = Module::getEnabled(true);
foreach ($all as $module) {
$cat = $module->getCategory();
- if ($cat === false)
+ if (empty($cat))
continue;
$modId = $module->getIdentifier();
- if (isset($modById[$modId])) {
+ if (array_key_exists($modId, $modById)) {
$modById[$modId] = $module;
} else {
$modByCategory[$cat][$modId] = $module;
@@ -43,10 +45,10 @@ class Dashboard
$categories = array();
foreach ($modByCategory as $catId => $modList) {
$collapse = true;
- /* @var Module[] $modList */
+ /* @var (?Module)[] $modList */
$modules = array();
- foreach ($modList as $modId => $module) {
- if ($module === false)
+ foreach ($modList as $module) {
+ if ($module === null)
continue; // Was set in $MENU_CAT_OVERRIDE, but is not enabled
$newEntry = array(
'displayName' => $module->getDisplayName(),
@@ -62,28 +64,25 @@ class Dashboard
}
$modules[] = $newEntry;
}
- $categories[] = array(
+ $categories[] = [
'icon' => self::getCategoryIcon($catId),
'displayName' => Dictionary::getCategoryName($catId),
'modules' => $modules,
'collapse' => $collapse,
- );
+ ];
}
- Render::setDashboard(array(
+ Render::setDashboard([
'categories' => $categories,
'url' => urlencode($_SERVER['REQUEST_URI']),
'langs' => Dictionary::getLanguages(true),
'user' => User::getName(),
'warning' => User::getName() !== false && User::hasPermission('.eventlog.*') && User::getLastSeenEvent() < Property::getLastWarningId(),
'needsSetup' => User::getName() !== false && Property::getNeedsSetup()
- ));
+ ]);
}
- public static function getCategoryIcon($category)
+ public static function getCategoryIcon(string $category): string
{
- if ($category === false) {
- return '';
- }
if (!preg_match('/^(\w+)\.(.*)$/', $category, $out)) {
error_log('Requested category icon for invalid category "' . $category . '"');
return '';
@@ -105,12 +104,12 @@ class Dashboard
return 'glyphicon glyphicon-' . self::$iconCache[$module][$icon];
}
- public static function addSubmenu($url, $name)
+ public static function addSubmenu(string $url, string $name): void
{
self::$subMenu[] = array('url' => $url, 'name' => $name);
}
- public static function getSubmenus()
+ public static function getSubmenus(): array
{
return self::$subMenu;
}
diff --git a/inc/database.inc.php b/inc/database.inc.php
index eddd4faf..83720baa 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Handle communication with the database
* This is a very thin layer between you and PDO.
@@ -11,37 +13,38 @@ class Database
* @var \PDO Database handle
*/
private static $dbh = false;
- /*
- * @var \PDOStatement[]
- */
- private static $statements = array();
- private static $returnErrors;
- private static $lastError = false;
+
+ /** @var bool */
+ private static $returnErrors = false;
+ /** @var ?string */
+ private static $lastError = null;
private static $explainList = array();
private static $queryCount = 0;
+ /** @var float */
private static $queryTime = 0;
/**
* Connect to the DB if not already connected.
*/
- public static function init($returnErrors = false)
+ public static function init(bool $returnErrors = false): bool
{
if (self::$dbh !== false)
return true;
self::$returnErrors = $returnErrors;
try {
- if (CONFIG_SQL_FORCE_UTF8) {
- self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
- } else {
- self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS);
- }
+ self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS, [
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => true,
+ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', // Somehow needed, even if charset=utf8mb4 is in DSN?
+ ]);
} 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 sql_mode='STRICT_TRANS_TABLES'");
+ Database::exec("SET SESSION sql_mode='STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ERROR_FOR_DIVISION_BY_ZERO'");
+ Database::exec("SET SESSION innodb_strict_mode=ON");
register_shutdown_function(function() {
self::examineLoggedQueries();
});
@@ -54,12 +57,12 @@ class Database
*
* @return array|boolean Associative array representing row, or false if no row matches the query
*/
- public static function queryFirst($query, $args = array(), $ignoreError = null)
+ public static function queryFirst(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
return false;
- return $res->fetch(PDO::FETCH_ASSOC);
+ return $res->fetch();
}
/**
@@ -69,12 +72,12 @@ class Database
*
* @return array|bool List of associative arrays representing rows, or false on error
*/
- public static function queryAll($query, $args = array(), $ignoreError = null)
+ public static function queryAll(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
return false;
- return $res->fetchAll(PDO::FETCH_ASSOC);
+ return $res->fetchAll();
}
/**
@@ -82,7 +85,7 @@ class Database
*
* @return array|bool List of values representing first column of query
*/
- public static function queryColumnArray($query, $args = array(), $ignoreError = null)
+ public static function queryColumnArray(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
@@ -91,15 +94,63 @@ class Database
}
/**
+ * Fetch two columns as key => value list.
+ *
+ * @return array|bool Associative array, first column is key, second column is value
+ */
+ public static function queryKeyValueList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_KEY_PAIR);
+ }
+
+ /**
+ * Fetch and group by first column. First column is key, value is a list of rows with remaining columns.
+ * [
+ * col1 => [
+ * [col2, col3],
+ * [col2, col3],
+ * ],
+ * ...,
+ * ]
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryGroupList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP);
+ }
+
+ /**
+ * Fetch and use first column as key of returned array.
+ * This is like queryGroup list, but it is assumed that the first column is unique, so
+ * the remaining columns won't be wrapped in another array.
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryIndexedList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_UNIQUE);
+ }
+
+ /**
* Execute the given query and return the number of rows affected.
* Mostly useful for UPDATEs or INSERTs
*
* @param string $query Query to run
* @param array $args Arguments to query
- * @param boolean $ignoreError Ignore query errors and just return false
+ * @param ?bool $ignoreError Ignore query errors and just return false
* @return int|boolean Number of rows affected, or false on error
*/
- public static function exec($query, $args = array(), $ignoreError = null)
+ public static function exec(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
@@ -108,19 +159,19 @@ class Database
}
/**
- * Get id (promary key) of last row inserted.
+ * Get id (primary key) of last row inserted.
*
* @return int the id
*/
- public static function lastInsertId()
+ public static function lastInsertId(): int
{
- return self::$dbh->lastInsertId();
+ return (int)self::$dbh->lastInsertId();
}
/**
- * @return string|bool return last error returned by query
+ * @return ?string return last error returned by query
*/
- public static function lastError()
+ public static function lastError(): ?string
{
return self::$lastError;
}
@@ -133,10 +184,10 @@ class Database
*
* @return \PDOStatement|false The query result object
*/
- public static function simpleQuery($query, $args = array(), $ignoreError = null)
+ public static function simpleQuery(string $query, array $args = [], bool $ignoreError = null)
{
self::init();
- if (CONFIG_DEBUG && !isset(self::$explainList[$query]) && preg_match('/^\s*SELECT/is', $query)) {
+ if (CONFIG_DEBUG && !isset(self::$explainList[$query]) && preg_match('/^\s*SELECT/i', $query)) {
self::$explainList[$query] = [$args];
}
// Support passing nested arrays for IN statements, automagically refactor
@@ -152,17 +203,13 @@ class Database
}
}
try {
- if (!isset(self::$statements[$query])) {
- self::$statements[$query] = self::$dbh->prepare($query);
- } else {
- //self::$statements[$query]->closeCursor();
- }
+ $stmt = self::$dbh->prepare($query);
$start = microtime(true);
- if (self::$statements[$query]->execute($args) === false) {
- self::$lastError = implode("\n", self::$statements[$query]->errorInfo());
+ if ($stmt->execute($args) === false) {
+ 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;
@@ -175,14 +222,13 @@ class Database
}
self::$queryCount += 1;
}
- return self::$statements[$query];
+ return $stmt;
} catch (Exception $e) {
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;
}
public static function examineLoggedQueries()
@@ -192,7 +238,7 @@ class Database
}
}
- private static function explainQuery($query, $data)
+ private static function explainQuery(string $query, array $data)
{
$args = array_shift($data);
$slow = false;
@@ -212,7 +258,7 @@ class Database
$res = self::simpleQuery('EXPLAIN ' . $query, $args, true);
if ($res === false)
return;
- $rows = $res->fetchAll(PDO::FETCH_ASSOC);
+ $rows = $res->fetchAll();
if (empty($rows))
return;
$log = $veryslow;
@@ -225,7 +271,7 @@ class Database
$log = true;
}
foreach ($row as $key => $col) {
- $l = strlen($col);
+ $l = strlen((string)($col ?? 'NULL'));
if ($l > $lens[$key]) {
$lens[$key] = $l;
}
@@ -246,7 +292,7 @@ class Database
foreach ($rows as $row) {
$line = '';
foreach ($lens as $key => $len) {
- $line .= '| '. str_pad($row[$key], $len) . ' ';
+ $line .= '| '. str_pad((string)($row[$key] ?? 'NULL'), $len) . ' ';
}
error_log($line . "|");
}
@@ -264,8 +310,9 @@ class Database
*
* @param string $query sql query string
* @param array $args query arguments
+ * @return void
*/
- private static function handleArrayArgument(&$query, &$args)
+ private static function handleArrayArgument(string &$query, array &$args)
{
$again = false;
foreach (array_keys($args) as $key) {
@@ -279,7 +326,7 @@ class Database
continue;
}
$newkey = $key;
- if ($newkey{0} !== ':') {
+ if ($newkey[0] !== ':') {
$newkey = ":$newkey";
}
$new = array();
@@ -306,7 +353,7 @@ class Database
* Simply calls PDO::prepare and returns the PDOStatement.
* You must call PDOStatement::execute manually on it.
*/
- public static function prepare($query)
+ public static function prepare(string $query)
{
self::init();
self::$queryCount += 1; // Cannot know actual count
@@ -334,17 +381,17 @@ class Database
* @param string $table table to insert into
* @param string $aiKey name of the AUTO_INCREMENT column
* @param array $uniqueValues assoc array containing columnName => value mapping
- * @param array $additionalValues assoc array containing columnName => value mapping
- * @return int[] list of AUTO_INCREMENT values matching the list of $values
+ * @param ?array $additionalValues assoc array containing columnName => value mapping
+ * @return int AUTO_INCREMENT value matching the given unique values entry
*/
- public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false)
+ public static function insertIgnore(string $table, string $aiKey, array $uniqueValues, array $additionalValues = null): int
{
// 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';
@@ -402,17 +449,17 @@ 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];
}
- public static function getQueryCount()
+ public static function getQueryCount(): int
{
return self::$queryCount;
}
- public static function getQueryTime()
+ public static function getQueryTime(): float
{
return self::$queryTime;
}
diff --git a/inc/dictionary.inc.php b/inc/dictionary.inc.php
index e366207f..3a2f9c2b 100644
--- a/inc/dictionary.inc.php
+++ b/inc/dictionary.inc.php
@@ -1,21 +1,22 @@
<?php
+declare(strict_types=1);
+
class Dictionary
{
/**
* @var string[] Array of languages, numeric index, two letter CC as values
*/
- private static $languages = false;
+ private static $languages = [];
/**
- * @var array Array of languages, numeric index, values are ['name' => 'Language Name', 'cc' => 'xx']
+ * @var array{'name': string, 'cc': string}|null Long name of language, and CC
*/
- private static $languagesLong = false;
- private static $stringCache = array();
+ private static $languagesLong = null;
+ private static $stringCache = [];
- public static function init()
+ public static function init(): void
{
- self::$languages = array();
foreach (glob('lang/??', GLOB_ONLYDIR) as $lang) {
if (!file_exists($lang . '/name.txt') && !file_exists($lang . '/flag.png'))
continue;
@@ -64,29 +65,56 @@ class Dictionary
}
/**
+ * Format given number using country-specific decimal point and thousands
+ * separator.
+ * @param float $num Number to format
+ * @param int $decimals How many decimals to display
+ */
+ public static function number(float $num, int $decimals = 0): string
+ {
+ static $dec = null, $tho = null;
+ if ($dec === null) {
+ if (LANG === 'de') {
+ $dec = ',';
+ $tho = '.';
+ } elseif (LANG !== 'en' && file_exists("lang/" . LANG . "/format.txt")) {
+ $tmp = file_get_contents("lang/" . LANG . "/format.txt");
+ $dec = $tmp[0];
+ $tho = $tmp[1];
+ } else {
+ $dec = '.';
+ $tho = ',';
+ }
+ }
+ return number_format($num, $decimals, $dec, $tho);
+ }
+
+ /**
* Get complete key=>value list for given module, file, language
*
* @param string $module Module name
* @param string $file Dictionary name
- * @param string|false $lang Language CC, false === current language
+ * @param ?string $lang Language CC, false === current language
* @return array assoc array mapping language tags to the translated strings
*/
- public static function getArray($module, $file, $lang = false)
+ public static function getArray(string $module, string $file, ?string $lang = null): array
{
- if ($lang === false)
+ if ($lang === null)
$lang = LANG;
$path = Util::safePath("modules/{$module}/lang/{$lang}/{$file}.json");
+ if ($path === null)
+ ErrorHandler::traceError("Invalid path");
if (isset(self::$stringCache[$path]))
return self::$stringCache[$path];
if (!file_exists($path))
- return array();
+ return [];
$content = file_get_contents($path);
if ($content === false) { // File does not exist for language
$content = '[]';
}
$json = json_decode($content, true);
if (!is_array($json)) {
- $json = array();
+ $json = [];
}
return self::$stringCache[$path] = $json;
}
@@ -101,7 +129,7 @@ class Dictionary
* @param bool $returnTagOnMissing If true, the tag name enclosed in {{}} will be returned if the tag does not exist
* @return string|false The requested tag's translation, or false if not found and $returnTagOnMissing === false
*/
- public static function translateFileModule($moduleId, $file, $tag, $returnTagOnMissing = false)
+ public static function translateFileModule(string $moduleId, string $file, string $tag, bool $returnTagOnMissing = true)
{
$strings = self::getArray($moduleId, $file);
if (!isset($strings[$tag])) {
@@ -121,7 +149,7 @@ class Dictionary
* @param bool $returnTagOnMissing If true, the tag name enclosed in {{}} will be returned if the tag does not exist
* @return string|false The requested tag's translation, or false if not found and $returnTagOnMissing === false
*/
- public static function translateFile($file, $tag, $returnTagOnMissing = false)
+ public static function translateFile(string $file, string $tag, bool $returnTagOnMissing = true)
{
if (!class_exists('Page') || Page::getModule() === false)
return false; // We have no page - return false for now, as we're most likely running in api or install mode
@@ -135,9 +163,9 @@ class Dictionary
* @param bool $returnTagOnMissing If true, the tag name enclosed in {{}} will be returned if the tag does not exist
* @return string|false The requested tag's translation, or false if not found and $returnTagOnMissing === false
*/
- public static function translate($tag, $returnTagOnMissing = false)
+ public static function translate(string $tag, bool $returnTagOnMissing = true)
{
- $string = self::translateFile('module', $tag);
+ $string = self::translateFile('module', $tag, false);
if ($string !== false)
return $string;
$string = self::translateFileModule('main', 'global-tags', $tag);
@@ -151,9 +179,8 @@ class Dictionary
*
* @param string $module Module the message belongs to
* @param string $id Message id
- * @return string|false
*/
- public static function getMessage($module, $id)
+ public static function getMessage(string $module, string $id): string
{
$string = self::translateFileModule($module, 'messages', $id);
if ($string === false) {
@@ -165,19 +192,18 @@ class Dictionary
/**
* Get translation of the given category.
*
- * @param string $category
+ * @param string $category Menu category to get localized name for
* @return string Category name, or some generic fallback to the given category id
*/
- public static function getCategoryName($category)
+ public static function getCategoryName(string $category): string
{
- if ($category === false) {
- return 'No Category';
- }
- if (!preg_match('/^(\w+)\.(.*)$/', $category, $out)) {
- return 'Invalid Category ID format: ' . $category;
+ if (!empty($category)) {
+ if (!preg_match('/^(\w+)\.(.*)$/', $category, $out)) {
+ return 'Invalid Category ID format: ' . $category;
+ }
+ $string = self::translateFileModule($out[1], 'categories', $out[2]);
}
- $string = self::translateFileModule($out[1], 'categories', $out[2]);
- if ($string === false) {
+ if (empty($category) || $string === false) {
return "!!{$category}!!";
}
return $string;
@@ -190,12 +216,12 @@ class Dictionary
* false = regular array containing only the ccs
* @return array List of languages
*/
- public static function getLanguages($withName = false)
+ public static function getLanguages(bool $withName = false): ?array
{
if (!$withName)
return self::$languages;
- if (self::$languagesLong === false) {
- self::$languagesLong = array();
+ if (self::$languagesLong === null) {
+ self::$languagesLong = [];
foreach (self::$languages as $lang) {
if (file_exists("lang/$lang/name.txt")) {
$name = file_get_contents("lang/$lang/name.txt");
@@ -205,10 +231,10 @@ class Dictionary
if (!isset($name) || $name === false) {
$name = $lang;
}
- self::$languagesLong[] = array(
+ self::$languagesLong[] = [
'cc' => $lang,
- 'name' => $name
- );
+ 'name' => $name,
+ ];
}
}
return self::$languagesLong;
@@ -217,11 +243,8 @@ class Dictionary
/**
* Get name of language matching given language CC.
* Default to the CC if the language isn't known.
- *
- * @param string $langCC
- * @return string
*/
- public static function getLanguageName($langCC)
+ public static function getLanguageName(string $langCC): string
{
if (file_exists("lang/$langCC/name.txt")) {
$name = file_get_contents("lang/$langCC/name.txt");
@@ -239,12 +262,12 @@ class Dictionary
* to the image, otherwise, it is just added as the title attribute.
*
* @param $caption bool with caption next to <img>
- * @param $langCC string Language cc to get flag code for - defaults to current language
+ * @param $langCC ?string Language cc to get flag code for - defaults to current language
* @return string html code of img tag for language
*/
- public static function getFlagHtml($caption = false, $langCC = false)
+ public static function getFlagHtml(bool $caption = false, string $langCC = null): string
{
- if ($langCC === false) {
+ if ($langCC === null) {
$langCC = LANG;
}
$flag = "lang/$langCC/flag.png";
diff --git a/inc/download.inc.php b/inc/download.inc.php
index 39f8e2e2..8358eaa3 100644
--- a/inc/download.inc.php
+++ b/inc/download.inc.php
@@ -1,67 +1,53 @@
<?php
+declare(strict_types=1);
+
class Download
{
+ /**
+ * @var false|resource
+ */
private static $curlHandle = false;
/**
* Common initialization for download and downloadToFile
* Return file handle to header file
+ * @return false|resource
*/
- private static function initCurl($url, $timeout, &$head)
+ private static function initCurl(string $url, int $timeout)
{
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);
curl_setopt(self::$curlHandle, CURLOPT_FOLLOWLOCATION, true);
curl_setopt(self::$curlHandle, CURLOPT_AUTOREFERER, true);
- curl_setopt(self::$curlHandle, CURLOPT_BINARYTRANSFER, true);
curl_setopt(self::$curlHandle, CURLOPT_MAXREDIRS, 6);
+ curl_setopt(self::$curlHandle, CURLOPT_ACCEPT_ENCODING, '');
+ curl_setopt(self::$curlHandle, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) SLX-Admin/1.0');
+ curl_setopt(self::$curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_FTP | CURLPROTO_FTPS | CURLPROTO_HTTP | CURLPROTO_HTTPS);
}
curl_setopt(self::$curlHandle, CURLOPT_URL, $url);
- $tmpfile = tempnam('/tmp/', 'bwlp-');
- $head = fopen($tmpfile, 'w+b');
- unlink($tmpfile);
- if ($head === false)
- Util::traceError("Could not open temporary head file $tmpfile for writing.");
- curl_setopt(self::$curlHandle, CURLOPT_WRITEHEADER, $head);
return self::$curlHandle;
}
/**
- * Read 10kb from the given file handle, seek to 0 first,
- * close the file after reading. Returns data read
- */
- private static function getContents($fh)
- {
- fseek($fh, 0, SEEK_SET);
- $data = fread($fh, 10000);
- fclose($fh);
- return $data;
- }
-
- /**
* Download file, obey given timeout in seconds
* Return data on success, false on failure
*/
- public static function asString($url, $timeout, &$code)
+ public static function asString(string $url, int $timeout, ?int &$code)
{
- $ch = self::initCurl($url, $timeout, $head);
+ $ch = self::initCurl($url, $timeout);
+ curl_setopt($ch, CURLOPT_FILE, null);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($ch);
- $head = self::getContents($head);
- if (preg_match_all('#^HTTP/\d+\.\d+ (\d+) #m', $head, $out)) {
- $code = (int) array_pop($out[1]);
- } else {
- $code = 999;
- }
+ $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
return $data;
}
@@ -71,9 +57,10 @@ class Download
* @param string $url URL to fetch
* @param array|false $params POST params to set in body, list of key-value-pairs
* @param int $timeout timeout in seconds
- * @param int $code HTTP response code, or 999 on error
+ * @param ?int $code HTTP response code, or 999 on error
+ * @return string|false
*/
- public static function asStringPost($url, $params, $timeout, &$code)
+ public static function asStringPost(string $url, $params, int $timeout, ?int &$code)
{
$string = '';
if (is_array($params)) {
@@ -84,17 +71,13 @@ class Download
$string .= $k . '=' . urlencode($v);
}
}
- $ch = self::initCurl($url, $timeout, $head);
+ $ch = self::initCurl($url, $timeout);
+ curl_setopt($ch, CURLOPT_FILE, null);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $string);
$data = curl_exec($ch);
- $head = self::getContents($head);
- if (preg_match_all('#^HTTP/\d+\.\d+ (\d+) #m', $head, $out)) {
- $code = (int) array_pop($out[1]);
- } else {
- $code = 999;
- }
+ $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
return $data;
}
@@ -104,28 +87,23 @@ class Download
* @param string $target destination path to download file to
* @param string $url URL of file to download
* @param int $timeout timeout in seconds
- * @param int $code HTTP status code passed out by reference
- * @return boolean
+ * @param ?int $code HTTP status code passed out by reference
*/
- public static function toFile($target, $url, $timeout, &$code)
+ public static function toFile(string $target, string $url, int $timeout, ?int &$code): bool
{
$fh = fopen($target, 'wb');
if ($fh === false)
- Util::traceError("Could not open $target for writing.");
- $ch = self::initCurl($url, $timeout, $head);
+ ErrorHandler::traceError("Could not open $target for writing.");
+ $ch = self::initCurl($url, $timeout);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_FILE, $fh);
$res = curl_exec($ch);
- $head = self::getContents($head);
+ $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
fclose($fh);
if ($res === false) {
@unlink($target);
return false;
}
- if (preg_match_all('#^HTTP/\d+\.\d+ (\d+) #m', $head, $out)) {
- $code = (int) array_pop($out[1]);
- } else {
- $code = '999 ' . curl_error($ch);
- }
return true;
}
diff --git a/inc/errorhandler.inc.php b/inc/errorhandler.inc.php
new file mode 100644
index 00000000..ce969966
--- /dev/null
+++ b/inc/errorhandler.inc.php
@@ -0,0 +1,153 @@
+<?php
+
+declare(strict_types=1);
+
+use JetBrains\PhpStorm\NoReturn;
+
+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)
+ */
+ #[NoReturn]
+ public static function traceError(string $message): void
+ {
+ 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(array $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(array $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, bool $expandArray = true): string
+ {
+ 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 (string)$arg;
+ }
+} \ No newline at end of file
diff --git a/inc/event.inc.php b/inc/event.inc.php
index 2d916b48..4d02b580 100644
--- a/inc/event.inc.php
+++ b/inc/event.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Class with static functions that are called when a specific event
* took place, like the server has been booted, or the interface address
@@ -15,7 +17,7 @@ class Event
* Called when the system (re)booted. Could be implemented
* by a @reboot entry in crontab (running as the same user php does)
*/
- public static function systemBooted()
+ public static function systemBooted(): void
{
EventLog::info('System boot...');
$everythingFine = true;
@@ -45,7 +47,7 @@ class Event
// Check status of all tasks
// Mount vm store
- if ($mountId === false) {
+ if ($mountId === null) {
EventLog::info('No VM store type defined.');
$everythingFine = false;
} else {
@@ -58,13 +60,13 @@ class Event
$everythingFine = false;
}
// iPXE generation
- if ($ipxeId === false) {
+ if ($ipxeId === null) {
EventLog::failure('Cannot generate PXE menu: Taskmanager unreachable!');
$everythingFine = false;
} else {
$res = Taskmanager::waitComplete($ipxeId, 5000);
if (Taskmanager::isFailed($res)) {
- EventLog::failure('Update PXE Menu failed', $res['data']['error'] ?? $res['data']['error'] ?? '');
+ EventLog::failure('Update PXE Menu failed', $res['data']['error'] ?? $res['statusCode'] ?? '');
$everythingFine = false;
}
}
@@ -78,10 +80,10 @@ class Event
$mountId = Trigger::mount();
$mountStatus = Taskmanager::waitComplete($mountId, 10000);
}
- if ($mountId !== false && Taskmanager::isFailed($mountStatus)) {
+ if ($mountId !== null && Taskmanager::isFailed($mountStatus)) {
EventLog::failure('Mounting VM store failed', $mountStatus['data']['messages'] ?? '');
$everythingFine = false;
- } elseif ($mountId !== false && !Taskmanager::isFinished($mountStatus)) {
+ } elseif ($mountId !== null && !Taskmanager::isFinished($mountStatus)) {
// TODO: Still running - create callback
}
@@ -96,7 +98,7 @@ class Event
/**
* Server's primary IP address changed.
*/
- public static function serverIpChanged()
+ public static function serverIpChanged(): void
{
Trigger::ipxe();
if (Module::isAvailable('sysconfig')) { // TODO: Modularize events
diff --git a/inc/eventlog.inc.php b/inc/eventlog.inc.php
index 3ebb82a4..99585abd 100644
--- a/inc/eventlog.inc.php
+++ b/inc/eventlog.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Class to add entries to the event log. Technically this class belongs to the
* eventlog module, but since it is used in so many places, this helper resides
@@ -10,46 +12,71 @@
class EventLog
{
- private static function log($type, $message, $details)
+ private static function log(string $type, string $message, string $details, bool $markWarning): void
{
if (!Module::isAvailable('eventlog')) {
// Eventlog module does not exist; the eventlog table might not exist, so bail out
error_log($message);
return;
}
- Database::exec("INSERT INTO eventlog (dateline, logtypeid, description, extra)"
- . " VALUES (UNIX_TIMESTAMP(), :type, :message, :details)", array(
- 'type' => $type,
- 'message' => $message,
- 'details' => $details
- ), true);
+ if (mb_strlen($message) > 255) {
+ $message = mb_substr($message, 0, 255);
+ }
+ if (mb_strlen($details) > 65535) {
+ $details = mb_substr($details, 0, 65535);
+ }
+ $data = [
+ 'type' => $type,
+ 'message' => $message,
+ 'details' => $details,
+ ];
+ if (Database::exec("INSERT INTO eventlog (dateline, logtypeid, description, extra)"
+ . " VALUES (UNIX_TIMESTAMP(), :type, :message, :details)", $data, true) === false) {
+ error_log($message);
+ } else {
+ // Insert ok, see if we should update the "latest warning" id
+ $id = Database::lastInsertId();
+ if ($id !== 0 && $markWarning) {
+ Property::setLastWarningId($id);
+ }
+ }
+ self::applyFilterRules('#serverlog', $data);
}
- public static function failure($message, $details = '')
+ public static function failure(string $message, string $details = ''): void
{
- self::log('failure', $message, $details);
- Property::setLastWarningId(Database::lastInsertId());
+ self::log('failure', $message, $details, true);
}
- public static function warning($message, $details = '')
+ public static function warning(string $message, string $details = ''): void
{
- self::log('warning', $message, $details);
- Property::setLastWarningId(Database::lastInsertId());
+ self::log('warning', $message, $details, true);
}
- public static function info($message, $details = '')
+ public static function info(string $message, string $details = ''): void
{
- self::log('info', $message, $details);
+ self::log('info', $message, $details, false);
}
/**
* DELETE ENTIRE EVENT LOG!
*/
- public static function clear()
+ public static function clear(): void
{
if (!Module::isAvailable('eventlog'))
return;
Database::exec("TRUNCATE eventlog");
}
-
+
+ /**
+ * @param string $type the event. Will either be client state like ~poweron, ~runstate etc. or a client log type
+ * @param array $data A structured array containing event specific data that can be matched.
+ */
+ public static function applyFilterRules(string $type, array $data): void
+ {
+ if (!Module::isAvailable('eventlog'))
+ return;
+ FilterRuleProcessor::applyFilterRules($type, $data);
+ }
+
}
diff --git a/inc/fileutil.inc.php b/inc/fileutil.inc.php
index f35f987e..e986b2de 100644
--- a/inc/fileutil.inc.php
+++ b/inc/fileutil.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class FileUtil
{
@@ -8,9 +10,9 @@ class FileUtil
*
* @param string $file file to read
* @param int $maxBytes maximum length to read
- * @return boolean|string data, false on error
+ * @return false|string data, false on error
*/
- public static function readFile($file, $maxBytes = 1000)
+ public static function readFile(string $file, int $maxBytes = 1000)
{
$fh = @fopen($file, 'rb');
if ($fh === false)
@@ -19,18 +21,18 @@ class FileUtil
fclose($fh);
return $data;
}
-
+
/**
* Read a file of key=value lines to assoc array.
*
* @param string $file Filename
- * @return boolean|array assoc array, false on error
+ * @return ?array assoc array, null on error
*/
- public static function fileToArray($file)
+ public static function fileToArray(string $file): ?array
{
$data = self::readFile($file, 2000);
if ($data === false)
- return false;
+ return null;
$data = explode("\n", str_replace("\r", "\n", $data));
$ret = array();
foreach ($data as $line) {
@@ -40,7 +42,7 @@ class FileUtil
}
return $ret;
}
-
+
/**
* Write given associative array to file as key=value pairs.
*
@@ -48,7 +50,7 @@ class FileUtil
* @param array $array Associative array to write
* @return boolean success of operation
*/
- public static function arrayToFile($file, $array)
+ public static function arrayToFile(string $file, array $array): bool
{
$fh = fopen($file, 'wb');
if ($fh === false)
diff --git a/inc/hook.inc.php b/inc/hook.inc.php
index 05078f72..f7ca617d 100644
--- a/inc/hook.inc.php
+++ b/inc/hook.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Generic helper for getting and executing hooks.
*/
@@ -11,11 +13,12 @@ class Hook
* Internally, this scans for "modules/<*>/hooks/$hookName.inc.php"
* and optionally checks if the module's dependencies are fulfilled,
* then returns a list of all matching modules.
+ *
* @param string $hookName Name of hook to search for.
* @param bool $filterBroken if true, modules that have a hook but have missing deps will not be returned
- * @return \Hook[] list of modules with requested hooks
+ * @return Hook[] list of modules with requested hooks
*/
- public static function load($hookName, $filterBroken = true)
+ public static function load(string $hookName, bool $filterBroken = true): array
{
$retval = array();
foreach (glob('modules/*/hooks/' . $hookName . '.inc.php', GLOB_NOSORT) as $file) {
@@ -33,17 +36,17 @@ class Hook
* @param string $moduleName Module
* @param string $hookName Hook
* @param bool $filterBroken return false if the module has missing deps
- * @return Hook|false hook instance, false on error or if module doesn't have given hook
+ * @return ?Hook hook instance, false on error or if module doesn't have given hook
*/
- public static function loadSingle($moduleName, $hookName, $filterBroken = true)
+ public static function loadSingle(string $moduleName, string $hookName, bool $filterBroken = true): ?Hook
{
if (Module::get($moduleName) === false) // No such module
- return false;
+ return null;
if ($filterBroken && !Module::isAvailable($moduleName)) // Broken
- return false;
+ return null;
$file = 'modules/' . $moduleName . '/hooks/' . $hookName . '.inc.php';
if (!file_exists($file)) // No hook
- return false;
+ return null;
return new Hook($moduleName, $file);
}
@@ -72,7 +75,7 @@ class Hook
try {
return (include $this->file);
} catch (Exception $e) {
- error_log($e);
+ error_log($e->getMessage());
return false;
}
}
diff --git a/inc/iputil.inc.php b/inc/iputil.inc.php
index 69311d7f..a50f22eb 100644
--- a/inc/iputil.inc.php
+++ b/inc/iputil.inc.php
@@ -1,40 +1,43 @@
<?php
+declare(strict_types=1);
+
class IpUtil
{
- public static function rangeToCidr($start, $end)
+ public static function rangeToCidr(int $start, int $end): string
{
- $value = (int)$start ^ (int)$end;
+ $value = $start ^ $end;
if (!self::isAllOnes($value))
return 'NOT SUBNET: ' . long2ip($start) . '-' . long2ip($end);
- $ones = self::countOnes($value);
+ $ones = self::bitLength($value);
return long2ip($start) . '/' . (32 - $ones);
}
- public static function isValidSubnetRange($start, $end)
+ public static function isValidSubnetRange(int $start, int $end): bool
{
- return self::isAllOnes((int)$start ^ (int)$end);
+ return self::isAllOnes($start ^ $end);
}
/**
- * Return number of one bits required to represent
- * this number. Assumes given number is 2^n - 1.
+ * Return number of bits required to represent
+ * this number.
+ * !! Assumes given number is 2^n - 1 !!
*/
- private static function countOnes($value)
+ private static function bitLength(int $value): int
{
// This is log(value) / log(2)
// It should actually be $value + 1, but floating point errors
// start to happen either way at higher values, so with
// the round() thrown in, it doesn't matter...
- return round(log($value) / 0.69314718055995);
+ return (int)round(log($value) / 0.69314718055995);
}
/**
* Is the given number just ones if converted to
* binary (ignoring leading zeros)?
*/
- private static function isAllOnes($value)
+ private static function isAllOnes(int $value): bool
{
return ($value & ($value + 1)) === 0;
}
@@ -44,35 +47,31 @@ class IpUtil
* ['start' => (int), 'end' => (int)] representing
* the according start and end addresses as integer
* values. Returns false on malformed input.
+ *
* @param string $cidr 192.168.101/24, 1.2.3.4/16, ...
- * @return array|false start and end address, false on error
+ * @return array{start: int, end: int}|null start and end address, false on error
*/
- public static function parseCidr($cidr)
+ public static function parseCidr(string $cidr): ?array
{
$parts = explode('/', $cidr);
if (count($parts) !== 2) {
$ip = ip2long($cidr);
if ($ip === false)
- return false;
- if (PHP_INT_SIZE === 4) {
- $ip = sprintf('%u', $ip);
- }
+ return null;
return ['start' => $ip, 'end' => $ip];
}
$ip = $parts[0];
$bits = $parts[1];
if (!is_numeric($bits) || $bits < 0 || $bits > 32)
- return false;
+ return null;
$dots = substr_count($ip, '.');
if ($dots < 3) {
$ip .= str_repeat('.0', 3 - $dots);
}
$ip = ip2long($ip);
if ($ip === false)
- return false;
- $bits = pow(2, 32 - $bits) - 1;
- if (PHP_INT_SIZE === 4)
- return ['start' => sprintf('%u', $ip & ~$bits), 'end' => sprintf('%u', $ip | $bits)];
+ return null;
+ $bits = (int)((2 ** (32 - $bits)) - 1);
return ['start' => $ip & ~$bits, 'end' => $ip | $bits];
}
diff --git a/inc/mailer.inc.php b/inc/mailer.inc.php
new file mode 100644
index 00000000..bfdcd320
--- /dev/null
+++ b/inc/mailer.inc.php
@@ -0,0 +1,186 @@
+<?php
+
+declare(strict_types=1);
+
+class Mailer
+{
+
+ /** @var array|null */
+ private static $configs = null;
+
+ /** @var array */
+ private $curlOptions;
+
+ /** @var string|null */
+ private $replyTo;
+
+ /** @var string */
+ private $errlog = '';
+
+ /** @var string */
+ private $from;
+
+ // $keys = array('host', 'port', 'ssl', 'senderAddress', 'replyTo', 'username', 'password', 'serverName');
+ public function __construct(string $hosturi, bool $startTls, string $from, string $user, string $pass, string $replyTo = null)
+ {
+ $this->from = $from;
+ if (preg_match('/[^<>"\'\s]+@[^<>"\'\s]+/i', $from, $out)) {
+ $from = $out[0];
+ }
+ $this->curlOptions = [
+ CURLOPT_URL => $hosturi,
+ CURLOPT_USE_SSL => $startTls ? CURLUSESSL_ALL : CURLUSESSL_TRY,
+ CURLOPT_MAIL_FROM => $from,
+ CURLOPT_UPLOAD => 1,
+ CURLOPT_VERBOSE => 1, // XXX
+ ];
+ if (!empty($user)) {
+ $this->curlOptions += [
+ CURLOPT_LOGIN_OPTIONS => 'AUTH=NTLM;AUTH=DIGEST-MD5;AUTH=CRAM-MD5;AUTH=PLAIN;AUTH=LOGIN',
+ CURLOPT_USERNAME => $user,
+ CURLOPT_PASSWORD => $pass,
+ ];
+ }
+ $this->replyTo = $replyTo;
+ }
+
+ /**
+ * Send a mail to given list of recipients.
+ * @param string[] $rcpts Recipients
+ * @param string $subject Mail subject
+ * @param string $text Mail body
+ * @return int curl error code, CURLE_OK on success
+ */
+ public function send(array $rcpts, string $subject, string $text): int
+ {
+ // Convert all line breaks to CRLF while trying to avoid introducing additional ones
+ $text = str_replace(["\r\n", "\r"], "\n", $text);
+ $text = str_replace("\n", "\r\n", $text);
+ $mail = ["Date: " . date('r')];
+ foreach ($rcpts as $r) {
+ $mail[] = self::mimeEncode('To', $r);
+ }
+ $mail[] = self::mimeEncode('Content-Type', 'text/plain; charset="utf-8"');
+ $mail[] = self::mimeEncode('Subject', $subject);
+ $mail[] = self::mimeEncode('From', $this->from);
+ $mail[] = self::mimeEncode('Message-ID', '<' . Util::randomUuid() . '@rfcpedant.example.org>');
+ if (!empty($this->replyTo)) {
+ $mail[] = self::mimeEncode('Reply-To', $this->replyTo);
+ }
+
+ $mail = implode("\r\n", $mail) . "\r\n\r\n" . $text;
+ $c = curl_init();
+ $pos = 0;
+ $this->curlOptions[CURLOPT_MAIL_RCPT] = array_map(function ($mail) {
+ return preg_replace('/\s.*$/', '', $mail);
+ }, $rcpts);
+ $this->curlOptions[CURLOPT_READFUNCTION] = function($ch, $fp, $len) use (&$pos, $mail) {
+ $txt = substr($mail, $pos, $len);
+ $pos += strlen($txt);
+ return $txt;
+ };
+ $err = fopen('php://temp', 'w+');
+ $this->curlOptions[CURLOPT_STDERR] = $err;
+ curl_setopt_array($c, $this->curlOptions);
+ curl_exec($c);
+ rewind($err);
+ $this->errlog = stream_get_contents($err);
+ fclose($err);
+ return curl_errno($c);
+ }
+
+ public function getLog(): string
+ {
+ // Remove repeated "Expire" messages, keep only last one
+ return preg_replace('/^\* Expire in \d+ ms for.*[\\r\\n]+(?=\* Expire)/m','$1', $this->errlog);
+ }
+
+ public static function queue(int $configid, array $rcpts, string $subject, string $text): void
+ {
+ foreach ($rcpts as $rcpt) {
+ Database::exec("INSERT INTO mail_queue (rcpt, subject, body, dateline, configid)
+ VALUES (:rcpt, :subject, :body, UNIX_TIMESTAMP(), :config)",
+ ['rcpt' => $rcpt, 'subject' => $subject, 'body' => $text, 'config' => $configid]);
+ }
+ }
+
+ public static function flushQueue(): void
+ {
+ $list = Database::queryGroupList("SELECT Concat(configid, rcpt, subject) AS keyval,
+ mailid, configid, rcpt, subject, body, dateline FROM mail_queue
+ WHERE nexttry <= UNIX_TIMESTAMP() LIMIT 20");
+ $cutoff = time() - 43200; // 12h
+ $mailers = [];
+ // Loop over mails, grouped by configid-rcpt-subject
+ foreach ($list as $mails) {
+ $delete = [];
+ $body = [];
+ // Loop over individual mails in current group
+ foreach ($mails as $mail) {
+ $delete[] = $mail['mailid'];
+ if ($mail['dateline'] < $cutoff) {
+ EventLog::info("Dropping queued mail '{$mail['subject']}' for {$mail['rcpt']} as it's too old.");
+ continue; // Ignore, too old
+ }
+ $body[] = ' *** ' . date('d.m.Y H:i:s', $mail['dateline']) . "\r\n"
+ . $mail['body'];
+ }
+ if (!empty($body) && isset($mail)) {
+ $body = implode("\r\n\r\n - - - - -\r\n\r\n", $body);
+ if (!isset($mailers[$mail['configid']])) {
+ $mailers[$mail['configid']] = self::instanceFromConfig($mail['configid']);
+ if ($mailers[$mail['configid']] === null) {
+ EventLog::failure("Invalid mailer config id: " . $mail['configid']);
+ }
+ }
+ if (($mailer = $mailers[$mail['configid']]) !== null) {
+ $ret = $mailer->send([$mail['rcpt']], $mail['subject'], $body);
+ if ($ret !== CURLE_OK) {
+ Database::exec("UPDATE mail_queue SET nexttry = UNIX_TIMESTAMP() + 7200
+ WHERE mailid IN (:ids)", ['ids' => $delete]);
+ $delete = [];
+ EventLog::info("Error sending mail '{$mail['subject']}' for {$mail['rcpt']}.",
+ $mailer->getLog());
+ }
+ }
+ }
+ // Now delete these, either sending succeeded or it's too old
+ if (!empty($delete)) {
+ Database::exec("DELETE FROM mail_queue WHERE mailid IN (:ids)", ['ids' => $delete]);
+ }
+ }
+ }
+
+ private static function mimeEncode(string $field, string $string): string
+ {
+ if (preg_match('/[\r\n\x7f-\xff]/', $string)) {
+ return iconv_mime_encode($field, $string);
+ }
+ return "$field: $string";
+ }
+
+ private static function getConfig(int $configId): array
+ {
+ if (!is_array(self::$configs)) {
+ self::$configs = Database::queryIndexedList("SELECT configid, host, port, `ssl`, senderaddress, replyto,
+ username, password
+ FROM mail_config");
+ }
+ return self::$configs[$configId] ?? [];
+ }
+
+ public static function instanceFromConfig(int $configId): ?Mailer
+ {
+ $config = self::getConfig($configId);
+ if (empty($config))
+ return null;
+ if ($config['ssl'] === 'IMPLICIT') {
+ $uri = "smtps://{$config['host']}:{$config['port']}";
+ } else {
+ $uri = "smtp://{$config['host']}:{$config['port']}";
+ }
+ return new Mailer($uri, $config['ssl'] === 'EXPLICIT', $config['senderaddress'], $config['username'],
+ $config['password'], $config['replyto']);
+ }
+
+} \ No newline at end of file
diff --git a/inc/message.inc.php b/inc/message.inc.php
index 9197e4c2..119bb2ba 100644
--- a/inc/message.inc.php
+++ b/inc/message.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class Message
{
private static $list = array();
@@ -11,40 +13,39 @@ class Message
* yet, it will be added to the queue, otherwise it will be added
* in place during rendering.
*/
- public static function addError($id)
+ public static function addError(string $id, ...$params): void
{
- self::add('danger', $id, func_get_args());
+ self::add('danger', $id, $params);
}
- public static function addWarning($id)
+ public static function addWarning(string $id, ...$params): void
{
- self::add('warning', $id, func_get_args());
+ self::add('warning', $id, $params);
}
- public static function addInfo($id)
+ public static function addInfo(string $id, ...$params): void
{
- self::add('info', $id, func_get_args());
+ self::add('info', $id, $params);
}
- public static function addSuccess($id)
+ public static function addSuccess(string $id, ...$params): void
{
- self::add('success', $id, func_get_args());
+ self::add('success', $id, $params);
}
/**
* Internal function that adds a message. Used by
* addError/Success/Info/... above.
*/
- private static function add($type, $id, $params)
+ private static function add(string $type, string $id, array $params): void
{
if (strstr($id, '.') === false) {
$id = Page::getModule()->getIdentifier() . '.' . $id;
}
- if (count($params) > 1 && $params[1] === true) {
- $params = array_slice($params, 2);
+ if (!empty($params) && $params[0] === true) {
+ $params = array_slice($params, 1);
$linkModule = true;
} else {
- $params = array_slice($params, 1);
$linkModule = false;
}
switch ($type) {
@@ -63,6 +64,7 @@ class Message
default:
$icon = '';
}
+ ArrayUtil::forceType($params, 'string');
self::$list[] = array(
'type' => $type,
'icon' => $icon,
@@ -70,7 +72,9 @@ class Message
'params' => $params,
'link' => $linkModule
);
- if (self::$flushed) self::renderList();
+ if (self::$flushed) {
+ self::renderList();
+ }
}
/**
@@ -78,7 +82,7 @@ class Message
* After calling this, any further calls to add* will be rendered in
* place in the current page output.
*/
- public static function renderList()
+ public static function renderList(): void
{
self::$flushed = true;
if (empty(self::$list))
@@ -122,7 +126,7 @@ class Message
* Get all queued messages, flushing the queue.
* Useful in api/ajax mode.
*/
- public static function asString()
+ public static function asString(): string
{
$return = '';
foreach (self::$list as $item) {
@@ -145,17 +149,20 @@ class Message
* Deserialize any messages from the current HTTP request and
* place them in the message queue.
*/
- public static function fromRequest()
+ public static function fromRequest(): void
{
$messages = is_array($_REQUEST['message']) ? $_REQUEST['message'] : array($_REQUEST['message']);
foreach ($messages as $message) {
$data = explode('|', $message);
+ if (count($data) < 2)
+ continue;
if (substr($data[0], -1) === '@') {
$data[0] = substr($data[0], 0, -1);
- array_splice($data, 1, 0, true);
+ array_splice($data, 2, 0, true);
}
- if (count($data) < 2 || !preg_match('/^(danger|warning|info|success)$/', $data[0])) continue;
- self::add($data[0], $data[1], array_slice($data, 1));
+ if (!preg_match('/^(danger|warning|info|success)$/', $data[0]))
+ continue;
+ self::add($data[0], $data[1], array_slice($data, 2));
}
}
@@ -163,7 +170,7 @@ class Message
* Turn the current message queue into a serialized version,
* suitable for appending to a GET or POST request
*/
- public static function toRequest()
+ public static function toRequest(): string
{
$parts = array();
foreach (array_merge(self::$list, self::$alreadyDisplayed) as $item) {
diff --git a/inc/module.inc.php b/inc/module.inc.php
index 55713cd0..b072c4a2 100644
--- a/inc/module.inc.php
+++ b/inc/module.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class Module
{
/*
@@ -7,16 +9,16 @@ class Module
*/
/**
- * @var \Module[]
+ * @var ?Module[]
*/
- private static $modules = false;
+ private static $modules = null;
/**
* @param string $name ID/Internal name of module
- * @param false $ignoreDepFail whether to return the module even if some of its dependencies failed
+ * @param bool $ignoreDepFail whether to return the module even if some of its dependencies failed
* @return false|Module
*/
- public static function get($name, $ignoreDepFail = false)
+ public static function get(string $name, bool $ignoreDepFail = false)
{
if (!isset(self::$modules[$name]))
return false;
@@ -28,13 +30,13 @@ class Module
/**
* Check whether given module is available, that is, all dependencies are
* met. If the module is available, it will be activated, so all its classes
- * are available through the auto-loader, and any js or css is added to the
+ * are available through the autoloader, and any js or css is added to the
* final page output.
*
* @param string $moduleId module to check
* @return bool true if module is available and activated
*/
- public static function isAvailable($moduleId, $activate = true)
+ public static function isAvailable(string $moduleId, bool $activate = true): bool
{
$module = self::get($moduleId);
if ($module === false)
@@ -45,7 +47,7 @@ class Module
return !$module->hasMissingDependencies();
}
- private static function resolveDepsByName($name)
+ private static function resolveDepsByName(string $name): bool
{
if (!isset(self::$modules[$name]))
return false;
@@ -57,7 +59,7 @@ class Module
* @param \Module $mod the module to check
* @return boolean true iff module deps are all found and enabled
*/
- private static function resolveDeps($mod)
+ private static function resolveDeps(Module $mod): bool
{
if (!$mod->depsChecked) {
$mod->depsChecked = true;
@@ -75,7 +77,7 @@ class Module
/**
* @return \Module[] List of valid, enabled modules
*/
- public static function getEnabled($sortById = false)
+ public static function getEnabled(bool $sortById = false): array
{
$ret = array();
$sort = array();
@@ -96,7 +98,7 @@ class Module
/**
* @return \Module[] List of all modules, including with missing deps
*/
- public static function getAll()
+ public static function getAll(): array
{
foreach (self::$modules as $module) {
self::resolveDeps($module);
@@ -107,7 +109,7 @@ class Module
/**
* @return \Module[] List of modules that have been activated
*/
- public static function getActivated()
+ public static function getActivated(): array
{
$ret = array();
$i = 0;
@@ -120,9 +122,9 @@ class Module
return $ret;
}
- public static function init()
+ public static function init(): void
{
- if (self::$modules !== false)
+ if (self::$modules !== null)
return;
$dh = opendir('modules');
if ($dh === false)
@@ -143,7 +145,8 @@ class Module
* Non-static
*/
- private $category = false;
+ /** @var ?string category id */
+ private $category = null;
private $clientPlugin = false;
private $depsMissing = false;
private $depsChecked = false;
@@ -161,7 +164,7 @@ class Module
*/
private $scripts = array();
- private function __construct($name)
+ private function __construct(string $name)
{
$file = 'modules/' . $name . '/config.json';
$json = @json_decode(@file_get_contents($file), true);
@@ -173,30 +176,30 @@ class Module
if (isset($json['category']) && is_string($json['category'])) {
$this->category = $json['category'];
}
- $this->collapse = isset($json['collapse']) && (bool)$json['collapse'];
+ $this->collapse = isset($json['collapse']) && $json['collapse'];
if (isset($json['client-plugin'])) {
$this->clientPlugin = (bool)$json['client-plugin'];
}
$this->name = $name;
}
- public function hasMissingDependencies()
+ public function hasMissingDependencies(): bool
{
return $this->depsMissing;
}
- public function newPage()
+ public function newPage(): Page
{
$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;
return new $class();
}
- public function activate($depth, $direct)
+ public function activate(?int $depth, ?bool $direct): bool
{
if ($this->depsMissing)
return false;
@@ -228,14 +231,14 @@ class Module
return true;
}
- public function getDependencies()
+ public function getDependencies(): array
{
$deps = array();
$this->getDepsInternal($deps);
return array_keys($deps);
}
- private function getDepsInternal(&$deps)
+ private function getDepsInternal(array &$deps): void
{
if (!is_array($this->dependencies))
return;
@@ -250,49 +253,49 @@ class Module
}
}
- public function getIdentifier()
+ public function getIdentifier(): string
{
return $this->name;
}
- public function getDisplayName()
+ public function getDisplayName(): string
{
- $string = Dictionary::translateFileModule($this->name, 'module', 'module_name');
+ $string = Dictionary::translateFileModule($this->name, 'module', 'module_name', false);
if ($string === false) {
return '!!' . $this->name . '!!';
}
return $string;
}
- public function getPageTitle()
+ public function getPageTitle(): string
{
- $val = Dictionary::translateFileModule($this->name, 'module', 'page_title');
+ $val = Dictionary::translateFileModule($this->name, 'module', 'page_title', false);
if ($val !== false)
return $val;
return $this->getDisplayName();
}
- public function getCategory()
+ public function getCategory(): ?string
{
return $this->category;
}
- public function getCategoryName()
+ public function getCategoryName(): string
{
return Dictionary::getCategoryName($this->category);
}
- public function doCollapse()
+ public function doCollapse(): bool
{
return $this->collapse;
}
- public function getDir()
+ public function getDir(): string
{
return 'modules/' . $this->name;
}
- public function getScripts()
+ public function getScripts(): array
{
if ($this->directActivation && $this->clientPlugin) {
if (!in_array('clientscript.js', $this->scripts) && file_exists($this->getDir() . '/clientscript.js')) {
@@ -303,7 +306,7 @@ class Module
return [];
}
- public function getCss()
+ public function getCss(): array
{
if ($this->directActivation && $this->clientPlugin) {
if (!in_array('style.css', $this->css) && file_exists($this->getDir() . '/style.css')) {
diff --git a/inc/paginate.inc.php b/inc/paginate.inc.php
index b212e252..7757228a 100644
--- a/inc/paginate.inc.php
+++ b/inc/paginate.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class Paginate
{
private $query;
@@ -9,43 +11,44 @@ class Paginate
private $totalRows = false;
/**
- * @query - The query that will return lines to show
- * @currentPage - 0based index of currently viewed page
- * @perPage - Number of items to show per page
- * @url - URL of current wegpage
+ * @param string $query - The query that will return lines to show
+ * @param int $perPage - Number of items to show per page
+ * @param ?string $url - URL of current wegpage
*/
- public function __construct($query, $perPage, $url = false)
+ public function __construct(string $query, int $perPage, string $url = null)
{
$this->currentPage = (isset($_GET['page']) ? (int)$_GET['page'] : 0);
- $this->perPage = (int)$perPage;
+ $this->perPage = $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!');
+ if (!preg_match('/\s*SELECT\s/i', $query)) {
+ ErrorHandler::traceError('Query has to start with SELECT!');
}
// XXX: MySQL only
- if (preg_match('/^mysql/i', CONFIG_SQL_DSN)) {
+ if (preg_match('/^(mysql|mariadb)/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!");
+ if (preg_match('/(\-\-|;)(\s|[^\'"`])*$/i', $query)) {
+ 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'];
+ if ($url === null) {
+ $url = $_SERVER['REQUEST_URI'];
+ }
if (strpos($url, '?') === false) {
$url .= '?';
} else {
@@ -60,7 +63,7 @@ class Paginate
/**
* Execute the query, returning the PDO query object
*/
- public function exec($args = array())
+ public function exec(array $args = [])
{
$countQuery = preg_replace('/ORDER\s+BY\s.*?(\sASC|\sDESC|$)/is', '', $this->query);
$countQuery = preg_replace('/SELECT\s.*?\sFROM\s/is', 'SELECT Count(*) AS rowcount FROM ', $countQuery);
@@ -71,7 +74,7 @@ class Paginate
return $retval;
}
- public function render($template, $data)
+ public function render(string $template, array $data): void
{
if ($this->totalRows == 0) {
// Shortcut for no content
diff --git a/inc/permission.inc.php b/inc/permission.inc.php
index cd9cc43c..f346f1da 100644
--- a/inc/permission.inc.php
+++ b/inc/permission.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
class Permission
{
private static $permissions = array(
@@ -9,18 +11,21 @@ class Permission
'translation' => 8, // Can edit translations
);
- public static function get($permission)
+ public static function get(string $permission): int
{
- if (!isset(self::$permissions[$permission])) Util::traceError('Invalid permission: ' . $permission);
+ if (!isset(self::$permissions[$permission])) ErrorHandler::traceError('Invalid permission: ' . $permission);
return self::$permissions[$permission];
}
// TODO: Doc/Refactor
- public static function addGlobalTags(&$array, $locationid, $disabled, $noneAvailDisabled = null)
+ public static function addGlobalTags(?array &$array, ?int $locationid, array $disabled, ?string $noneAvailDisabled = null): void
{
if (Module::get('permissionmanager') === false)
return;
+ if ($array === null) {
+ $array = [];
+ }
$one = false;
foreach ($disabled as $perm) {
if (User::hasPermission($perm, $locationid)) {
@@ -47,7 +52,7 @@ class Permission
}
}
- public static function moduleHasPermissions($moduleId)
+ public static function moduleHasPermissions(string $moduleId): bool
{
if (Module::get('permissionmanager') === false)
return true;
@@ -58,13 +63,8 @@ class Permission
* Takes a list of locations, removes any locations from it where the user doesn't have permission,
* and then re-adds locations resulting from the given query. The given query should return only
* one column per row, which is a location id.
- * @param $passedLocations
- * @param $permission
- * @param $query
- * @param $params
- * @return array
*/
- public static function mergeWithDisallowed($passedLocations, $permission, $query, $params)
+ public static function mergeWithDisallowed(array $passedLocations, string $permission, string $query, array $params): array
{
$allowed = User::getAllowedLocations($permission);
if (in_array(0, $allowed))
diff --git a/inc/property.inc.php b/inc/property.inc.php
index 3911b0d4..aaf03254 100644
--- a/inc/property.inc.php
+++ b/inc/property.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Get or set simple key-value-pairs, backed by the database
* to make them persistent.
@@ -16,12 +18,13 @@ class Property
* @param mixed $default value to return if $key does not exist in the property store
* @return mixed the value attached to $key, or $default if $key does not exist
*/
- public static function get($key, $default = false)
+ public static function get(string $key, $default = false)
{
if (self::$cache === false) {
+ self::$cache = [];
$NOW = time();
$res = Database::simpleQuery("SELECT name, dateline, value FROM property");
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if ($row['dateline'] != 0 && $row['dateline'] < $NOW)
continue;
self::$cache[$row['name']] = $row['value'];
@@ -33,24 +36,30 @@ class Property
}
/**
- * Set value in property store.
+ * Set value in property store. Passing null or false as the value deletes the
+ * entry from the property table.
*
* @param string $key key of value to set
- * @param string $value the value to store for $key
+ * @param string|null|false $value the value to store for $key
* @param int $maxAgeMinutes how long to keep this entry around at least, in minutes. 0 for infinite
*/
- public static function set($key, $value, $maxAgeMinutes = 0)
+ public static function set(string $key, $value, int $maxAgeMinutes = 0): void
{
- if (self::$cache === false || self::get($key) != $value) { // Simple compare, so it works for numbers accidentally casted to string somewhere
+ if ($value === false || $value === null) {
+ Database::exec("DELETE FROM property WHERE name = :key", ['key' => $key]);
+ if (self::$cache !== false) {
+ unset(self::$cache[$key]);
+ }
+ } else {
Database::exec("INSERT INTO property (name, value, dateline) VALUES (:key, :value, :dateline)"
- . " ON DUPLICATE KEY UPDATE value = VALUES(value), dateline = VALUES(dateline)", array(
+ . " ON DUPLICATE KEY UPDATE value = VALUES(value), dateline = VALUES(dateline)", [
'key' => $key,
'value' => $value,
'dateline' => ($maxAgeMinutes === 0 ? 0 : time() + ($maxAgeMinutes * 60))
- ));
- }
- if (self::$cache !== false) {
- self::$cache[$key] = $value;
+ ]);
+ if (self::$cache !== false) {
+ self::$cache[$key] = $value;
+ }
}
}
@@ -60,33 +69,76 @@ class Property
* @param string $key Key of list to get all items for
* @return array All the items matching the key
*/
- public static function getList($key)
+ public static function getList(string $key): array
{
- $res = Database::simpleQuery("SELECT dateline, value FROM property_list WHERE name = :key", compact('key'));
+ $res = Database::simpleQuery("SELECT subkey, dateline, value FROM property_list
+ WHERE `name` = :key", compact('key'));
$NOW = time();
- $return = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $return = [];
+ foreach ($res as $row) {
if ($row['dateline'] != 0 && $row['dateline'] < $NOW)
continue;
- $return[] = $row['value'];
+ $return[$row['subkey']] = $row['value'];
}
return $return;
}
/**
+ * @return ?string entry from property list
+ */
+ public static function getListEntry(string $key, int $subkey): ?string
+ {
+ $row = Database::queryFirst("SELECT dateline, `value` FROM property_list
+ WHERE `name` = :key AND subkey = :subkey", ['key' => $key, 'subkey' => $subkey]);
+ if ($row === false || ($row['dateline'] != 0 && $row['dateline'] < time()))
+ return null;
+ return $row['value'];
+ }
+
+ /**
* Add item to property list.
*
* @param string $key key of value to set
* @param string $value the value to add for $key
* @param int $maxAgeMinutes how long to keep this entry around at least, in minutes. 0 for infinite
+ * @return int The auto generated sub-key
*/
- public static function addToList($key, $value, $maxAgeMinutes = 0)
+ public static function addToList(string $key, string $value, int $maxAgeMinutes = 0): int
{
Database::exec("INSERT INTO property_list (name, value, dateline) VALUES (:key, :value, :dateline)", array(
'key' => $key,
'value' => $value,
'dateline' => ($maxAgeMinutes === 0 ? 0 : time() + ($maxAgeMinutes * 60))
));
+ return Database::lastInsertId();
+ }
+
+ /**
+ * Update existing entry in property list.
+ *
+ * @param string $key key of list
+ * @param int $subkey subkey of entry in list
+ * @param string $value new value to set entry to
+ * @param int $maxAgeMinutes the new lifetime of that entry
+ * @param ?string $expectedValue if not null, the value will only be updated if it currently has this value
+ * @return bool whether the entry existed and has been updated
+ */
+ public static function updateListEntry(string $key, int $subkey, string $value,
+ int $maxAgeMinutes = 0, string $expectedValue = null): bool
+ {
+ $args = [
+ 'name' => $key,
+ 'subkey' => $subkey,
+ 'newvalue' => $value,
+ 'dateline' => ($maxAgeMinutes === 0 ? 0 : time() + ($maxAgeMinutes * 60)),
+ ];
+ if ($expectedValue !== null) {
+ $args['oldvalue'] = $expectedValue;
+ return Database::exec("UPDATE property_list SET `value` = :newvalue, dateline = :dateline
+ WHERE `name` = :name AND subkey = :subkey AND `value` = :oldvalue", $args) > 0;
+ }
+ return Database::exec("UPDATE property_list SET `value` = :newvalue, dateline = :dateline
+ WHERE `name` = :name AND subkey = :subkey", $args) > 0;
}
/**
@@ -97,7 +149,7 @@ class Property
* @param string $value item to remove
* @return int number of items removed
*/
- public static function removeFromList($key, $value)
+ public static function removeFromListByVal(string $key, string $value): int
{
return Database::exec("DELETE FROM property_list WHERE name = :key AND value = :value", array(
'key' => $key,
@@ -106,12 +158,28 @@ class Property
}
/**
+ * Remove given item from property list. If the list contains this item
+ * multiple times, they will all be removed.
+ *
+ * @param string $key Key of list
+ * @param int $value item to remove
+ * @return bool whether item was found and removed
+ */
+ public static function removeFromListByKey(string $key, int $subkey): bool
+ {
+ return Database::exec("DELETE FROM property_list WHERE name = :key AND subkey = :subkey", array(
+ 'key' => $key,
+ 'subkey' => $subkey,
+ )) > 0;
+ }
+
+ /**
* Delete entire list with given key.
*
* @param string $key Key of list
* @return int number of items removed
*/
- public static function clearList($key)
+ public static function clearList(string $key): int
{
return Database::exec("DELETE FROM property_list WHERE name = :key", compact('key'));
}
@@ -120,12 +188,12 @@ class Property
* Legacy getters/setters
*/
- public static function getServerIp()
+ public static function getServerIp(): string
{
- return self::get('server-ip', 'none');
+ return self::get('server-ip', 'invalid');
}
- public static function setServerIp($value, $automatic = false)
+ public static function setServerIp(string $value, bool $automatic = false): bool
{
if ($value === self::getServerIp())
return false;
@@ -135,19 +203,15 @@ class Property
return true;
}
- public static function getBootMenu()
- {
- return json_decode(self::get('ipxe-menu'), true);
- }
-
- public static function setBootMenu($value)
- {
- self::set('ipxe-menu', json_encode($value));
- }
-
- public static function getVmStoreConfig()
+ public static function getVmStoreConfig(): array
{
- return json_decode(self::get('vmstore-config'), true);
+ $data = self::get('vmstore-config');
+ if (!is_string($data))
+ return [];
+ $data = json_decode($data, true);
+ if (!is_array($data))
+ return [];
+ return $data;
}
public static function getVmStoreUrl()
@@ -169,21 +233,6 @@ class Property
self::set('vmstore-config', json_encode($value));
}
- public static function getDownloadTask($name)
- {
- return self::get('dl-' . $name);
- }
-
- public static function setDownloadTask($name, $taskId)
- {
- self::set('dl-' . $name, $taskId, 5);
- }
-
- public static function getCurrentSchemaVersion()
- {
- return self::get('webif-version');
- }
-
public static function setLastWarningId($id)
{
self::set('last-warn-event-id', $id);
@@ -194,22 +243,22 @@ class Property
return self::get('last-warn-event-id', 0);
}
- public static function setNeedsSetup($value)
+ public static function setNeedsSetup(bool $value)
{
- self::set('needs-setup', $value);
+ self::set('needs-setup', (int)$value);
}
- public static function getNeedsSetup()
+ public static function getNeedsSetup(): bool
{
- return self::get('needs-setup');
+ return self::get('needs-setup') != 0;
}
- public static function setPasswordFieldType($value)
+ public static function setPasswordFieldType(string $value)
{
self::set('password-type', $value);
}
- public static function getPasswordFieldType()
+ public static function getPasswordFieldType(): string
{
return self::get('password-type', 'password');
}
diff --git a/inc/render.inc.php b/inc/render.inc.php
index 2c3a1da7..a636382e 100644
--- a/inc/render.inc.php
+++ b/inc/render.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
require_once('inc/util.inc.php');
require_once('Mustache/Autoloader.php');
@@ -14,21 +16,22 @@ class Render
{
/**
- * @var Mustache_Engine
+ * @var ?Mustache_Engine
*/
- private static $mustache = false;
+ private static $mustache = null;
private static $body = '';
private static $header = '';
- private static $dashboard = false;
+ /** @var ?array */
+ private static $dashboard = null;
private static $footer = '';
private static $title = '';
private static $templateCache = array();
private static $tags = array();
- public static function init()
+ public static function init(): void
{
- if (self::$mustache !== false)
- Util::traceError('Called Render::init() twice!');
+ if (self::$mustache !== null)
+ ErrorHandler::traceError('Called Render::init() twice!');
$options = array();
$tmp = '/tmp/bwlp-cache';
$dir = is_dir($tmp);
@@ -41,7 +44,7 @@ class Render
self::$mustache = new Mustache_Engine($options);
}
- private static function cssEsc($str)
+ private static function cssEsc(string $str): string
{
return str_replace(array('"', '&', '<', '>'), array('\\000022', '\\000026', '\\00003c', '\\00003e'), $str);
}
@@ -49,12 +52,10 @@ class Render
/**
* Output the buffered, generated page
*/
- public static function output()
+ public static function output(): void
{
Header('Content-Type: text/html; charset=utf-8');
- /* @var $modules Module[] */
$modules = array_reverse(Module::getActivated());
- $pageModule = Page::getModule();
$title = Property::get('page-title-prefix', '');
$bgcolor = Property::get('logo-background', '');
if (!empty($bgcolor) || !empty($title)) {
@@ -99,7 +100,7 @@ class Render
' </head>
<body>
',
- (self::$dashboard !== false ? self::parse('main-menu', self::$dashboard, 'main') : ''),
+ (self::$dashboard !== null ? self::parse('main-menu', self::$dashboard, 'main') : ''),
'<div class="main" id="mainpage"><div class="container-fluid">
',
self::$body
@@ -128,7 +129,7 @@ class Render
/**
* Set the page title (title-tag)
*/
- public static function setTitle($title, $override = true)
+ public static function setTitle(string $title, bool $override = true): void
{
if (!$override && !empty(self::$title))
return;
@@ -138,7 +139,7 @@ class Render
/**
* Add raw html data to the header-section of the generated page
*/
- public static function addHeader($html)
+ public static function addHeader(string $html): void
{
self::$header .= $html . "\n";
}
@@ -146,7 +147,7 @@ class Render
/**
* Add raw html data to the footer-section of the generated page (right before the closing body tag)
*/
- public static function addFooter($html)
+ public static function addFooter(string $html): void
{
self::$footer .= $html . "\n";
}
@@ -154,7 +155,7 @@ class Render
/**
* Add the given template to the output, using the given params for placeholders in the template
*/
- public static function addTemplate($template, $params = false, $module = false)
+ public static function addTemplate(string $template, array $params = [], ?string $module = null)
{
self::$body .= self::parse($template, $params, $module);
}
@@ -167,7 +168,7 @@ class Render
* @param string $template template used to fill the dialog body
* @param array $params parameters for rendering the body template
*/
- public static function addDialog($title, $next, $template, $params = false)
+ public static function addDialog(string $title, bool $next, string $template, array $params = []): void
{
self::addTemplate('dialog-generic', array(
'title' => $title,
@@ -179,7 +180,7 @@ class Render
/**
* Add error message to page
*/
- public static function addError($message)
+ public static function addError($message): void
{
self::addTemplate('messagebox-error', array('message' => $message));
}
@@ -188,12 +189,13 @@ class Render
* Parse template with given params and return; do not add to body
* @param string $template name of template, relative to templates/, without .html extension
* @param array $params tags to render into template
- * @param string $module name of module to load template from; defaults to currently active module
+ * @param ?string $module name of module to load template from; defaults to currently active module
+ * @param ?string $lang override language if not null
* @return string Rendered template
*/
- public static function parse($template, $params = false, $module = false, $lang = false)
+ public static function parse(string $template, array $params = [], ?string $module = null, ?string $lang = null): string
{
- if ($module === false && class_exists('Page')) {
+ if ($module === null && class_exists('Page', false)) {
$module = Page::getModule()->getIdentifier();
}
// Load html snippet
@@ -201,9 +203,6 @@ class Render
if ($html === false) {
return '<h3>Template ' . htmlspecialchars($template) . '</h3>' . nl2br(htmlspecialchars(print_r($params, true))) . '<hr>';
}
- if (!is_array($params)) {
- $params = array();
- }
// Now find all language tags in this array
if (preg_match_all('/{{\s*(lang_.+?)\s*}}/', $html, $out) > 0) {
$dictionary = Dictionary::getArray($module, 'template-tags', $lang);
@@ -211,14 +210,14 @@ class Render
foreach ($out[1] as $tag) {
if ($fallback === false && empty($dictionary[$tag])) {
$fallback = true; // Fallback to general dictionary of main module
- $dictionary = $dictionary + Dictionary::getArray('main', 'global-tags');
+ $dictionary += Dictionary::getArray('main', 'global-tags');
}
// Add untranslated strings to the dictionary, so their tag is seen in the rendered page
if (empty($dictionary[$tag])) {
$dictionary[$tag] = '{{' . $tag . '}}';
}
}
- $params = $params + $dictionary;
+ $params += $dictionary;
}
// Always add token to parameter list
$params['token'] = Session::get('token');
@@ -244,7 +243,7 @@ class Render
*/
public static function openTag($tag, $params = false)
{
- array_push(self::$tags, $tag);
+ self::$tags[] = $tag;
if (!is_array($params)) {
self::$body .= '<' . $tag . '>';
} else {
@@ -262,17 +261,18 @@ 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 . '>';
}
/**
* Private helper: Load the given template and return it
+ * @return false|string
*/
- private static function getTemplate($template, $module)
+ private static function getTemplate(string $template, string $module)
{
$id = "$template/$module";
if (isset(self::$templateCache[$id])) {
@@ -287,12 +287,13 @@ class Render
/**
* Create the dashboard menu
*/
- public static function setDashboard($params)
+ public static function setDashboard(array $params): void
{
self::$dashboard = $params;
}
- public static function readableColor($hex) {
+ public static function readableColor(string $hex): string
+ {
if (strlen($hex) <= 4) {
$cnt = 1;
} else {
diff --git a/inc/request.inc.php b/inc/request.inc.php
index 7e9ed97e..cd782d99 100644
--- a/inc/request.inc.php
+++ b/inc/request.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Wrapper for getting fields from the request (GET, POST, ...)
*/
@@ -23,7 +25,7 @@ class Request
* @param string $type if the parameter exists, cast it to given type
* @return mixed Field from $_GET, or $default if not set
*/
- public static function get($key, $default = false, $type = false)
+ public static function get(string $key, $default = false, $type = false)
{
return self::handle($_GET, $key, $default, $type);
}
@@ -34,7 +36,7 @@ class Request
* @param string $default Value to return if $_POST does not contain $key
* @return mixed Field from $_POST, or $default if not set
*/
- public static function post($key, $default = false, $type = false)
+ public static function post(string $key, $default = false, $type = false)
{
return self::handle($_POST, $key, $default, $type);
}
@@ -45,7 +47,7 @@ class Request
* @param string $default Value to return if $_REQUEST does not contain $key
* @return mixed Field from $_REQUEST, or $default if not set
*/
- public static function any($key, $default = false, $type = false)
+ public static function any(string $key, $default = false, $type = false)
{
return self::handle($_REQUEST, $key, $default, $type);
}
@@ -53,7 +55,7 @@ class Request
/**
* @return true iff the request is a POST request
*/
- public static function isPost()
+ public static function isPost(): bool
{
return $_SERVER['REQUEST_METHOD'] === 'POST';
}
@@ -61,21 +63,21 @@ class Request
/**
* @return true iff the request is a GET request
*/
- public static function isGet()
+ public static function isGet(): bool
{
return $_SERVER['REQUEST_METHOD'] === 'GET';
}
private static function handle(&$array, $key, $default, $type)
{
- if (!isset($array[$key])) {
+ if (!array_key_exists($key, $array)) {
if ($default === self::REQUIRED || $default === self::REQUIRED_EMPTY) {
Message::addError('main.parameter-missing', $key);
Util::redirect('?do=' . $_REQUEST['do']);
}
return $default;
}
- if ($default === self::REQUIRED && (string)$array[$key] === '') {
+ if ($default === self::REQUIRED && $array[$key] === '') {
Message::addError('main.parameter-empty', $key);
Util::redirect('?do=' . $_REQUEST['do']);
}
diff --git a/inc/session.inc.php b/inc/session.inc.php
index cb52cd38..ccb878cd 100644
--- a/inc/session.inc.php
+++ b/inc/session.inc.php
@@ -1,19 +1,21 @@
<?php
-require_once('config.php');
+declare(strict_types=1);
-@mkdir(CONFIG_SESSION_DIR, 0700, true);
-@chmod(CONFIG_SESSION_DIR, 0700);
-if (!is_writable(CONFIG_SESSION_DIR)) die('Config error: Session Path not writable!');
+require_once('config.php');
class Session
{
private static $sid = false;
private static $data = false;
+ private static $dataChanged = false;
+ private static $userId = 0;
+ private static $updateSessionDateline = false;
- private static function generateSessionId($salt)
+ private static function generateSessionId(string $salt): void
{
- if (self::$sid !== false) Util::traceError('Error: Asked to generate session id when already set.');
+ if (self::$sid !== false)
+ ErrorHandler::traceError('Error: Asked to generate session id when already set.');
self::$sid = sha1($salt . ','
. mt_rand(0, 65535)
. $_SERVER['REMOTE_ADDR']
@@ -27,26 +29,42 @@ class Session
);
}
- public static function create($salt = '')
+ public static function create(string $salt, int $userId, bool $fixedAddress): void
{
self::generateSessionId($salt);
- self::$data = array();
+ self::$data = [];
+ self::$userId = $userId;
+ Database::exec("INSERT INTO session (sid, userid, dateline, lastip, fixedip, data)
+ VALUES (:sid, :userid, 0, '', :fixedip, '')", [
+ 'sid' => self::$sid,
+ 'userid' => $userId,
+ 'fixedip' => $fixedAddress ? 1 : 0,
+ ]);
+ self::setupSessionAccounting(true);
}
- public static function load()
+ public static function load(): bool
{
// Try to load session id from cookie
- if (!self::loadSessionId()) return false;
+ if (!self::loadSessionId())
+ return false;
// Succeeded, now try to load session data. If successful, job is done
- if (self::readSessionData()) return true;
+ if (self::readSessionData())
+ return true;
// Loading session data failed
- self::delete();
+ self::$sid = false;
return false;
}
- public static function get($key)
+ public static function getUserId(): int
{
- if (!isset(self::$data[$key]) || !is_array(self::$data[$key])) return false;
+ return self::$userId;
+ }
+
+ public static function get(string $key)
+ {
+ if (!isset(self::$data[$key]) || !is_array(self::$data[$key]))
+ return false;
return self::$data[$key][0];
}
@@ -55,80 +73,132 @@ class Session
* @param mixed $value data to store for key, false = delete
* @param int|false $validMinutes validity in minutes, or false = forever
*/
- public static function set($key, $value, $validMinutes = false)
+ public static function set(string $key, $value, $validMinutes = 60): void
{
- if (self::$data === false) Util::traceError('Tried to set session data with no active session');
+ if (self::$data === false)
+ ErrorHandler::traceError('Tried to set session data with no active session');
if ($value === false) {
unset(self::$data[$key]);
} else {
self::$data[$key] = [$value, $validMinutes === false ? false : time() + $validMinutes * 60];
}
+ self::$dataChanged = true;
}
- private static function loadSessionId()
+ private static function loadSessionId(): bool
{
- if (self::$sid !== false) die('Error: Asked to load session id when already set.');
- if (empty($_COOKIE['sid'])) return false;
+ if (self::$sid !== false)
+ 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']);
- if (empty($id)) return false;
+ if (empty($id))
+ return false;
self::$sid = $id;
return true;
}
- public static function delete()
+ public static function delete(): void
{
- if (self::$sid === false) return;
- @unlink(self::getSessionFile());
+ if (self::$sid === false)
+ return;
+ Database::exec("DELETE FROM session WHERE sid = :sid",
+ ['sid' => self::$sid]);
self::deleteCookie();
self::$sid = false;
self::$data = false;
}
- public static function deleteCookie()
+ /**
+ * Kill all sessions of currently logged-in user. This can be used as
+ * a security measure if the user suspects that a session left open on
+ * another device could be/is being abused.
+ */
+ public static function deleteAllButCurrent(): void
{
- Util::clearCookie('sid');
+ if (self::$sid === false)
+ return;
+ Database::exec("DELETE FROM session WHERE sid <> :sid AND userid = :uid",
+ ['sid' => self::$sid, 'uid' => self::$userId]);
}
-
- private static function getSessionFile()
+
+ public static function deleteCookie(): void
{
- if (self::$sid === false) Util::traceError('Error: Tried to access session file when no session id was set.');
- return CONFIG_SESSION_DIR . '/' . self::$sid;
+ Util::clearCookie('sid');
}
- private static function readSessionData()
+ private static function readSessionData(): bool
{
- if (self::$data !== false) Util::traceError('Tried to call read session data twice');
- $sessionfile = self::getSessionFile();
- if (!is_readable($sessionfile) || filemtime($sessionfile) + CONFIG_SESSION_TIMEOUT < time()) {
- @unlink($sessionfile);
- return false;
- }
- self::$data = @unserialize(@file_get_contents($sessionfile));
- if (self::$data === false)
- return false;
+ if (self::$data !== false)
+ 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();
- $save = false;
+ if ($row === false || $row['dateline'] < $now) {
+ self::delete();
+ return false;
+ }
+ if ($row['fixedip'] && $row['lastip'] !== $_SERVER['REMOTE_ADDR']) {
+ return false; // Ignore but don't invalidate
+ }
+ // Refresh cookie if appropriate
+ self::setupSessionAccounting(Request::isGet() && $row['dateline'] + 86400 < $now + CONFIG_SESSION_TIMEOUT);
+ self::$userId = (int)$row['userid'];
+ self::$data = @json_decode($row['data'], true);
+ if (!is_array(self::$data)) {
+ self::$data = [];
+ }
foreach (array_keys(self::$data) as $key) {
if (self::$data[$key][1] !== false && self::$data[$key][1] < $now) {
unset(self::$data[$key]);
- $save = true;
+ self::$dataChanged = true;
}
}
- if ($save) {
- self::save();
- }
return true;
}
-
- public static function save()
+
+ private static function setupSessionAccounting(bool $cookie): void
{
- if (self::$sid === false || self::$data === false) return; //Util::traceError('Called saveSession with no active session');
- $sessionfile = self::getSessionFile();
- $ret = @file_put_contents($sessionfile, @serialize(self::$data));
- if (!$ret) Util::traceError('Storing session data in ' . $sessionfile . ' failed.');
- Util::clearCookie('sid');
- $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)');
+ if ($cookie) {
+ self::$updateSessionDateline = true;
+ $ret = setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT,
+ '', '', !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
+ if (!$ret)
+ ErrorHandler::traceError('Error: Could not set Cookie for Client (headers already sent)');
+ }
+ register_shutdown_function(function () {
+ self::saveOnShutdown();
+ });
}
-}
+ private static function saveOnShutdown(): void
+ {
+ $now = time();
+ $args = ['lastip' => $_SERVER['REMOTE_ADDR']];
+ if (self::$updateSessionDateline) {
+ $args['dateline'] = $now + CONFIG_SESSION_TIMEOUT;
+ }
+ if (self::$dataChanged) {
+ $args['data'] = json_encode(self::$data);
+ }
+ self::saveData($args);
+ }
+
+ public static function saveExtraData(): void
+ {
+ if (!self::$dataChanged)
+ return;
+ self::saveData(['data' => json_encode(self::$data)]);
+ self::$dataChanged = false;
+ }
+
+ private static function saveData(array $args): void
+ {
+ $query = "UPDATE session SET " . implode(', ', array_map(function ($key) {
+ return "$key = :$key";
+ }, array_keys($args))) . " WHERE sid = :sid";
+ $args['sid'] = self::$sid;
+ Database::exec($query, $args);
+ }
+
+}
diff --git a/inc/taskmanager.inc.php b/inc/taskmanager.inc.php
index f7c72e04..d9396901 100644
--- a/inc/taskmanager.inc.php
+++ b/inc/taskmanager.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Interface to the external task manager.
*/
@@ -19,7 +21,7 @@ class Taskmanager
*/
private static $sock = false;
- private static function init()
+ private static function init(): void
{
if (self::$sock !== false)
return;
@@ -32,7 +34,7 @@ class Taskmanager
self::send(CONFIG_TM_PASSWORD);
}
- private static function send($message)
+ private static function send(string $message): bool
{
$len = strlen($message);
$sent = socket_send(self::$sock, pack('N', $len) . $message, $len + 4, 0);
@@ -49,9 +51,9 @@ class Taskmanager
* @param array $data data to pass to the task. the structure depends on the task.
* @param boolean $async if true, the function will not wait for the reply of the taskmanager, which means
* the return value is just true (and you won't know if the task could actually be started)
- * @return array|false struct representing the task status (as a result of submit); false on communication error
+ * @return array{id: string, statusCode: string, data: array}|bool struct representing the task status (as a result of submit); false on communication error
*/
- public static function submit($task, $data = false, $async = false)
+ public static function submit(string $task, array $data = null, bool $async = false)
{
self::init();
$seq = (string) mt_rand();
@@ -109,7 +111,7 @@ class Taskmanager
* @param string|array $taskid a task id or a task array returned by ::status or ::submit
* @return boolean true if taskid exists in taskmanager
*/
- public static function isTask($task)
+ public static function isTask($task): bool
{
if ($task === false)
return false;
@@ -127,7 +129,7 @@ class Taskmanager
* @param int $timeout maximum time in ms to wait for completion of task
* @return array|false result/status of task, or false if it couldn't be queried
*/
- public static function waitComplete($task, $timeout = 2500)
+ public static function waitComplete($task, int $timeout = 2500)
{
if (is_array($task) && isset($task['id'])) {
if ($task['statusCode'] !== Taskmanager::TASK_PROCESSING && $task['statusCode'] !== Taskmanager::TASK_WAITING) {
@@ -140,8 +142,9 @@ class Taskmanager
return false;
$done = false;
$deadline = microtime(true) + $timeout / 1000;
+ $status = false;
while (($remaining = $deadline - microtime(true)) > 0) {
- usleep(min(100000, $remaining * 100000));
+ usleep((int)min(100000, $remaining * 100000));
$status = self::status($task);
if (!isset($status['statusCode']))
break;
@@ -163,7 +166,7 @@ class Taskmanager
* @param array|false $task struct representing task, obtained by ::status
* @return boolean true if task failed, false if finished successfully or still waiting/running
*/
- public static function isFailed($task)
+ public static function isFailed($task): bool
{
if (!is_array($task) || !isset($task['statusCode']) || !isset($task['id']))
return true;
@@ -176,10 +179,10 @@ class Taskmanager
* Check whether the given task is finished, i.e. either failed or succeeded,
* but is not running, still waiting for execution or simply unknown.
*
- * @param array $task struct representing task, obtained by ::status
+ * @param mixed $task struct representing task, obtained by ::status
* @return boolean true if task failed or finished, false if waiting for execution or currently executing, no valid task, etc.
*/
- public static function isFinished($task)
+ public static function isFinished($task): bool
{
if (!is_array($task) || !isset($task['statusCode']) || !isset($task['id']))
return false;
@@ -192,10 +195,10 @@ class Taskmanager
* Check whether the given task is running, that is either waiting for execution
* or currently executing.
*
- * @param array $task struct representing task, obtained by ::status
+ * @param mixed $task struct representing task, obtained by ::status
* @return boolean true if task is waiting or executing, false if waiting for execution or currently executing, no valid task, etc.
*/
- public static function isRunning($task)
+ public static function isRunning($task): bool
{
if (!is_array($task) || !isset($task['statusCode']) || !isset($task['id']))
return false;
@@ -204,7 +207,7 @@ class Taskmanager
return false;
}
- public static function addErrorMessage($task)
+ public static function addErrorMessage($task): void
{
static $failure = false;
if ($task === false) {
@@ -231,7 +234,7 @@ class Taskmanager
* @param string|array $task task to release. can either be its id, or a struct representing the task, as returned
* by ::submit() or ::status()
*/
- public static function release($task)
+ public static function release($task): void
{
if (is_array($task) && isset($task['id'])) {
$task = $task['id'];
@@ -247,7 +250,6 @@ class Taskmanager
/**
* Read reply from socket for given sequence number.
*
- * @param string $seq
* @return mixed the decoded json data for that message as an array, or false on error
*/
private static function readReply(string $seq)
@@ -288,7 +290,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 {
@@ -324,7 +326,7 @@ class Taskmanager
* sending or receiving and the send or receive
* buffer might be in an undefined state.
*/
- private static function reset()
+ private static function reset(): void
{
if (self::$sock === false)
return;
@@ -335,7 +337,7 @@ class Taskmanager
/**
* @param float $deadline end time
*/
- private static function updateRecvTimeout($deadline)
+ private static function updateRecvTimeout(float $deadline): void
{
$to = $deadline - microtime(true);
if ($to <= 0) {
diff --git a/inc/taskmanagercallback.inc.php b/inc/taskmanagercallback.inc.php
index 5f153baa..c6b447c9 100644
--- a/inc/taskmanagercallback.inc.php
+++ b/inc/taskmanagercallback.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* Contains all callbacks for detached taskmanager tasks.
*/
@@ -13,7 +15,7 @@ class TaskmanagerCallback
* @param string|array $task Task or Task ID to define callback for
* @param string $callback name of callback function, must be a static method in this class
*/
- public static function addCallback($task, $callback, $args = NULL)
+ public static function addCallback($task, string $callback, $args = NULL): void
{
if (!call_user_func_array('method_exists', array('TaskmanagerCallback', $callback))) {
EventLog::warning("addCallback: Invalid callback function: $callback");
@@ -50,13 +52,13 @@ class TaskmanagerCallback
*
* @return array list of array(taskid => list of callbacks)
*/
- public static function getPendingCallbacks()
+ public static function getPendingCallbacks(): array
{
$res = Database::simpleQuery("SELECT taskid, cbfunction, args FROM callback", array(), true);
if ($res === false)
return array();
$retval = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$retval[$row['taskid']][] = $row;
}
return $retval;
@@ -67,9 +69,9 @@ class TaskmanagerCallback
* table if appropriate.
*
* @param array $callback entry from the callback table (cbfunction + taskid + args)
- * @param array $status status of the task as returned by the taskmanager. If NULL it will be queried.
+ * @param ?array $status status of the task as returned by the taskmanager. If NULL it will be queried.
*/
- public static function handleCallback($callback, $status = NULL)
+ public static function handleCallback(array $callback, array $status = NULL): void
{
if (is_null($status))
$status = Taskmanager::status($callback['taskid']);
@@ -82,12 +84,13 @@ class TaskmanagerCallback
}
}
if (Taskmanager::isFinished($status)) {
+ Taskmanager::release($status);
$func = array('TaskmanagerCallback', preg_replace('/\W/', '', $callback['cbfunction']));
if (!call_user_func_array('method_exists', $func)) {
Eventlog::warning("handleCallback: Callback {$callback['cbfunction']} doesn't exist.");
} else {
if (empty($callback['args']))
- call_user_func($func, $status);
+ call_user_func($func, $status, null);
else
call_user_func($func, $status, unserialize($callback['args']));
}
@@ -99,7 +102,7 @@ class TaskmanagerCallback
/**
* Result of trying to (re)launch ldadp.
*/
- public static function ldadpStartup($task)
+ public static function ldadpStartup(array $task)
{
if (Taskmanager::isFailed($task)) {
if (!isset($task['data']['messages'])) {
@@ -112,14 +115,14 @@ class TaskmanagerCallback
/**
* Result of restoring the server configuration
*/
- public static function dbRestored($task)
+ public static function dbRestored(array $task)
{
if (!Taskmanager::isFailed($task)) {
EventLog::info('Configuration backup restored.');
}
}
- public static function adConfigCreate($task)
+ public static function adConfigCreate(array $task)
{
if (Taskmanager::isFailed($task))
EventLog::warning("Could not generate Active Directory configuration", $task['data']['error']);
@@ -131,7 +134,7 @@ class TaskmanagerCallback
* @param array $task task obj
* @param array $args has keys 'moduleid' and optionally 'deleteOnError' and 'tmpTgz'
*/
- public static function cbConfModCreated($task, $args)
+ public static function cbConfModCreated(array $task, array $args)
{
$mod = Module::get('sysconfig');
if ($mod === false)
@@ -150,7 +153,7 @@ class TaskmanagerCallback
* @param array $task task obj
* @param array $args has keys 'configid' and optionally 'deleteOnError'
*/
- public static function cbConfTgzCreated($task, $args)
+ public static function cbConfTgzCreated(array $task, array $args)
{
$mod = Module::get('sysconfig');
if ($mod === false)
@@ -163,7 +166,7 @@ class TaskmanagerCallback
}
}
- public static function manualMount($task, $args)
+ public static function manualMount(array $task, $args)
{
if (!isset($task['data']['exitCode']))
return;
@@ -180,11 +183,10 @@ class TaskmanagerCallback
unset($data['storetype']);
Property::setVmStoreConfig($data);
}
- return;
}
}
- public static function mlGotList($task, $args)
+ public static function mlGotList(array $task, $args)
{
$mod = Module::get('minilinux');
if ($mod === false)
@@ -193,7 +195,7 @@ class TaskmanagerCallback
MiniLinux::listDownloadCallback($task, $args);
}
- public static function mlGotLinux($task, $args)
+ public static function mlGotLinux(array $task, $args)
{
$mod = Module::get('minilinux');
if ($mod === false)
@@ -202,7 +204,7 @@ class TaskmanagerCallback
MiniLinux::linuxDownloadCallback($task, $args);
}
- public static function rbcConnCheck($task, $args)
+ public static function rbcConnCheck(array $task, $args)
{
$mod = Module::get('rebootcontrol');
if ($mod === false)
@@ -211,4 +213,31 @@ class TaskmanagerCallback
RebootControl::connectionCheckCallback($task, $args);
}
+ public static function ipxeVersionSet(array $task)
+ {
+ $mod = Module::get('serversetup');
+ if ($mod === false)
+ return;
+ $mod->activate(1, false);
+ IPxeBuilder::setIPxeVersionCallback($task);
+ }
+
+ public static function ipxeCompileDone(array $task)
+ {
+ $mod = Module::get('serversetup');
+ if ($mod === false)
+ return;
+ $mod->activate(1, false);
+ IPxeBuilder::compileCompleteCallback($task);
+ }
+
+ public static function ssUpgradable(array $task): void
+ {
+ $mod = Module::get('systemstatus');
+ if ($mod === false)
+ return;
+ $mod->activate(1, false);
+ SystemStatus::setUpgradableData($task);
+ }
+
}
diff --git a/inc/trigger.inc.php b/inc/trigger.inc.php
index 5024b907..d0d5d365 100644
--- a/inc/trigger.inc.php
+++ b/inc/trigger.inc.php
@@ -1,5 +1,7 @@
<?php
+declare(strict_types=1);
+
/**
* This is one giant class containing various functions that will generate
* required config files, daemon instances and more, mostly through the Taskmanager.
@@ -14,20 +16,21 @@ class Trigger
/**
* Compile iPXE pxelinux menu. Needs to be done whenever the server's IP
* address changes.
- *
- * @param boolean $force force recompilation even if it seems up to date
- * @return boolean|string false if launching task failed, task-id otherwise
+ *
+ * @return ?string null if launching task failed, task-id otherwise
*/
- public static function ipxe()
+ public static function ipxe(string $taskId = null): ?string
{
+ static $lastResult = null;
+ if ($lastResult !== null)
+ return $lastResult;
$hooks = Hook::load('ipxe-update');
- static $taskId = false;
foreach ($hooks as $hook) {
$ret = function($taskId) use ($hook) {
$ret = include_once($hook->file);
if (is_string($ret))
return $ret;
- return isset($taskId) ? $taskId : false;
+ return $taskId;
};
$ret = $ret($taskId);
if (is_string($ret)) {
@@ -36,7 +39,7 @@ class Trigger
$taskId = $ret['id'];
}
}
- Property::set('ipxe-task-id', $taskId, 15);
+ $lastResult = $taskId;
return $taskId;
}
@@ -48,7 +51,7 @@ class Trigger
* @return boolean true if current configured IP address is still valid, or if a new address could
* successfully be determined, false otherwise
*/
- public static function autoUpdateServerIp()
+ public static function autoUpdateServerIp(): bool
{
for ($i = 0; $i < 5; ++$i) {
$task = Taskmanager::submit('LocalAddressesList');
@@ -59,7 +62,7 @@ class Trigger
if ($task === false)
return false;
$task = Taskmanager::waitComplete($task, 10000);
- if (!isset($task['data']['addresses']) || empty($task['data']['addresses']))
+ if (empty($task['data']['addresses']))
return false;
$serverIp = Property::getServerIp();
@@ -96,22 +99,18 @@ class Trigger
/**
* Mount the VM store into the server.
*
- * @param array $vmstore VM Store configuration to use. If false, read from properties
+ * @param array|false $vmstore VM Store configuration to use. If false, read from properties
* @param bool $ifLocalOnly Only execute task if the storage type is local (used for DNBD3)
- * @return string|false task id of mount procedure, or false on error
+ * @return ?string task id of mount procedure, or false on error
*/
- public static function mount($vmstore = false, $ifLocalOnly = false)
+ public static function mount($vmstore = false, bool $ifLocalOnly = false): ?string
{
if ($vmstore === false) {
$vmstore = Property::getVmStoreConfig();
}
if (!is_array($vmstore))
- return false;
- if (isset($vmstore['storetype'])) {
- $storetype = $vmstore['storetype'];
- } else {
- $storetype = 'unknown';
- }
+ return null;
+ $storetype = $vmstore['storetype'] ?? 'unknown';
if ($storetype === 'nfs') {
$addr = $vmstore['nfsaddr'];
$opts = 'nfsopts';
@@ -124,12 +123,8 @@ class Trigger
}
// Bail out if storage is not local, and we only want to run it in that case
if ($ifLocalOnly && $addr !== 'null')
- return false;
- if (isset($vmstore[$opts])) {
- $opts = $vmstore[$opts];
- }else {
- $opts = null;
- }
+ return null;
+ $opts = $vmstore[$opts] ?? null;
$status = Taskmanager::submit('MountVmStore', array(
'address' => $addr,
'type' => 'images',
@@ -143,7 +138,7 @@ class Trigger
// for the taskmanager to give us the existing id
$status = Taskmanager::waitComplete($status, 100);
}
- return $status['data']['existingTask'] ?? $status['id'] ?? false;
+ return $status['data']['existingTask'] ?? $status['id'] ?? null;
}
/**
@@ -151,7 +146,7 @@ class Trigger
*
* @return boolean Whether there are still callbacks pending
*/
- public static function checkCallbacks()
+ public static function checkCallbacks(): bool
{
$tasksLeft = false;
$callbackList = TaskmanagerCallback::getPendingCallbacks();
@@ -162,15 +157,16 @@ class Trigger
foreach ($callbacks as $callback) {
TaskmanagerCallback::handleCallback($callback, $status);
}
- if (Taskmanager::isFailed($status) || Taskmanager::isFinished($status))
+ if (Taskmanager::isFailed($status) || Taskmanager::isFinished($status)) {
Taskmanager::release($status);
- else
+ } else {
$tasksLeft = true;
+ }
}
return $tasksLeft;
}
- private static function triggerDaemons($action, $parent, &$taskids)
+ private static function triggerDaemons(string $action, ?string $parent, array &$taskids): ?string
{
$task = Taskmanager::submit('Systemctl', array(
'operation' => $action,
@@ -198,7 +194,7 @@ class Trigger
/**
* Stop any daemons that might be sitting on the VMstore, or database.
*/
- public static function stopDaemons($parent, &$taskids)
+ public static function stopDaemons(?string $parent, array &$taskids): ?string
{
$parent = self::triggerDaemons('stop', $parent, $taskids);
$task = Taskmanager::submit('LdadpLauncher', array(
diff --git a/inc/user.inc.php b/inc/user.inc.php
index 20e8cd3d..9ef27cd0 100644
--- a/inc/user.inc.php
+++ b/inc/user.inc.php
@@ -1,5 +1,9 @@
<?php
+declare(strict_types=1);
+
+use JetBrains\PhpStorm\NoReturn;
+
require_once('inc/session.inc.php');
class User
@@ -7,7 +11,7 @@ class User
private static $user = false;
- public static function isLoggedIn()
+ public static function isLoggedIn(): bool
{
return self::$user !== false;
}
@@ -26,12 +30,12 @@ class User
return self::$user['fullname'];
}
- public static function hasPermission($permission, $locationid = NULL)
+ public static function hasPermission(string $permission, ?int $locationid = NULL): bool
{
if (!self::isLoggedIn())
return false;
if (Module::isAvailable("permissionmanager")) {
- if ($permission{0} === '.') {
+ if ($permission[0] === '.') {
$permission = substr($permission, 1);
} else {
if (class_exists('Page')) {
@@ -54,11 +58,12 @@ class User
/**
* Confirm current user has the given permission, stop execution and show error message
* otherwise.
+ *
* @param string $permission Permission to check for
* @param null|int $locationid location this permission has to apply to, NULL if any location is sufficient
* @param null|string $redirect page to redirect to if permission is not given, NULL defaults to main page
*/
- public static function assertPermission($permission, $locationid = NULL, $redirect = NULL)
+ public static function assertPermission(string $permission, ?int $locationid = NULL, ?string $redirect = NULL): void
{
if (User::hasPermission($permission, $locationid))
return;
@@ -70,7 +75,7 @@ class User
Message::addError('main.no-permission');
Util::redirect($redirect);
} elseif (Module::isAvailable('permissionmanager')) {
- if ($permission{0} !== '.') {
+ if ($permission[0] !== '.') {
$module = Page::getModule();
if ($module !== false) {
$permission = '.' . $module->getIdentifier() . '.' . $permission;
@@ -83,12 +88,12 @@ class User
}
}
- public static function getAllowedLocations($permission)
+ public static function getAllowedLocations(string $permission): array
{
if (!self::isLoggedIn())
return [];
if (Module::isAvailable("permissionmanager")) {
- if ($permission{0} === '.') {
+ if ($permission[0] === '.') {
$permission = substr($permission, 1);
} else {
$module = Page::getModule();
@@ -105,16 +110,19 @@ class User
}
return $a;
}
- return array();
+ return [];
}
- public static function load()
+ public static function load(): bool
{
if (self::isLoggedIn())
return true;
if (Session::load()) {
- $uid = Session::get('uid');
- if ($uid === false || $uid < 1)
+ if (empty(Session::get('token'))) {
+ self::generateToken();
+ }
+ $uid = Session::getUserId();
+ if ($uid < 1)
self::logout();
self::$user = Database::queryFirst('SELECT * FROM user WHERE userid = :uid LIMIT 1', array(':uid' => $uid));
if (self::$user === false)
@@ -125,7 +133,7 @@ class User
return false;
}
- public static function testPassword($userid, $password)
+ public static function testPassword(string $userid, string $password): bool
{
$ret = Database::queryFirst('SELECT passwd FROM user WHERE userid = :userid LIMIT 1', compact('userid'));
if ($ret === false)
@@ -133,7 +141,7 @@ class User
return Crypto::verify($password, $ret['passwd']);
}
- public static function updatePassword($password)
+ public static function updatePassword(string $password): bool
{
if (!self::isLoggedIn())
return false;
@@ -142,36 +150,27 @@ class User
return Database::exec('UPDATE user SET passwd = :passwd WHERE userid = :userid LIMIT 1', compact('userid', 'passwd')) > 0;
}
- public static function login($user, $pass)
+ public static function login(string $user, string $pass, bool $fixedIp): bool
{
$ret = Database::queryFirst('SELECT userid, passwd FROM user WHERE login = :user LIMIT 1', array(':user' => $user));
if ($ret === false)
return false;
if (!Crypto::verify($pass, $ret['passwd']))
return false;
- Session::create($ret['passwd']);
- Session::set('uid', $ret['userid']);
- Session::set('token', md5($ret['passwd'] . ','
- . rand() . ','
- . time() . ','
- . rand() . ','
- . $_SERVER['REMOTE_ADDR'] . ','
- . rand() . ','
- . $_SERVER['REMOTE_PORT'] . ','
- . rand() . ','
- . $_SERVER['HTTP_USER_AGENT']));
- Session::save();
+ Session::create($ret['passwd'], (int)$ret['userid'], $fixedIp);
+ self::generateToken($ret['passwd']);
return true;
}
- public static function logout()
+ #[NoReturn]
+ public static function logout(): void
{
Session::delete();
Header('Location: ?do=Main&fromlogout');
exit(0);
}
- public static function setLastSeenEvent($eventid)
+ public static function setLastSeenEvent(int $eventid): void
{
if (!self::isLoggedIn())
return;
@@ -189,4 +188,17 @@ class User
return self::$user['lasteventid'];
}
+ private static function generateToken($salt = ''): void
+ {
+ Session::set('token', md5($salt . ','
+ . rand() . ','
+ . time() . ','
+ . rand() . ','
+ . $_SERVER['REMOTE_ADDR'] . ','
+ . rand() . ','
+ . $_SERVER['REMOTE_PORT'] . ','
+ . rand() . ','
+ . $_SERVER['HTTP_USER_AGENT']), false);
+ }
+
}
diff --git a/inc/util.inc.php b/inc/util.inc.php
index 365dc045..267a3971 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -1,164 +1,27 @@
<?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 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)
+ #[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
@@ -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,67 +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
{
// round doesn't reliably work for large floats, pick workaround depending on OS
- if (PHP_INT_SIZE === 4) {
- $bytes = sprintf('%.0f', $bytes);
- } else {
- $bytes = sprintf('%u', $bytes);
- }
+ $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;
}
@@ -289,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:
@@ -327,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;
@@ -349,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
@@ -375,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();
@@ -403,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 {
@@ -442,7 +292,7 @@ SADFACE;
}
}
if ($secure) {
- return false;
+ return null;
}
$bytes = '';
while ($length > 0) {
@@ -454,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"
@@ -482,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)
@@ -508,25 +359,25 @@ 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;
@@ -542,7 +393,8 @@ SADFACE;
$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);
}
/**
@@ -551,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 = '';
@@ -568,7 +421,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 = '/
@@ -587,15 +440,34 @@ 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 = '/
(
- (?: [\x20-\xFF] ){1,100} # ignore lower non-printable range
+ [\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');
+ }
+ }
+
}