Skip to content
Snippets Groups Projects
Select Git revision
  • 41dc18c9b1eed78cf0391b9953518c7eb071b526
  • main default protected
  • studip-rector
  • ci-opt
  • course-members-export-as-word
  • data-vue-app
  • pipeline-improvements
  • webpack-optimizations
  • rector
  • icon-renewal
  • http-client-and-factories
  • jsonapi-atomic-operations
  • vueify-messages
  • tic-2341
  • 135-translatable-study-areas
  • extensible-sorm-action-parameters
  • sorm-configuration-trait
  • jsonapi-mvv-routes
  • docblocks-for-magic-methods
19 results

ResourceManager.class.php

Blame
  • Forked from Stud.IP / Stud.IP
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    ResourceManager.class.php 48.58 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;
            }
        }
    
        /**
         * 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)
        {
            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);
        }
    }