Skip to content
Snippets Groups Projects
StructuralElement.php 33.7 KiB
Newer Older
<?php

namespace Courseware;

use User;

/**
 * Courseware's structural elements.
 *
 * @author  Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>
 * @author  Till Glöggler <gloeggler@elan-ev.de>
 * @author  Ron Lucke <lucke@elan-ev.de>
 * @license GPL2 or any later version
 *
 * @since   Stud.IP 5.0
 *
 * @property int                            $id                 database column
 * @property int                            $parent_id          database column
 * @property int                            $is_link            database column
 * @property int                            $target_id          database column
 * @property string                         $range_id           database column
 * @property string                         $range_type         database column
 * @property string                         $owner_id           database column
 * @property string                         $editor_id          database column
 * @property string                         $edit_blocker_id    database column
 * @property int                            $position           database column
 * @property string                         $title              database column
 * @property string                         $image_id           database column
 * @property string                         $purpose            database column
 * @property \JSONArrayObject               $payload            database column
 * @property int                            $public             database column
 * @property string                         $release_date       database column
 * @property string                         $withdraw_date      database column
 * @property \JSONArrayObject               $read_approval      database column
 * @property \JSONArrayObject               $write_approval     database column
 * @property \JSONArrayObject               $copy_approval      database column
 * @property \JSONArrayObject               $external_relations database column
 * @property int                            $mkdate             database column
 * @property int                            $chdate             database column
 * @property \SimpleORMapCollection         $children           has_many Courseware\StructuralElement
 * @property \SimpleORMapCollection         $containers         has_many Courseware\Container
 * @property ?\Courseware\StructuralElement $parent             belongs_to Courseware\StructuralElement
 * @property \User                          $user               belongs_to User
 * @property \Course                        $course             belongs_to Course
 * @property \User                          $owner              belongs_to User
 * @property \User                          $editor             belongs_to User
 * @property ?\User                         $edit_blocker       belongs_to User
 * @property ?\FileRef                      $image              has_one FileRef
Ron Lucke's avatar
Ron Lucke committed
 * @property ?\Courseware\Task              $task               has_one Courseware\Task
 * @property \SimpleORMapCollection         $comments           has_many Courseware\StructuralElementComment
 * @property \SimpleORMapCollection         $feedback           has_many Courseware\StructuralElementFeedback
 *
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 */
class StructuralElement extends \SimpleORMap
{
    protected static function configure($config = [])
    {
        $config['db_table'] = 'cw_structural_elements';

        $config['serialized_fields']['payload'] = 'JSONArrayObject';
        $config['serialized_fields']['read_approval'] = 'JSONArrayObject';
        $config['serialized_fields']['write_approval'] = 'JSONArrayObject';
        $config['serialized_fields']['copy_approval'] = 'JSONArrayObject';
        $config['serialized_fields']['external_relations'] = 'JSONArrayObject';

        $config['has_many']['children'] = [
            'class_name' => StructuralElement::class,
            'assoc_foreign_key' => 'parent_id',
            'on_delete' => 'delete',
            'on_store' => 'store',
            'order_by' => 'ORDER BY position',
        ];

        $config['has_many']['containers'] = [
            'class_name' => Container::class,
            'assoc_foreign_key' => 'structural_element_id',
            'on_delete' => 'delete',
            'on_store' => 'store',
            'order_by' => 'ORDER BY position',
        ];

Ron Lucke's avatar
Ron Lucke committed
        $config['has_one']['task'] = [
            'class_name' => Task::class,
            'assoc_foreign_key' => 'structural_element_id',
            'on_delete' => 'delete',
        ];

        $config['belongs_to']['parent'] = [
            'class_name' => StructuralElement::class,
            'foreign_key' => 'parent_id',
        ];

        $config['belongs_to']['user'] = [
            'class_name' => \User::class,
            'foreign_key' => 'range_id',
            'assoc_foreign_key' => 'user_id',
        ];

        $config['belongs_to']['course'] = [
            'class_name' => \Course::class,
            'foreign_key' => 'range_id',
            'assoc_foreign_key' => 'seminar_id',
        ];

        $config['belongs_to']['owner'] = [
            'class_name' => User::class,
            'foreign_key' => 'owner_id',
        ];
Ron Lucke's avatar
Ron Lucke committed

        $config['belongs_to']['editor'] = [
            'class_name' => User::class,
            'foreign_key' => 'editor_id',
        ];
Ron Lucke's avatar
Ron Lucke committed

        $config['belongs_to']['edit_blocker'] = [
            'class_name' => User::class,
            'foreign_key' => 'edit_blocker_id',
        ];
Ron Lucke's avatar
Ron Lucke committed

        $config['has_one']['image'] = [
            'class_name' => \FileRef::class,
            'foreign_key' => 'image_id',
            'on_delete' => 'delete',
            'on_store' => 'store',
        ];

Ron Lucke's avatar
Ron Lucke committed
        $config['has_many']['comments'] = [
            'class_name' => StructuralElementComment::class,
            'assoc_foreign_key' => 'structural_element_id',
            'on_delete' => 'delete',
            'on_store' => 'store',
            'order_by' => 'ORDER BY chdate',
        ];

        $config['has_many']['feedback'] = [
            'class_name' => StructuralElementFeedback::class,
            'assoc_foreign_key' => 'structural_element_id',
            'on_delete' => 'delete',
            'on_store' => 'store',
            'order_by' => 'ORDER BY chdate',
        ];

        parent::configure($config);
    }

