Skip to content
Snippets Groups Projects
Forked from Stud.IP / Stud.IP
4206 commits behind, 575 commits ahead of the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ResourceManager.class.php 48.48 KiB
<?php

/**
 * ResourceManager.class.php
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      Moritz Strohm <strohm@data-quest.de>
 * @copyright   2017-2018
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */


require_once 'lib/dates.inc.php';


/**
 * The ResourceManager class contains methods that simplify the use of
 * Resources.
 */
class ResourceManager
{
    // Factory methods

    /**
     * Simplifies the creation of a resource category.
     *
     * @param string $name The name of the new resource category.
     * @param string $description The description of the new resource category.
     * @param string $class_name The class (derived from Resource) which
     *     shall be used for resources created with the new category.
     * @param bool $is_system_category True, if the category shall be a system
     *     category, false otherwise.
     * @param string $iconnr The number of the icon for the resource category.
     * @param mixed[] $properties A two-dimensional array with the
     *     names and requestable, protected and system flags.
     *     The second dimension of the array must have the following structure:
     *     [
     *          property name (string),
     *          requestable flag (boolean),
     *          protected flag (boolean),
     *          system-flag (boolean)
     *     ].
     *
     * @throws ResourceCategoryException In case if no name is set or a resource
     *     property doesn't exist, if the category's name is ambigous
     *     or if the new category cannot be stored, a ResourceCategoryException
     *     is thrown.
     *
     * @return ResourceCategory A new ResourceCategory object.
     */
    public static function createCategory(
        $name = null,
        $description = null,
        $class_name = 'Resource',
        $is_system_category = false,
        $iconnr = '1',
        $properties = []
    )
    {
        if (!$name) {
            //A name must be set!
            throw new InvalidResourceCategoryException(
                _('Es wurde kein Name für die neue Ressourcenkategorie angegeben!')
            );
        }

        $property_data = [];

        //We must check, if all the properties exist:
        if ($properties && is_array($properties)) {
            foreach ($properties as $property) {
                $property_object = ResourcePropertyDefinition::findByName(
                    $property[0]
                );

                if (!$property_object) {
                    throw new ResourcePropertyException(
                        sprintf(
                            _('Die Ressourceneigenschaft %s ist nicht definiert!'),
                            $property[0]
                        )
                    );
                } elseif (count($property_object) > 1) {
                    throw new ResourcePropertyException(
                        sprintf(
                            _('Es gibt mehrere Ressourceneigenschaften mit dem Namen %s!'),
                            $property[0]
                        )
                    );
                }

                //$property_object is an array of ResourcePropertyDefinition objects:
                $property_data[] = [
                    'object' => $property_object[0],
                    'config' => $property
                ];
            }
        }

        //Ok, all properties exist: We can create the category:

        $new_category = new ResourceCategory();
        $new_category->name = $name;
        if ($description != null) {
            $new_category->description = $description;
        }
        $new_category->class_name = $class_name;
        $new_category->system = ($is_system_category ? '1' : '0');
        $new_category->iconnr = $iconnr;

        if ($new_category->store()) {
            //Add the properties:
            foreach ($property_data as $p) {
                $rcp = new ResourceCategoryProperty();
                $rcp->category_id = $new_category->id;
                $rcp->property_id = $p['object']->id;
                $rcp->requestable = $p['config'][1];
                $rcp->protected = $p['config'][2];
                $rcp->system = $p['config'][3];
                $rcp->store();
            }
            //die();
        } else {
            throw new InvalidResourceCategoryException(
                sprintf(
                    _('Die Ressourcenkategorie %s konnte nicht gespeichert werden!'),
                    $name
                )
            );
        }

        return $new_category;
    }

    /**
     * Simplifies the creation of a location resource category.
     *
     * @param string $name The name of the new resource category.
     * @param string $description The description of the new resource category.
     * @param string[] $additional_properties A two-dimensional array with the
     *     names and requestable, protected and system flags. See
     *     ResourceManager::createCategory for a description of the format
     *     of the second array dimension.
     *
     * @throws ResourceCategoryException In case if no name is set or a resource
     *     property doesn't exist, if the category's name is ambigous
     *     or if the new category cannot be stored, a ResourceCategoryException
     *     is thrown.
     *
     * @return ResourceCategory A new ResourceCategory object.
     */
    public static function createLocationCategory(
        $name = null,
        $description = null,
        $additional_properties = []
    )
    {
        $property_names = array_merge(
            [
                [
                    'geo_coordinates',
                    true,
                    true,
                    true
                ]
            ],
            $additional_properties
        );

        return self::createCategory(
            $name,
            $description,
            'Location',
            false,
            '1',
            $property_names
        );
    }


