adm_program/system/bootstrap/function.php
<?php
/**
***********************************************************************************************
* Various common functions
*
* @copyright The Admidio Team
* @see https://www.admidio.org/
* @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
***********************************************************************************************
*/
use Ramsey\Uuid\Uuid;
use Admidio\Exception;
if (basename($_SERVER['SCRIPT_FILENAME']) === 'function.php') {
exit('This page may not be called directly!');
}
/**
* Function checks if the user is a member of the role.
* If **userId** is not set than this will be checked for the current user
* @param string $roleName The name of the role where the membership of the user should be checked
* @param int $userId The id of the user who should be checked if he is a member of the role.
* If @userId is not set than this will be checked for the current user
* @return bool Returns **true** if the user is a member of the role
*/
function hasRole(string $roleName, int $userId = 0): bool
{
if ($userId === 0) {
$userId = $GLOBALS['gCurrentUserId'];
}
$sql = 'SELECT mem_id
FROM ' . TBL_MEMBERS . '
INNER JOIN ' . TBL_ROLES . '
ON rol_id = mem_rol_id
INNER JOIN ' . TBL_CATEGORIES . '
ON cat_id = rol_cat_id
WHERE mem_usr_id = ? -- $userId
AND mem_begin <= ? -- DATE_NOW
AND mem_end > ? -- DATE_NOW
AND rol_name = ? -- $roleName
AND rol_valid = true
AND ( cat_org_id = ? -- $GLOBALS[\'gCurrentOrgId\'
OR cat_org_id IS NULL )';
$statement = $GLOBALS['gDb']->queryPrepared($sql, array($userId, DATE_NOW, DATE_NOW, $roleName, $GLOBALS['gCurrentOrgId']));
return $statement->rowCount() === 1;
}
/**
* Function checks if the user is a member in a role of the current organization.
* @param int $userId The id of the user who should be checked if he is a member of the current organization
* @return bool Returns **true** if the user is a member
*/
function isMember(int $userId): bool
{
if ($userId === 0) {
return false;
}
$sql = 'SELECT COUNT(*) AS count
FROM ' . TBL_MEMBERS . '
INNER JOIN ' . TBL_ROLES . '
ON rol_id = mem_rol_id
INNER JOIN ' . TBL_CATEGORIES . '
ON cat_id = rol_cat_id
WHERE mem_usr_id = ? -- $userId
AND mem_begin <= ? -- DATE_NOW
AND mem_end > ? -- DATE_NOW
AND rol_valid = true
AND ( cat_org_id = ? -- $GLOBALS[\'gCurrentOrgId\']
OR cat_org_id IS NULL )';
$statement = $GLOBALS['gDb']->queryPrepared($sql, array($userId, DATE_NOW, DATE_NOW, $GLOBALS['gCurrentOrgId']));
return $statement->fetchColumn() > 0;
}
/**
* Function checks if the user is a group leader in a role of the current organization.
* If you use the **roleId** parameter you can check if the user is group leader of that role.
* @param int $userId The id of the user who should be checked if he is a group leader
* @param int $roleId (optional) If set <> 0 than the function checks if the user is group leader of this role
* otherwise it checks if the user is group leader in one role of the current organization
* @return bool Returns **true** if the user is a group leader
*/
function isGroupLeader(int $userId, int $roleId = 0): bool
{
if ($userId === 0) {
return false;
}
$sql = 'SELECT mem_id
FROM ' . TBL_MEMBERS . '
INNER JOIN ' . TBL_ROLES . '
ON rol_id = mem_rol_id
INNER JOIN ' . TBL_CATEGORIES . '
ON cat_id = rol_cat_id
WHERE mem_usr_id = ? -- $userId
AND mem_begin <= ? -- DATE_NOW
AND mem_end > ? -- DATE_NOW
AND mem_leader = true
AND rol_valid = true
AND ( cat_org_id = ? -- $GLOBALS[\'gCurrentOrgId\']
OR cat_org_id IS NULL )';
$queryParams = array($userId, DATE_NOW, DATE_NOW, $GLOBALS['gCurrentOrgId']);
if ($roleId > 0) {
$sql .= ' AND mem_rol_id = ? -- $roleId';
$queryParams[] = $roleId;
}
$statement = $GLOBALS['gDb']->queryPrepared($sql, $queryParams);
return $statement->rowCount() > 0;
}
/**
* This function returns a page navigation depending on the number of pages.
* Parts of this function are from generatePagination from phpBB2.
* Example:
* Page: < Previous 1 2 3 ... 9 10 11 Next >
*
* @param string $baseUrl Basislink zum Modul
* @param int $itemsCount Gesamtanzahl an Elementen
* @param int $itemsPerPage Anzahl Elemente pro Seite
* @param int $pageStartItem Mit dieser Elementnummer beginnt die aktuelle Seite
* @param bool $addPrevNextText Show link with "Previous" and "Next"
* @param string $queryParamName (optional) You can set a new name for the parameter that should be used as start parameter.
* @return string
* @throws Exception
*/
function admFuncGeneratePagination(string $baseUrl, int $itemsCount, int $itemsPerPage, int $pageStartItem, bool $addPrevNextText = true, string $queryParamName = 'start'): string
{
global $gL10n;
if ($itemsCount === 0 || $itemsPerPage === 0) {
return '';
}
$totalPagesCount = (int)ceil($itemsCount / $itemsPerPage);
if ($totalPagesCount <= 1) {
return '';
}
/**
* @param int $start
* @param int $end
* @param int $page
* @param string $url
* @param string $paramName
* @param int $itemsPerPage
* @return string
*/
function getListElementsFromTo(int $start, int $end, int $page, string $url, string $paramName, int $itemsPerPage): string
{
$pageNavString = '';
for ($i = $start; $i < $end; ++$i) {
if ($i === $page) {
$pageNavString .= getListElementString((string)$i, 'page-item active');
} else {
$pageNavString .= getListElementString((string)$i, 'page-item', $url, $paramName, ($i - 1) * $itemsPerPage);
}
}
return $pageNavString;
}
/**
* @param string $linkText
* @param string $className
* @param string $url
* @param string $paramName
* @param int|null $paramValue
* @return string
*/
function getListElementString(string $linkText, string $className = '', string $url = '', string $paramName = '', int $paramValue = 0): string
{
$urlString = '#';
if ($url !== '') {
$urlString = $url . '&' . $paramName . '=' . $paramValue;
}
return '<li class="page-item ' . $className . '"><a class="page-link" href="' . $urlString . '">' . $linkText . '</a></li>';
}
$onPage = (int)floor($pageStartItem / $itemsPerPage) + 1;
$pageNavigationString = '';
if ($totalPagesCount > 7) {
$initPageMax = 3;
$pageNavigationString .= getListElementsFromTo(1, $initPageMax + 1, $onPage, $baseUrl, $queryParamName, $itemsPerPage);
$disabledLink = '<li class="page-item disabled"><a>...</a></li>';
if ($onPage > 1 && $onPage < $totalPagesCount) {
$pageNavigationString .= ($onPage > 5) ? $disabledLink : ' ';
$initPageMin = ($onPage > 4) ? $onPage : 5;
$initPageMax = ($onPage < $totalPagesCount - 4) ? $onPage : $totalPagesCount - 4;
$pageNavigationString .= getListElementsFromTo($initPageMin - 1, $initPageMax + 2, $onPage, $baseUrl, $queryParamName, $itemsPerPage);
$pageNavigationString .= ($onPage < $totalPagesCount - 4) ? $disabledLink : ' ';
} else {
$pageNavigationString .= $disabledLink;
}
$pageNavigationString .= getListElementsFromTo($totalPagesCount - 2, $totalPagesCount + 1, $onPage, $baseUrl, $queryParamName, $itemsPerPage);
} else {
$pageNavigationString .= getListElementsFromTo(1, $totalPagesCount + 1, $onPage, $baseUrl, $queryParamName, $itemsPerPage);
}
if ($addPrevNextText) {
$pageNavClassPrev = '';
if ($onPage === 1) {
$pageNavClassPrev = 'disabled';
}
$pageNavClassNext = '';
if ($onPage === $totalPagesCount) {
$pageNavClassNext = 'disabled';
}
$pageNavigationPrevText = getListElementString($gL10n->get('SYS_BACK'), $pageNavClassPrev, $baseUrl, $queryParamName, ($onPage - 2) * $itemsPerPage);
$pageNavigationNextText = getListElementString($gL10n->get('SYS_PAGE_NEXT'), $pageNavClassNext, $baseUrl, $queryParamName, $onPage * $itemsPerPage);
$pageNavigationString = $pageNavigationPrevText . $pageNavigationString . $pageNavigationNextText;
}
return '<nav><ul class="pagination">' . $pageNavigationString . '</ul></nav>';
}
/**
* Verify the content of an array element if it's the expected datatype
*
* The function is designed to check the content of **$_GET** and **$_POST** elements and should be used at the
* beginning of a script. If the value of the defined datatype is not valid then an error will be shown. If no
* value was set then the parameter will be initialized. The function can be used with every array and their elements.
* You can set several flags (like required value, datatype …) that should be checked.
*
* @param array<string,mixed> $array The array with the element that should be checked
* @param string $variableName Name of the array element that should be checked
* @param string $datatype The datatype like **string**, **uuid**, **numeric**, **int**, **float**, **bool**, **boolean**, **html**,
* **url**, **date**, **file** or **folder** that is expected and which will be checked.
* Datatype **date** expects a date that has the Admidio default format from the
* preferences or the english date format **Y-m-d**
* @param array<string,mixed> $options (optional) An array with the following possible entries:
* - defaultValue : A value that will be set if the variable has no value
* - **requireValue** : If set to **true** than a value is required otherwise the function
* returns an error
* - **validValues** : An array with all values that the variable could have. If another
* value is found than the function returns an error
* - **directOutput** : If set to **true** the function returns only the error string, if set
* to false a html message with the error will be returned
* @return mixed|null Returns the value of the element or the error message if a test failed
*
* **Code example**
* ```
* // numeric value that would get a default value 0 if not set
* $getDateId = admFuncVariableIsValid($_GET, 'dat_id', 'numeric', array('defaultValue' => 0));
*
* // string that will be initialized with text of id SYS_EVENTS
* $getHeadline = admFuncVariableIsValid($_GET, 'headline', 'string', array('defaultValue' => $gL10n->get('SYS_EVENTS')));
*
* // string initialized with actual and the only allowed values are actual and old
* $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'actual', 'validValues' => array('actual', 'old')));
* ```
* @throws Exception
*/
function admFuncVariableIsValid(array $array, string $variableName, string $datatype, array $options = array())
{
global $gSettingsManager;
// create array with all options
$optionsDefault = array('defaultValue' => null, 'requireValue' => false, 'validValues' => null, 'directOutput' => null);
$optionsAll = array_replace($optionsDefault, $options);
// set default value for each datatype if no value is given and no value was required
if (array_key_exists($variableName, $array) && $array[$variableName] !== '') {
if ($datatype === 'bool' || $datatype === 'boolean') {
$value = (bool)$array[$variableName];
} elseif ($datatype === 'numeric' || $datatype === 'int') {
$value = (int)$array[$variableName];
} elseif ($datatype === 'float') {
$value = (float)$array[$variableName];
} elseif ($datatype === 'array') {
$value = $array[$variableName];
} else {
$value = (string)$array[$variableName];
}
} else {
if ($optionsAll['requireValue']) {
// if value is required and no value is given then show error
throw new Exception('The mandatory parameter "' . $variableName . '" has no value!');
} elseif ($optionsAll['defaultValue'] !== null) {
// if a default value was set then take this value
if (is_string($optionsAll['defaultValue'])) {
$value = html_entity_decode($optionsAll['defaultValue']);
} else {
$value = $optionsAll['defaultValue'];
}
} else {
// no value set then initialize the parameter
if ($datatype === 'bool' || $datatype === 'boolean') {
$value = false;
} elseif ($datatype === 'numeric' || $datatype === 'int') {
$value = 0;
} elseif ($datatype === 'float') {
$value = 0.0;
} else {
$value = '';
}
return $value;
}
}
// check if parameter has a valid value
// do a strict check with in_array because the function don't work properly
if ($optionsAll['validValues'] !== null && !in_array($value, $optionsAll['validValues'], true)) {
throw new Exception('The parameter "' . $variableName . '" has an invalid value!');
}
switch ($datatype) {
case 'file': // fallthrough
case 'folder':
if ($value !== '') {
StringUtils::strIsValidFileName($value, false);
}
break;
case 'date':
// check if date is a valid Admidio date format
$objAdmidioDate = DateTime::createFromFormat($gSettingsManager->getString('system_date'), $value);
if (!$objAdmidioDate) {
// check if date has english format
$objEnglishDate = DateTime::createFromFormat('Y-m-d', $value);
if (!$objEnglishDate) {
throw new Exception('The date parameter "' . $variableName . '" has an invalid date format!');
}
}
break;
case 'bool': // fallthrough
case 'boolean':
$valid = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($valid === null) {
throw new Exception('The boolean parameter "' . $variableName . '" has an invalid value!');
}
$value = $valid;
break;
case 'int': // fallthrough
case 'float': // fallthrough
case 'numeric':
// numeric datatype should only contain numbers
if (!is_numeric($value)) {
throw new Exception('The numeric parameter ' . $variableName . ' has an invalid value!');
} else {
if ($datatype === 'int') {
$value = filter_var($value, FILTER_VALIDATE_INT);
} elseif ($datatype === 'float') {
$value = filter_var($value, FILTER_VALIDATE_FLOAT);
} else {
// https://www.php.net/manual/en/function.is-numeric.php#107326
$value += 0;
}
}
break;
case 'string':
$value = SecurityUtils::encodeHTML(StringUtils::strStripTags($value));
break;
case 'html':
// check html string vor invalid tags and scripts
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Doctype', 'HTML 4.01 Transitional');
$config->set('Attr.AllowedFrameTargets', array('_blank', '_top', '_self', '_parent'));
$config->set('Cache.SerializerPath', ADMIDIO_PATH . FOLDER_DATA . '/templates');
$filter = new HTMLPurifier($config);
$value = $filter->purify($value);
break;
case 'uuid':
if (!Uuid::isValid($value)) {
throw new Exception('The parameter "' . $variableName . '" is not a valid UUID!');
}
break;
case 'url':
if (!StringUtils::strValidCharacters($value, 'url')) {
throw new Exception('The parameter "' . $variableName . '" has an invalid URL!');
}
break;
}
return $value;
}
/**
* Creates a html fragment with information about user and time when the recordset was created
* and when it was at last edited. Therefore, all necessary data must be set in the function
* parameters. If userId is not set then the function will show **deleted user**.
* @param int $userIdCreated ID of the user who create the recordset.
* @param string $timestampCreate Date and time of the moment when the user create the recordset.
* @param int $userIdEdited ID of the user last changed the recordset.
* @param string $timestampEdited Date and time of the moment when the user last changed the recordset
* @return string Returns a html string with usernames who creates item and edit item the last time
* @throws Exception
* @deprecated 5.0.0:5.1.0 "admFuncShowCreateChangeInfoById()" is deprecated, use "TableAccess::getNameOfCreatingUser()" instead.
*/
function admFuncShowCreateChangeInfoById(int $userIdCreated, string $timestampCreate, int $userIdEdited = 0, string $timestampEdited = ''): string
{
global $gDb, $gProfileFields, $gL10n, $gSettingsManager;
// only show info if system setting is activated
if ((int)$gSettingsManager->get('system_show_create_edit') === 0) {
return '';
}
// compose name of user who create the recordset
$htmlCreateName = '';
$userUuidCreated = '';
if ($timestampCreate !== '') {
if ($userIdCreated > 0) {
$userCreate = new User($gDb, $gProfileFields, $userIdCreated);
$userUuidCreated = $userCreate->getValue('usr_uuid');
if ((int)$gSettingsManager->get('system_show_create_edit') === 1) {
$htmlCreateName = $userCreate->getValue('FIRST_NAME') . ' ' . $userCreate->getValue('LAST_NAME');
} else {
$htmlCreateName = $userCreate->getValue('usr_login_name');
}
} else {
$htmlCreateName = $gL10n->get('SYS_DELETED_USER');
}
}
// compose name of user who edit the recordset
$htmlEditName = '';
$userUuidEdited = '';
if ($timestampEdited !== '') {
if ($userIdEdited > 0) {
$userEdit = new User($gDb, $gProfileFields, $userIdEdited);
$userUuidEdited = $userEdit->getValue('usr_uuid');
if ((int)$gSettingsManager->get('system_show_create_edit') === 1) {
$htmlEditName = $userEdit->getValue('FIRST_NAME') . ' ' . $userEdit->getValue('LAST_NAME');
} else {
$htmlEditName = $userEdit->getValue('usr_login_name');
}
} else {
$htmlEditName = $gL10n->get('SYS_DELETED_USER');
}
}
if ($htmlCreateName === '' && $htmlEditName === '') {
return '';
}
// get html output from other function
return admFuncShowCreateChangeInfoByName(
$htmlCreateName,
$timestampCreate,
$htmlEditName,
$timestampEdited,
$userUuidCreated,
$userUuidEdited
);
}
/**
* Creates a html fragment with information about user and time when the recordset was created
* and when it was at last edited. Therefore, all necessary data must be set in the function
* parameters. If username is not set then the function will show **deleted user**.
* @param string $userNameCreated ID of the user who create the recordset.
* @param string $timestampCreate Date and time of the moment when the user create the recordset.
* @param string|null $userNameEdited ID of the user last changed the recordset.
* @param string|null $timestampEdited Date and time of the moment when the user last changed the recordset
* @param string $userUuidCreated (optional) The uuid of the user who create the recordset.
* If uuid is set than a link to the user profile will be created
* @param string $userUuidEdited (optional) The uuid of the user last changed the recordset.
* If uuid is set than a link to the user profile will be created
* @return string Returns a html string with usernames who creates item and edit item the last time
* @throws Exception
* @deprecated 5.0.0:5.1.0 "admFuncShowCreateChangeInfoByName()" is deprecated, use "TableAccess::getNameOfCreatingUser()" instead.
*/
function admFuncShowCreateChangeInfoByName(string $userNameCreated, string $timestampCreate, string $userNameEdited = '', string $timestampEdited = '', string $userUuidCreated = '', string $userUuidEdited = ''): string
{
global $gL10n, $gValidLogin, $gSettingsManager;
// only show info if system setting is activated
if ((int)$gSettingsManager->get('system_show_create_edit') === 0) {
return '';
}
$html = '';
// compose name of user who create the recordset
if ($timestampCreate !== '') {
$userNameCreated = trim($userNameCreated);
if ($userNameCreated === '') {
$userNameCreated = $gL10n->get('SYS_DELETED_USER');
}
// if valid login and a user id is given than create a link to the profile of this user
if ($gValidLogin && $userUuidCreated !== '' && $userNameCreated !== $gL10n->get('SYS_SYSTEM')) {
$userNameCreated = '<a href="' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/profile/profile.php', array('user_uuid' => $userUuidCreated)) .
'">' . $userNameCreated . '</a>';
}
$html .= '<span class="admidio-info-created">' . $gL10n->get('SYS_CREATED_BY_AND_AT', array($userNameCreated, $timestampCreate)) . '</span>';
}
// compose name of user who edit the recordset
if ($timestampEdited !== '') {
$userNameEdited = trim($userNameEdited);
if ($userNameEdited === '') {
$userNameEdited = $gL10n->get('SYS_DELETED_USER');
}
// if valid login and a user id is given than create a link to the profile of this user
if ($gValidLogin && $userUuidEdited !== '' && $userNameEdited !== $gL10n->get('SYS_SYSTEM')) {
$userNameEdited = '<a href="' . SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES . '/profile/profile.php', array('user_uuid' => $userUuidEdited)) .
'">' . $userNameEdited . '</a>';
}
$html .= '<span class="info-edited">' . $gL10n->get('SYS_LAST_EDITED_BY', array($userNameEdited, $timestampEdited)) . '</span>';
}
if ($html === '') {
return '';
}
return '<div class="admidio-info-created-edited">' . $html . '</div>';
}
/**
* Prefix url with "http://" if no protocol is defined and check if is valid url
* @param $url string
* @return false|string
*/
function admFuncCheckUrl(string $url)
{
// Homepage url have to start with "http://"
if (!StringUtils::strStartsWith($url, 'http://', false) && !StringUtils::strStartsWith($url, 'https://', false)) {
$url = 'http://' . $url;
}
// For Homepage only valid url chars are allowed
if (!StringUtils::strValidCharacters($url, 'url')) {
return false;
}
return $url;
}
/**
* This is a safe method for redirecting.
* @param string $url The URL where redirecting to. Must be an absolute URL. (www.example.org)
* @param int $statusCode The status-code which should be sent. (301, 302, 303 (default), 307)
* @throws Exception
* @see https://www.owasp.org/index.php/Open_redirect
*/
function admRedirect(string $url, int $statusCode = 303)
{
global $gLogger, $gMessage, $gL10n;
$loggerObject = array('url' => $url, 'statusCode' => $statusCode);
if (headers_sent()) {
$gLogger->error('REDIRECT: Header already sent!', $loggerObject);
$gMessage->show($gL10n->get('SYS_HEADER_ALREADY_SENT'));
// => EXIT
}
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
$gLogger->error('REDIRECT: URL is not a valid URL!', $loggerObject);
$gMessage->show($gL10n->get('SYS_REDIRECT_URL_INVALID'));
// => EXIT
}
if (!in_array($statusCode, array(301, 302, 303, 307), true)) {
$gLogger->error('REDIRECT: Status Code is not allowed!', $loggerObject);
$gMessage->show($gL10n->get('SYS_STATUS_CODE_INVALID'));
// => EXIT
}
// Check if $url starts with the Admidio URL
if (strpos($url, ADMIDIO_URL) === 0) {
$gLogger->info('REDIRECT: Redirecting to internal URL!', $loggerObject);
// TODO check if user is authorized for url
$redirectUrl = $url;
} else {
$gLogger->notice('REDIRECT: Redirecting to external URL!', $loggerObject);
$redirectUrl = SecurityUtils::encodeUrl(ADMIDIO_URL . '/adm_program/system/redirect.php', array('url' => $url));
}
header('Location: ' . $redirectUrl, true, $statusCode);
exit();
}
/**
* Calculates and formats the execution time
* @param float $startTime The start time
* @return string Returns the formated execution time
*/
function getExecutionTime(float $startTime): string
{
$stopTime = microtime(true);
return number_format(($stopTime - $startTime) * 1000, 6, '.', '') . ' ms';
}