    /**
     * Returns the root element of a courseware instance for a user.
     *
     * @param string $userId the user's id to return the root element for
     *
     * @return ?StructuralElement null, if there is none, the root StructuralElement if there is one
     */
    public static function getCoursewareUser(string $userId): ?StructuralElement
    {
        return self::getCourseware($userId, 'user');
    }

    /**
     * Returns the root element of a courseware instance for a course.
     *
     * @param string $courseId the course's id to return the root element for
     *
     * @return ?StructuralElement null, if there is none, the root StructuralElement if there is one
     */
    public static function getCoursewareCourse(string $courseId): ?StructuralElement
    {
        return self::getCourseware($courseId, 'course');
    }

    public static function getSharedCoursewareUser(string $root_id): ?StructuralElement
        return self::getCourseware('', '', $root_id);
    }

    private static function getCourseware(string $rangeId, string $rangeType, string $root_id = null): ?StructuralElement
    {
        if ($root_id) {
            $result = self::find($root_id);
        } else {
            $result = self::findOneBySQL(
                'range_id = ?
                AND range_type = ? AND parent_id IS NULL',
                [$rangeId, $rangeType]
            );
        }

        return $result;
    }

    /**
     * Returns the ID of the associated range.
     *
     * @return string the id of the range
     */
    public function getRangeId(): string
    {
        return $this->range_id;
    }

    /**
     * @return bool true, if this object is the root of a courseware, false otherwise
     */
    public function isRootNode(): bool
    {
        return null === $this->parent_id;
    }