    /**
     * Simplifies the creation of a building resource category.
     *
     * @param string $name The name of the new resource category.
     * @param string $description The description of the new resource category.
     * @param string[] $additional_properties A two-dimensional array with the
     *     names and requestable, protected and system flags. See
     *     ResourceManager::createCategory for a description of the format
     *     of the second array dimension.
     *
     * @throws ResourceCategoryException In case if no name is set or a resource
     *     property doesn't exist, if the category's name is ambigous
     *     or if the new category cannot be stored, a ResourceCategoryException
     *     is thrown.
     *
     * @return ResourceCategory A new ResourceCategory object.
     */
    public static function createBuildingCategory(
        $name = null,
        $description = null,
        $additional_properties = []
    )
    {
        $property_names = array_merge(
            [
                [
                    'address',
                    false,
                    true,
                    true
                ],
                [
                    'accessible',
                    false,
                    true,
                    true
                ],
                [
                    'number',
                    false,
                    true,
                    true
                ],
                [
                    'geo_coordinates',
                    true,
                    true,
                    true
                ]
            ],
            $additional_properties
        );

        return self::createCategory(
            $name,
            $description,
            'Building',
            false,
            '2',
            $property_names
        );
    }


    /**
     * Simplifies the creation of a room resource category.
     *
     * @param string $name The name of the new resource category.
     * @param string $description The description of the new resource category.
     * @param string[] $additional_properties A two-dimensional array with the
     *     names and requestable, protected and system flags. See
     *     ResourceManager::createCategory for a description of the format
     *     of the second array dimension.
     *
     * @throws ResourceCategoryException In case if no name is set or a resource
     *     property doesn't exist, if the category's name is ambigous
     *     or if the new category cannot be stored, a ResourceCategoryException
     *     is thrown.
     *
     * @return ResourceCategory A new ResourceCategory object.
     */
    public static function createRoomCategory(
        $name = null,
        $description = null,
        $additional_properties = []
    )
    {
        $property_names = array_merge(
            [
                [
                    'room_type',
                    true,
                    true,
                    true
                ],
                [
                    'seats',
                    true,
                    true,
                    true
                ],
                [
                    'booking_plan_is_public',
                    true,
                    true,
                    true
                ]
            ],
            $additional_properties
        );

        return self::createCategory(
            $name,
            $description,
            'Room',
            false,
            '3',
            $property_names
        );
    }


    // Resource methods:

    /**
     * Creates a copy of a resource and stores the copy in the database.
     *
     * @param Resource $resource The resource which shall be copied.
     * @param bool $copy_hierarchy True, if the resource's children shall also
     *     be copied (default). False otherwise.
     * @param string $new_parent_id If this is set the original parent_id will
     *     be overwritten with the ID in $new_parent_id.
     *
     * @throws ResourceException If the copy cannot be stored.
     *
     * @returns Resource A copy of the resource.
     */
    public static function copyResource(
        Resource $resource,
        $copy_hierarchy = true,
        $new_parent_id = null
    )
    {
        //We can clone all the data but we must explicitly
        //create a new ID and set the new flag of the copy
        //to prevent updating the original object.
        $copy = clone $resource;
        $copy->id = $copy->getNewId();
        $copy->setNew(true);
        if ($new_parent_id) {
            $copy->parent_id = $new_parent_id;
        }
        if (!$copy->store()) {
            throw new ResourceException();
        }
        if ($copy_hierarchy) {
            //get all children of the original resource and clone them, too.
            //If $copy_hierarchy is set all children and their descendants etc.
            //are cloned recursively.
            $children = $resource->children;
            foreach ($children as $child) {
                $copied_child = self::copyResource(
                    $child,
                    $copy_hierarchy,
                    $copy->id //$copy is the new parent node.
                );
            }
        }
        return $copy;
    }

    /**
     * Moves a resource below another resource and does checks to prevent
     * resource hierarchies with misplaced resource objects.
     * This is just a convenience method which calls the addChild method
     * of the destination resource.
     *
     * @param Resource $target The resource which shall be moved.
     * @param Resource $destination The resource where $target shall be a new child.
     *
     * @throws InvalidArgumentException If $target cannot be placed below $destination.
     *
     * @returns bool True, if $target was successful placed below $destination.
     */
    public static function moveResource(Resource $target, Resource $destination)
    {
        return $destination->addChild($target);
    }


    //Resource retrieval methods:


