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
* @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',
];
$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',
];
$config['belongs_to']['editor'] = [
'class_name' => User::class,
'foreign_key' => 'editor_id',
];
$config['belongs_to']['edit_blocker'] = [
'class_name' => User::class,
'foreign_key' => 'edit_blocker_id',
];
$config['has_one']['image'] = [
'class_name' => \FileRef::class,
'foreign_key' => 'image_id',
'on_delete' => 'delete',
'on_store' => 'store',
];
$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',
];
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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;
}
/**
* @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);
$hasEditingPermission = $this->hasEditingPermission($user);
if ($this->isTask()) {
$task = $this->task;
if (!$task) {
$task = $this->findParentTask();
if (!$task) {
return false;
}
}
if ($hasEditingPermission) {
return false;
}
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) {
return true;
}
return $this->hasReadApproval($user);
$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;
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;
}
$task = $this->task;
if (!$task) {
$task = $this->findParentTask();
if (!$task) {
return false;
}
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.');
}
}
/**
* @param \User|\Seminar_User $user
*/
public function hasEditingPermission($user): bool
{

Jan-Hendrik Willms
committed
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',
private function hasReadApproval($user): bool
{
// this property is shared between all range types.
if ($this->read_approval['all']) {
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
// 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);
}
// find user in users
$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) {
}
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']) {
return true;
}
// 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;
}
}
}
return false;
}
private function hasUserWriteApproval($user): bool
{
if (!count($this->write_approval)) {
if ($this->isRootNode()) {
return false;
}
return $this->parent->hasUserWriteApproval($user);
}
// find user in users
$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
{
$unit = $this->findUnit();
if (!$unit->config['sequential_progression']) {
return true;
}
return $this->previousProgressAchieved($user);
}
/**
* @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 */
$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 {
return self::findBySQL('range_id = ? AND parent_id = ? AND purpose = ? ORDER BY position ASC', [
$userId,
$root->id,
$purpose,
]);
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
}
}
/**
* 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];
}

Jan-Hendrik Willms
committed
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));
}
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
}
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];
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;
}
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
807
808
/**
* 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
*/
public function copy(User $user, StructuralElement $parent, string $purpose = ''): StructuralElement
$ancestorIds = array_column($parent->findAncestors(), 'id');
$ancestorIds[] = $parent->id;
if (in_array($this->id, $ancestorIds)) {
throw new \InvalidArgumentException('Cannot copy into descendants.');
}

Elmar Ludwig
committed
$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,
'position' => $parent->countChildren(),
'payload' => $this->payload,
'image_id' => $file_ref_id,
'read_approval' => $parent->read_approval,
'write_approval' => $parent->write_approval
]);
$element->store();

Elmar Ludwig
committed
$this->copyContainers($user, $element);

Elmar Ludwig
committed
$this->copyChildren($user, $element, $purpose);
return $element;
}
private function copyImage(User $user, StructuralElement $parent) : ?string
{
$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) {

Elmar Ludwig
committed
$target->image_id = $this->copyImage($user, $target);
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
}
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

Elmar Ludwig
committed
$this->copyContainers($user, $target);

Elmar Ludwig
committed
$this->copyChildren($user, $target);
private function copyContainers(User $user, StructuralElement $newElement): void
{

Elmar Ludwig
committed
foreach ($this->containers as $container) {
$container->copy($user, $newElement);
}
}
private function copyChildren(User $user, StructuralElement $newElement, string $purpose = ''): void

Elmar Ludwig
committed
foreach ($this->children as $child) {
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);
}
}
{
$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()));
}
$doc->addContent(_('Diese Seite steht Ihnen nicht zur Verfügung!'));
return $doc;
}
$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();
}