* @copyright 2017 * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * @package resources * @since 4.1 * * @property string $id database column * @property string $name database column * @property string $description database column * @property string $class_name database column: The name of the SORM class * that handles the resource object, defaults to Resource. * @property bool $system database column * @property int $iconnr database column * @property int $mkdate database column * @property int $chdate database column * * @property ResourcePropertyDefinition[]|SimpleORMapCollection $property_definitions * @property ResourceCategoryProperty[]|SimpleORMapCollection $property_links */ class ResourceCategory extends SimpleORMap { private static $cache; protected static function configure($config = []) { $config['db_table'] = 'resource_categories'; $config['has_and_belongs_to_many']['property_definitions'] = [ 'class_name' => ResourcePropertyDefinition::class, 'assoc_foreign_key' => 'property_id', 'thru_table' => 'resource_category_properties', 'thru_key' => 'category_id', 'order_by' => 'ORDER BY name ASC' ]; $config['has_many']['property_links'] = [ 'class_name' => ResourceCategoryProperty::class, 'assoc_func' => 'findByCategory_id', 'foreign_key' => 'id', 'on_delete' => 'delete' ]; $config['registered_callbacks']['after_create'][] = function ($category) { self::$cache[$category->id] = $category; }; $config['registered_callbacks']['after_store'][] = function ($category) { self::$cache[$category->id] = $category; }; $config['registered_callbacks']['after_delete'][] = function ($category) { unset(self::$cache[$category->id]); }; parent::configure($config); } /** * Retrieves all resource categories from the database. * @param bool $force_reload * @return ResourceCategory[] An array of ResourceCategory objects * or an empty array if no resource categories are defined. */ public static function findAll($force_reload = false) { if (!is_array(self::$cache) || $force_reload) { self::$cache = []; foreach (self::findBySql('1 ORDER BY name') as $one) { self::$cache[$one->id] = $one; } } return self::$cache; } public static function find($id) { $all = self::findAll(); return $all[$id] ?: null; } /** * "Converts" a category-ID to a class name by looking up the * class name of a specified category. * * @param string $category_id The category-ID of the specified category. * * @return string The class name field of the category which is specified * by $category_id. In case no category could be found, an empty * string is returned. */ public static function getClassNameById($category_id) { $category = self::find($category_id); if ($category) { return $category->class_name; } return ''; } public function hasResources() { $db = DBManager::get(); $stmt = $db->prepare( "SELECT 1 FROM resources WHERE category_id = :category_id LIMIT 1" ); $stmt->execute(['category_id' => $this->id]); return $stmt->fetchColumn() !== false; } /** * Retrieves the definitions of all properties that are available * for this resource category. * * @param string[] excluded_properties An array with the names * of the properties that shall be excluded from the result set. * * @return ResourcePropertyDefinition[] An array of resource property * definitions. */ public function getPropertyDefinitions($excluded_properties = []) { if (is_array($excluded_properties) && count($excluded_properties)) { return ResourcePropertyDefinition::findBySql( "INNER JOIN resource_category_properties rcp ON resource_property_definitions.property_id = rcp.property_id WHERE rcp.category_id = :category_id AND resource_property_definitions.name NOT IN ( :excluded_properties ) ORDER BY resource_property_definitions.name ASC", [ 'category_id' => $this->id, 'excluded_properties' => $excluded_properties ] ); } //No excluded properties are specified. //We can return all property definitions. return ResourcePropertyDefinition::findBySql( "INNER JOIN resource_category_properties rcp ON resource_property_definitions.property_id = rcp.property_id WHERE rcp.category_id = :category_id ORDER BY resource_property_definitions.name ASC", [ 'category_id' => $this->id ] ); } /** * This method returns the same properties as getPropertyDefinitions, * but grouped and ordered by the property groups and the position of the * property in that group. * * @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 getGroupedPropertyDefinitions($excluded_properties = []) { if (is_array($excluded_properties) && count($excluded_properties)) { $definitions = ResourcePropertyDefinition::findBySql( "INNER JOIN resource_category_properties rcp ON resource_property_definitions.property_id = rcp.property_id LEFT JOIN resource_property_groups rpg ON resource_property_definitions.property_group_id = rpg.id WHERE rcp.category_id = :category_id AND resource_property_definitions.name NOT IN ( :excluded_properties ) ORDER BY type DESC, rpg.position ASC, rpg.name ASC, resource_property_definitions.property_group_pos, resource_property_definitions.name ASC", [ 'category_id' => $this->id, 'excluded_properties' => $excluded_properties ] ); $empty_index = _('Sonstige'); $empty_property_group = [ $empty_index => [] ]; $property_groups = []; foreach ($definitions as $definition) { if ($definition->group && $definition->group->name) { $group_name = $definition->group->name; if (!is_array($property_groups[$group_name])) { $property_groups[$group_name] = []; } $property_groups[$group_name][] = $definition; } else { $empty_property_group[$empty_index][] = $definition; } } if ($empty_property_group[$empty_index]) { return array_merge($property_groups, $empty_property_group); } else { return $property_groups; } } //No excluded properties are specified. //We can return all property definitions. $definitions = ResourcePropertyDefinition::findBySql( "INNER JOIN resource_category_properties rcp ON resource_property_definitions.property_id = rcp.property_id LEFT JOIN resource_property_groups rpg ON resource_property_definitions.property_group_id = rpg.id WHERE rcp.category_id = :category_id ORDER BY rpg.position ASC, rpg.name ASC, resource_property_definitions.property_group_pos, resource_property_definitions.name ASC", [ 'category_id' => $this->id ] ); $empty_index = _('Sonstige'); $empty_property_group = [ $empty_index => [] ]; $property_groups = []; foreach ($definitions as $definition) { if ($definition->group->name) { $group_name = $definition->group->name; if (!is_array($property_groups[$group_name])) { $property_groups[$group_name] = []; } $property_groups[$group_name][] = $definition; } else { $empty_property_group[$empty_index][] = $definition; } } if ($empty_property_group[$empty_index]) { return array_merge($property_groups, $empty_property_group); } else { return $property_groups; } } /** * Adds a property to this category. If the property doesn't exist * it will be created. * * @param string $name The name of the property. * @param string $type The type of the property. * @param bool $requestable Whether the property is requestable or not. * Defaults to false. * @param bool $protected Whether the property is protected or not. * Defaults to false. * @param string $write_permission_level * @return ResourceCategoryProperty The created or updated * resource category property. * @throws ResourcePropertyDefinitionException If the property definition * cannot be created. * * @throws ResourcePropertyException If the property cannot be created. */ public function addProperty( $name = '', $type = 'bool', $requestable = false, $protected = false, $write_permission_level = 'autor' ) { if (!$name) { throw new ResourcePropertyException( _('Es wurde kein Name für die Eigenschaft angegeben!') ); } $defined_types = ResourcePropertyDefinition::getDefinedTypes(); if (!in_array($type, $defined_types)) { throw new ResourcePropertyException( sprintf( _('Der Eigenschaftstyp %s ist ungültig!'), $type ) ); } if (!in_array($write_permission_level, ['user', 'autor', 'tutor', 'admin'])) { throw new ResourcePropertyException( sprintf( _('Die Rechtestufe %s ist ungültig!'), $write_permission_level ) ); } $existing_property = ResourceCategoryProperty::findOneBySql( 'INNER JOIN resource_property_definitions rpd ON resource_category_properties.property_id = rpd.property_id WHERE resource_category_properties.category_id = :category_id AND rpd.name = :name AND rpd.type = :type', [ 'category_id' => $this->id, 'name' => $name, 'type' => $type ] ); if ($existing_property) { $existing_property->requestable = $requestable ? '1' : '0'; $existing_property->protected = $protected ? '1' : '0'; if ($existing_property->isDirty() && !$existing_property->store()) { throw new ResourcePropertyException( sprintf( _('Fehler beim Aktualisieren der Eigenschaft %1$s (vom Typ %2$s)!'), $name, $type ) ); } return $existing_property; } else { $definition = ResourcePropertyDefinition::findOneBySql( 'name = :name AND type = :type', [ 'name' => $name, 'type' => $type ] ); if (!$definition) { $definition = new ResourcePropertyDefinition(); $definition->name = $name; $definition->type = $type; if (!$definition->store()) { throw new ResourcePropertyDefinitionException( sprintf( _('Fehler beim Speichern der Definition der Eigenschaft %1$s (vom Typ %2$s)!'), $name, $type ) ); } } $property = new ResourceCategoryProperty(); $property->property_id = $definition->id; $property->category_id = $this->id; $property->requestable = $requestable ? '1' : '0'; $property->protected = $protected ? '1' : '0'; if ($property->store()) { return $property; } else { throw new ResourcePropertyException( sprintf( _('Fehler beim Speichern der neuen Eigenschaft %1$s (vom Typ %2$s)!'), $name, $type ) ); } } } /** * Creates a resource object which belongs to this category. * All properties which are mandatory for resources of this * category are set to default values unless they are specified in the * properties array. * * @param string $name The name of the new resource. * @param string $description The description of the new resource. * @param string $parent_id The parent resource's ID (if any). * @param array $properties An associative array in the form [$key] = $value * containing the defined properties which can be set to this resource. * @param bool $ignore_invalid If set to true, invalid values or invalid * property names are ignored and no exception is thrown if an invalid * value or property name occurs. Instead an invalid valud will be replaced * with a default value and an invalid property name will not result * in a set property. * * @return Resource New Resource object which is a member of this resource category. * @throws InvalidResourceException if the resource cannot be stored. * @throws ResourcePropertyException If the name of the resource property * is not defined for this resource category. * @throws InvalidResourceCategoryException if the class_name attribute of * the resource category contains a class name of a class which is not * derived from the Resource class. */ public function createResource( $name = '', $description = '', $parent_id = '', $properties = [], $ignore_invalid = false ) { if (($this->class_name != 'Resource') and !is_subclass_of($this->class_name, 'Resource')) { //Invalid resource category specification: //All class names for a resource category must be derived from the //resource class! throw new InvalidResourceCategoryException( sprintf( _('Die Ressourcenkategorie %1$s ist ungültig, da die dort angegebene Klasse %2$s nicht von der Klasse Resource abgeleitet ist!'), $this->name, $this->class_name ) ); } $resource = new $this->class_name(); $resource->parent_id = $parent_id; $resource->category_id = $this->id; $resource->name = $name; $resource->description = $description; if (!$resource->store()) { throw new InvalidResourceException( sprintf( _('Fehler beim Speichern der Resource %1$s!'), $resource->name ) ); } //The resource is stored. We can now store its attributes: if ($properties) { foreach ($properties as $name => $state) { try { $resource->setProperty($name, $state); } catch (ResourcePropertyException $e) { if (!$ignore_invalid) { throw $e; } } } } return $resource; } /** * Returns the default state for a resource property. * Depending on the type of the resource property * different state is returned. * * @param ResourcePropertyDefinition $definition The definition of a * resource property whose default state shall be returned. * * @return mixed The default state for the property type, * specified by the given property definition. */ protected function setPropertyDefaultState( ResourcePropertyDefinition $definition ) { switch ($definition->type) { case 'bool': return false; case 'num': return 0; case 'select': //Set the first option as default. //For that, we have to split the option list first, //since it containts semicolon separated values: $options = explode(';', $definition->options); return $options[0]; case 'user': //Return the ID of the current user: return User::findCurrent()->id; case 'position': return '+0.0+0.0+0.0CRSWGS_84/'; case 'institute': case 'fileref': case 'url': case 'text': return ''; } } /** * Creates a ResourceProperty object for a specified Resource object. * * @param Resource $resource The resource for which the ResourceProperty * object shall be built. * @param string $name The name of the property. * @param string $state The value of the property. * * @return ResourceProperty A ResourceProperty object * for the given Resource object. * @throws ResourcePropertyException If the name of the resource property * is not defined for this resource category. * * @throws InvalidResourceCategoryException If this resource category * doesn't match the category of the resource object. */ public function createDefinedResourceProperty(Resource $resource, $name, $state = null) { if ($resource->category_id != $this->id) { throw new InvalidResourceCategoryException( sprintf( _('Die Ressource %1$s ist kein Mitglied der Ressourcenkategorie %2$s!'), $resource->name, $this->name ) ); } if (!$resource->id) { //The resource has no ID: probably it is a new resource. if ($resource->isNew()) { //We need an ID so we have to create one: $resource->getNewId(); } else { throw new InvalidResourceException( sprintf( _('Die Ressource %1$s besitzt keine ID!'), $resource->name ) ); } } //get property definition: $definition = ResourcePropertyDefinition::findOneBySql( 'INNER JOIN resource_category_properties rcp ON rcp.property_id = resource_property_definitions.property_id WHERE category_id = :category_id AND name = :name LIMIT 1', [ 'category_id' => $this->id, 'name' => $name ] ); if (!$definition) { //Property is undefined for this resource category: throw new ResourcePropertyException( sprintf( _('Die Eigenschaft %1$s ist für die Ressourcenkategorie %2$s nicht definiert!'), $name, $this->name ) ); } $property = new ResourceProperty(); $property->resource_id = $resource->id; $property->property_id = $definition->id; //if state is not set we can set it to defined default values //for some state types: if ($state === null) { $property->state = self::setPropertyDefaultState($definition); } else { $property->state = $state; } return $property; } /** * Creates a ResourceRequestProperty object * for a specified ResourceRequest object. * * @param ResourceRequest $request The resource request for which the * ResourceRequestProperty object shall be built. * @param string $name The name of the property. * @param string $state The value of the property. * * @return ResourceRequestProperty A ResourceProperty object for the * given Resource object. * @throws ResourcePropertyException If the name of the resource property is * not defined for the resource category of the resource request. * * @throws InvalidResourceCategoryException If this resource category * doesn't match the resource category of the resource request object. */ public function createDefinedResourceRequestProperty( ResourceRequest $request, $name, $state = null ) { if ($request->category_id != $this->id) { throw new InvalidResourceCategoryException( sprintf( _('Die Resourcenanfrage %1$s ist kein Mitglied der Ressourcenkategorie %2$s!'), $request->name, $this->name ) ); } if (!$request->id) { //The request has no ID: probably it is a new resource request. if ($request->isNew()) { //We need an ID so we have to create one: $request->id = $request->getNewId(); } else { throw new InvalidResourceRequestException( sprintf( _('Die Ressourcenanfrage %1$s besitzt keine ID!'), $request->name ) ); } } //get property definition: $definition = ResourcePropertyDefinition::findOneBySql( 'INNER JOIN resource_category_properties rcp ON rcp.property_id = resource_property_definitions.property_id WHERE category_id = :category_id AND name = :name LIMIT 1', [ 'category_id' => $this->id, 'name' => $name ] ); if (!$definition) { //Property is undefined for this resource category: throw new ResourcePropertyException( sprintf( _('Die Eigenschaft %1$s ist für die Ressourcenkategorie %2$s nicht definiert!'), $name, $this->name ) ); } $property = new ResourceRequestProperty(); $property->request_id = $request->id; $property->property_id = $definition->id; //if state is not set we can set it to defined default values //for some state types: if ($state === null) { $property->state = self::setPropertyDefaultState($definition); } else { $property->state = $state; } return $property; } public function getRequestableProperties() { return ResourcePropertyDefinition::findBySql( "INNER JOIN resource_category_properties USING (property_id) WHERE resource_category_properties.category_id = :category_id AND resource_category_properties.requestable = '1' ORDER BY type DESC, name ASC", [ 'category_id' => $this->id ] ); } /** * Determines if this resource category has a property with the * specified name and type. * * @param string $name The requested property name. * @param string $type The requested property type (optional). * * @return bool True, if a property with the specified name and type * exists, false otherwise. */ public function hasProperty($name = '', $type = null) { if ($type) { return ResourceCategoryProperty::countBySql( 'INNER JOIN resource_property_definitions USING (property_id) WHERE name = :name AND type = :type AND category_id = :category_id', [ 'name' => $name, 'type' => $type, 'category_id' => $this->id ] ) > 0; } else { return ResourceCategoryProperty::countBySql( 'INNER JOIN resource_property_definitions USING (property_id) WHERE name = :name AND category_id = :category_id', [ 'name' => $name, 'category_id' => $this->id ] ) > 0; } } /** * Determines if the user has write permissions for the * resource property specified by its name. * * @param string The name of the resource property definition. * @param User $user The user whose permissions shall be checked. * @param Resource|null $resource An optional resource that shall be used * to check for non-global permissions. * * @return bool True, if the user has write permissions, false otherwise. * @throws ResourcePropertyDefinitionException If no property is found. * */ public function userHasPropertyWritePermissions(string $name, User $user, $resource = null) { $property = ResourcePropertyDefinition::findOneBySql( 'name = :name', [ 'name' => $name ] ); if (!$property) { throw new ResourcePropertyDefinitionException( sprintf( _('Die Ressourceneigenschaft %s existiert nicht!'), $name ) ); } if ($property->write_permission_level == 'admin-global') { return ResourceManager::userHasGlobalPermission( $user, 'admin' ); } elseif ($resource instanceof Resource) { //It must be a permission for the specified resource. return $resource->userHasPermission( $user, $property->write_permission_level ); } else { //We cannot check permissions. return false; } } /** * Get the icon of a category * @param string $role * @return Icon */ public function getIcon($role = Icon::ROLE_INFO) { if ($this->iconnr == 0) { //No special icon return Icon::create('resources', $role); } elseif ($this->iconnr == 1) { return Icon::create('home', $role); } else { //No known icon return Icon::create('resources', $role); } } }