    /**
     * Helper method that creates the identical SQL query for the
     * countUserResources and findUserResources methods.
     */
    protected static function getUserResourcesSqlData(
        User $user,
        $level = 'user',
        $time = null,
        $class_names = []
    )
    {
        $used_time = time();
        if ($time instanceof DateTime) {
            $used_time = $time->getTimestamp();
        } elseif ($time) {
            $used_time = $time;
        }

        $sql = '';

        if (count($class_names)) {
            //Make sure that all class names specify names
            //of classes derived from the Resource class.
            $valid_class_names = [];
            foreach ($class_names as $class_name) {
                if (is_a($class_name, 'Resource', true)) {
                    $valid_class_names[] = $class_name;
                }
            }
            $class_names = $valid_class_names;
        }
        if (count($class_names)) {
            $sql .= 'INNER JOIN resource_categories rc
                ON resources.category_id = rc.id
                WHERE ';
        }

        $user_is_resource_admin = self::userHasGlobalPermission(
            $user,
            'admin'
        ) || $GLOBALS['perm']->have_perm('root', $user->id);
        if (!$user_is_resource_admin) {
            $sql .= "resources.id IN (
                    SELECT resource_id FROM resource_permissions
                    WHERE user_id = :user_id
                    AND perms IN ( :perms )
                    UNION
                    SELECT resource_id FROM resource_temporary_permissions
                    WHERE user_id = :user_id
                    AND perms IN ( :perms )
                    AND (begin <= :time)
                    AND (end >= :time)
                ) ";
            $data = [
                'user_id' => $user->id,
                'time' => $used_time
            ];
        }

        if (count($class_names) && !$user_is_resource_admin) {
            $sql .= 'AND ';
        }

        if (count($class_names)) {
            $sql .= "rc.class_name IN ( :class_names ) ";
            $data['class_names'] = $class_names;
        }
        $sql .= "GROUP BY resources.id
            ORDER BY sort_position DESC, resources.name ASC";

        $perms = self::getHigherPermissionLevels($level);
        array_push($perms, $level);
        $data['perms'] = $perms;

