init(); return self::$eventPage; } public static function getDbConnection() { return dbConnector::getDbConnection(); } private static $db = null; private static $message = [ "error" => null, "success" => null, "notice" => null, ]; /** id of session user * * Session user describes the currently logged in user * * @var int>0 */ private static $userId = null; private static $sessionUser = null; /** get session users id * * if not set so far it is loaded from the session cookies * * @return int id of session user */ public static function getSessionUserId() { if (!isset(self::$userId)) { self::$userId = $_SESSION["user"]["userId"] ?? null; } self::$userId = filterId(self::$userId); return self::$userId; } /** lazy loading of the session user */ public static function sessionUser(bool $forceLoading = true) { if (is_null(self::$sessionUser) || $forceLoading) { self::$sessionUser = User::loadFromDb(self::getSessionUserId()); } return self::$sessionUser; } /** Returns the current login status * * The login status is stored in the session cookie. If it is not even set it means the login is invalid. * * @return bool true if the login status is true, false otherwise */ public static function isLoginValid() { return isset($_SESSION) && array_key_exists("login", $_SESSION) && $_SESSION["login"] == true; } /** Remove all login data from the session data * * @return void */ public static function logout() { foreach (["login", "user"] as $key) { unset($_SESSION[$key]); } } /** A little Box with the login status as html entity * * @return string htmlEntity showing the login status */ public static function htmlLoginStatus() { return '
' . "Datum: " . date("Y-m-d") . "
" . "Eingeloggt als " . htmlspecialchars($_SESSION["user"]["username"]) . ".
" . 'Sitzung beenden' . "
"; } /** Checking if an action is allowed. A present apiKey overrides (and deletes) a present login. * * 1. If apiKey is present: * - chancel current login * - If apiKey is valid for requested action: * - set some session data * - return * 2. If there is a valid Login session: * - return * 3. redirect to Login Page * * @todo rename to authenticate (authorize?) * * @param $action the action we want to get authorized for (default=['login']) * * @retval void */ public static function authentificate($action = "login") { // Ensure a session is started session_start(); // check if an apiKey was received if (array_key_exists("apiKey", $_GET)) { self::logout(); $key = ApiKey::loadFromDb($_GET["apiKey"]); if ($key !== null && $key->isValidFor($action)) { $user = User::loadFromDb($key->getUserId()); // case valid login: Set the session data $_SESSION = [ "login" => true, //false, "apiKey" => $key->getKey(), "user" => [ "username" => $user->getLoginName(), "userId" => $user->getId(), "userConfig" => $user->getConfig(), ], ]; logLoginsToJsonFile($user->getLoginName()); // we're not logged in, but authorized for the stuff we want to do. So don't redirect return; } } // if not returned yet: no login, no valid apiKey -> redirect to login page if (!self::isLoginValid()) { header( "Location: login?returnToUrl=" . urlencode( $_SERVER["REQUEST_URI"] . ($_POST["fragment"] ?? ""), ), true, 301, ); exit(); // shouldn't matter } } /** Initialize the participoApp * * - init the db connection * - validate the login * * @param array $config configuration values (e.g. for the database) * @param array $secrets secret values (e.g. passwords) * @return void */ public static function init(array $config, array $secrets): void { self::initDb( $config["db"]["host"], $config["db"]["name"], $config["db"]["user"], $secrets["db"][$config["db"]["user"]], $config["db"]["dbCharset"], $config["db"]["outCharset"], ); self::authentificate(); } public static function initDb( $host, $name, $user, $password, $dbCharset, $outCharset, ) { dbConnector::setOptions([ "dbCharset" => $dbCharset, "outCharset" => $outCharset, ]); dbConnector::connect($host, $name, $user, $password); } /** Framework to parse parameters get/post requests * * - Each param given in to this function is looked up in the request and put through the parsing function. * - Params in the request that aren't in given a parsing function aren't parsed and hence not returned. * * @param array $params [array(paramName => parseFunction)] array of the name of the param and a sanitizer/parsing/input function * @return array array(parsedParam=>paramValue) Associative array of the name of the param and its parsed value */ public static function parseParams(array $params): array { $method = $_SERVER["REQUEST_METHOD"]; // $request = explode("/", substr(@$_SERVER["PATH_INFO"], 1)); $parsedParams = []; foreach ($params as $paramName => $parseFunction) { $parsedParams[$paramName] = null; switch ($method) { // case 'PUT': // do_something_with_put($request); // break; case "POST": $parsedParams[$paramName] = $parseFunction( $_POST[$paramName] ?? null, ); break; case "GET": $parsedParams[$paramName] = $parseFunction( $_GET[$paramName] ?? null, ); break; default: // handle_error($request); break; } } return $parsedParams; } public static function getMessages() { return self::$message; } public static function addMessage($type, $message) { self::$message[$type] = (self::$message[$type] ?? "") . $message; } /** check password for user * * @param string $loginName user who wants to get in * @param string $password password for the user * * @retval true $password belongs to $loginName * @retval false otherwise */ public static function checkCredentials($loginName, $password) { sleep(1); // just to discourage brute force attacks // Check for dbConnection if (!dbConnector::getDbConnection()) { self::addMessage("error", "
No DbConnection available
"); return false; } $user = User::loadFromDbByLoginName($loginName); // If there is no such user OR the password isn't valid the login fails if ($user == null || !$user->verifyPassword($password)) { sleep(5); // discourage brute force attacks self::addMessage( "error", "
Falsches Passwort oder LoginName
", ); return false; } session_start(); // case valid login: Set the session data $_SESSION = [ "login" => true, "user" => [ "username" => $user->getLoginName(), "userId" => $user->getId(), "userConfig" => $user->getConfig(), ], ]; // Logging Logins logLoginsToJsonFile($_SESSION["user"]["username"]); self::addMessage("success", "
Anmeldung erfolgreich
"); return true; } /** Checks, if a user is an admin * * @param int $userId id of the user to check (defaults to `null`)) * @return bool `true` if user with id $userId has attribute "isAdmin", `false` otherwise */ public static function isUserAdmin(?int $userId = null): bool { $userId ??= $_SESSION["user"]["userId"]; return self::hasUserAttribute($userId, "isAdmin"); } public static function getUserId() {} /** get current logged in users kids */ public static function getKids($userId = null) { $userId = $userId ?? ($_SESSION["user"]["userId"] ?? null); $query = "SELECT * FROM `wkParticipo_Users` " . "INNER JOIN `vormundschaft` " . "ON `wkParticipo_Users`.`id` = `vormundschaft`.`kidId` " . "INNER JOIN `wkParticipo_user<=>userAttributes` " . "ON `wkParticipo_Users`.`id` = `wkParticipo_user<=>userAttributes`.`userId`" . "WHERE `vormundschaft`.`userId` = :userId " . "AND `vormundschaft`.`userId` = :userId " . "AND `wkParticipo_user<=>userAttributes`.`attributeId` = 4;"; $params = [ ":userId" => ["value" => $userId, "data_type" => PDO::PARAM_INT], ]; $response = dbConnector::query($query, $params); $kids = []; foreach ($response as $r) { $kids[] = User::fromDbArray($r, ["id" => "kidId"]); } return $kids; } /** Check if one user is ward of another * * @param integer $kidId ID of the ward * @param ?integer $userId ID of the guardian, if null it is tried to read it from the session data * @return bool true if kid is ward of user, false otherwise */ public static function isWardOf(int $kidId, ?int $userId = null): bool { // Try to get the Guard from the session data. $userId ??= $_SESSION["user"]["userId"] ?? null; $query = "SELECT `kidId` FROM `vormundschaft` WHERE `userId` = :userId AND `kidId` = :kidId;"; $params = [ ":userId" => ["value" => $userId, "data_type" => PDO::PARAM_INT], ":kidId" => ["value" => $kidId, "data_type" => PDO::PARAM_INT], ]; $response = dbConnector::query($query, $params); return count($response) >= 1; } /** Checks, if a user as a certain attribute * * @param [type] $userId id of the user to check * @param [type] $attributeName string name of the attribute to check * @return boolean */ public static function hasUserAttribute($userId, $attributeName) { // sqlQuery: Select the user if it has the given attribute $query = <<userAttributes`.userId, `wkParticipo_userAttributes`.name FROM `wkParticipo_user<=>userAttributes` LEFT JOIN `wkParticipo_userAttributes` ON `wkParticipo_user<=>userAttributes`.`attributeId` = `wkParticipo_userAttributes`.`id` WHERE `wkParticipo_userAttributes`.name = :attributeName AND userId=:userId; SQL; $params = [ ":userId" => ["value" => $userId, "data_type" => PDO::PARAM_INT], ":attributeName" => [ "value" => $attributeName, "data_type" => PDO::PARAM_STR, ], ]; $attributedUsers = dbConnector::query($query, $params); // Since the id should be unique, there should only be one result this is just for dealing with empty arrays foreach ($attributedUsers as $u) { if ($u["userId"] == $userId) { return true; } } return false; } public static function getEventStarter($sinceDate = null) { $userId = $_SESSION["user"]["userId"]; if (!$sinceDate) { $sinceDate = "CURDATE()"; } else { $sinceDate = 'DATE("' . $sinceDate . '")'; } $query = <<= $sinceDate AND `vormundschaft`.`userId` = $userId ORDER BY `wkParticipo_Events`.`date` DESC; SQL; $comingStarts = dbConnector::query($query); return $comingStarts; } } /** Action element of an MaterializeCss (App-)card */ class AppCardAction { private $caption = null; //< Caption for the action private $link = "."; //< link for the action /** Constructor for the AppAction * * @param string $caption caption for the action * @param string $link link to the action */ public function __construct($caption, $link = ".") { //! @todo input sanitation $this->link = $link; $this->caption = $caption; } /** Create htmlCode for the action * * @return string with htmlCode of the action */ public function htmlCode() { return '' . $this->caption . ""; } /** Create AppCardAction from assoziative array * * @param array $member array with the member values * @return AppCardAction */ public static function fromArray($member) { $caption = $member["caption"] ?? null; $link = $member["link"] ?? "."; return new AppCardAction($caption, $link); } } /** MaterializeCss card for an App */ class AppCard { private $title = ""; //< title of the card private $description = ""; //< description of the App private $link = null; //< link for the card-content private $imgUrl = null; //< url for an image right under the title private $actionList = []; //< list of actions for the bottom of the card /** * Constructor for the AppCard * * @param string $title title of the card * @param string $description description of the card * @param string $link link for the card-content * @param string $imgUrl url for an image right under the title * @param array $actionList list of actions at the bottom of the card */ public function __construct( $title, $description, $link = null, $imgUrl = null, $actionList = [], ) { //! @todo input sanitation $this->title = $title; $this->description = $description; $this->link = $link; $this->imgUrl = $imgUrl; $this->actionList = $actionList; } /** * Create htmlCode for the AppCard * * @return string html code for the AppCard */ public function htmlCode($options = []) { $extraClass = $options["extraClass"] ?? ""; $actionListCode = ""; foreach ($this->actionList as $a) { $actionListCode .= $a->htmlCode(); } return '
' . '
' . '
' . ($this->link != null ? '' : "") . '' . $this->title . "" . ($this->link != null ? "" : "") . ($this->imgUrl != null ? '' .
                    $this->title .
                    '' : "") . "

