diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/resources/ResourceManager.php | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/resources/ResourceManager.php')
| -rw-r--r-- | lib/resources/ResourceManager.php | 1472 |
1 files changed, 1472 insertions, 0 deletions
diff --git a/lib/resources/ResourceManager.php b/lib/resources/ResourceManager.php new file mode 100644 index 0000000..b6115e7 --- /dev/null +++ b/lib/resources/ResourceManager.php @@ -0,0 +1,1472 @@ +<?php + +/** + * ResourceManager.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; + } + } + + /** + * Check if the coordinate are in appropriate CRSWGS_84 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 + * + * @param string $coordinate_string + * @return bool + */ + public static function validateCoordinates(string $coordinate_string): bool + { + return preg_match( + ResourcePropertyDefinition::CRSWGS84_REGEX, + $coordinate_string + ); + } + + + // 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; + + + // Show error message when coordinates are invalid + if (!self::validateCoordinates($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 = null) + { + if (!$user) { + return ''; + } + + 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 ''; + } + + 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 = null, + $level = 'admin', + $time = null + ) + { + if (!$user) { + return false; + } + + //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|null $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 = null, + $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); + } +} |