        return [
            'query' => $sql,
            'data' => $data
        ];
    }


    /**
     * Counts all resources for which the specified user has permanent or
     * temporary permissions.
     *
     * @param User $user The user whose resources shall be retrieved.
     *
     * @param string $level The minimum permission level the user must have
     *     on a resource so that it will be included in the result set.
     *
     * @param DateTime|int|null $time The timestamp for the check on
     *     temporary permissions. If this parameter is not set
     *     the current timestamp will be used.
     *
     * @param string[] $class_names A list of resource classes that will
     *     be used to filter the result set so that only resources being
     *     a member of one of the specified resource classes will be retrieved.
     *
     */
    public static function countUserResources(
        User $user,
        $level = 'user',
        $time = null,
        $class_names = []
    )
    {
        $sql = self::getUserResourcesSqlData($user, $level, $time, $class_names);
        return Resource::countBySql($sql['query'], $sql['data']);
    }

    /**
     * Retrieves all resources for which the specified user has permanent or
     * temporary permissions.
     *
     * @param User $user The user whose resources shall be retrieved.
     *
     * @param string $level The minimum permission level the user must have
     *     on a resource so that it will be included in the result set.
     *
     * @param DateTime|int|null $time The timestamp for the check on
     *     temporary permissions. If this parameter is not set
     *     the current timestamp will be used.
     *
     * @param string[] $class_names A list of resource classes that will
     *     be used to filter the result set so that only resources being
     *     a member of one of the specified resource classes will be retrieved.
     *
     * @param bool $convert_objects If the resource objects
     *     in the result set shall be converted to objects of the derived
     *     resource classes set this to true, otherwise false.
     *     Defaults to true.
     *
     * @returns Resource[] An array of Resource objects
     *     or objects of derived resource classes.
     */
    public static function getUserResources(
        User $user,
        $level = 'user',
        $time = null,
        $class_names = [],
        $convert_objects = true
    )
    {
        $sql = self::getUserResourcesSqlData($user, $level, $time, $class_names);
        $resources = Resource::findBySql($sql['query'], $sql['data']);
        if ($convert_objects) {
            $result = [];
            foreach ($resources as $resource) {
                $result[] = $resource->getDerivedClassInstance();
            }
            return $result;
        } else {
            return $resources;
        }
    }


    // Static methods for position properties:

    public static function getPositionArray(ResourceProperty $property)
    {
        if (!$property->definition) {
            //An orphaned Resource property: we cannot generate an array for it!
            throw new ResourcePropertyDefinitionException(
                _('Die Positionsangabe kann nicht umgewandelt werden, da die angegebene Ressourceneigenschaft verwaist (ohne zugehörige Definition) ist!')
            );
        }

        if ($property->definition->type != 'position') {
            //We cannot generate an array for attributes other than the position type!
            throw new ResourcePropertyException(
                _("Die Positionsangabe kann nicht umgewandelt werden, da die angegebene Ressourceneigenschaft nicht vom Typ 'position' ist!")
            );
        }

        //Parse the ISO-6709 coordinates from $property->value:
        $coordinate_string = $property->state;


        //Check, if the coordinate string ends with "CRSWGS_84/"
        //and if all the numbers are in the appropriate format:
        //- latitude: up to 2 digits, decimal point, 1 to 10 digits for fraction
        //- longitude: up to 3 digits, decimal point, 1 to 10 digits for fraction
        //- altitude: up to 5 digits, decimal point, 1 to 10 digits for fraction
        //before the decimal point. After the decimal point,
        //In that case it is a coordinate format we can parse:
        if(!preg_match(
            ResourcePropertyDefinition::CRSWGS84_REGEX,
            $coordinate_string
        )) {
            PageLayout::postError(_('Die Positionsangabe kann nicht umgewandelt werden, da sie ungültige Daten enthält!'));
        }

        //With the first split we separate the numbers in the coordinate string.
        //The second split lets us retrieve the sign for each number.

        $coordinate_parts = preg_split(
            '/([+-]|(CRSWGS_84\/))+/',
            $coordinate_string,
            -1,
            PREG_SPLIT_NO_EMPTY
        );

        //We can simply split the coordinate string by each dot,
        //since there has to be a dot in every three coordinates!
        //This is because the coordinates are always stored
        //with decimal separators.

        $coordinate_signs = preg_split(
            '/\.\d{1,10}/',
            $coordinate_string,
            -1,
            PREG_SPLIT_NO_EMPTY
        );

        //The array position of $coordinate_parts and $coordinate_signs
        //are the same! We can directly use the indexes.
        //If a sign index is less than zero we must invert the value
        //of the corresponding coordinate part.

        for ($i = 0; $i < 3; $i++) {
            if ($coordinate_signs[$i] < 0) {
                $coordinate_parts[$i] *= -1;
            }
        }

        return $coordinate_parts;
    }


    /**
     * This method allows locking the resource management globally.
     * The user who creates the lock must have admin permissions
     * and the time interval must not lie in another global resource lock
     * interval.
     *
     * @param User $user The user who wishes to lock the room and resource
     *      management globally.
     * @param DateTime $begin The begin timestamp of the lock.
     * @param DateTime $end The end timestamp of the lock.
     *
     * @throws InvalidArgumentException If $begin lies after $end or if
     *     $begin is equal to $end.
     * @throws ResourcePermissionException If the specified user does not
     *     have sufficient permissions to lock the resource management globally.
     * @throws GlobalResourceLockException If the resource lock could not be stored.
     *
     * @returns GlobalResourceLock object.
     */
    public function createGlobalLock(
        User $user,
        DateTime $begin,
        DateTime $end,
        $ignore_bookings = false
    )
    {
        if ($begin > $end) {
            throw new InvalidArgumentException(
                _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!')
            );
        }
        if ($begin == $end) {
            throw new InvalidArgumentException(
                _('Startzeitpunkt und Endzeitpunkt dürfen nicht identisch sein!')
            );
        }

        if (!self::userHasGlobalPermission($user, 'admin')) {
            throw new ResourcePermissionException(
                _('Unzureichende Berechtigungen zum globalen Sperren der Raumverwaltung!')
            );
        }

        if (GlobalResourceLock::existsInTimeRange($begin, $end)) {
            throw new GlobalResourceLockOverlapException(
                sprintf(
                    _('Im Zeitbereich vom %1$s bis %2$s gibt es bereits eine globale Sperrung der Raumverwaltung!'),
                    $begin->format('d.m.Y H:i'),
                    $end->format('d.m.Y H:i')
                )
            );
        }

        $lock = new GlobalResourceLock();
        $lock->begin = $begin->getTimestamp();
        $lock->end = $end->getTimestamp();
        $lock->user_id = $user->id;
        $lock->type = '0';
        if (!$lock->store()) {
            throw new GlobalResourceLockException(
                sprintf(
                    _('Fehler beim Speichern der globalen Sperre der Raumverwaltung im Zeitbereich vom %1$s bis %2$s!'),
                    $begin->format('d.m.Y H:i'),
                    $end->format('d.m.Y H:i')
                )
            );
        }
        return $lock;
    }


    //Special methods for attributes of type position:

    public static function getPositionString(
        ResourceProperty $property,
        $with_altitude = false
    )
    {
        $coordinate_parts = [];
        $string = '';
        try {
            $coordinate_parts = self::getPositionArray($property);
        } catch (ResourcePropertyDefinitionException $e) {
            //An orphaned Resource property: we cannot generate a string for it!
            throw new ResourcePropertyDefinitionException(
                _('Die Positionsangabe kann nicht formatiert werden, da die angegebene Ressourceneigenschaft verwaist (ohne zugehörige Definition) ist!')
            );
        } catch (ResourcePropertyException $e) {
            //We cannot generate a string for attributes other than the position type!
            throw new ResourcePropertyException(
                _('Die Positionsangabe kann nicht formatiert werden, da die angegebene Ressourceneigenschaft nicht vom Typ "position" ist!')
            );
        } catch (ResourcePropertyStateException $e) {
            //We cannot generate a string from invalid data!
            return '';
        }

        $locale = localeconv();

        if ($coordinate_parts[0] < 0) {
            $string .= sprintf(
                _('%s°S'),
                number_format(
                    abs($coordinate_parts[0]),
                    7,
                    $locale['decimal_point'],
                    $locale['thousands_sep']
                )
            ) . ' ';
        } else {
            $string .= sprintf(
                _('%s°N'),
                number_format(
                    abs($coordinate_parts[0]),
                    7,
                    $locale['decimal_point'],
                    $locale['thousands_sep']
                )
            ) . ' ';
        }

        if ($coordinate_parts[1] < 0) {
            $string .= sprintf(
                _('%s°O'),
                number_format(
                    abs($coordinate_parts[1]),
                    7,
                    $locale['decimal_point'],
                    $locale['thousands_sep']
                )
            ) . ' ';
        } else {
            $string .= sprintf(
                _('%s°W'),
                number_format(
                    abs($coordinate_parts[1]),
                    7,
                    $locale['decimal_point'],
                    $locale['thousands_sep']
                )
            );
        }

        if ($with_altitude) {
            $string .=  ' ';
            if ($coordinate_parts[2] < 0) {
                $string .= sprintf(
                    _('%s m unter NHN'),
                    number_format(
                        abs($coordinate_parts[2]),
                        1,
                        $locale['decimal_point'],
                        $locale['thousands_sep']
                    )
                );
            } else {
                $string .= sprintf(
                    _('%s m über NHN'),
                    number_format(
                        abs($coordinate_parts[2]),
                        1,
                        $locale['decimal_point'],
                        $locale['thousands_sep']
                    )
                );
            }
        }

        return $string;
    }


    public static function getMapUrlForResourcePosition(
        ResourceProperty $property
    )
    {
        $coordinate_parts = [];
        try {
            $coordinate_parts = self::getPositionArray($property);
        } catch (ResourcePropertyDefinitionException $e) {
            //An orphaned Resource property: we cannot generate an URL for it!
            throw new InvalidArgumentException(
                _('Eine URL zur Straßenkarte kann nicht erzeugt werden, da die angegebene Ressourceneigenschaft verwaist (ohne zugehörige Definition) ist!')
            );
        } catch (ResourcePropertyException $e) {
            //We cannot generate an URL for attributes other than the position type!
            throw new InvalidArgumentException(
                _("Eine URL zur Straßenkarte kann nicht erzeugt werden, da die angegebene Ressourceneigenschaft nicht vom Typ 'position' ist!")
            );
        } catch (ResourcePropertyStateException $e) {
            //We cannot generate an URL from invalid data!
            return '';
        }

        $map_service_url = Config::get()->RESOURCES_MAP_SERVICE_URL;
        if ($map_service_url) {
            //Replace the strings LATITUDE and LONGITUDE in the URL
            //with the coordinates.
            return str_replace(
                [
                    'LATITUDE',
                    'LONGITUDE'
                ],
                [
                    $coordinate_parts[0],
                    $coordinate_parts[1]
                ],
                $map_service_url
            );
        } else {
            //Default to OpenStreepMap:
            return sprintf(
                'https://www.openstreetmap.org/#map=17/%1$s/%2$s',
                $coordinate_parts[0],
                $coordinate_parts[1]
            );
        }
    }


    // User permission methods:

    /**
     * Returns the resource management global permissions for a user,
     * determined by the assigned roles and by the user's global permissions.
     * This method does the mapping from the old resource management permissions
     * to the new resource management permissions.
     */
    public static function getGlobalResourcePermission(User $user)
    {
        global $perm;
        //First we check if the user is a root user:

        if ($perm->get_perm($user->id) == 'root') {
            return 'admin';
        }

        //The user is not a root user:
        //We must check if he has special permissions
        //for the virtual resource with id "global":

        $permission = ResourcePermission::findOneBySql(
            "user_id = :user_id AND resource_id = 'global'",
            [
                'user_id' => $user->id
            ]
        );

        if (!$permission) {
            //No global permissions in the resource management:
            return '';
        }

        if (GlobalResourceLock::currentlyLocked()) {
            //A global permission object exist. But since the
            //resource management is locked only 'user' permissions
            //are allowed, when the user does not have 'admin' permissions:
            return (
                $permission->perms == 'admin'
                ? 'admin'
                : 'user'
            );
        }
        return $permission->perms;
    }


    /**
     * Determines if the specified user has the specified permission level set
     * for at least one resource.
     *
     * @param User $user The users whose resource permissions shall be retrieved.
     * @param string $level The permission level the user should have
     *     on at least one resource.
     * @param string|int|DateTime|null $time The timestamp
     *     for the temporary permission level check.
     *     If this is not set the current timestamp will be used.
     */
    public static function userHasResourcePermissions(
        User $user,
        $level = 'admin',
        $time = null
    )
    {
        //Get all permissions and temporary permissions of the user:

        $permissions = ResourcePermission::findBySQL(
            "user_id = :user_id AND resource_id <> 'global'",
            [
                'user_id' => $user->id
            ]
        );

        if ($permissions) {
            foreach ($permissions as $permission) {
                if (self::comparePermissionLevels($permission->perms, $level) >= 0) {
                    //We have found a permission which is higher or equal
                    //to the requested permission level and can therefore
                    //return true:
                    return true;
                }
            }
        }

        //No (sufficient) permanent permissions exist for the user
        //for at least one resource. We must check the temporary permissions:

        $used_time = time();
        if ($time instanceof DateTime) {
            $used_time = $time->getTimestamp();
        } elseif ($time) {
            $used_time = $time;
        }

        $temp_permissions = ResourceTemporaryPermission::findBySql(
            "user_id = :user_id AND begin <= :time AND end >= :time
            AND resource_id != 'global'",
            [
                'user_id' => $user->id,
                'time' => $used_time
            ]
        );

        if ($temp_permissions) {
            foreach ($temp_permissions as $permission) {
                if (self::comparePermissionLevels($permission->perms, $level) >= 0) {
                    //We have found a temporary permission which is higher or
                    //equal to the requested permission level and can therefore
                    //return true:
                    return true;
                }
            }
        }

        //We haven't found any permanent or temporary permission for the user
        //that match the requested permission level on the specified timestamp.
        return false;
    }


    //Helper methods:


    /**
     * Returns all permission levels lower than the specified level.
     */
    public static function getLowerPermissionLevels($level = 'user')
    {
        $defined_levels = ['user', 'autor', 'tutor', 'admin'];
        if (!in_array($level, $defined_levels)) {
            return [];
        }

        if ($level == 'admin') {
            return array_slice($defined_levels, 0, 3);
        } elseif ($level == 'tutor') {
            return array_slice($defined_levels, 0, 2);
        } elseif ($level == 'autor') {
            return array_slice($defined_levels, 0, 1);
        } else {
            //There is no lower authority than user:
            return [];
        }
    }


    /**
     * Returns all permission levels higher than the specified level.
     */
    public static function getHigherPermissionLevels($level = 'user')
    {
        $defined_levels = ['user', 'autor', 'tutor', 'admin'];
        if (!in_array($level, $defined_levels)) {
            return [];
        }

        if ($level == 'admin') {
            //There is no higher authority than admin:
            return [];
        } elseif ($level == 'tutor') {
            return array_slice($defined_levels, -1, 1);
        } elseif ($level == 'autor') {
            return array_slice($defined_levels, -2, 2);
        } elseif ($level == 'user') {
            return array_slice($defined_levels, -3, 3);
        } else {
            //We haven't found what you're looking for:
            return [];
        }
    }


    /**
     * Compares two resource permission levels and returns an integer telling if
     * the first level is less than (-1), equal (0) or greater than (1) the second level.
     *
     * @param string $level The first permission level.
     * @param string $other_level The second permission level.
     *
     * @throws InvalidArgumentException if $level or $other_level are either not set
     *     or if they contain invalid permission level strings.
     * @returns integer -1 if $level is less than $other_level,
     *     0 if both are equal,
     *     1 if $level is greater than $other_level.
     */
    public static function comparePermissionLevels(
        $level = 'user',
        $other_level = 'user'
    )
    {
        if (!$level or !$other_level) {
            throw new InvalidArgumentException(
                _('Mindestens eine Rechtestufe fehlt zum Vergleich!')
            );
        }

        //The level list starts with the lowest permission level
        //and ends with the highest permission level.
        //The levels are compared by comparing the index values
        //of the list.
        $defined_levels = ['user', 'autor', 'tutor', 'admin'];

        if (!in_array($level, $defined_levels)) {
            throw new InvalidArgumentException(
                _('Die angegebene Rechtestufe ist ungültig!')
            );
        }
        if (!in_array($other_level, $defined_levels)) {
            throw new InvalidArgumentException(
                _('Die angegebene Rechtestufe ist ungültig!')
            );
        }

        $level_index = array_search($level, $defined_levels);
        $other_level_index = array_search($other_level, $defined_levels);

        $diff = $level_index - $other_level_index;

        if ($diff > 0) {
            //$level is a higher permission level than $other_level.
            return 1;
        } elseif ($diff < 0) {
            //$level is a lower permission level than $other_level.
            return -1;
        } else {
            //$level and $other_level are the same permission level.
            return 0;
        }
    }


    /**
     * Checks if the specified user has the specified permission level
     * for the resource management system.
     *
     * @param User $user The user whose global resource permissions
     *     shall be checked.
     * @param string $requested_permission The required permission level
     *     for the user.
     *
     * @returns bool True, if the user has the required permission level,
     *     false otherwise.
     */
    public static function userHasGlobalPermission(
        User $user,
        $requested_permission = 'user'
    )
    {
        //ResourceManager::getGlobalResourcePermission also checks
        //for global resource locks and returns 'admin' if the
        //user is 'admin' user or 'user' in all other cases
        //where the user has permissions on the resource management system.
        $existing_permission = self::getGlobalResourcePermission($user);

        if (!$existing_permission) {
            //No permissions in the resource management:
            return false;
        }

        return self::comparePermissionLevels(
            $existing_permission,
            $requested_permission
        ) > -1;
    }


    /**
     * Counts the resources where the specified user has explicit
     * permissions set, optionally limiting the result to permanent
     * permissions.
     *
     * @param User $user The user whose resource permissions
     *     shall be checked.
     * @param string $requested_permission The required minimum permission
     *     level for the user.
     *
     * @return int The amount of resources where the specified user has
     *     explicit permissions for.
     */
    public static function countResourcesWithPermissions(
        User $user,
        $requested_permission = 'user',
        $exclude_temporary_permissions = false
    )
    {
        $perms = self::getHigherPermissionLevels($requested_permission);
        array_push($perms, $requested_permission);
        $total = Resource::countBySql(
            "INNER JOIN resource_permissions rp
            USING (resource_id)
            WHERE
            rp.perms IN ( :perms )
            AND
            rp.user_id = :user_id",
            [
                'perms' => $perms,
                'user_id' => $user->id
            ]
        );

        if (!$exclude_temporary_permissions) {
            $now = time();
            $total += Resource::countBySql(
                "INNER JOIN resource_temporary_permissions rtp
                USING (resource_id)
                WHERE
                rtp.perms IN ( :perms )
                AND
                rtp.user_id = :user_id
                AND
                rtp.begin <= :now
                AND
                rtp.end >= :now",
                [
                    'perms' => $perms,
                    'user_id' => $user->id,
                    'now' => $now
                ]
            );
        }

        return $total;
    }


    /**
     * Checks if the specified user has the specified permission level
     * on at least one specific resource.
     *
     * @param User $user The user whose resource permissions
     *     shall be checked.
     * @param string $requested_permission The required permission level
     *     for the user.
     *
     * @returns bool True, if the user has the required permission level
     *     for at least one resource, false otherwise.
     */
    public static function userHasSpecialPermissions(
        User $user,
        $requested_permission = 'user'
    )
    {
        return self::countResourcesWithPermissions(
            $user,
            $requested_permission
        ) > 0;
    }


    /**
     * Get time ranges by looking at the object specified by its ID.
     * This method works with CourseDate, SeminarCycleDate
     * and Course objects.
     *
     * @param string $range_id The ID of a Stud.IP object.
     *
     * @returns Array An Array consisting of arrays of DateTime objects.
     *     The structure of the array is as follows:
     *     [
     *         [
     *             begin timestamp DateTime object
     *             end timestamp DateTime object
     *         ],
     *         ...
     *     ]
     */
    public static function getTimeRangesFromRangeId($range_id = null)
    {
        if (!$range_id) {
            return [];
        }

        //We try a course date first, then a cycle date and finally
        //a course as this is the standard order to check for dates.

        $time_ranges = [];

        $course_date = CourseDate::find($range_id);

        if ($course_date) {
            $begin = new DateTime();
            $begin->setTimestamp($course_date->date);
            $end = new DateTime();
            $end->setTimestamp($course_date->end_time);
            $time_ranges[] = [
                $begin,
                $end
            ];
        } else {
            //No course date, but maybe a cycle date?
            $cycle_date = SeminarCycleDate::find($range_id);

            if ($cycle_date) {
                if ($cycle_date->dates) {
                    foreach ($cycle_date->dates as $date) {
                        $begin = new DateTime();
                        $begin->setTimestamp($date->date);
                        $end = new DateTime();
                        $end->setTimestamp($date->end_time);
                        $time_ranges[] = [
                            $begin,
                            $end
                        ];
                    }
                }
            } else {
                //No cycle date. It must be a course then!
                $course = Course::find($range_id);

                if ($course) {
                    if ($course->dates) {
                        foreach ($course->dates as $date) {
                            $begin = new DateTime();
                            $begin->setTimestamp($date->date);
                            $end = new DateTime();
                            $end->setTimestamp($date->end_time);
                            $time_ranges[] = [
                                $begin,
                                $end
                            ];
                        }
                    }
                    if ($course->cycles) {
                        foreach ($course->cycles as $cycle) {
                            if ($cycle->dates) {
                                foreach ($cycle->dates as $date) {
                                    $begin = new DateTime();
                                    $begin->setTimestamp($date->date);
                                    $end = new DateTime();
                                    $end->setTimestamp($date->end_time);
                                    $time_ranges[] = [
                                        $begin,
                                        $end
                                    ];
                                }
                            }
                        }
                    }
                }
                //No else here: Enough is enough.
            }
        }

        return $time_ranges;
    }


    /**
     * Determines the last booking of the user and calculates the timespan
     * from the last booking until now.
     *
     * @returns DateInterval|null|false Either a date interval from the last
     *     activity to the provided DateTime object or null in case the user
     *     has never been active. False is returned in case the timestamp
     *     comparison fails. @see DateTime::diff in the PHP Documentation.
     */
    public static function getUserInactivityInterval(User $user, DateTime $time)
    {
        $db = DBManager::get();

        $stmt = $db->prepare(
            "SELECT MAX(mkdate) FROM resource_bookings
            WHERE range_id = :user_id"
        );

        $stmt->execute(['user_id' => $user->id]);

        $last_activity_timestamp = $stmt->fetchColumn();

        if (($last_activity_timestamp === false) or ($last_activity_timestamp === null)) {
            //No activity found
            return null;
        }

        $last_activity = new DateTime();
        $last_activity->setTimestamp($last_activity_timestamp);

        //Calculate the difference between the last activity and $time,
        //if $time is greater or equal than $last_activity.
        //If $time is less than $last_activity return null since the user
        //has not been active at the timestamp $time.

        if ($time < $last_activity) {
            //The user has not been active at $time.
            return null;
        }

        return $time->diff($last_activity);
    }


    /**
     * Retrieves booking plan objects like resource bookings and requests.
     *
     * @param string|null $included_request_types If this parameter is a string,
     *     it can have the values 'all' for retrieving all requests or the
     *     ID of a user so that only requests of that user are retrieved.
     *
     */
    public static function getBookingPlanObjects(
        Resource $resource,
        $time_ranges = [],
        $allowed_booking_types = [],
        $included_requests = null
    )
    {
        if (!count($time_ranges)) {
            return [];
        }

        $objects = [];

        $bookings = \ResourceBooking::findByResourceAndTimeRanges(
            $resource,
            $time_ranges,
            $allowed_booking_types
        );
        $objects = array_merge($objects, $bookings);

        if ($included_requests == 'all') {
            $requests = \ResourceRequest::findByResourceAndTimeRanges(
                $resource,
                $time_ranges,
                0
            );
            $objects = array_merge($objects, $requests);
        } elseif ($included_requests) {
            $requests = \ResourceRequest::findByResourceAndTimeRanges(
                $resource,
                $time_ranges,
                0,
                [],
                'user_id = :user_id',
                ['user_id' => $included_requests]
            );
            $objects = array_merge($objects, $requests);
        }

        return $objects;
    }


    public static function getAllResourceClassNames($excluded_classes = [])
    {
        $class_names = [];
        //We have to make the autoloader load all resource model classes,
        //otherwise the get_declared_classes() statement below won't find
        //any class derived from Resource!
        foreach (
            scandir($GLOBALS['STUDIP_BASE_PATH'] . '/lib/models/resources')
            as $resource_model_file) {
            $path = pathinfo($resource_model_file);

            if ($path['extension'] == 'php') {
                $class_name = explode('.class', $path['filename'])[0];
                class_exists($class_name);
            }
        }

        foreach (get_declared_classes() as $class_name) {
            if (is_a($class_name, 'Resource', true)) {
                foreach ($excluded_classes as $excl_class) {
                    //For the resource base class, we must not check
                    //derived classes.
                    if ($excl_class == 'Resource') {
                        if ($class_name == 'Resource') {
                            continue 2;
                        }
                    } else {
                        if (is_a($class_name, $excl_class, true)) {
                            //The class belongs to one of the
                            //excluded resource classes.
                            continue 2;
                        }
                    }
                }
                $class_names[] = $class_name;
            }
        }
        sort($class_names);

        return $class_names;
    }


    /**
     * Returns the names of all hierarchy elements from the root to the
     * specified resource.
     *
     * @param Resource $room The resource to start with.
     *
     * @returns string[] An array with the names of the hierarchy elements,
     *     starting with the top resource's name.
     */
    public static function getHierarchyNames(Resource $resource)
    {
        $names = [$resource->name];
        $current_node = $resource->parent;
        while ($current_node instanceof Resource) {
            $names[] = $current_node->name;
            $current_node = $current_node->parent;
        }
        return array_reverse($names);
    }


    /**
     * Returns the hierarchy elements from the root to the
     * specified resource.
     *
     * @param Resource $room The resource to start with.
     *
     * @returns Resource[] An array with the hierarchy elements,
     *     starting with the top resource.
     */
    public static function getHierarchy(Resource $resource)
    {
        $hierarchy_ids = [$resource->id];
        $items = [$resource];
        $current_node = $resource->parent;
        while ($current_node instanceof Resource) {
            if (in_array($current_node->id, $hierarchy_ids)) {
                //Circular hierarchy. That's a big error!
                throw new InvalidResourceException(
                    sprintf(
                        _('Zirkuläre Hierarchie: Die Ressource %1$s ist ein Elternknoten von sich selbst!'),
                        $current_node->name
                    )
                );
            }
            $items[] = $current_node;
            $hierarchy_ids[] = $current_node->id;
            $current_node = $current_node->parent;
        }
        return array_reverse($items);
    }
}