Forked from
Stud.IP / Stud.IP
4206 commits behind, 575 commits ahead of the upstream repository.
-
David Siegfried authored
Closes #871, #1055, and #1046 Merge request studip/studip!607
David Siegfried authoredCloses #871, #1055, and #1046 Merge request studip/studip!607
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ResourceManager.class.php 48.48 KiB
<?php
/**
* ResourceManager.class.php
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* @author Moritz Strohm <strohm@data-quest.de>
* @copyright 2017-2018
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
require_once 'lib/dates.inc.php';
/**
* The ResourceManager class contains methods that simplify the use of
* Resources.
*/
class ResourceManager
{
// Factory methods
/**
* Simplifies the creation of a resource category.
*
* @param string $name The name of the new resource category.
* @param string $description The description of the new resource category.
* @param string $class_name The class (derived from Resource) which
* shall be used for resources created with the new category.
* @param bool $is_system_category True, if the category shall be a system
* category, false otherwise.
* @param string $iconnr The number of the icon for the resource category.
* @param mixed[] $properties A two-dimensional array with the
* names and requestable, protected and system flags.
* The second dimension of the array must have the following structure:
* [
* property name (string),
* requestable flag (boolean),
* protected flag (boolean),
* system-flag (boolean)
* ].
*
* @throws ResourceCategoryException In case if no name is set or a resource
* property doesn't exist, if the category's name is ambigous
* or if the new category cannot be stored, a ResourceCategoryException
* is thrown.
*
* @return ResourceCategory A new ResourceCategory object.
*/
public static function createCategory(
$name = null,
$description = null,
$class_name = 'Resource',
$is_system_category = false,
$iconnr = '1',
$properties = []
)
{
if (!$name) {
//A name must be set!
throw new InvalidResourceCategoryException(
_('Es wurde kein Name für die neue Ressourcenkategorie angegeben!')
);
}
$property_data = [];
//We must check, if all the properties exist:
if ($properties && is_array($properties)) {
foreach ($properties as $property) {
$property_object = ResourcePropertyDefinition::findByName(
$property[0]
);
if (!$property_object) {
throw new ResourcePropertyException(
sprintf(
_('Die Ressourceneigenschaft %s ist nicht definiert!'),
$property[0]
)
);
} elseif (count($property_object) > 1) {
throw new ResourcePropertyException(
sprintf(
_('Es gibt mehrere Ressourceneigenschaften mit dem Namen %s!'),
$property[0]
)
);
}
//$property_object is an array of ResourcePropertyDefinition objects:
$property_data[] = [
'object' => $property_object[0],
'config' => $property
];
}
}
//Ok, all properties exist: We can create the category:
$new_category = new ResourceCategory();
$new_category->name = $name;
if ($description != null) {
$new_category->description = $description;
}
$new_category->class_name = $class_name;
$new_category->system = ($is_system_category ? '1' : '0');
$new_category->iconnr = $iconnr;
if ($new_category->store()) {
//Add the properties:
foreach ($property_data as $p) {
$rcp = new ResourceCategoryProperty();
$rcp->category_id = $new_category->id;
$rcp->property_id = $p['object']->id;
$rcp->requestable = $p['config'][1];
$rcp->protected = $p['config'][2];
$rcp->system = $p['config'][3];
$rcp->store();
}
//die();
} else {
throw new InvalidResourceCategoryException(
sprintf(
_('Die Ressourcenkategorie %s konnte nicht gespeichert werden!'),
$name
)
);
}
return $new_category;
}
/**
* Simplifies the creation of a location resource category.
*
* @param string $name The name of the new resource category.
* @param string $description The description of the new resource category.
* @param string[] $additional_properties A two-dimensional array with the
* names and requestable, protected and system flags. See
* ResourceManager::createCategory for a description of the format
* of the second array dimension.
*
* @throws ResourceCategoryException In case if no name is set or a resource
* property doesn't exist, if the category's name is ambigous
* or if the new category cannot be stored, a ResourceCategoryException
* is thrown.
*
* @return ResourceCategory A new ResourceCategory object.
*/
public static function createLocationCategory(
$name = null,
$description = null,
$additional_properties = []
)
{
$property_names = array_merge(
[
[
'geo_coordinates',
true,
true,
true
]
],
$additional_properties
);
return self::createCategory(
$name,
$description,
'Location',
false,
'1',
$property_names
);
}
/**
* Simplifies the creation of a building resource category.
*
* @param string $name The name of the new resource category.
* @param string $description The description of the new resource category.
* @param string[] $additional_properties A two-dimensional array with the
* names and requestable, protected and system flags. See
* ResourceManager::createCategory for a description of the format
* of the second array dimension.
*
* @throws ResourceCategoryException In case if no name is set or a resource
* property doesn't exist, if the category's name is ambigous
* or if the new category cannot be stored, a ResourceCategoryException
* is thrown.
*
* @return ResourceCategory A new ResourceCategory object.
*/
public static function createBuildingCategory(
$name = null,
$description = null,
$additional_properties = []
)
{
$property_names = array_merge(
[
[
'address',
false,
true,
true
],
[
'accessible',
false,
true,
true
],
[
'number',
false,
true,
true
],
[
'geo_coordinates',
true,
true,
true
]
],
$additional_properties
);
return self::createCategory(
$name,
$description,
'Building',
false,
'2',
$property_names
);
}
/**
* Simplifies the creation of a room resource category.
*
* @param string $name The name of the new resource category.
* @param string $description The description of the new resource category.
* @param string[] $additional_properties A two-dimensional array with the
* names and requestable, protected and system flags. See
* ResourceManager::createCategory for a description of the format
* of the second array dimension.
*
* @throws ResourceCategoryException In case if no name is set or a resource
* property doesn't exist, if the category's name is ambigous
* or if the new category cannot be stored, a ResourceCategoryException
* is thrown.
*
* @return ResourceCategory A new ResourceCategory object.
*/
public static function createRoomCategory(
$name = null,
$description = null,
$additional_properties = []
)
{
$property_names = array_merge(
[
[
'room_type',
true,
true,
true
],
[
'seats',
true,
true,
true
],
[
'booking_plan_is_public',
true,
true,
true
]
],
$additional_properties
);
return self::createCategory(
$name,
$description,
'Room',
false,
'3',
$property_names
);
}
// Resource methods:
/**
* Creates a copy of a resource and stores the copy in the database.
*
* @param Resource $resource The resource which shall be copied.
* @param bool $copy_hierarchy True, if the resource's children shall also
* be copied (default). False otherwise.
* @param string $new_parent_id If this is set the original parent_id will
* be overwritten with the ID in $new_parent_id.
*
* @throws ResourceException If the copy cannot be stored.
*
* @returns Resource A copy of the resource.
*/
public static function copyResource(
Resource $resource,
$copy_hierarchy = true,
$new_parent_id = null
)
{
//We can clone all the data but we must explicitly
//create a new ID and set the new flag of the copy
//to prevent updating the original object.
$copy = clone $resource;
$copy->id = $copy->getNewId();
$copy->setNew(true);
if ($new_parent_id) {
$copy->parent_id = $new_parent_id;
}
if (!$copy->store()) {
throw new ResourceException();
}
if ($copy_hierarchy) {
//get all children of the original resource and clone them, too.
//If $copy_hierarchy is set all children and their descendants etc.
//are cloned recursively.
$children = $resource->children;
foreach ($children as $child) {
$copied_child = self::copyResource(
$child,
$copy_hierarchy,
$copy->id //$copy is the new parent node.
);
}
}
return $copy;
}
/**
* Moves a resource below another resource and does checks to prevent
* resource hierarchies with misplaced resource objects.
* This is just a convenience method which calls the addChild method
* of the destination resource.
*
* @param Resource $target The resource which shall be moved.
* @param Resource $destination The resource where $target shall be a new child.
*
* @throws InvalidArgumentException If $target cannot be placed below $destination.
*
* @returns bool True, if $target was successful placed below $destination.
*/
public static function moveResource(Resource $target, Resource $destination)
{
return $destination->addChild($target);
}
//Resource retrieval methods:
/**
* Helper method that creates the identical SQL query for the
* countUserResources and findUserResources methods.
*/
protected static function getUserResourcesSqlData(
User $user,
$level = 'user',
$time = null,
$class_names = []
)
{
$used_time = time();
if ($time instanceof DateTime) {
$used_time = $time->getTimestamp();
} elseif ($time) {
$used_time = $time;
}
$sql = '';
if (count($class_names)) {
//Make sure that all class names specify names
//of classes derived from the Resource class.
$valid_class_names = [];
foreach ($class_names as $class_name) {
if (is_a($class_name, 'Resource', true)) {
$valid_class_names[] = $class_name;
}
}
$class_names = $valid_class_names;
}
if (count($class_names)) {
$sql .= 'INNER JOIN resource_categories rc
ON resources.category_id = rc.id
WHERE ';
}
$user_is_resource_admin = self::userHasGlobalPermission(
$user,
'admin'
) || $GLOBALS['perm']->have_perm('root', $user->id);
if (!$user_is_resource_admin) {
$sql .= "resources.id IN (
SELECT resource_id FROM resource_permissions
WHERE user_id = :user_id
AND perms IN ( :perms )
UNION
SELECT resource_id FROM resource_temporary_permissions
WHERE user_id = :user_id
AND perms IN ( :perms )
AND (begin <= :time)
AND (end >= :time)
) ";
$data = [
'user_id' => $user->id,
'time' => $used_time
];
}
if (count($class_names) && !$user_is_resource_admin) {
$sql .= 'AND ';
}
if (count($class_names)) {
$sql .= "rc.class_name IN ( :class_names ) ";
$data['class_names'] = $class_names;
}
$sql .= "GROUP BY resources.id
ORDER BY sort_position DESC, resources.name ASC";
$perms = self::getHigherPermissionLevels($level);
array_push($perms, $level);
$data['perms'] = $perms;
return [
'query' => $sql,
'data' => $data
];
}
/**
* Counts all resources for which the specified user has permanent or
* temporary permissions.
*
* @param User $user The user whose resources shall be retrieved.
*
* @param string $level The minimum permission level the user must have
* on a resource so that it will be included in the result set.
*
* @param DateTime|int|null $time The timestamp for the check on
* temporary permissions. If this parameter is not set
* the current timestamp will be used.
*
* @param string[] $class_names A list of resource classes that will
* be used to filter the result set so that only resources being
* a member of one of the specified resource classes will be retrieved.
*
*/
public static function countUserResources(
User $user,
$level = 'user',
$time = null,
$class_names = []
)
{
$sql = self::getUserResourcesSqlData($user, $level, $time, $class_names);
return Resource::countBySql($sql['query'], $sql['data']);
}
/**
* Retrieves all resources for which the specified user has permanent or
* temporary permissions.
*
* @param User $user The user whose resources shall be retrieved.
*
* @param string $level The minimum permission level the user must have
* on a resource so that it will be included in the result set.
*
* @param DateTime|int|null $time The timestamp for the check on
* temporary permissions. If this parameter is not set
* the current timestamp will be used.
*
* @param string[] $class_names A list of resource classes that will
* be used to filter the result set so that only resources being
* a member of one of the specified resource classes will be retrieved.
*
* @param bool $convert_objects If the resource objects
* in the result set shall be converted to objects of the derived
* resource classes set this to true, otherwise false.
* Defaults to true.
*
* @returns Resource[] An array of Resource objects
* or objects of derived resource classes.
*/
public static function getUserResources(
User $user,
$level = 'user',
$time = null,
$class_names = [],
$convert_objects = true
)
{
$sql = self::getUserResourcesSqlData($user, $level, $time, $class_names);
$resources = Resource::findBySql($sql['query'], $sql['data']);
if ($convert_objects) {
$result = [];
foreach ($resources as $resource) {
$result[] = $resource->getDerivedClassInstance();
}
return $result;
} else {
return $resources;
}
}
// Static methods for position properties:
public static function getPositionArray(ResourceProperty $property)
{
if (!$property->definition) {
//An orphaned Resource property: we cannot generate an array for it!
throw new ResourcePropertyDefinitionException(
_('Die Positionsangabe kann nicht umgewandelt werden, da die angegebene Ressourceneigenschaft verwaist (ohne zugehörige Definition) ist!')
);
}
if ($property->definition->type != 'position') {
//We cannot generate an array for attributes other than the position type!
throw new ResourcePropertyException(
_("Die Positionsangabe kann nicht umgewandelt werden, da die angegebene Ressourceneigenschaft nicht vom Typ 'position' ist!")
);
}
//Parse the ISO-6709 coordinates from $property->value:
$coordinate_string = $property->state;
//Check, if the coordinate string ends with "CRSWGS_84/"
//and if all the numbers are in the appropriate format:
//- latitude: up to 2 digits, decimal point, 1 to 10 digits for fraction
//- longitude: up to 3 digits, decimal point, 1 to 10 digits for fraction
//- altitude: up to 5 digits, decimal point, 1 to 10 digits for fraction
//before the decimal point. After the decimal point,
//In that case it is a coordinate format we can parse:
if(!preg_match(
ResourcePropertyDefinition::CRSWGS84_REGEX,
$coordinate_string
)) {
PageLayout::postError(_('Die Positionsangabe kann nicht umgewandelt werden, da sie ungültige Daten enthält!'));
}
//With the first split we separate the numbers in the coordinate string.
//The second split lets us retrieve the sign for each number.
$coordinate_parts = preg_split(
'/([+-]|(CRSWGS_84\/))+/',
$coordinate_string,
-1,
PREG_SPLIT_NO_EMPTY
);
//We can simply split the coordinate string by each dot,
//since there has to be a dot in every three coordinates!
//This is because the coordinates are always stored
//with decimal separators.
$coordinate_signs = preg_split(
'/\.\d{1,10}/',
$coordinate_string,
-1,
PREG_SPLIT_NO_EMPTY
);
//The array position of $coordinate_parts and $coordinate_signs
//are the same! We can directly use the indexes.
//If a sign index is less than zero we must invert the value
//of the corresponding coordinate part.
for ($i = 0; $i < 3; $i++) {
if ($coordinate_signs[$i] < 0) {
$coordinate_parts[$i] *= -1;
}
}
return $coordinate_parts;
}
/**
* This method allows locking the resource management globally.
* The user who creates the lock must have admin permissions
* and the time interval must not lie in another global resource lock
* interval.
*
* @param User $user The user who wishes to lock the room and resource
* management globally.
* @param DateTime $begin The begin timestamp of the lock.
* @param DateTime $end The end timestamp of the lock.
*
* @throws InvalidArgumentException If $begin lies after $end or if
* $begin is equal to $end.
* @throws ResourcePermissionException If the specified user does not
* have sufficient permissions to lock the resource management globally.
* @throws GlobalResourceLockException If the resource lock could not be stored.
*
* @returns GlobalResourceLock object.
*/
public function createGlobalLock(
User $user,
DateTime $begin,
DateTime $end,
$ignore_bookings = false
)
{
if ($begin > $end) {
throw new InvalidArgumentException(
_('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!')
);
}
if ($begin == $end) {
throw new InvalidArgumentException(
_('Startzeitpunkt und Endzeitpunkt dürfen nicht identisch sein!')
);
}
if (!self::userHasGlobalPermission($user, 'admin')) {
throw new ResourcePermissionException(
_('Unzureichende Berechtigungen zum globalen Sperren der Raumverwaltung!')
);
}
if (GlobalResourceLock::existsInTimeRange($begin, $end)) {
throw new GlobalResourceLockOverlapException(
sprintf(
_('Im Zeitbereich vom %1$s bis %2$s gibt es bereits eine globale Sperrung der Raumverwaltung!'),
$begin->format('d.m.Y H:i'),
$end->format('d.m.Y H:i')
)
);
}
$lock = new GlobalResourceLock();
$lock->begin = $begin->getTimestamp();
$lock->end = $end->getTimestamp();
$lock->user_id = $user->id;
$lock->type = '0';
if (!$lock->store()) {
throw new GlobalResourceLockException(
sprintf(
_('Fehler beim Speichern der globalen Sperre der Raumverwaltung im Zeitbereich vom %1$s bis %2$s!'),
$begin->format('d.m.Y H:i'),
$end->format('d.m.Y H:i')
)
);
}
return $lock;
}
//Special methods for attributes of type position:
public static function getPositionString(
ResourceProperty $property,
$with_altitude = false
)
{
$coordinate_parts = [];
$string = '';
try {
$coordinate_parts = self::getPositionArray($property);
} catch (ResourcePropertyDefinitionException $e) {
//An orphaned Resource property: we cannot generate a string for it!
throw new ResourcePropertyDefinitionException(
_('Die Positionsangabe kann nicht formatiert werden, da die angegebene Ressourceneigenschaft verwaist (ohne zugehörige Definition) ist!')
);
} catch (ResourcePropertyException $e) {
//We cannot generate a string for attributes other than the position type!
throw new ResourcePropertyException(
_('Die Positionsangabe kann nicht formatiert werden, da die angegebene Ressourceneigenschaft nicht vom Typ "position" ist!')
);
} catch (ResourcePropertyStateException $e) {
//We cannot generate a string from invalid data!
return '';
}
$locale = localeconv();
if ($coordinate_parts[0] < 0) {
$string .= sprintf(
_('%s°S'),
number_format(
abs($coordinate_parts[0]),
7,
$locale['decimal_point'],
$locale['thousands_sep']
)
) . ' ';
} else {
$string .= sprintf(
_('%s°N'),
number_format(
abs($coordinate_parts[0]),
7,
$locale['decimal_point'],
$locale['thousands_sep']
)
) . ' ';
}
if ($coordinate_parts[1] < 0) {
$string .= sprintf(
_('%s°O'),
number_format(
abs($coordinate_parts[1]),
7,
$locale['decimal_point'],
$locale['thousands_sep']
)
) . ' ';
} else {
$string .= sprintf(
_('%s°W'),
number_format(
abs($coordinate_parts[1]),
7,
$locale['decimal_point'],
$locale['thousands_sep']
)
);
}
if ($with_altitude) {
$string .= ' ';
if ($coordinate_parts[2] < 0) {
$string .= sprintf(
_('%s m unter NHN'),
number_format(
abs($coordinate_parts[2]),
1,
$locale['decimal_point'],
$locale['thousands_sep']
)
);
} else {
$string .= sprintf(
_('%s m über NHN'),
number_format(
abs($coordinate_parts[2]),
1,
$locale['decimal_point'],
$locale['thousands_sep']
)
);
}
}
return $string;
}
public static function getMapUrlForResourcePosition(
ResourceProperty $property
)
{
$coordinate_parts = [];
try {
$coordinate_parts = self::getPositionArray($property);
} catch (ResourcePropertyDefinitionException $e) {
//An orphaned Resource property: we cannot generate an URL for it!
throw new InvalidArgumentException(
_('Eine URL zur Straßenkarte kann nicht erzeugt werden, da die angegebene Ressourceneigenschaft verwaist (ohne zugehörige Definition) ist!')
);
} catch (ResourcePropertyException $e) {
//We cannot generate an URL for attributes other than the position type!
throw new InvalidArgumentException(
_("Eine URL zur Straßenkarte kann nicht erzeugt werden, da die angegebene Ressourceneigenschaft nicht vom Typ 'position' ist!")
);
} catch (ResourcePropertyStateException $e) {
//We cannot generate an URL from invalid data!
return '';
}
$map_service_url = Config::get()->RESOURCES_MAP_SERVICE_URL;
if ($map_service_url) {
//Replace the strings LATITUDE and LONGITUDE in the URL
//with the coordinates.
return str_replace(
[
'LATITUDE',
'LONGITUDE'
],
[
$coordinate_parts[0],
$coordinate_parts[1]
],
$map_service_url
);
} else {
//Default to OpenStreepMap:
return sprintf(
'https://www.openstreetmap.org/#map=17/%1$s/%2$s',
$coordinate_parts[0],
$coordinate_parts[1]
);
}
}
// User permission methods:
/**
* Returns the resource management global permissions for a user,
* determined by the assigned roles and by the user's global permissions.
* This method does the mapping from the old resource management permissions
* to the new resource management permissions.
*/
public static function getGlobalResourcePermission(User $user)
{
global $perm;
//First we check if the user is a root user:
if ($perm->get_perm($user->id) == 'root') {
return 'admin';
}
//The user is not a root user:
//We must check if he has special permissions
//for the virtual resource with id "global":
$permission = ResourcePermission::findOneBySql(
"user_id = :user_id AND resource_id = 'global'",
[
'user_id' => $user->id
]
);
if (!$permission) {
//No global permissions in the resource management:
return '';
}
if (GlobalResourceLock::currentlyLocked()) {
//A global permission object exist. But since the
//resource management is locked only 'user' permissions
//are allowed, when the user does not have 'admin' permissions:
return (
$permission->perms == 'admin'
? 'admin'
: 'user'
);
}
return $permission->perms;
}
/**
* Determines if the specified user has the specified permission level set
* for at least one resource.
*
* @param User $user The users whose resource permissions shall be retrieved.
* @param string $level The permission level the user should have
* on at least one resource.
* @param string|int|DateTime|null $time The timestamp
* for the temporary permission level check.
* If this is not set the current timestamp will be used.
*/
public static function userHasResourcePermissions(
User $user,
$level = 'admin',
$time = null
)
{
//Get all permissions and temporary permissions of the user:
$permissions = ResourcePermission::findBySQL(
"user_id = :user_id AND resource_id <> 'global'",
[
'user_id' => $user->id
]
);
if ($permissions) {
foreach ($permissions as $permission) {
if (self::comparePermissionLevels($permission->perms, $level) >= 0) {
//We have found a permission which is higher or equal
//to the requested permission level and can therefore
//return true:
return true;
}
}
}
//No (sufficient) permanent permissions exist for the user
//for at least one resource. We must check the temporary permissions:
$used_time = time();
if ($time instanceof DateTime) {
$used_time = $time->getTimestamp();
} elseif ($time) {
$used_time = $time;
}
$temp_permissions = ResourceTemporaryPermission::findBySql(
"user_id = :user_id AND begin <= :time AND end >= :time
AND resource_id != 'global'",
[
'user_id' => $user->id,
'time' => $used_time
]
);
if ($temp_permissions) {
foreach ($temp_permissions as $permission) {
if (self::comparePermissionLevels($permission->perms, $level) >= 0) {
//We have found a temporary permission which is higher or
//equal to the requested permission level and can therefore
//return true:
return true;
}
}
}
//We haven't found any permanent or temporary permission for the user
//that match the requested permission level on the specified timestamp.
return false;
}
//Helper methods:
/**
* Returns all permission levels lower than the specified level.
*/
public static function getLowerPermissionLevels($level = 'user')
{
$defined_levels = ['user', 'autor', 'tutor', 'admin'];
if (!in_array($level, $defined_levels)) {
return [];
}
if ($level == 'admin') {
return array_slice($defined_levels, 0, 3);
} elseif ($level == 'tutor') {
return array_slice($defined_levels, 0, 2);
} elseif ($level == 'autor') {
return array_slice($defined_levels, 0, 1);
} else {
//There is no lower authority than user:
return [];
}
}
/**
* Returns all permission levels higher than the specified level.
*/
public static function getHigherPermissionLevels($level = 'user')
{
$defined_levels = ['user', 'autor', 'tutor', 'admin'];
if (!in_array($level, $defined_levels)) {
return [];
}
if ($level == 'admin') {
//There is no higher authority than admin:
return [];
} elseif ($level == 'tutor') {
return array_slice($defined_levels, -1, 1);
} elseif ($level == 'autor') {
return array_slice($defined_levels, -2, 2);
} elseif ($level == 'user') {
return array_slice($defined_levels, -3, 3);
} else {
//We haven't found what you're looking for:
return [];
}
}
/**
* Compares two resource permission levels and returns an integer telling if
* the first level is less than (-1), equal (0) or greater than (1) the second level.
*
* @param string $level The first permission level.
* @param string $other_level The second permission level.
*
* @throws InvalidArgumentException if $level or $other_level are either not set
* or if they contain invalid permission level strings.
* @returns integer -1 if $level is less than $other_level,
* 0 if both are equal,
* 1 if $level is greater than $other_level.
*/
public static function comparePermissionLevels(
$level = 'user',
$other_level = 'user'
)
{
if (!$level or !$other_level) {
throw new InvalidArgumentException(
_('Mindestens eine Rechtestufe fehlt zum Vergleich!')
);
}
//The level list starts with the lowest permission level
//and ends with the highest permission level.
//The levels are compared by comparing the index values
//of the list.
$defined_levels = ['user', 'autor', 'tutor', 'admin'];
if (!in_array($level, $defined_levels)) {
throw new InvalidArgumentException(
_('Die angegebene Rechtestufe ist ungültig!')
);
}
if (!in_array($other_level, $defined_levels)) {
throw new InvalidArgumentException(
_('Die angegebene Rechtestufe ist ungültig!')
);
}
$level_index = array_search($level, $defined_levels);
$other_level_index = array_search($other_level, $defined_levels);
$diff = $level_index - $other_level_index;
if ($diff > 0) {
//$level is a higher permission level than $other_level.
return 1;
} elseif ($diff < 0) {
//$level is a lower permission level than $other_level.
return -1;
} else {
//$level and $other_level are the same permission level.
return 0;
}
}
/**
* Checks if the specified user has the specified permission level
* for the resource management system.
*
* @param User $user The user whose global resource permissions
* shall be checked.
* @param string $requested_permission The required permission level
* for the user.
*
* @returns bool True, if the user has the required permission level,
* false otherwise.
*/
public static function userHasGlobalPermission(
User $user,
$requested_permission = 'user'
)
{
//ResourceManager::getGlobalResourcePermission also checks
//for global resource locks and returns 'admin' if the
//user is 'admin' user or 'user' in all other cases
//where the user has permissions on the resource management system.
$existing_permission = self::getGlobalResourcePermission($user);
if (!$existing_permission) {
//No permissions in the resource management:
return false;
}
return self::comparePermissionLevels(
$existing_permission,
$requested_permission
) > -1;
}
/**
* Counts the resources where the specified user has explicit
* permissions set, optionally limiting the result to permanent
* permissions.
*
* @param User $user The user whose resource permissions
* shall be checked.
* @param string $requested_permission The required minimum permission
* level for the user.
*
* @return int The amount of resources where the specified user has
* explicit permissions for.
*/
public static function countResourcesWithPermissions(
User $user,
$requested_permission = 'user',
$exclude_temporary_permissions = false
)
{
$perms = self::getHigherPermissionLevels($requested_permission);
array_push($perms, $requested_permission);
$total = Resource::countBySql(
"INNER JOIN resource_permissions rp
USING (resource_id)
WHERE
rp.perms IN ( :perms )
AND
rp.user_id = :user_id",
[
'perms' => $perms,
'user_id' => $user->id
]
);
if (!$exclude_temporary_permissions) {
$now = time();
$total += Resource::countBySql(
"INNER JOIN resource_temporary_permissions rtp
USING (resource_id)
WHERE
rtp.perms IN ( :perms )
AND
rtp.user_id = :user_id
AND
rtp.begin <= :now
AND
rtp.end >= :now",
[
'perms' => $perms,
'user_id' => $user->id,
'now' => $now
]
);
}
return $total;
}
/**
* Checks if the specified user has the specified permission level
* on at least one specific resource.
*
* @param User $user The user whose resource permissions
* shall be checked.
* @param string $requested_permission The required permission level
* for the user.
*
* @returns bool True, if the user has the required permission level
* for at least one resource, false otherwise.
*/
public static function userHasSpecialPermissions(
User $user,
$requested_permission = 'user'
)
{
return self::countResourcesWithPermissions(
$user,
$requested_permission
) > 0;
}
/**
* Get time ranges by looking at the object specified by its ID.
* This method works with CourseDate, SeminarCycleDate
* and Course objects.
*
* @param string $range_id The ID of a Stud.IP object.
*
* @returns Array An Array consisting of arrays of DateTime objects.
* The structure of the array is as follows:
* [
* [
* begin timestamp DateTime object
* end timestamp DateTime object
* ],
* ...
* ]
*/
public static function getTimeRangesFromRangeId($range_id = null)
{
if (!$range_id) {
return [];
}
//We try a course date first, then a cycle date and finally
//a course as this is the standard order to check for dates.
$time_ranges = [];
$course_date = CourseDate::find($range_id);
if ($course_date) {
$begin = new DateTime();
$begin->setTimestamp($course_date->date);
$end = new DateTime();
$end->setTimestamp($course_date->end_time);
$time_ranges[] = [
$begin,
$end
];
} else {
//No course date, but maybe a cycle date?
$cycle_date = SeminarCycleDate::find($range_id);
if ($cycle_date) {
if ($cycle_date->dates) {
foreach ($cycle_date->dates as $date) {
$begin = new DateTime();
$begin->setTimestamp($date->date);
$end = new DateTime();
$end->setTimestamp($date->end_time);
$time_ranges[] = [
$begin,
$end
];
}
}
} else {
//No cycle date. It must be a course then!
$course = Course::find($range_id);
if ($course) {
if ($course->dates) {
foreach ($course->dates as $date) {
$begin = new DateTime();
$begin->setTimestamp($date->date);
$end = new DateTime();
$end->setTimestamp($date->end_time);
$time_ranges[] = [
$begin,
$end
];
}
}
if ($course->cycles) {
foreach ($course->cycles as $cycle) {
if ($cycle->dates) {
foreach ($cycle->dates as $date) {
$begin = new DateTime();
$begin->setTimestamp($date->date);
$end = new DateTime();
$end->setTimestamp($date->end_time);
$time_ranges[] = [
$begin,
$end
];
}
}
}
}
}
//No else here: Enough is enough.
}
}
return $time_ranges;
}
/**
* Determines the last booking of the user and calculates the timespan
* from the last booking until now.
*
* @returns DateInterval|null|false Either a date interval from the last
* activity to the provided DateTime object or null in case the user
* has never been active. False is returned in case the timestamp
* comparison fails. @see DateTime::diff in the PHP Documentation.
*/
public static function getUserInactivityInterval(User $user, DateTime $time)
{
$db = DBManager::get();
$stmt = $db->prepare(
"SELECT MAX(mkdate) FROM resource_bookings
WHERE range_id = :user_id"
);
$stmt->execute(['user_id' => $user->id]);
$last_activity_timestamp = $stmt->fetchColumn();
if (($last_activity_timestamp === false) or ($last_activity_timestamp === null)) {
//No activity found
return null;
}
$last_activity = new DateTime();
$last_activity->setTimestamp($last_activity_timestamp);
//Calculate the difference between the last activity and $time,
//if $time is greater or equal than $last_activity.
//If $time is less than $last_activity return null since the user
//has not been active at the timestamp $time.
if ($time < $last_activity) {
//The user has not been active at $time.
return null;
}
return $time->diff($last_activity);
}
/**
* Retrieves booking plan objects like resource bookings and requests.
*
* @param string|null $included_request_types If this parameter is a string,
* it can have the values 'all' for retrieving all requests or the
* ID of a user so that only requests of that user are retrieved.
*
*/
public static function getBookingPlanObjects(
Resource $resource,
$time_ranges = [],
$allowed_booking_types = [],
$included_requests = null
)
{
if (!count($time_ranges)) {
return [];
}
$objects = [];
$bookings = \ResourceBooking::findByResourceAndTimeRanges(
$resource,
$time_ranges,
$allowed_booking_types
);
$objects = array_merge($objects, $bookings);
if ($included_requests == 'all') {
$requests = \ResourceRequest::findByResourceAndTimeRanges(
$resource,
$time_ranges,
0
);
$objects = array_merge($objects, $requests);
} elseif ($included_requests) {
$requests = \ResourceRequest::findByResourceAndTimeRanges(
$resource,
$time_ranges,
0,
[],
'user_id = :user_id',
['user_id' => $included_requests]
);
$objects = array_merge($objects, $requests);
}
return $objects;
}
public static function getAllResourceClassNames($excluded_classes = [])
{
$class_names = [];
//We have to make the autoloader load all resource model classes,
//otherwise the get_declared_classes() statement below won't find
//any class derived from Resource!
foreach (
scandir($GLOBALS['STUDIP_BASE_PATH'] . '/lib/models/resources')
as $resource_model_file) {
$path = pathinfo($resource_model_file);
if ($path['extension'] == 'php') {
$class_name = explode('.class', $path['filename'])[0];
class_exists($class_name);
}
}
foreach (get_declared_classes() as $class_name) {
if (is_a($class_name, 'Resource', true)) {
foreach ($excluded_classes as $excl_class) {
//For the resource base class, we must not check
//derived classes.
if ($excl_class == 'Resource') {
if ($class_name == 'Resource') {
continue 2;
}
} else {
if (is_a($class_name, $excl_class, true)) {
//The class belongs to one of the
//excluded resource classes.
continue 2;
}
}
}
$class_names[] = $class_name;
}
}
sort($class_names);
return $class_names;
}
/**
* Returns the names of all hierarchy elements from the root to the
* specified resource.
*
* @param Resource $room The resource to start with.
*
* @returns string[] An array with the names of the hierarchy elements,
* starting with the top resource's name.
*/
public static function getHierarchyNames(Resource $resource)
{
$names = [$resource->name];
$current_node = $resource->parent;
while ($current_node instanceof Resource) {
$names[] = $current_node->name;
$current_node = $current_node->parent;
}
return array_reverse($names);
}
/**
* Returns the hierarchy elements from the root to the
* specified resource.
*
* @param Resource $room The resource to start with.
*
* @returns Resource[] An array with the hierarchy elements,
* starting with the top resource.
*/
public static function getHierarchy(Resource $resource)
{
$hierarchy_ids = [$resource->id];
$items = [$resource];
$current_node = $resource->parent;
while ($current_node instanceof Resource) {
if (in_array($current_node->id, $hierarchy_ids)) {
//Circular hierarchy. That's a big error!
throw new InvalidResourceException(
sprintf(
_('Zirkuläre Hierarchie: Die Ressource %1$s ist ein Elternknoten von sich selbst!'),
$current_node->name
)
);
}
$items[] = $current_node;
$hierarchy_ids[] = $current_node->id;
$current_node = $current_node->parent;
}
return array_reverse($items);
}
}