<?php /** * Resource.php - model class for a resource * * The Resource class is the base class of the new * Room and Resource management system in Stud.IP. * It provides core functionality for handling general resources * and can be derived for handling special resources. * * 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-2019 * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * @package resources * @since 4.5 * * @property string $id database column * @property string $parent_id database column * @property string $category_id database column * @property int|null $level database column * @property string $name database column * @property I18NString|null $description database column * @property int $requestable database column * @property int $lockable database column * @property int $mkdate database column * @property int $chdate database column * @property int $sort_position database column * @property SimpleORMapCollection|ResourceProperty[] $properties has_many ResourceProperty * @property SimpleORMapCollection|ResourcePermission[] $permissions has_many ResourcePermission * @property SimpleORMapCollection|ResourceRequest[] $requests has_many ResourceRequest * @property SimpleORMapCollection|ResourceBooking[] $bookings has_many ResourceBooking * @property SimpleORMapCollection|Resource[] $children has_many Resource * @property ResourceCategory $category belongs_to ResourceCategory * @property Resource $parent belongs_to Resource * @property mixed $class_name additional field */ class Resource extends SimpleORMap implements StudipItem { protected static function configure($config = []) { $config['db_table'] = 'resources'; $config['belongs_to']['category'] = [ 'class_name' => ResourceCategory::class, 'foreign_key' => 'category_id', 'assoc_func' => 'find' ]; $config['has_many']['properties'] = [ 'class_name' => ResourceProperty::class, 'assoc_foreign_key' => 'resource_id', 'on_delete' => 'delete', 'on_store' => 'store' ]; $config['has_many']['permissions'] = [ 'class_name' => ResourcePermission::class, 'assoc_foreign_key' => 'resource_id', 'on_delete' => 'delete', 'on_store' => 'store' ]; $config['has_many']['requests'] = [ 'class_name' => ResourceRequest::class, 'assoc_foreign_key' => 'resource_id', 'on_delete' => 'delete', 'on_store' => 'store' ]; $config['has_many']['bookings'] = [ 'class_name' => ResourceBooking::class, 'assoc_foreign_key' => 'resource_id', 'on_delete' => 'delete', 'on_store' => 'store' ]; $config['has_many']['children'] = [ 'class_name' => Resource::class, 'assoc_func' => 'findChildren', 'on_delete' => 'delete', 'on_store' => 'store' ]; $config['belongs_to']['parent'] = [ 'class_name' => Resource::class, 'foreign_key' => 'parent_id' ]; $config['i18n_fields']['description'] = true; $config['additional_fields']['class_name'] = ['category', 'class_name']; $config['registered_callbacks']['before_store'][] = 'cbValidate'; parent::configure($config); } /** * This is a cache for permissions that users have on resources. * It is meant to reduce the database requests in cases where the * same permission is queried a lot of times. */ protected static $permission_cache; /** * Returns the children of a resource. * The children are converted to an instance of the derived class, * if they are not instances of the default Resource class. */ public static function findChildren($resource_id) { $children = self::findBySql( 'parent_id = :parent_id ORDER BY name ASC', ['parent_id' => $resource_id] ); if (!$children) { return []; } foreach ($children as &$child) { $child = $child->getDerivedClassInstance(); } return $children; } /** * Returns a translation of the resource class name. * The translated name can be singular or plural, depending * on the value of the parameter $item_count. * * @param int $item_count The amount of items the translation shall be * made for. This is only used to determine, if a singular or a * plural form shall be returned. * * @return string The translated form of the class name, either in * singular or plural. * */ public static function getTranslatedClassName($item_count = 1) { return ngettext( 'Ressource', 'Ressourcen', $item_count ); } /** * Retrieves all resources which don't have a parent resource assigned. * Such resources are called root resources since they are roots of * a resource hierarchy (or a resource tree). * * @return Resource[] An array of Resource objects * which are root resources. */ public static function getRootResources() { return self::findBySql("parent_id = '' ORDER BY name"); } /** * A method for overloaded classes so that they can define properties * that are required for that resource class. * * @return string[] An array with the names of the required properties. * Example: The properties with the names "foo", "bar" and "baz" * are required properties. The array would have the following content: * [ * 'foo', * 'bar', * 'baz' * ] */ public static function getRequiredProperties() { return []; } /** * Returns the part of the URL for getLink and getURL which will be * placed inside the calls to URLHelper::getLink and URLHelper::getURL * in these methods. * * @param string $action The action for the resource. * @param string $id The ID of the resource. * * @return string The URL path for the specified action. * @throws InvalidArgumentException If $resource_id is empty. * */ protected static function buildPathForAction($action = 'show', $id = null) { $actions_without_id = ['add']; if (!$id && !in_array($action, $actions_without_id)) { throw new InvalidArgumentException( _('Zur Erstellung der URL fehlt eine Ressourcen-ID!') ); } switch ($action) { case 'show': return 'dispatch.php/resources/resource/index/' . $id; case 'add': return 'dispatch.php/resources/resource/add'; case 'edit': return 'dispatch.php/resources/resource/edit/' . $id; case 'files': return 'dispatch.php/resources/resource/files/' . $id . '/'; case 'permissions': return 'dispatch.php/resources/resource/permissions/' . $id; case 'temporary_permissions': return 'dispatch.php/resources/resource/temporary_permissions/' . $id; case 'booking_plan': return 'dispatch.php/resources/room_planning/booking_plan/' . $id; case 'request_plan': return 'dispatch.php/resources/room_planning/request_plan/' . $id; case 'semester_plan': return 'dispatch.php/resources/room_planning/semester_plan/' . $id; case 'assign-undecided': return 'dispatch.php/resources/booking/add/' . $id; case 'assign': return 'dispatch.php/resources/booking/add/' . $id . '/0'; case 'reserve': return 'dispatch.php/resources/booking/add/' . $id . '/1'; case 'lock': return 'dispatch.php/resources/booking/add/' . $id . '/2'; case 'delete_bookings': return 'dispatch.php/resources/resource/delete_bookings/' . $id; case 'export_bookings': return 'dispatch.php/resources/export/resource_bookings/' . $id; case 'delete': return 'dispatch.php/resources/resource/delete/' . $id; default: return 'dispatch.php/resources/resource/show/' . $id; } } /** * Returns the appropriate link for the resource action that shall be * executed on a resource. * * @param string $action The action which shall be executed. * For default Resources the actions 'show', 'add', 'edit' and 'delete' * are defined. * @param string $id The ID of the resource on which the specified * action shall be executed. * @param array $link_parameters Optional parameters for the link. * * @return string The Link for the resource action. * @throws InvalidArgumentException If $resource_id is empty. * */ public static function getLinkForAction( $action = 'show', $id = null, $link_parameters = [] ) { return URLHelper::getLink( self::buildPathForAction($action, $id), $link_parameters ); } /** * Returns the appropriate URL for the resource action that shall be * executed on a resource. * * @param string $action The action which shall be executed. * For default Resources the actions 'show', 'add', 'edit' and 'delete' * are defined. * @param string $id The ID of the resource on which the specified * action shall be executed. * @param array $url_parameters Optional parameters for the URL. * * @return string The URL for the resource action. * @throws InvalidArgumentException If $resource_id is empty. * */ public static function getURLForAction( $action = 'show', $id = null, $url_parameters = [] ) { return URLHelper::getURL( self::buildPathForAction($action, $id), $url_parameters ); } /** * The SORM store method is overloaded to assure that the right level * attribute is stored. */ public function store() { //Set the level attribute according to the parent's //level attribute. If no parents are defined //set the level to zero. if ($this->parent_id && $this->parent) { $this->level = $this->parent->level + 1; } else { $this->level = 0; } //Store the folder, if it hasn't been stored before: $folder = $this->getFolder(); if ($folder) { $folder->store(); } return parent::store(); } public function delete() { //Delete the folder: $folder = $this->getFolder(false); if ($folder) { $folder->delete(); } return parent::delete(); } public function cbValidate() { if (!$this->category_id) { throw new InvalidResourceException( sprintf( _('Die Ressource %s ist keiner Ressourcenkategorie zugeordnet!'), $this->name ) ); } return true; } /** * @see StudipItem::__toString */ public function __toString() { return $this->getFullName(); } /** * Retrieves the folder for this resource. * * @param bool $create_if_missing Whether to create a folder (true) or * not (false) in case no folder exists for this resource. * Defaults to true. * * @returns ResourceFolder|null Either a ResourceFolder instance or null * in case no such instance can be retrieved or created. */ public function getFolder($create_if_missing = true) { $folder = Folder::findOneByRange_id($this->id); if ($folder) { $folder = $folder->getTypedFolder(); if ($folder instanceof ResourceFolder) { //Only return ResourceFolder instances. return $folder; } } elseif ($create_if_missing) { $folder = $this->createFolder(); if ($folder instanceof ResourceFolder) { return $folder; } } //In all other cases return null: return null; } public function setFolder(ResourceFolder $folder) { if ($this->isNew()) { $this->store(); } $folder->range_id = $this->id; $folder->range_type = 'Resource'; return $folder->store(); } public function createFolder() { if ($this->isNew()) { $this->id = $this->getNewId(); } $folder = Folder::createTopFolder( $this->id, 'Resource', 'ResourceFolder' ); if ($folder) { $folder = $folder->getTypedFolder(); if ($folder) { $folder->store(); return $folder; } } return null; } /** * Returns a list of property names that are required * for the resource class. * * @return string[] An array with the property names. */ public function getRequiredPropertyNames() { return []; } /** * This is a simplified version of the createBooking method. * @param User $user * @param DateTime $begin * @param DateTime $end * @param int $preparation_time * @param string $description * @param string $internal_comment * @param int $booking_type * @return ResourceBooking * @see Resource::createBooking */ public function createSimpleBooking( User $user, DateTime $begin, DateTime $end, $preparation_time = 0, $description = '', $internal_comment = '', $booking_type = ResourceBooking::TYPE_NORMAL ) { return $this->createBooking( $user, $user->id, [ [ 'begin' => $begin, 'end' => $end ] ], null, 0, null, $preparation_time, $description, $internal_comment, $booking_type ); } /** * Creates bookings from a request. * @param User $user * @param ResourceRequest $request The request from which * a resource booking shall be built. * @param int $preparation_time * @param string $description * @param string $internal_comment * @param int $booking_type * @param bool $prepend_preparation_time . If this is set to true, * the preparation time will end before the start of the * requested time. If $prepend_preparation_time is set to false * (the default) the preparation time starts with the start of the * requested time. * @param bool $notify_lecturers * @return ResourceBooking[] A list of resource bookings * matching the request. * @throws ResourceRequestException if the request could not be marked * as resolved. * * @throws ResourceUnavailableException if the resource cannot be assigned * in at least one of the time ranges specified by the resource request. */ public function createBookingFromRequest( User $user, ResourceRequest $request, $preparation_time = 0, $description = '', $internal_comment = '', $booking_type = ResourceBooking::TYPE_NORMAL, $prepend_preparation_time = false, $notify_lecturers = false ) { $course_dates = $request->getAffectedDates(); $bookings = []; if ($course_dates) { foreach ($course_dates as $course_date) { $booking = $this->createBooking( $user, $course_date->id, [ [ 'begin' => ( $prepend_preparation_time ? $course_date->date - $preparation_time : $course_date->date ), 'end' => $course_date->end_time ] ], null, 0, $course_date->end_time, $preparation_time, $description, $internal_comment, $booking_type ); if ($booking instanceof ResourceBooking) { $bookings[] = $booking; } } } elseif (count($request->appointments)) { //It is a request for multiple single dates. //Such requests are resolved into multiple bookings. foreach ($request->appointments as $appointment) { $begin = ( $prepend_preparation_time ? $appointment->appointment->date - $preparation_time : $appointment->appointment->date ); $end = $appointment->appointment->end_time; $booking = $this->createBooking( $user, $appointment->appointment_id, [ [ 'begin' => $begin, 'end' => $end ] ], null, 0, $end, $preparation_time, $description, $internal_comment, $booking_type ); if ($booking instanceof ResourceBooking) { $bookings[] = $booking; } } } else { //No date objects for the request. //It is a simple request: $booking = $this->createBooking( $user, $request->user->id, [ [ 'begin' => ( $prepend_preparation_time ? $request->begin - $preparation_time : $request->begin ), 'end' => $request->end ] ], null, 0, $request->end, $preparation_time, $description, $internal_comment, $booking_type ); if ($booking instanceof ResourceBooking) { $bookings[] = $booking; } } if (!$request->closeRequest($notify_lecturers)) { throw new ResourceRequestException( _('Die Anfrage konnte nicht als bearbeitet markiert werden!') ); } return $bookings; } /** * A factory method for creating a ResourceBooking object * for this resource. * * @param User $user The user who wishes to create a resource booking. * @param string $range_id The ID of the user (or the Stud.IP object) * which owns the ResourceBooking. * @param array[][] $time_ranges The time ranges for the booking. * At least one time range has to be specified using unix timestamps * or DateTime objects. * This array has the following structure: * [ * [ * 'begin' => The begin timestamp or DateTime object. * 'end' => The end timestamp or DateTime object. * ] * ] * @param DateInterval|null $repetition_interval The repetition interval * for the new booking. This must be a DateInterval object if * repetitions shall be stored. * Otherwise this parameter must be set to null. * @param int $repeat_amount The amount of repetitions. * This parameter is only regarded if $repetition_interval contains * a DateInterval object. * In case repetitions are specified by their end date set this * parameter to 0. * @param DateTime|string|null $repetition_end_date The end date of the * repetition. This can either be an unix timestamp or a DateTime object * and will only be regarded if $repetition_interval contains a * DateInterval object. * In case repetitions are specified by their amount set this * parameter to null. * @param int $repetition_amount (obsolete, has no effect) * @param int $preparation_time The preparation time which is needed before * the real start time. This will be substracted * from the begin timestamp and stored in an extra column of the * resource_bookings table. * @param string $description An optional description for the booking. * This fields was previously known as "user_free_name". * @param string $internal_comment An optional comment for the * booking which is intended to be used internally * in the room and resource administration staff. * @param int $booking_type The booking type. * 0 = normal booking * 1 = reservation * 2 = lock booking * @param bool $force_booking If this parameter is set to true, * overlapping bookings are removed before storing this booking. * * @return ResourceBooking object. * @throws InvalidArgumentException If no time ranges are specified * or if there is an error regarding the time ranges. * @throws ResourceBookingRangeException If $range_id is not set. * @throws ResourceBookingOverlapException If the booking overlaps * with another booking or a resource lock. * @throws ResourcePermissionException If the specified user does not * have sufficient permissions to create a resource booking. * @throws ResourceBookingException If the repetition interval * is invalid or if the resource booking cannot be stored. * */ public function createBooking( User $user, $range_id = null, $time_ranges = [], $repetition_interval = null, $repetition_amount = 0, $repetition_end_date = null, $preparation_time = 0, $description = '', $internal_comment = '', $booking_type = ResourceBooking::TYPE_NORMAL, $force_booking = false ) { if (!is_array($time_ranges)) { throw new InvalidArgumentException( _('Es wurden keine Zeitbereiche für die Buchung angegeben!') ); } $booking_begin = null; $booking_end = null; //Check if each entry of the $time_intervals array is in the right //format and if it contains either timestamps or DateTime objects. //After that the time ranges are checked for validity (begin > end) //and if there are locks or bookings in one of the time ranges. //Furthermore all reservations that are affected by this booking //are collected so that the persons who made the reservations //can be informed about the new booking. $affected_reservations = []; foreach ($time_ranges as $index => $time_range) { $begin = $time_range['begin']; $end = $time_range['end']; if ($begin === null || $end === null) { throw new InvalidArgumentException( _('Mindestens eines der Zeitintervalls ist im falschen Format!') ); } if (!($begin instanceof DateTime)) { $b = new DateTime(); $b->setTimestamp($begin); $begin = $b; } if (!($end instanceof DateTime)) { $e = new DateTime(); $e->setTimestamp($end); $end = $e; } $real_begin = clone $begin; if ($preparation_time > 0) { $real_begin = $real_begin->sub( new DateInterval('PT' . $preparation_time . 'S') ); } if ($real_begin > $end) { throw new InvalidArgumentException( _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!') ); } $duration = $end->getTimestamp() - $begin->getTimestamp(); $min_duration = Config::get()->RESOURCES_MIN_BOOKING_TIME; if ($min_duration && ($duration < ($min_duration * 60))) { throw new InvalidArgumentException( sprintf( _('Die minimale Buchungsdauer von %1$d Minuten wurde unterschritten!'), $min_duration ) ); } if ($index == array_keys($time_ranges)[0]) { $booking_begin = clone $begin; $booking_end = clone $end; } if ($repetition_interval instanceof DateInterval) { //We must calculate the end of the repetition interval //by using $repetition_amount or $repetition_end_date. $repetition_end = null; if ($repetition_end_date instanceof DateTime) { $repetition_end = $repetition_end_date; } elseif ($repetition_end_date) { //convert $repetition_end_date to a DateTime object: $red = new DateTime(); $red->setTimestamp($repetition_end_date); $repetition_end = $red; } else { //$repetition_end_date is not set: Use $repetition_amount. //Add the repetition interval $repetition_amount times //to the $real_begin DateTime object to get the end date //of the repetition: $repetition = clone $real_begin; for ($i = 0; $i < $repetition_amount; $i++) { $repetition = $repetition->add($repetition_interval); } $repetition_end = $repetition; } $current_date = clone $real_begin; //Check for each repetition if the resource is available //or locked: while ($current_date <= $repetition_end) { $current_begin = clone $current_date; $current_end = clone $current_date; $current_end->setTime( intval($end->format('H')), intval($end->format('i')), intval($end->format('s')) ); if ($current_begin < $current_end) { $affected_reservations = array_merge( ResourceBooking::findByResourceAndTimeRanges( $this, [ [ 'begin' => $current_begin->getTimestamp(), 'end' => $current_end->getTimestamp(), ] ], [1, 3] ), $affected_reservations ); } $current_date = $current_date->add($repetition_interval); } } else { $affected_reservations = array_merge( ResourceBooking::findByResourceAndTimeRanges( $this, [ [ 'begin' => $real_begin->getTimestamp(), 'end' => $end->getTimestamp(), ] ], [1, 3] ), $affected_reservations ); } } $booking = new ResourceBooking(); $booking->resource_id = $this->id; $booking->booking_user_id = $user->id; $booking->range_id = $range_id; $booking->description = $description; $booking->begin = $booking_begin->getTimestamp(); $booking->end = $booking_end->getTimestamp(); if ($repetition_interval instanceof DateInterval) { if ($repetition_end_date) { if ($repetition_end_date instanceof DateTime) { $booking->repeat_end = $repetition_end_date->getTimestamp(); } else { $booking->repeat_end = $repetition_end_date; } } $booking->repetition_interval = $repetition_interval->format('P%YY%MM%DD'); } if ($preparation_time) { $booking->preparation_time = $preparation_time; } else { $booking->preparation_time = '0'; } $booking->internal_comment = $internal_comment; $booking->booking_type = (int)$booking_type; //We can finally store the new booking. try { $booking->store($force_booking); } catch (ResourceBookingOverlapException $e) { if ($begin->format('Ymd') == $end->format('Ymd')) { throw new ResourceBookingException( sprintf( _('%1$s: Die Buchung vom %2$s bis %3$s konnte wegen Überlappungen nicht gespeichert werden: %4$s'), $this->getFullName(), $begin->format('d.m.Y H:i'), $end->format('H:i'), $e->getMessage() ) ); } else { throw new ResourceBookingException( sprintf( _('%1$s: Die Buchung vom %2$s bis %3$s konnte wegen Überlappungen nicht gespeichert werden: %4$s'), $this->getFullName(), $begin->format('d.m.Y H:i'), $end->format('d.m.Y H:i'), $e->getMessage() ) ); } } catch (Exception $e) { if ($begin->format('Ymd') == $end->format('Ymd')) { throw new ResourceBookingException( sprintf( _('%1$s: Die Buchung vom %2$s bis %3$s konnte aus folgendem Grund nicht gespeichert werden: %4$s'), $this->getFullName(), $begin->format('d.m.Y H:i'), $end->format('H:i'), $e->getMessage() ) ); } else { throw new ResourceBookingException( sprintf( _('%1$s: Die Buchung vom %2$s bis %3$s konnte aus folgendem Grund nicht gespeichert werden: %4$s'), $this->getFullName(), $begin->format('d.m.Y H:i'), $end->format('d.m.Y H:i'), $e->getMessage() ) ); } } return $booking; } /** * This method creates a simple request for this resource. * A simple request is not bound to a date, metadate * or course object and its time ranges. Instead the time * range is specified directly. * Note that simple resource requests do not support recurrence. * * @param User $user The user who wishes to create a simple request. * @param DateTime $begin The begin timestamp of the request. * @param DateTime $end The end timestamp of the request. * @param string $comment A comment for the resource request. * @param int $preparation_time The requested preparation time before * the begin of the requested time range. This parameter must be * specified in seconds. Only positive values are accepted. * * @return ResourceRequest A resource request object. * @throws AccessDeniedException If the user is not permitted * to request this resource. * @throws InvalidArgumentException If the the timestamps provided by * $begin and $end are invalid or if $begin is greater than or equal * to $end which results in an invalid time range. * @throws ResourceUnavailableException If the resource is not available * in the selected time range. * @throws ResourceRequestException If the resource request * cannot be stored. * */ public function createSimpleRequest( User $user, DateTime $begin, DateTime $end, $comment = '', $preparation_time = 0 ) { //All users are permitted to create a request, //if the resource is requestable. if (!$this->requestable) { throw new InvalidArgumentException( _('Diese Ressource kann nicht angefragt werden!') ); } if ($begin > $end) { throw new InvalidArgumentException( _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!') ); } elseif ($begin == $end) { throw new InvalidArgumentException( _('Startzeitpunkt und Endzeitpunkt sind identisch!') ); } if (!$this->isAvailable($begin, $end)) { throw new ResourceUnavailableException( sprintf( _('Die Ressource %1$s ist im Zeitraum von %2$s bis %3$s nicht verfügbar!'), $this->name, $begin->format('d.m.Y H:i'), $end->format('d.m.Y H:i') ) ); } $request = new ResourceRequest(); $request->resource_id = $this->id; $request->category_id = $this->category_id; $request->user_id = $user->id; $request->begin = $begin->getTimestamp(); $request->end = $end->getTimestamp(); $request->preparation_time = ( $preparation_time > 0 ? $preparation_time : 0 ); $request->closed = '0'; $request->comment = $comment; if (!$request->store()) { throw new ResourceRequestException( sprintf( _('Die Anfrage zur Ressource %s konnte nicht gespeichert werden!'), $this->name ) ); } return $request; } /** * This method creates a resource request for this resource. * * @param User $user The user who wishes to create a request. * @param string|array $date_range_ids One or more IDs of Stud.IP objects * which can provide at least one time range. * Objects which fulfill this requirement are * course dates (CourseDate objects), * cycle dates (SeminarCycleDate objects) * and courses (Course objects). * If only one ID is provided it can be passed as string. * If multiple IDs are provided they have to be passed as array. * @param string $comment A comment for the resource request. * @param mixed[] $properties The wishable properties * for the resource request. The format of the array is as follows: * [ * 'property name' => 'property state' * ] * @param int $preparation_time The requested preparation time before * the begin of the requested time range. This parameter must be * specified in seconds. Only positive values are accepted. * * @return ResourceRequest A resource request object. * @throws InvalidArgumentException If $date_range_id is not set. * or no object which can provide at least one time range * can be found with the specified ID. * @throws ResourceNoTimeRangeException If no time range can be found * by looking at the object, specified by its ID in $date_range_id. * @throws ResourceUnavailableException If the resource is not available * in the selected time range. * @throws ResourceRequestException If the resource request * cannot be stored. * */ public function createRequest( User $user, $date_range_ids = null, $comment = '', $properties = [], $preparation_time = 0 ) { if (!$date_range_ids) { throw new InvalidArgumentException( _('Es wurde keine ID eines Objektes angegeben, welches Zeiträume für eine Ressourcenanfrage liefern kann!') ); } if (!$this->requestable) { throw new InvalidArgumentException( _('Diese Ressource kann nicht angefragt werden!') ); } //We must get the date ranges by looking at $date_range_id //and the object which lies behind that ID. if (!is_array($date_range_ids)) { $date_range_ids = [$date_range_ids]; } $time_ranges = []; foreach ($date_range_ids as $date_range_id) { $time_ranges = array_merge( $time_ranges, ResourceManager::getTimeRangesFromRangeId( $date_range_id ) ); } if (!$time_ranges) { //We couldn't find any time range. throw new ResourceNoTimeRangeException( sprintf( _('Es konnte kein Zeitbereich für die Anfrage der Ressource %s gefunden werden.'), $this->name ) ); } //Default resource request handling: //Check if the resource is available in all requested time ranges. foreach ($time_ranges as $time_range) { if (!$this->isAvailable($time_range[0], $time_range[1])) { throw new ResourceUnavailableException( sprintf( _('Die Ressource %1$s ist im Zeitraum vom %2$s bis %3$s nicht verfügbar!'), $this->name, $time_range[0]->format('d.m.Y H:i'), $time_range[1]->format('d.m.Y H:i') ) ); } } //We must check, if all the properties exist: if ($properties and is_array($properties)) { foreach ($properties as $property_name => $property_state) { $property_object = ResourcePropertyDefinition::findByName( $property_name ); if (!$property_object) { throw new ResourcePropertyException( sprintf( _('Die Ressourceneigenschaft %s ist nicht definiert!'), $property_name ) ); } elseif (count($property_object) > 1) { throw new ResourcePropertyException( sprintf( _('Es gibt mehrere Ressourceneigenschaften mit dem Namen %s!'), $property_name ) ); } //$property_object is an array of ResourcePropertyDefinition objects: $property_data[] = [ 'object' => $property_object[0], 'state' => $property_state ]; } } $request = new ResourceRequest(); $request->resource_id = $this->id; $request->category_id = $this->category_id; $request->user_id = $user->id; $request->comment = $comment; $request->preparation_time = ( $preparation_time > 0 ? $preparation_time : 0 ); $request->closed = '0'; //Resolve the date range ID and set the //appropriate field in the request object: if (count($date_range_ids) <= 1) { $course_date = CourseDate::find($date_range_ids[0]); if ($course_date) { $request->termin_id = $course_date->id; } else { $cycle_date = SeminarCycleDate::find($date_range_ids[0]); if ($cycle_date) { $request->metadate_id = $cycle_date->id; } else { $course = Course::find($date_range_ids[0]); if ($course) { $request->course_id = $course->id; } } } if (!$request->store()) { throw new ResourceRequestException( sprintf( _('Die Anfrage zur Ressource %s konnte nicht gespeichert werden!'), $this->name ) ); } } else { if (!$request->store()) { throw new ResourceRequestException( sprintf( _('Die Anfrage zur Ressource %s konnte nicht gespeichert werden!'), $this->name ) ); } //More than one entry: //We must use ResourceBookingAppointment objects. foreach ($date_range_ids as $date_range_id) { $appointment_id = null; $course_date = CourseDate::find($date_range_id); if ($course_date) { $appointment_id = $course_date->id; } else { $cycle_date = SeminarCycleDate::find($date_range_id); if ($cycle_date) { $appointment_id = $cycle_date->id; } else { $course = Course::find($date_range_id); if ($course) { $appointment_id = $course->id; } } } if ($appointment_id) { $rra = new ResourceRequestAppointment(); $rra->request_id = $request->id; $rra->appointment_id = $appointment_id; if (!$rra->store()) { throw new ResourceRequestException( _('Die Terminzuordnungen zur Anfrage konnten nicht gespeichert werden!') ); } } } } //The request has been created: Now we need to link the properties: if(!empty($property_data)) { foreach ($property_data as $property) { $rrp = new ResourceRequestProperty(); $rrp->request_id = $request->id; $rrp->property_id = $property['object']->id; $rrp->state = intval($property['state']); if (!$rrp->store()) { throw new InvalidResourceRequestException( sprintf( _('%1$s: Die Eigenschaft %2$s zur Anfrage konnte nicht gespeichert werden!'), $this->getFullName(), $property['object']->name ) ); } } } return $request; } /** * Creates a lock booking for this resource. * * @param User $user The user who wishes to create a lock booking. * @param DateTime $begin The begin of the lock time range. * @param DateTime $end The end of the lock time range. * @param string $internal_comment An optional comment for the * lock booking which is intended to be used internally * in the room and resource administration staff. * * @return ResourceBooking A ResourceBooking object. * @throws ResourceUnavailableException If a lock booking already * exists in the specified time range. * * @throws AccessDeniedException If the user does not have sufficient * permissions to lock this resource. */ public function createLock( User $user, DateTime $begin, DateTime $end, $internal_comment = '' ) { if (!$this->userHasPermission($user, 'admin', [$begin, $end])) { throw new AccessDeniedException( sprintf( _('%s: Unzureichende Berechtigungen zum Erstellen einer Sperrbuchung!'), $this->getFullName() ) ); } if ($this->isLocked($begin, $end)) { throw new ResourceUnavailableException( sprintf( _('%1$s: Im Zeitbereich von %2$s bis %3$s gibt es bereits Sperrbuchungen!'), $this->getFullName(), $begin->format('d.m.Y H:i'), $end->format('d.m.Y H:i') ) ); } $lock = new ResourceBooking(); $lock->booking_type = ResourceBooking::TYPE_LOCK; $lock->range_id = $user->id; $lock->resource_id = $this->id; $lock->begin = $begin->getTimestamp(); $lock->end = $end->getTimestamp(); $lock->internal_comment = $internal_comment; if (!$lock->store()) { throw new ResourceBookingException( sprintf( _('%1$s: Fehler beim Speichern der Sperrbuchung für den Zeitbereich von %2$s bis %3$s!'), $begin->format('d.m.Y H:i'), $end->format('d.m.Y H:i') ) ); } return $lock; } /** * Retrieves the properties grouped by their property groups * and in the order specified in that group. * * @param string[] excluded_properties An array with the names * of the properties that shall be excluded from the result set. * * @return array An array with the group names as keys and the properties * in the second array dimension. The structure of the array * is as follows: * [ * group1 name => [ * property1, * property2, * ... * ], * group2 name => [ * ... * ] * ] */ public function getGroupedProperties($excluded_properties = []) { if (is_array($excluded_properties) && count($excluded_properties)) { $properties = ResourceProperty::findBySql( "INNER JOIN resource_property_definitions rpd USING (property_id) LEFT JOIN resource_property_groups rpg ON rpd.property_group_id = rpg.id WHERE resource_properties.resource_id = :resource_id AND rpd.name NOT IN ( :excluded_properties ) ORDER BY rpg.position ASC, rpg.name ASC, rpd.property_group_pos ASC, rpd.name ASC", [ 'resource_id' => $this->id, 'excluded_properties' => $excluded_properties ] ); } else { $properties = ResourceProperty::findBySql( "INNER JOIN resource_property_definitions rpd USING (property_id) LEFT JOIN resource_property_groups rpg ON rpd.property_group_id = rpg.id WHERE resource_properties.resource_id = :resource_id ORDER BY rpg.position ASC, rpg.name ASC, rpd.property_group_pos ASC, rpd.name ASC", [ 'resource_id' => $this->id ] ); } if (!$properties) { return []; } $property_groups = []; foreach ($properties as $property) { if (!$property->state) { continue; } $group_name = ''; if (!empty($property->definition->group->name)) { $group_name = $property->definition->group->name; } if (empty($property_groups[$group_name]) || !is_array($property_groups[$group_name])) { $property_groups[$group_name] = []; } $property_groups[$group_name][] = $property; } return $property_groups; } /** * Determines wheter this resource has a property * with the specified name. * * @param string $name The name of the resource property. * * @return bool True, if this resource has a property with * the specified name, false otherwise. */ public function propertyExists($name = '') { if (!$name) { return false; } $db = DBManager::get(); $exists_stmt = $db->prepare( "SELECT TRUE FROM resource_properties INNER JOIN resource_property_definitions rpd ON resource_properties.property_id = rpd.property_id WHERE resource_properties.resource_id = :resource_id AND rpd.name = :name"); $exists_stmt->execute( [ 'resource_id' => $this->id, 'name' => $name ] ); $exists = $exists_stmt->fetchColumn(0); return (bool)$exists; } /** * Retrieves a ResourceProperty object for a property of this resource * which has the specified name. If the property has not been set for this * resource, but is defined for this resource's category, a new * ResourceProperty object will be created, stored and returned. * * @param string $name The name of the resource property. * * @return ResourceProperty|null Either a ResourceProperty object for * the resource property matching the specified name or null, * if no resource property with the specified name can be found. * @throws InvalidResourceCategoryException If this resource category * doesn't match the category of the resource object. * */ public function getPropertyObject(string $name) { if (!$this->propertyExists($name)) { if ($name === 'geo_coordinates') { return null; } //A property with the name $name does not exist for this //resource object. If it is a defined property //we can still try to create it: if ($this->category->hasProperty($name)) { $property = $this->category->createDefinedResourceProperty( $this, $name ); $property->store(); return $property; } else { return null; } } return ResourceProperty::findOneBySql( "INNER JOIN resource_property_definitions rpd ON resource_properties.property_id = rpd.property_id WHERE resource_properties.resource_id = :resource_id AND rpd.name = :name", [ 'resource_id' => $this->id, 'name' => $name ] ); } /** * Returns all info-label properties * * @return SimpleCollection */ public function getInfolabelProperties() { return SimpleCollection::createFromArray( ResourceProperty::findBySQL('INNER JOIN `resource_property_definitions` USING (`property_id`) WHERE `info_label` = 1 AND `state` != "" AND `resource_id` = ?', [$this->id] ) ); } /** * Returns the state of the property specified by $name. * If the property has not been set for this resource, but is defined * for this resource's category, a new ResourceProperty object * will be created, stored and its state will be returned. * * @param string $name The name of the resource property. * * @return string|null The state of the specified property or null * if the propery can't be found. */ public function getProperty(string $name) { if (!$this->propertyExists($name)) { //A property with the name $name does not exist for this //resource object. If it is a defined property //we can still try to create it: if ($this->category->hasProperty($name)) { $property = $this->category->createDefinedResourceProperty( $this, $name, '' ); $property->store(); return $property->state; } else { return null; } } $db = DBManager::get(); $value_stmt = $db->prepare( "SELECT resource_properties.state FROM resource_properties INNER JOIN resource_property_definitions rpd ON resource_properties.property_id = rpd.property_id WHERE resource_properties.resource_id = :resource_id AND rpd.name = :name"); $value_stmt->execute( [ 'resource_id' => $this->id, 'name' => $name ] ); $value = $value_stmt->fetchColumn(0); if (!$value) { return null; } return $value; } /** * Retrieves an object by the state of a property of this resource, * specified by the property's name. * This method is useful for properties of type user, institute * or fileref. Those properties store IDs of User, Institute * or FileRef objects. Therefore the IDs can be resolved directly * to get the corresponding User, Institute or FileRef object directly. * * @param string $name The name of the resource property. * * @return SimpleORMap|null A SimpleORMap-based object or null, * if no such object can be retrieved from the property's state. */ public function getPropertyRelatedObject(string $name) { //Get the property state first: $property = $this->getPropertyObject($name); //Now we return the object which is referenced by the property's state: if ($property) { switch ($property->definition->type) { case 'user': return User::find($property->state); case 'institute': return Institute::find($property->state); case 'fileref' : return FileRef::find($property->state); default: //For all other property types where we cannot create an object //we return the raw state value: return $property->state; } } return null; } /** * Sets a specified property of this resource to the specified state. * If the property has not been set for this resource, but is defined * for this resource's category, a new ResourceProperty object * will be created, stored and its state will be returned. * * @param string $name The name of the resource property. * @param mixed $state The state of the resource property. * @param User|null $user The user who wishes to set the property. * * @return bool True, if the property state could be set, false otherwise. */ public function setProperty(string $name, $state = '', $user = null) { if (!($user instanceof User)) { $user = User::findCurrent(); if (!$user) { //We cannot continue without a user object! return false; } } //Get the minimum permission level required for modifying the property: if (!$this->userHasPermission($user, 'admin')) { throw new AccessDeniedException( sprintf( _('Unzureichende Berechtigungen zum Ändern der Ressource %s!'), $this->name ) ); } if (!$this->category->userHasPropertyWritePermissions($name, $user, $this)) { throw new AccessDeniedException( sprintf( _('Unzureichende Berechtigungen zum Ändern der Eigenschaft %s!'), $name ) ); } if (!$this->propertyExists($name)) { //A property with the name $name does not exist for this //resource object. If it is a defined property //we can still try to create it: if ($this->category->hasProperty($name)) { $property = $this->category->createDefinedResourceProperty( $this, $name, $state ); return $property->store(); } else { return false; } } $property = $this->getPropertyObject($name); if ($property) { $property->state = $state; if ($property->isDirty()) { return $property->store(); } return true; } return false; } /** * Sets the properties (specified by their names) to the specified values. * * @param array $properties The properties array in the format "key-value". * The array keys must contain the property name while the * items of the array contain the values. * Example: * ['bar' => 'foo']: Sets the value 'foo' for the property * with the name 'bar'. * * @param User|null $user The user who wishes to set the properties. * If this is left empty, the current user will be used. * * @return array If properties cannot be set, their names (as key) and the * error messages (if any) are returned. * The array has the following structure: * [ * (property name) => (error message or empty string) * ] */ public function setPropertiesByName(array $properties, User $user) { $failed_properties = []; if (!($user instanceof User)) { $user = User::findCurrent(); if (!$user) { //No property can be set. foreach ($properties as $name => $state) { $failed_properties[$name] = ''; } return $failed_properties; } } foreach ($properties as $name => $state) { try { $this->setProperty($name, $state, $user); } catch (Exception $e) { $this->failed_properties[$name] = $e->getMessage(); } } return $failed_properties; } /** * Sets the properties (specified by their IDs) to the specified values. * * @param array $properties The properties array in the format "key-value". * The array keys must contain the property-ID while the * items of the array contain the values. * Example: * ['1' => 'foo']: Sets the value 'foo' for the property * with the ID '1'. * * @param User|null $user The user who wishes to set the properties. * If this is left empty, the current user will be used. * * @return array If properties cannot be set, their ids (as key) and the * error messages (if any) are returned. * The array has the following structure: * [ * (property-ID) => (error message or empty string) * ] */ public function setPropertiesById(array $properties, User $user = null) { $failed_properties = []; if (!($user instanceof User)) { $user = User::findCurrent(); if (!$user) { //No property can be set. foreach ($properties as $id => $state) { $failed_properties[$id] = ''; } return $failed_properties; } } foreach ($properties as $id => $state) { $property = ResourcePropertyDefinition::find($id); if (!$property) { //Invalid property: $this->failed_properties[$id] = _('Die Eigenschaft wurde nicht gefunden!'); continue; } try { $this->setProperty($property->name, $state, $user); } catch (Exception $e) { $failed_properties[$id] = $e->getMessage(); } } return $failed_properties; } /** * Determines if the specified user has sufficient permissions to edit * the property specified by its name. * * @param string $name The name of the resource property. * @param user $user The user whose edit permissions shall be checked. * * @return bool True, if the user has edit permissions for the property, * false otherwise. */ public function isPropertyEditable(string $name, User $user) { return $this->category->userHasPropertyWritePermissions($name, $user, $this); } /** * Sets the state of a property by its definition_id rather than its name. * * @param string $property_definition_id The definition-ID of the property. * @param string $state The state of the property. * * @return bool True, if the property state can be stored, false otherwise. * @throws ResourcePropertyStateException If the provided state is invalid * for the specified resource property. * */ public function setPropertyByDefinitionId($property_definition_id = null, $state = null) { if (!$property_definition_id and !$state) { return false; } //Get property definition: $definition = ResourcePropertyDefinition::find($property_definition_id); if (!$definition) { return false; } //Check if the state matches the property definition's rules: $definition->validateState($state); //Check if the property for this resource already exists. //If so, update it. Otherwise create it. $property = ResourceProperty::findOneBySql( '(property_id = :property_id) AND (resource_id = :resource_id)', [ 'property_id' => $definition->id, 'resource_id' => $this->id ] ); if (!$property) { $property = new ResourceProperty(); $property->property_id = $definition->id; $property->resource_id = $this->id; } $property->state = $state; return $property->store(); } /** * Sets the property state by specifying an SimpleORMap object. * This method is meant for resource properties of type user, * institute or fileref. * * @param string $name The name of the resource property. * @param SimpleORMap $object The object for the resource property. * * @return bool True, if the property has been saved, false otherwise. */ public function setPropertyRelatedObject(string $name, SimpleORMap $object) { //Get the property state first: $property = $this->getPropertyObject($name); if (!$property) { return false; } //Now we return the object which is referenced by the property's state: switch ($property->definition->type) { case 'user': if (!($object instanceof User)) { throw new ResourcePropertyException( _("Eine Ressourceneigenschaft vom Typ 'user' benötigt ein Nutzer-Objekt zur Wertzuweisung!") ); } break; case 'institute': if (!($object instanceof Institute)) { throw new ResourcePropertyException( _("Eine Ressourceneigenschaft vom Typ 'institute' benötigt ein Institut-Objekt zur Wertzuweisung!") ); } break; case 'fileref': if (!($object instanceof FileRef)) { throw new ResourcePropertyException( _("Eine Ressourceneigenschaft vom Typ 'fileref' benötigt ein FileRef-Objekt zur Wertzuweisung!") ); } break; default: break; } //When no exception is thrown above we can set the object's ID //as the property's state: $property->state = $object->id; return $property->store(); } /** * Deletes a property for a resource. * * @param string $name The name of the property to be deleted. * * @param User $user The user who wishes to delete the property. * @return number */ public function deleteProperty(string $name, User $user) { //Get the user object and the minimum permission level //required for modifying the property: if (!$this->userHasPermission($user, 'admin')) { throw new AccessDeniedException( sprintf( _('Unzureichende Berechtigungen zum Ändern der Ressource %s!'), $this->name ) ); } if (!$this->category->userHasPropertyWritePermissions($name, $user)) { throw new AccessDeniedException( sprintf( _('Unzureichende Berechtigungen zum Löschen der Eigenschaft %s!'), $name ) ); } return ResourceProperty::deleteBySql( "INNER JOIN resource_property_definitions rpd ON resource_properties.property_id = rpd.property_id WHERE rpd.name = :name AND resource_properties.resource_id = :resource_id", [ 'name' => $name, 'resource_id' => $this->id ] ); } /** * Returns the path for the resource's image. * If the resource has no image the path for a general * resource icon will be returned. * * Classes derived from the Resource class should only re-implement * this method if they have an alternative storage method for * resource pictures than the Stud.IP file system. * * @return string The URL to the resource picture. */ public function getPictureUrl() { return ''; } /** * Returns the default picture for the resource class. * * Classes derived from Resource should re-implement this method * if they want to get a different default picture than the resource icon. * The call to getPictureUrl will call the getDefaultPictureUrl method * from the derived class. * * @return string The URL to the picture. */ public function getDefaultPictureUrl() { return $this->getIcon()->asImagePath(); } /** * Returns the Icon for the resource class. * * Classes derived from Resource should re-implement this method * if they want to get a different icon than the resource icon. * @param string $role * @return Icon The icon for the resource. */ public function getIcon($role = Icon::ROLE_INFO) { return Icon::create('resources', $role); } /** * Returns all properties in a two-dimensional array with the following * property data inside of the second dimension: * [ * 'name' => (the property's name) * 'display_name' => (the display name of the property) * 'type' => (the property's type) * 'state' => (the property's state) * 'requestable' => (if the property is requestable or not (true or false)) * ] * * @param bool $only_requestable_properties If only requestable properties * shall be returned set this to true. If all properties shall be * returned, set this to false. * * @return array[] A two-dimensional array containing property data. */ public function getPropertyArray($only_requestable_properties = false) { $property_array = []; if ($this->properties) { foreach ($this->properties as $property) { if ($only_requestable_properties) { $category_property = ResourceCategoryProperty::findByNameAndCategoryId( $property->name, $this->category_id ); if ($category_property) { if ($category_property->requestable) { $property_array[] = [ 'name' => $property->name, 'display_name' => $property->display_name, 'type' => $property->type, 'state' => $property->state, 'requestable' => $property->isRequestable() ]; } } } else { $property_array[] = [ 'name' => $property->name, 'display_name' => $property->display_name, 'type' => $property->type, 'state' => $property->state, 'requestable' => $property->isRequestable() ]; } } } return $property_array; } /** * Shortcut method for ResourceBooking::countByResourceAndTimeRanges. * Determines whether normal resource bookings exist * in the specified time range. * * @param DateTime $begin Time range start timestamp. * * @param DateTime $end Time range end timestamp. * * @param array $excluded_booking_ids The IDs of bookings that shall * be excluded from the determination of the "assigned" status. * * @return bool True, if the resource is assigned in the specified * time range, false otherwise. */ public function isAssigned( DateTime $begin, DateTime $end, $excluded_booking_ids = [] ) { return ResourceBooking::countByResourceAndTimeRanges( $this, [ [ 'begin' => $begin->getTimestamp(), 'end' => $end->getTimestamp() ] ], [0], $excluded_booking_ids ) > 0; } /** * Shortcut method for ResourceBooking::countByResourceAndTimeRanges. * Determines whether resource reservations exist * in the specified time range. * * @param DateTime $begin Time range start timestamp. * * @param DateTime $end Time range end timestamp. * * @param array $excluded_reservation_ids The IDs of reservation bookings that shall * be excluded from the determination of the "reserved" status. * * @return bool True, if the resource is reserved in the specified * time range, false otherwise. */ public function isReserved( DateTime $begin, DateTime $end, $excluded_reservation_ids = [] ) { //One second is added to the begin timestamp to avoid //getting "false" overlaps where another booking ends on exactly //the begin timestamp. return ResourceBooking::countByResourceAndTimeRanges( $this, [ [ 'begin' => $begin->getTimestamp(), 'end' => $end->getTimestamp() ] ], [1, 3], $excluded_reservation_ids ) > 0; } /** * Shortcut method for ResourceBooking::countByResourceAndTimeRanges. * Determines whether resource locks exist * in the specified time range. * * @param DateTime $begin Time range start timestamp. * * @param DateTime $end Time range end timestamp. * * @param array $excluded_lock_ids The IDs of lock bookings that shall * be excluded from the determination of the "locked" status. * * @return bool True, if the resource is locked in the specified * time range, false otherwise. */ public function isLocked( DateTime $begin, DateTime $end, $excluded_lock_ids = [] ) { //One second is added to the begin timestamp to avoid //getting "false" overlaps where another booking ends on exactly //the begin timestamp. return ResourceBooking::countByResourceAndTimeRanges( $this, [ [ 'begin' => $begin->getTimestamp(), 'end' => $end->getTimestamp() ] ], [2], $excluded_lock_ids ) > 0; } /** * Determines, if the resource is available (not assigned or locked) * in a specified time range. * * @param DateTime $begin Time range start timestamp. * @param DateTime $end Time range end timestamp. * * @param array $excluded_booking_ids The IDs of available bookings that shall * be excluded from the determination of the "available" status. * * @return bool True, if the resource is available in the specified * time range, false otherwise. */ public function isAvailable( DateTime $begin, DateTime $end, $excluded_booking_ids = [] ) { return ResourceBooking::countByResourceAndTimeRanges( $this, [ [ 'begin' => $begin->getTimestamp(), 'end' => $end->getTimestamp() ] ], [0, 2], $excluded_booking_ids ) == 0; } /** * Determines, if the resource is available (not assigned or locked) * in the time ranges specified by a resource request. * * @param ResourceRequest $request A resource request object. * * @return bool True, if the resource is available in the * time ranges of the resource request, false otherwise. */ public function isAvailableForRequest(ResourceRequest $request) { $time_intervals = $request->getTimeIntervals(true); if (!$time_intervals) { //Without a single time interval we cannot check //if the resource is available. return false; } foreach ($time_intervals as $time_interval) { $begin = new DateTime(); $end = new DateTime(); $begin->setTimestamp($time_interval['begin']); $end->setTimestamp($time_interval['end']); if (!$this->isAvailable($begin, $end)) { //The resource is not available in the time interval. //We can stop here and return false. return false; } //If code execution reaches this point the resource is //available in all time intervals of the resource request: return true; } } /** * Returns the full (localised) name of the resource. * * @return string The full name of the resource. */ public function getFullName() { return sprintf( _('Ressource %s'), $this->name ); } /** * Sets the permission for one user for this resource. * * @param User $user The user whose permission shall be set. * @param string $perm The permission level for the specified user. * The levels 'user', 'autor', 'tutor' and 'admin' are allowed. * * @return bool True, if the permission has been stored successfully, * false otherwise. */ public function setUserPermission(User $user, $perm = 'autor') { if (!in_array($perm, ['user', 'autor', 'tutor', 'admin'])) { return false; } $perm_object = ResourcePermission::findOneBySql( '(user_id = :user_id) AND (resource_id = :resource_id)', [ 'user_id' => $user->id, 'resource_id' => $this->id ] ); if (!$perm_object) { $perm_object = new ResourcePermission(); $perm_object->user_id = $user->id; $perm_object->resource_id = $this->id; } $perm_object->perms = $perm; $stored = (bool)$perm_object->store(); if ($stored) { if (!isset(self::$permission_cache[$this->id])) { self::$permission_cache[$this->id] = []; } //Update the permission cache. self::$permission_cache[$this->id][$user->id] = $perm; } return $stored; } /** * Deletes the permission a specified user has on this resource. * * @param User $user The user whose permission shall be deleted. * * @return bool True */ public function deleteUserPermission(User $user) { $deleted = ResourcePermission::deleteBySql( '(user_id = :user_id) AND (resource_id = :resource_id)', [ 'user_id' => $user->id, 'resource_id' => $this->id ] ); if ($deleted && is_array(self::$permission_cache[$this->id])) { //Update the permission cache. self::$permission_cache[$this->id][$user->id] = null; } return true; } /** * Deletes all permissions of all users for this resource. * * @return bool True */ public function deleteAllPermissions() { ResourcePermission::deleteBySql( 'resource_id = :resource_id', [ 'resource_id' => $this->id ] ); //Update the permission cache: self::$permission_cache[$this->id] = []; return true; } /** * Retrieves the permission level a specified user * has on this resource. * * Setting the optional $time_range parameter will also enable checks for * temporary global permissions. * * @param User $user The user whose permission shall be retrieved. * * @param array $time_range (DateTime) This is an optional parameter that can * be used to pass two DateTime objects to this method. The first object * will be treated as the begin timestamp and the second one as the * end timestamp. * * @param bool $permanent_only Whether to retrieve only permanent permissions * (true) or permanent and temporary permissions (false). * Defaults to false. * * @return string The permission level, expressed as string. * The level can be 'user', 'autor', 'tutor' or 'admin'. */ public function getUserPermission(User $user, $time_range = [], $permanent_only = false) { if (ResourceManager::getGlobalResourcePermission($user) === 'admin') { return 'admin'; } $perm_string = ''; $temp_perm = null; $begin = time(); $end = $begin; //Check for a temporary permission first: //check only against current timestamp if (!$permanent_only) { $temp_perm = ResourceTemporaryPermission::findOneBySql( '(resource_id = :resource_id) AND (user_id = :user_id) AND (begin <= :begin) AND (end >= :end)', [ 'resource_id' => $this->id, 'user_id' => $user->id, 'begin' => $begin, 'end' => $end ] ); } if ($temp_perm) { $perm_string = $temp_perm->perms; } else { //No temporary permission exist or has been retrieved. //Check for a "normal" permission. $cached_perms = self::$permission_cache[$this->id][$user->id] ?? null; if ($cached_perms === null) { //The permission of the specified user is not in the //permission cache. Load it from the database and store //it in the permission cache before returning it. $perms = ResourcePermission::findOneBySql( '(resource_id = :resource_id) AND (user_id = :user_id)', [ 'resource_id' => $this->id, 'user_id' => $user->id ] ); if ($perms) { if (!isset(self::$permission_cache[$this->id])) { self::$permission_cache[$this->id] = []; } self::$permission_cache[$this->id][$user->id] = $perms->perms; $perm_string = $perms->perms; } } else { $perm_string = $cached_perms; } } if (!$perm_string) { //A user which doesn't have special permissions for this resource //can have global resource permissions: $global_perm = ResourceManager::getGlobalResourcePermission($user); if ($global_perm) { //Set the permission cache: if (!isset(self::$permission_cache[$this->id])) { self::$permission_cache[$this->id] = []; } self::$permission_cache[$this->id][$user->id] = $global_perm; } $perm_string = $global_perm; } //Now we must check for global resource locks: if ($perm_string && $time_range && $this->lockable) { if ($time_range[0] instanceof DateTime) { $begin = $time_range[0]->getTimestamp(); } else { $begin = $time_range[0]; } if ($time_range[1] instanceof DateTime) { $end = $time_range[1]->getTimestamp(); } else { $end = $time_range[1]; } if (GlobalResourceLock::isLocked($begin, $end)) { //A permission level exists for the user. //The user gets "user" permissions in case //a global lock is active. $perm_string = 'user'; } } //No global resource lock exists. We must return //the permission string if it is set: if ($perm_string) { return $perm_string; } return ''; } /** * Determines if a user has the specified permission. * * @param ?User $user The user whose permissions shall be checked on this * resource object. May be null. * @param string $permission The permission level. * @param $time_range @TODO * * @return bool True, if the specified user has the specified permission, * false otherwise. */ public function userHasPermission( ?User $user, string $permission = 'user', array $time_range = [] ) { if (!in_array($permission, ['user', 'autor', 'tutor', 'admin']) || $user === null) { return false; } if (ResourceManager::getGlobalResourcePermission($user) === 'admin') { return true; } $perm_level = $this->getUserPermission($user, $time_range); if ($permission === 'user') { //No check for global resource locks here: //If only user permissions are requested we can safely grant them //since 'user' users may only perform reading actions but //no writing actions. if (in_array($perm_level, ['user', 'autor', 'tutor', 'admin'])) { return true; } else { return false; } } elseif ($permission === 'autor') { if (in_array($perm_level, ['autor', 'tutor', 'admin'])) { return true; } else { return false; } } elseif ($permission === 'tutor') { if (in_array($perm_level, ['tutor', 'admin'])) { return true; } else { return false; } } elseif ($permission === 'admin') { if ($perm_level == 'admin') { return true; } else { return false; } } //Code execution should be finished at this point. //If this point is reached the user has no permissions for the //resource management system at all. return false; } /** * Determines whether the user may create a child resource * on this resource. * * @param User $user The user whose permission to create a child * resource shall be checked. * * @return bool True, if the user may create a child resource * on this resource, false otherwise. */ public function userMayCreateChild(User $user) { return $this->userHasPermission($user, 'admin'); } /** * Checks if the specified user has sufficient permissions to make resource * requests, according to the setting RESOURCES_MIN_REQUEST_PERMISSION. * This permission check is only relevant for creating requests that are not * bound to a course. * * @param User $user The user whose request permissions shall be checked. * * @return bool True, if the user has request permissions, false otherwise. */ public function userHasRequestRights(User $user) { if (!Config::get()->RESOURCES_ALLOW_ROOM_REQUESTS || !$this->booking_plan_request) { return false; } $min_perm = Config::get()->RESOURCES_MIN_REQUEST_PERMISSION; if (!in_array($min_perm, ['', 'user', 'autor', 'tutor', 'admin'])) { //Invalid permission level! return false; } if (!$min_perm) { //No minimum permission set: Every logged-in user //can create requests. return true; } return $this->userHasPermission($user, $min_perm); } /** * Determines whether the user may book the resource or not. * An optional time range can be set to check the user's * temporary permissions on another date than the current date. * * @param User $user The user whose booking permissions shall be checked. * * @param int|string|DateTime $begin The begin timestamp of the * optional time range. * * @param int|string|DateTime $end The end timestamp of the * optional time range. * * @return bool True, if the user may book the resource, false otherwise. */ public function userHasBookingRights( User $user, $begin = null, $end = null ) { if ($begin && $end) { $time_range = [$begin, $end]; } else { $time_range = []; } //Check the permissions on this resource and the global permissions: return $this->userHasPermission($user, 'autor', $time_range); } /** * Determines if the booking plan of the resource is visible for a * specified user. * * @param ?User $user The user whose permission to view the booking plan * shall be determined. May be null. * * @param DateTime[] $time_range An optional time range for the * permission check. * @return bool True, if the user can see the resource booking plan, * false otherwise. * @see Resource::getUserPermission * */ public function bookingPlanVisibleForUser(?User $user, $time_range = []) { return $this->userHasPermission($user, 'user', $time_range); } /** * Retrieves a parent resource object that matches the specified * class name. The search stops when either a parent resource * with the class name is found or when the root resource object * is reached. * * @param string $class_name The class name of the parent. * * @return Resource|null Either a resource object or null * in case a matching parent resource cannot be found. */ public function findParentByClassName($class_name = 'Resource') { $resource_ids = [$this->id]; $resource = $this->parent; while ($resource) { //We should check for circular hierarchies first //to avoid an endless while loop: if (in_array($resource->id, $resource_ids)) { //We have a circular hierarchy: this resource is //the parent of itself which is an invalid state! throw new InvalidResourceException( sprintf( _('Zirkuläre Hierarchie: Die Ressource %1$s ist ein Elternknoten von sich selbst!'), $resource->name ) ); } if (is_a($resource->class_name, $class_name, true)) { //We have found a parent node which has the //specified class name: return that parent. return $resource; } //The current parent was not the one we were looking for. //Therefore we must go one layer up in the resource //hierarchy and continue search: $resource_ids[] = $resource->id; $resource = $resource->parent; } //The search was not successful: //We have reached the root resource (whose parent_id field //is set to an equivalend of NULL) and we haven't found a //resource matching the specified class name. return null; } /** * This method searches the hierarchy below this resource * to find resources matching the specified class name. * Via the optional parameter $depth the search can be limited * to a specific amount of layers. * * @param string $class_name The name of the resource class * where resources shall be found to. * @param int $depth The (optional) maximum depth below this resource * which shall be searched. * @param bool $convert_objects True, if objects shall be converted to * $class_name (default), false otherwise. * @param bool $order_by_name Order the children by name. * Defaults to true. * * @return Resource[] An array of resource objects or an empty array * if no matching resources can be found. */ public function findChildrenByClassName( $class_name = 'Resource', $depth = 0, $convert_objects = true, $order_by_name = true ) { $result = []; if ($this->children) { //this resource has children: iterate over them and //check if they match the search criteria. foreach ($this->children as $child) { if (is_a($child->class_name, $class_name, true)) { if ($convert_objects) { $result[] = $child->getDerivedClassInstance(); } else { $result[] = $child; } } if (($depth > 1) || ($depth == 0)) { //Search the child and lower depth by one when calling this //method on the child. $result = array_merge( $result, $child->findChildrenByClassName( $class_name, (($depth > 1) ? $depth - 1 : 0), $convert_objects ) ); } } if ($order_by_name) { usort( $result, function ($a, $b) { if ($a->name == $b->name) { return 0; } elseif ($a->name < $b->name) { return -1; } else { return 1; } } ); } } return $result; } /** * Adds a resource as child resource to this resource. * * @param Resource $resource The child resource. * * @return bool True on success, false on failure. */ public function addChild(Resource $resource) { $old_parent = $resource->parent; $old_parent_id = $resource->parent_id; $resource->parent = $this; $resource->parent_id = $this->id; if (!$resource->checkHierarchy()) { //We must revert the parent fields since $resource //may be used in other code pieces afterwards. $resource->parent = $old_parent; $resource->parent_id = $old_parent_id; throw new InvalidArgumentException( sprintf( _('Die Ressource %1$s (Typ %2$s) kann nicht unterhalb der Ressource %3$s (Typ %4$s) platziert werden!'), $resource->name, $resource->class_name, $this->name, $this->class_name ) ); } if ($resource->isDirty()) { //Only store the resource object if setting the parent_id field //did change it: return $resource->store(); } //The resource object hasn't changed by setting the parent_id field: //We can return true. return true; } /** * Get all resource requests for the resource in a given timeframe. * * @param DateTime $begin Begin of timeframe. * @param DateTime $end End of timeframe. * * @return ResourceRequest[] An array of ResourceRequest objects. */ public function getOpenResourceRequests(DateTime $begin, DateTime $end) { //We must get all requests that either have a start and end date //set or that have a start date, repeate end, repeat interval and //repeat quantity set. return ResourceRequest::findByResourceAndTimeRanges( $this, [ [ 'begin' => $begin->getTimestamp(), 'end' => $end->getTimestamp() ] ], 0 ); } /** * Get all resource bookings for the resource in a given timeframe. * * @param DateTime $begin Begin of timeframe. * @param DateTime $end End of timeframe. * @param array $booking_types * * @return ResourceBooking[] An array of ResourceBooking objects. */ public function getResourceBookings(DateTime $begin, DateTime $end, array $booking_types = [0]) { return ResourceBooking::findByResourceAndTimeRanges( $this, [ [ 'begin' => $begin->getTimestamp(), 'end' => $end->getTimestamp() ] ], $booking_types ); } /** * Get all resource locks for the resource in a given timeframe. * * @param DateTime $begin Begin of timeframe. * @param DateTime $end End of timeframe. * * @return ResourceBooking[] An array of ResourceBooking objects. */ public function getResourceLocks(DateTime $begin, DateTime $end) { return ResourceBooking::findByResourceAndTimeRanges( $this, [ [ $begin->getTimestamp(), $end->getTimestamp() ] ], [2] ); } /** * Determines if files are attached to this resource. * If a folder exists for this resource its files are counted. * Depending on whether the folder has files in it or not * this method returns true or false. * * @return bool True, if there are files attached to this resource, * false otherwise. */ public function hasFiles() { $folder = Folder::findOneBySql( 'range_id = :range_id', [ 'range_id' => $this->id ] ); if (!$folder) { return false; } //Since files from resources shall always be stored in the //Stud.IP file system we can skip the conversion from Folder //to FolderType and count the FileRef-objects for this resource //directly in the database. Since resource folders do not //have subfolders we will count any file of the resource: return FileRef::countBySql( 'folder_id = :folder_id', [ 'folder_id' => $folder->id ] ) > 0; } /** * Converts a Resource object to an object of a specialised resource class. * * @return Resource|other An object of a specialised resource class * or a Resource object, if the resource is a standard resource * with the class_name 'Resource' in its resource category. * If the derived resource class is not available, an instance of * BrokenResource is returned. */ public function getDerivedClassInstance() { $class_name = $this->class_name; if ($class_name == 'Resource') { //It is a standard resource which is managed by this class. return $this; } if (is_subclass_of($class_name, 'Resource')) { $converted_resource = $class_name::buildExisting( $this->toRawArray() ); return $converted_resource; } else { //$class_name does not contain the name of a subclass //of Resource. That's an error! $broken_resource = BrokenResource::buildExisting( $this->toRawArray() ); return $broken_resource; } } /** * Checks if the place in the resource hierarchy (resource tree) * is correct for this resource. * This method has no function in this class but can be filled * with logic in one of the classes derived from Resource. * * @return bool True, if this resource is correctly placed, * false otherwise. * @throws NoResourceClassException * if the class name of this resource is not a derived class * of the Resource class. * */ public function checkHierarchy() { if ($this->class_name == 'Resource') { //Objects of the Resource class are always in the right //place of the resource hierarchy. return true; } //The object does not use the Resource class name and uses //a derived class instead. We must check the hierarchy //using the checkHierarchy method of the derived class. $converted_resource = $this->getDerivedClassInstance(); return $converted_resource->checkHierarchy(); } /** * Returns the link for an action for this resource. * This is the non-static variant of Resource::getLinkForAction. * * @param string $action The action which shall be executed. * For default Resources the actions 'show', 'add', 'edit' and 'delete' * are defined. * @param array $link_parameters Optional parameters for the link. * @return string @TODO */ public function getActionLink($action = 'show', $link_parameters = []) { //We must check the class name and call the appropriate //getLinkForAction method for derived classes: $class_name = $this->class_name; if (is_subclass_of($class_name, 'Resource')) { return $class_name::getLinkForAction( $action, $this->id, $link_parameters ); } else { return self::getLinkForAction( $action, $this->id, $link_parameters ); } } /** * Returns the URL for an action for this resource. * This is the non-static variant of Resource::getURLForAction. * * @param string $action The action which shall be executed. * For default Resources the actions 'show', 'add', 'edit' and 'delete' * are defined. * @param array $url_parameters Optional parameters for the URL. * @return string @TODO */ public function getActionURL($action = 'show', $url_parameters = []) { //We must check the class name and call the appropriate //getURLForAction method for derived classes: $class_name = $this->class_name; if (is_subclass_of($class_name, 'Resource')) { return $class_name::getURLForAction( $action, $this->id, $url_parameters ); } else { return self::getURLForAction( $action, $this->id, $url_parameters ); } } public function getItemName($long_format = true) { if ($long_format) { //In some cases the general Resource class may be used //when the resource objects are in fact instances //of derived classes. To make sure that the correct prefix //is always displayed, we retrieve the derived class first //before returning the name: $derived_class = $this->getDerivedClassInstance(); return $derived_class->getFullName(); } else { return $this->name; } } public function getItemURL() { return $this->getActionURL('show'); } public function getItemAvatarURL() { return Icon::create('resources', Icon::ROLE_INFO)->asImagePath(); } public function getLink() : StudipLink { return new StudipLink($this->getActionURL(), $this->name, Icon::create('resources')); } }