Skip to content
Snippets Groups Projects
ResourceCategory.class.php 28.4 KiB
Newer Older
<?php

/**
 * ResourceCategory.class.php - model class for resource categories
 *
 * The ResourceCategory class can be used as a Factory for
 * Resource objects.
 *
 * 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
 * @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
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
 * @property int $system database column
 * @property int|null $iconnr database column
 * @property string $class_name database column
 * @property int $mkdate database column
 * @property int $chdate database column
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
 * @property SimpleORMapCollection|ResourceCategoryProperty[] $property_links has_many ResourceCategoryProperty
 * @property SimpleORMapCollection|ResourcePropertyDefinition[] $property_definitions has_and_belongs_to_many ResourcePropertyDefinition
 */
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();
    }

    /**
     * "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) {
Moritz Strohm's avatar
Moritz Strohm committed
                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;
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
        } 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);
        }
    }
}