" . $this->description . "

" . "
" . '
' . $actionListCode . "
" . "
" . "
"; } /** * Create AppCard from an associative array * * @param array $member array with member as keys and values as the member values * @return AppCard from array values */ public static function fromArray($member) { $title = $member["title"] ?? ""; $description = $member["description"] ?? ""; $link = $member["link"] ?? null; $imgUrl = $member["imgUrl"] ?? null; $actionList = $member["actions"] ?? []; return new AppCard($title, $description, $link, $imgUrl, $actionList); } } /** Generate a html table of the last logins of the users * * @param string $jsonFileName path to the json file with the logged logins * @return string Html table of users last logins */ function lastLoginTable($jsonFileName = "lastLogins.json") { // load the jsonfile into an associative array $lastLogins = is_file($jsonFileName) ? json_decode(file_get_contents($jsonFileName), true) : []; // collecting the last login of the users ... $lastLoginRows = []; foreach ($lastLogins as $userName => $lastLogins) { $lastLoginRows[$userName] = $lastLogins["lastLogins"][0]; } // and sort it so the last login is first in line arsort($lastLoginRows); // build the table $lastLoginsTable = "" . "" . ""; foreach ($lastLoginRows as $userName => $lastLogin) { $lastLoginsTable .= ""; } $lastLoginsTable .= "" . "
userNamelastLogin
" . $userName . "" . $lastLogin . "
"; return $lastLoginsTable; } /// Eine Fehler/Warnung/Notiz/Erfolgsmeldung als divBox im String zurückgeben function htmlRetMessage($anRetMessage) { $retHtmlString = ""; if (!empty($anRetMessage)) { $retHtmlString .= '
'; if (!empty($anRetMessage["error"])) { $retHtmlString .= '
'; $retHtmlString .= "ERROR:
"; $retHtmlString .= $anRetMessage["error"]; $retHtmlString .= "
"; } if (!empty($anRetMessage["warning"])) { $retHtmlString .= '
'; $retHtmlString .= "WARNING:
"; $retHtmlString .= $anRetMessage["warning"]; $retHtmlString .= "
"; } if (!empty($anRetMessage["notice"])) { $retHtmlString .= '
'; $retHtmlString .= "Info:
"; $retHtmlString .= $anRetMessage["notice"]; $retHtmlString .= "
"; } if (!empty($anRetMessage["success"])) { $retHtmlString .= '
'; $retHtmlString .= "SUCCESS:
"; $retHtmlString .= $anRetMessage["success"]; $retHtmlString .= "
"; } $retHtmlString .= "
"; } return $retHtmlString; } /** load a MarkdownFile with yaml header * * @param string $fileName filename of the markdown file * @return array associative array('yaml'=>array(..), 'mdText'=>string) containing the yamlHeader as associative array and the markdown text as string */ function loadMarkdownFile($fileName) { // load the whole file $fileText = file_get_contents($fileName); // split at '---' to get ((),yamls,array) $fileParts = preg_split('/[\n]*[-]{3}[\n]/', $fileText, 3); // not all mdFiles have a yamlHeader, so the mdText can be at different indices $yaml = []; $mdText = ""; switch (count($fileParts)) { case 1: $mdText = $fileParts[0]; break; case 3: $yaml = Spyc::YAMLLoadString($fileParts[1]); $mdText = $fileParts[2]; break; default: $mdText = $fileText; } // get a title, if none is in the markdown if (!array_key_exists("title", $yaml)) { // find the first heading, set it as header and remove it from the markdown if (preg_match('/^#(.*)$/m', $mdText, $matches)) { $yaml["title"] = $matches[1]; $mdText = preg_replace('/^#(.*)$/m', "", $mdText, 1); } else { // fallback for the title, if not even one heading is found $yaml["title"] = ""; } } return [ "yaml" => $yaml, "mdText" => $mdText, ]; } /** Log the Login of an user into a logFile * * @param string $userName name of the user * @param string $fileName filename to log to * @return void */ function logLoginsToJsonFile($userName, $fileName = "lastLogins.json") { try { $lastLogins = is_file($fileName) ? json_decode(file_get_contents($fileName), true) : null; if ($lastLogins == null) { return; } if (!array_key_exists($userName, $lastLogins)) { $lastLogins[$userName] = []; } if (!array_key_exists("lastLogins", $lastLogins[$userName])) { $lastLogins[$userName]["lastLogins"] = []; } $usersLastLogins = $lastLogins[$userName]["lastLogins"]; $usersLastLogins = array_merge([date("Y-m-d H:i:s")], $usersLastLogins); $usersLastLogins = array_slice($usersLastLogins, 0, 10); $lastLogins[$userName]["lastLogins"] = $usersLastLogins; file_put_contents($fileName, json_encode($lastLogins)); } catch (Exception $e) { // silently ignore errors } } /// @brief Gibt die URL der gerade aufgerufenen Seite zurück function getCurPagesUrl() { $pageURL = "http"; if ($_SERVER["HTTPS"] ?? "off" == "on") { $pageURL .= "s"; } $pageURL .= "://"; if ($_SERVER["SERVER_PORT"] != "80") { $pageURL .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"]; } else { $pageURL .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"]; } return $pageURL; } /** space saving way to put a date * * @param [DateTime] $date * @return string html tag containing the date */ function getHtmlSquareDate($date = null) { $date = $date ?? new DateTime(); $year = $date->format("Y"); $month = $date->format("M"); $day = $date->format("d"); return "
" . "
" . '' . $day . "" . '' . $month . "" . "
" . '
' . $year . "
" . "
"; } /** filter_var for a pos int * * check for int; null is default; only values > 0 are excepted * * @param [type] $id * @return int (>0) filtered id or null on failure * */ function filterPosInt(mixed $id): ?int { return filter_var($id, FILTER_VALIDATE_INT, [ "options" => ["default" => null, "min_range" => 1], ]); } /** filter_var for a (db)id * * check for valid id; null is default; only values > 0 are excepted * * @param mixed $id * @return ?int filtered id or null on failure */ function filterId(mixed $id): ?int { return filterPosInt($id); } /** filter a variable as count * * - count means a non negative integer * - helper function to stay DRY * * @param mixed $variable * @param integer $min * @return integer the input variable as integer >= 0 */ function filterCount($variable, int $min = 0) { return filter_var($variable, FILTER_VALIDATE_INT, [ "options" => ["default" => null, "min_range" => 1], ]); }