Ron Lucke's avatar
Ron Lucke committed
    /**
     * @return bool true, if this object purpose is task, false otherwise
     */
    public function isTask(): bool
    {
        return $this->purpose === 'task';
    }

    /**
     * @param User $user the user to validate
     *
     * @return bool true if the user may edit this instance
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    public function canEdit($user): bool
    {
        if ($GLOBALS['perm']->have_perm('root', $user->id)) {
            return true;
        }

        switch ($this->range_type) {
            case 'user':
                if ($this->range_id === $user->id) {
                    return true;
                }

                return $this->hasWriteApproval($user);
Ron Lucke's avatar
Ron Lucke committed
                $hasEditingPermission = $this->hasEditingPermission($user);
                if ($this->isTask()) {
Ron Lucke's avatar
Ron Lucke committed
                    $task = $this->task;
                    if (!$task) {
                        $task = $this->findParentTask();
                        if (!$task) {
                            return false;
                        }
Ron Lucke's avatar
Ron Lucke committed
                    }

                    if ($hasEditingPermission) {
                        return false;
                    }

Ron Lucke's avatar
Ron Lucke committed
                    if ($task->isSubmitted()) {
Ron Lucke's avatar
Ron Lucke committed
                        return false;
                    }

Ron Lucke's avatar
Ron Lucke committed
                    return $task->userIsASolver($user);
Ron Lucke's avatar
Ron Lucke committed
                }

                if ($hasEditingPermission) {
                return $this->hasWriteApproval($user);

            default:
                throw new \InvalidArgumentException('Unknown range type.');
        }
    }

    /**
     * @param mixed $user the user to validate
     *
     * @return bool true if the user may read this instance
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    public function canRead($user): bool
    {
        // root darf immer
        if ($GLOBALS['perm']->have_perm('root', $user->id)) {
        switch ($this->range_type) {
            case 'user':
                if ($this->range_id === $user->id) {
                $link = StructuralElement::findOneBySQL('target_id = ?', [$this->id]);
                if ($link) {
                    return true;
                }

                return false;
            case 'course':
                if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) {
                    return false;
                }

                if ($this->canEdit($user)) {
                    return true;
                }

                if (!$this->releasedForReaders($this)) {
                    return false;
                }

                return $this->hasReadApproval($user);

            default:
                throw new \InvalidArgumentException('Unknown range type.');
        }
    }

    public function canVisit($user): bool
    {
        // root darf immer
        if ($GLOBALS['perm']->have_perm('root', $user->id)) {
            return true;
Ron Lucke's avatar
Ron Lucke committed
        }

        switch ($this->range_type) {
            case 'user':
                if ($this->range_id === $user->id) {
                    return true;
                }

                return $this->hasReadApproval($user);

            case 'course':
                if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) {
                    return false;
                }

Ron Lucke's avatar
Ron Lucke committed
                if ($this->isTask()) {
Ron Lucke's avatar
Ron Lucke committed
                    $task = $this->task;
                    if (!$task) {
                        $task = $this->findParentTask();
                        if (!$task) {
                            return false;
                        }
Ron Lucke's avatar
Ron Lucke committed
                    }

Ron Lucke's avatar
Ron Lucke committed
                    if ($task->isSubmitted() && $this->hasEditingPermission($user)) {
Ron Lucke's avatar
Ron Lucke committed
                        return true;
                    }

Ron Lucke's avatar
Ron Lucke committed
                    return $task->userIsASolver($user);
Ron Lucke's avatar
Ron Lucke committed
                }

                if ($this->canEdit($user)) {
                    return true;
                }

                if (!$this->releasedForReaders($this)) {
                    return false;
                }

                return $this->hasReadApproval($user) && $this->canReadSequential($user);

            default:
                throw new \InvalidArgumentException('Unknown range type.');
        }
    }

Ron Lucke's avatar
Ron Lucke committed
    /**
     * @param \User|\Seminar_User $user
     */
    public function hasEditingPermission($user): bool
    {
        return $GLOBALS['perm']->have_perm('root', $user->id)
            || $GLOBALS['perm']->have_studip_perm(
                \CourseConfig::get($this->range_id)->COURSEWARE_EDITING_PERMISSION[$this->getCoursewareCourse($this->range_id)->id] ?? 'tutor',
Ron Lucke's avatar
Ron Lucke committed
                $this->range_id,
                $user->id
            );
    }

    private function hasReadApproval($user): bool
    {
        // this property is shared between all range types.
        if ($this->read_approval['all']) {
        // now we also check against the perms for contents.
        if ($this->range_type === 'user') {
            return $this->hasUserReadApproval($user);
        } else {
            if (!count($this->read_approval)) {
                return true;
            }

            // find user in users
            $users = $this->read_approval['users'];
            foreach ($users as $approvedUserId) {
                if ($approvedUserId === $user->id) {
                    return true;
                }
            }

            // find user in groups
            $groups = $this->read_approval['groups'];
            foreach ($groups as $groupId) {
                /** @var ?\Statusgruppen $group */
                $group = \Statusgruppen::find($groupId);
                if ($group && $group->isMember($user->id)) {
                    return true;
                }
            }
        }

        return false;
    }

    private function hasUserReadApproval($user): bool
    {
        if (!count($this->read_approval)) {
            if ($this->isRootNode()) {
                return false;
            }
            return $this->parent->hasUserReadApproval($user);
        $users = $this->read_approval['users'];
        foreach ($users as $listedUserPerm) {
            // now for contents, there is an expiry date defined.
            if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) {
                if ($this->isRootNode()) {
                    return false;
                }
                return $this->parent->hasUserReadApproval($user);
            }
            // In order to have a record of the users in the perms list of contents,
            // we keep a full perm record in read_approval column, and set read property to true or false,
            // this won't apply to write_approval column.
            if ($listedUserPerm['id'] == $user->id && $listedUserPerm['read'] == true) {
        //User not found.
        return false;
    }

    private function hasWriteApproval($user): bool
    {
        // this property is shared between all range types.
        if ($this->write_approval['all']) {
            return true;
        }
        // now we also check against the perms for contents.
        if ($this->range_type === 'user') {
            return $this->hasUserWriteApproval($user);
        } else {
            if (!count($this->write_approval)) {
                return false;
            }

            if ($this->write_approval['all']) {

            // find user in users
            $users = $this->write_approval['users']->getArrayCopy();
            if (in_array($user->id, $users)) {
                return true;
            }

            // find user in groups
            foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) {
                if ($group->isMember($user->id)) {
                    return true;
                }
            }
    private function hasUserWriteApproval($user): bool
    {
        if (!count($this->write_approval)) {
            if ($this->isRootNode()) {
                return false;
            }
            return $this->parent->hasUserWriteApproval($user);
        $users = $this->write_approval['users'];
        foreach ($users as $listedUserPerm) {
            // now for contents, there is an expiry date defined.
            if (!empty($listedUserPerm['expiry']) && strtotime($listedUserPerm['expiry']) < strtotime('today')) {
                if ($this->isRootNode()) {
                    return false;
                }
                return $this->parent->hasUserWriteApproval($user);
            }
            if ($listedUserPerm['id'] == $user->id) {
        if ($this->isRootNode()) {
            return false;
        }
        return $this->parent->hasUserWriteApproval($user);
    }

    /**
     * @param mixed $user the user to validate
     *
     * @return bool true if the user may read this instance in sequential progression
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    private function canReadSequential($user): bool
    {
Ron Lucke's avatar
Ron Lucke committed
        $unit = $this->findUnit();
        if (!$unit->config['sequential_progression']) {
            return true;
        }

        return $this->previousProgressAchieved($user);
    }

Ron Lucke's avatar
Ron Lucke committed
    /**
     * @return bool true if the user may read this instance in time interval
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    private function releasedForReaders(StructuralElement $element): bool
    {
        $released = false;
        if (!$element->release_date || $element->release_date <= time()) {
            $released = true;
        }

        if ($element->withdraw_date && $element->withdraw_date <= time()) {
            $released = false;
        }

        $parent_released = true;
        if (!$element->isRootNode()) {
            $parent_released = $this->releasedForReaders($element->parent);
        }

        return $released && $parent_released;
    }

    /**
     * @param mixed $user the user to validate
     *
     * @return bool true if the user has achieved previous elements
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    private function previousProgressAchieved($user): bool
    {
        $elements = $this->findCoursewareElements($user);

        foreach ($elements as $element) {
            // found me in depth-first order
            // so everything before me was fine and we're done
            if ($element->id == $this->id) {
                break;
            }

            if (!$element->hasBeenAchieved($user)) {
                return false;
            }
        }

        return true;
    }

    private function findCoursewareElements($user): array
    {
        $root = $this->getCourseware($this->range_id, $this->range_type);
        $elements = array_merge([$root], $root->findDescendants($user));

        return $elements;
    }

    private function hasBeenAchieved($user): bool
    {
        foreach ($this->containers as $container) {
            foreach ($container->blocks as $block) {
                /** @var ?UserProgress $progress */
Ron Lucke's avatar
Ron Lucke committed
                $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [$user->id, $block->id]);

                if (!$progress || $progress->grade != 1) {
                    return false;
    }

    /**
     * Returns all projects of a user and a given purpose.
     *
     * @param string $userId  the ID of the user
     * @param string $purpose a string containing the purpose of the projects
     *
     * @return StructuralElement[] a list of projects
     */
    public static function findProjects(string $userId, string $purpose = 'all'): array
    {
        $root = self::getCoursewareUser($userId);
        if ('all' == $purpose) {
            return self::findBySQL('range_id = ? AND parent_id = ? ORDER BY position ASC', [$userId, $root->id]);
        } else {
Ron Lucke's avatar
Ron Lucke committed
            return self::findBySQL('range_id = ? AND parent_id = ? AND purpose = ? ORDER BY position ASC', [
                $userId,
                $root->id,
                $purpose,
            ]);
        }
    }

    /**
     * Return the number of children of this instance.
     *
     * @return int the number of children
     */
    public function countChildren(): int
    {
        return self::countBySQL('parent_id= ? ', [$this->id]);
    }

    /**
     * Returns the list of all ancestors traversing up to the root.
     *
     * @return array a list of all ancestors of this instance up to the root
     */
    public function findAncestors(): array
    {
        $ancestors = [];

        if ($this->parent) {
            $ancestors[] = $this->parent;
            $ancestors = array_merge($this->parent->findAncestors(), $ancestors);
        }

        return $ancestors;
    }

    public function findUnit()
    {
        if ($this->isRootNode()) {
            $root = $this;
        } else {
            $root = $this->findAncestors()[0];
        }

        return Unit::findOneBySQL('structural_element_id = ?', [$root->id]);
     * Returns the list of all descendants of this instance in depth-first search order.
     *
     * @param ?User  $user  the user whose bookmarked structural elements will be returned
     * @return StructuralElement[] a list of all descendants
    public function findDescendants(User $user = null)
    {
        $descendants = [];
        foreach ($this->children as $child) {
            if ($user === null || $child->canRead($user)) {
                $descendants[] = $child;
                $descendants = array_merge($descendants, $child->findDescendants($user));
            }
        }

        return $descendants;
    }

    /**
     * Creates a new and empty courseware instance for a given range.
     *
     * @param string $rangeId   the ID of the range
     * @param string $rangeType the type of the range
     *
     * @return Instance the created courseware instance
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     */
    public static function createEmptyCourseware(string $rangeId, string $rangeType): Instance
    {
        if ('user' == $rangeType) {
            $user = \User::find($rangeId);
        } else {
            /** @var ?\Course $course */
            $course = \Course::find($rangeId);
            /** @var ?\User $user */
            $user = \User::find($GLOBALS['user']->id); //must be dozent
            if ('dozent' != $course->getParticipantStatus($user->id)) {
                $coursemembers = $course->getMembersWithStatus('dozent'); //get studip perm
                $user = $coursemembers[0]->user;
            }
        }

        $struct = self::build([
            'parent_id' => null,
            'range_id' => $rangeId,
            'range_type' => $rangeType,
            'owner_id' => $user->id,
            'editor_id' => $user->id,
            'title' => _('neue Seite'),
        ]);

        $struct->store();

        return new Instance($struct);
    }

    /**
     * Counts and returns the number of containers in this structural element.
     *
     * @return int the number of containers of this structural element
     */
    public function countContainers(): int
    {
        return Container::countBySql('structural_element_id = ?', [$this->id]);
    }

    /**
     * Returns all structural elements that a user bookmarked in a range.
     *
     * @param User   $user  the user whose bookmarked structural elements will be returned
     * @param \Range $range the range in which the user bookmarked structural elements
     *
     * @return array a list of bookmarked structural elements
     */
    public static function findUsersBookmarksByRange(User $user, \Range $range): array
    {
        if (!in_array($range->getRangeType(), ['course', 'user'])) {
            throw new \InvalidArgumentException();
        }

        $sql = <<<'SQL'
            SELECT s.* FROM cw_structural_elements s
            JOIN cw_bookmarks b
            ON s.id = b.element_id
            WHERE s.range_id = ? AND s.range_type = ? AND b.user_id = ?
            ORDER BY b.chdate DESC
SQL;
        $params = [$range->getRangeId(), $range->getRangeType(), $user->id];

Ron Lucke's avatar
Ron Lucke committed
        return \DBManager::get()->fetchAll($sql, $params, StructuralElement::class . '::buildExisting');
    }

    /**
     * Returns the URL of the image associated to this structural element.
     *
     * @return string the image URL, if it exists; an empty string otherwise
     */
    public function getImageUrl()
    {
        return $this->image ? $this->image->getDownloadURL() : null;
    }

    /**
     * Copies this instance into another course oder users contents.
     *
     * @param User              $user   this user will be the owner of the copy
     * @param Range $parent the target where to copy this instance
     *
     * @return StructuralElement the copy of this instance
     */
    public function copyToRange(User $user, string $rangeId, string $rangeType, string $purpose = ''): StructuralElement
    {
        $element = self::build([
            'parent_id' => null,
            'range_id' => $rangeId,
            'range_type' => $rangeType,
            'owner_id' => $user->id,
            'editor_id' => $user->id,
            'edit_blocker_id' => null,
            'title' => $this->title,
            'purpose' => $purpose ?: $this->purpose,
            'position' => 0,
            'payload' => $this->payload,
        ]);

        $element->store();

        $file_ref_id = $this->copyImage($user, $element);
        $element->image_id = $file_ref_id;
        $element->store();

        $this->copyContainers($user, $element);

        $this->copyChildren($user, $element, $purpose);

        return $element;
    }

    /**
     * Copies this instance as a child into another structural element.
     *
     * @param User              $user   this user will be the owner of the copy
     * @param StructuralElement $parent the target where to copy this instance
     *
     * @return StructuralElement the copy of this instance
     */
Ron Lucke's avatar
Ron Lucke committed
    public function copy(User $user, StructuralElement $parent, string $purpose = ''): StructuralElement
Ron Lucke's avatar
Ron Lucke committed
        $ancestorIds = array_column($parent->findAncestors(), 'id');
        $ancestorIds[] = $parent->id;
        if (in_array($this->id, $ancestorIds)) {
            throw new \InvalidArgumentException('Cannot copy into descendants.');
        }

        $file_ref_id = $this->copyImage($user, $parent);

        $element = self::build([
            'parent_id' => $parent->id,
            'range_id' => $parent->range_id,
            'range_type' => $parent->range_type,
            'owner_id' => $user->id,
            'editor_id' => $user->id,
            'edit_blocker_id' => null,
            'title' => $this->title,
Ron Lucke's avatar
Ron Lucke committed
            'purpose' => empty($purpose) ? $this->purpose : $purpose,
            'position' => $parent->countChildren(),
            'payload' => $this->payload,
            'image_id' => $file_ref_id,
            'read_approval' => $parent->read_approval,
            'write_approval' => $parent->write_approval
        $this->copyContainers($user, $element);
        $this->copyChildren($user, $element, $purpose);
Ron Lucke's avatar
Ron Lucke committed
    private function copyImage(User $user, StructuralElement $parent) : ?string
Ron Lucke's avatar
Ron Lucke committed
    {
        $file_ref_id = null;

        /** @var ?\FileRef $original_file_ref */
        $original_file_ref = \FileRef::find($this->image_id);
        if ($original_file_ref) {
            $instance = new Instance($this->getCourseware($parent->range_id, $parent->range_type));
            $folder = \Courseware\Filesystem\PublicFolder::findOrCreateTopFolder($instance);
            /** @var \FileRef $file_ref */
            $file_ref = \FileManager::copyFile($original_file_ref->getFileType(), $folder, $user);
            $file_ref_id = $file_ref->id;
        }

        return $file_ref_id;
    }

    public function merge(User $user, StructuralElement $target): StructuralElement
    {
        // merge with target
        if (!$target->image_id) {
            $target->image_id = $this->copyImage($user, $target);
Ron Lucke's avatar
Ron Lucke committed
        }

        if ($target->title === 'neue Seite' || $target->title === 'New page') {
            $target->title = $this->title;
        }

        if (!$target->purpose) {
            $target->purpose = $this->purpose;
        }

        if (!$target->payload['color']) {
            $target->payload['color'] = $this->payload['color'];
        }

        if (!$target->payload['description']) {
            $target->payload['description'] = $this->payload['description'];
        }

        if (!$target->payload['license_type']) {
            $target->payload['license_type'] = $this->payload['license_type'];
        }

        if (!$target->payload['required_time']) {
            $target->payload['required_time'] = $this->payload['required_time'];
        }

        if (!$target->payload['difficulty_start']) {
            $target->payload['difficulty_start'] = $this->payload['difficulty_start'];
        }

        if (!$target->payload['difficulty_end']) {
            $target->payload['difficulty_end'] = $this->payload['difficulty_end'];
        }

        $target->store();

        // add Containers to target
        $this->copyContainers($user, $target);
Ron Lucke's avatar
Ron Lucke committed

        // copy Children
Ron Lucke's avatar
Ron Lucke committed

        return $this;
    }

    private function copyContainers(User $user, StructuralElement $newElement): void
    {
        foreach ($this->containers as $container) {
            $container->copy($user, $newElement);
        }
    }

Ron Lucke's avatar
Ron Lucke committed
    private function copyChildren(User $user, StructuralElement $newElement, string $purpose = ''): void
Ron Lucke's avatar
Ron Lucke committed
            $child->copy($user, $newElement, $purpose);
Ron Lucke's avatar
Ron Lucke committed

    public function link(User $user, StructuralElement $parent): StructuralElement
    {
        $element = self::build([
            'parent_id' => $parent->id,
            'is_link' => 1,
            'target_id' => $this->id,
            'range_id' => $parent->range_id,
            'range_type' => $parent->range_type,
            'owner_id' => $user->id,
            'editor_id' => $user->id,
            'edit_blocker_id' => null,
            'title' => $this->title,
            'purpose' => $this->purpose,
            'position' => $parent->countChildren(),
            'payload' => $this->payload,
            'read_approval' => $parent->read_approval,
            'write_approval' => $parent->write_approval
        ]);

        $element->store();

        $this->linkChildren($user, $element);

        return $element;
    }

    private function linkChildren(User $user, StructuralElement $newElement): void
    {
        $children = self::findBySQL('parent_id = ?', [$this->id]);

        foreach ($children as $child) {
            $child->link($user, $newElement);
        }
    }

Ron Lucke's avatar
Ron Lucke committed
    public function pdfExport($user, bool $with_children = false)
Ron Lucke's avatar
Ron Lucke committed
    {
        $doc = new \ExportPDF('P', 'mm', 'A4', true, 'UTF-8', false);
        $doc->setHeaderTitle(_('Courseware'));
        if ($this->course) {
            $doc->setHeaderTitle(sprintf(_('Courseware aus %s'), $this->course->name));
        }
        if ($this->user) {
            $doc->setHeaderTitle(sprintf(_('Courseware von %s'), $this->user->getFullname()));
        }

Ron Lucke's avatar
Ron Lucke committed
        if (!self::canVisit($user)) {
Ron Lucke's avatar
Ron Lucke committed
            $doc->addPage();
Ron Lucke's avatar
Ron Lucke committed
            $doc->addContent(_('Diese Seite steht Ihnen nicht zur Verfügung!'));

            return $doc;
        }

Ron Lucke's avatar
Ron Lucke committed
        $this->getElementPdfExport(0, $with_children, $user, $doc);

        if ($with_children) {
            $doc->addTOCPage();
            $doc->SetFont('helvetica', 'B', 16);
            $doc->MultiCell(0, 0, _('Inhaltsverzeichnis'), 0, 'C', 0, 1, '', '', true, 0);
            $doc->Ln();
            $doc->SetFont('helvetica', '', 12);
            $doc->addTOC(1, 'helvetica', '.', _('Inhaltsverzeichnis'), 'B', array(0,0,0));
            $doc->endTOCPage();
        }

Ron Lucke's avatar
Ron Lucke committed

        return $doc;
    }