From 6a4673b9ccbe821f4e531f53a66175168e798ad3 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 24 Jun 2026 11:28:21 +0200 Subject: [PATCH 001/333] Test: Show Deleted Users as Deleted See: https://mantis.ilias.de/view.php?id=47978 --- components/ILIAS/Test/src/Participants/Participant.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Test/src/Participants/Participant.php b/components/ILIAS/Test/src/Participants/Participant.php index 21bf4258b985..d51eaf4f4b93 100644 --- a/components/ILIAS/Test/src/Participants/Participant.php +++ b/components/ILIAS/Test/src/Participants/Participant.php @@ -253,12 +253,16 @@ public function getDisplayName(Language $language, bool $anonymous_test = false) return $language->txt('anonymous'); } + if ($this->login === '' && $this->firstname === '' && $this->lastname === '') { + return $language->txt('user_deleted'); + } + $display_name = ''; - if ($this->firstname) { + if ($this->firstname !== '') { $display_name .= $this->firstname . ' '; } - if ($this->lastname) { + if ($this->lastname !== '') { $display_name .= $this->lastname; } From 81ee5a848531c6f818917ce4c53d9083bf80ebb2 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 24 Jun 2026 11:29:16 +0200 Subject: [PATCH 002/333] Test: Retrieve Importname in Participant Repo --- .../ILIAS/Test/src/Participants/ParticipantRepository.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/ILIAS/Test/src/Participants/ParticipantRepository.php b/components/ILIAS/Test/src/Participants/ParticipantRepository.php index 553b99256877..ac3dda5140d0 100755 --- a/components/ILIAS/Test/src/Participants/ParticipantRepository.php +++ b/components/ILIAS/Test/src/Participants/ParticipantRepository.php @@ -362,6 +362,7 @@ private function getActiveParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, + ta.importname, ud.firstname, ud.lastname, ud.login, @@ -401,6 +402,7 @@ private function getInvitedParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, + ta.importname, ud.firstname, ud.lastname, ud.login, From 0923cd57a73cafffdc058c67eaff132aaa943af7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 24 Jun 2026 11:53:32 +0200 Subject: [PATCH 003/333] Test: Retrieve Importname in Participant Repo This reverts commit b7dc09a5961f960299d1e9872f43392811aaf635. It should not have been picked to ILIAS 11. --- .../ILIAS/Test/src/Participants/ParticipantRepository.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ILIAS/Test/src/Participants/ParticipantRepository.php b/components/ILIAS/Test/src/Participants/ParticipantRepository.php index ac3dda5140d0..553b99256877 100755 --- a/components/ILIAS/Test/src/Participants/ParticipantRepository.php +++ b/components/ILIAS/Test/src/Participants/ParticipantRepository.php @@ -362,7 +362,6 @@ private function getActiveParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, - ta.importname, ud.firstname, ud.lastname, ud.login, @@ -402,7 +401,6 @@ private function getInvitedParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, - ta.importname, ud.firstname, ud.lastname, ud.login, From f62fa2cbd25050982bc40a25f797f54e637ae468 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 25 Jun 2026 08:46:30 +0200 Subject: [PATCH 004/333] Test: Set Original Id on Test Copy See: https://mantis.ilias.de/view.php?id=47983 --- .../class.ilTestFixedQuestionSetConfig.php | 19 ++++++++++----- .../classes/class.assQuestion.php | 23 +++++++++++++++++-- .../tests/assQuestionTest.php | 2 +- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php b/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php index 2f429f9fcb02..92db879f27f7 100755 --- a/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php +++ b/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php @@ -71,12 +71,19 @@ public function cloneQuestionSetRelatedData(ilObjTest $clone_test_obj): void foreach ($this->test_obj->questions as $key => $question_id) { $question_orig = assQuestion::instantiateQuestion($question_id); - $clone_test_obj->questions[$key] = $question_orig->duplicate(true, '', '', -1, $clone_test_obj->getId()); - - $original_id = $this->questionrepository->getForQuestionId($question_id)->getOriginalId(); - - $question_clone = assQuestion::instantiateQuestion($clone_test_obj->questions[$key]); - $question_clone->saveToDb($original_id); + $clone_test_obj->questions[$key] = $question_orig->duplicate( + false, + '', + '', + -1, + $clone_test_obj->getId() + ); + $question_clone = assQuestion::instantiateQuestion( + $clone_test_obj->questions[$key] + ); + $question_clone->updateOriginalId( + $question_orig->getOriginalId() + ); // Save the mapping of old question id <-> new question id // This will be used in class.ilObjCourse::cloneDependencies to copy learning objectives diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 8a8cf44b2daf..7f3e20ad0df0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -1228,8 +1228,27 @@ public function saveQuestionDataToDb(?int $original_id = null): void ]); } + /** + * + * @deprecated This is a momentary helper function to update the original id, + * when cloning a test. Do not use anywhere else. 2026-06-25, sk + */ + public function updateOriginalId(?int $original_id = null): void + { + $this->original_id = $original_id; + $this->db->update( + 'qpl_questions', + [ + 'original_id' => [ilDBConstants::T_INTEGER, $original_id], + ], + [ + 'question_id' => [ilDBConstants::T_INTEGER, $this->getId()] + ] + ); + } + public function duplicate( - bool $for_test = true, + bool $set_original_id = true, string $title = '', string $author = '', int $owner = -1, @@ -1256,7 +1275,7 @@ public function duplicate( if ($owner) { $clone->setOwner($owner); } - if ($for_test) { + if ($set_original_id) { $clone->saveToDb($this->id); } else { $clone->saveToDb(); diff --git a/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php b/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php index 96e8a43ff484..99ecf3210d54 100644 --- a/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php +++ b/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php @@ -69,7 +69,7 @@ public function getQuestionType(): string return ''; } - public function duplicate(bool $for_test = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int + public function duplicate(bool $set_original_id = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int { return 0; } From 9a513ff2ba80f9ffa1cdfd7848a3f167c889bc42 Mon Sep 17 00:00:00 2001 From: selyesa Date: Thu, 25 Jun 2026 13:48:08 +0200 Subject: [PATCH 005/333] Update PRIVACY.md --- components/ILIAS/AdvancedMetaData/PRIVACY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/AdvancedMetaData/PRIVACY.md b/components/ILIAS/AdvancedMetaData/PRIVACY.md index 27292bb1bb08..aef68ffaa631 100644 --- a/components/ILIAS/AdvancedMetaData/PRIVACY.md +++ b/components/ILIAS/AdvancedMetaData/PRIVACY.md @@ -1,4 +1,4 @@ -# AdvancedMetaData +# AdvancedMetaData Privacy Disclaimer: This documentation does not guarantee completeness or correctness. Please report any missing or incorrect information using the ILIAS issue tracker or contribute a fix via Pull Request (docs/development/contributing.md#pull-request-to-the-repositories). ## General Information Advanced Metadata is labeled Custom Metadata in the user interface. @@ -23,4 +23,4 @@ The Advanced Metadata itself does not present any personal data. The Advanced Metadata itself does not store or delete any personal data. ## Data being exported -The Advanced Metadata module itself does not export any personal data. \ No newline at end of file +The Advanced Metadata module itself does not export any personal data. From 351f06b127e1a830686182d8a800830ecd19a0af Mon Sep 17 00:00:00 2001 From: selyesa Date: Thu, 25 Jun 2026 14:00:00 +0200 Subject: [PATCH 006/333] Create PRIVACY.md --- components/ILIAS/ADT/PRIVACY.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 components/ILIAS/ADT/PRIVACY.md diff --git a/components/ILIAS/ADT/PRIVACY.md b/components/ILIAS/ADT/PRIVACY.md new file mode 100644 index 000000000000..d95ba5b7390f --- /dev/null +++ b/components/ILIAS/ADT/PRIVACY.md @@ -0,0 +1,23 @@ +# ADT Privacy +Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via Pull Request (docs/development/contributing.md#pull-request-to-the-repositories). +## General Information +ADT stands for Advanced Definition Type. +[Please provide a brief description of the component here] + + +## Integrated Services +The Advanced Definition Type component employs the following components, please consult the respective privacy.mds: +- [Please list all components here] + + +## Data being stored +The Advanced Definition Type component itself does not store any personal data. + +## Data being presented +The Advanced Definition Type component itself does not present any personal data. + +## Data being deleted +The Advanced Definition Type component itself does not store or delete any personal data. + +## Data being exported +The Advanced Definition Type component itself does not export any personal data. From 4ca7cef30a470b2939fdc87ce18190e342b66677 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 20 Jan 2026 13:33:46 +0100 Subject: [PATCH 007/333] BookingPool: Remove reservations on pool deletion See: https://mantis.ilias.de/view.php?id=21705 Deleting or trashing a booking pool left reservations and calendar entries behind. The Booking Manager module now listens for ILIAS object `delete` and `toTrash` events; `ObjectEvent::handleDeletion` resolves each affected pool and calls `ilBookingObject::deleteReservationsAndCalEntries` for every booking object. --- .../BookingManager/Objects/ObjectEvent.php | 48 +++++++++++++++++++ .../Service/class.InternalDomainService.php | 6 +++ ...class.ilBookingManagerAppEventListener.php | 8 ++++ components/ILIAS/BookingManager/module.xml | 3 +- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 components/ILIAS/BookingManager/Objects/ObjectEvent.php diff --git a/components/ILIAS/BookingManager/Objects/ObjectEvent.php b/components/ILIAS/BookingManager/Objects/ObjectEvent.php new file mode 100644 index 000000000000..f51b65947bf1 --- /dev/null +++ b/components/ILIAS/BookingManager/Objects/ObjectEvent.php @@ -0,0 +1,48 @@ +getId(); + } catch (ilObjectTypeMismatchException) { + continue; + } + + foreach (ilBookingObject::getList($pool_id) as $booking_object) { + $booking_object_id = $booking_object['booking_object_id'] ?? null; + if ($booking_object_id === null) { + continue; + } + + (new ilBookingObject($booking_object_id))->deleteReservationsAndCalEntries($booking_object_id); + } + } + } +} diff --git a/components/ILIAS/BookingManager/Service/class.InternalDomainService.php b/components/ILIAS/BookingManager/Service/class.InternalDomainService.php index 2b59b3ad4a11..ab2fb7defe18 100755 --- a/components/ILIAS/BookingManager/Service/class.InternalDomainService.php +++ b/components/ILIAS/BookingManager/Service/class.InternalDomainService.php @@ -20,6 +20,7 @@ namespace ILIAS\BookingManager; +use ILIAS\BookingManager\Objects\ObjectEvent; use ILIAS\DI\Container; use ILIAS\Repository\GlobalDICDomainServices; use ILIAS\BookingManager\BookingProcess\BookingProcessManager; @@ -128,6 +129,11 @@ public function userEvent(): UserEvent return self::$instances["user_event"] ??= new UserEvent($this); } + public function objectEvent(): ObjectEvent + { + return new ObjectEvent(); + } + public function bookingSettings(): SettingsManager { return self::$instances["settings"] ??= new SettingsManager( diff --git a/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php b/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php index de3c29c33729..39344fef5569 100644 --- a/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php +++ b/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php @@ -42,6 +42,14 @@ public static function handleEvent( break; } break; + case "components/ILIAS/ILIASObject": + switch ($a_event) { + case "toTrash": + case "delete": + $DIC->bookingManager()->internal()->domain()->objectEvent()->handleDeletion([$a_parameter["ref_id"]]); + break; + } + break; } } } diff --git a/components/ILIAS/BookingManager/module.xml b/components/ILIAS/BookingManager/module.xml index a5f775ba7db2..a981b186622d 100755 --- a/components/ILIAS/BookingManager/module.xml +++ b/components/ILIAS/BookingManager/module.xml @@ -5,7 +5,7 @@ cat crs @@ -17,6 +17,7 @@ + From 3e76fa31ac56e7651b1350431db3aee039e42101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Jou=C3=9Fen?= Date: Wed, 3 Dec 2025 12:20:30 +0100 Subject: [PATCH 008/333] T&A Streamline usage of getAdditionalTableName to never expect a array return value --- .../classes/class.assQuestion.php | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 7f3e20ad0df0..5fe7dd438d73 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -816,20 +816,14 @@ public function deleteAnswers(int $question_id): void public function deleteAdditionalTableData(int $question_id): void { - $additional_table_name = $this->getAdditionalTableName(); + $table_name = $this->getAdditionalTableName(); - if (!is_array($additional_table_name)) { - $additional_table_name = [$additional_table_name]; - } - - foreach ($additional_table_name as $table) { - if (strlen($table)) { - $this->db->manipulateF( - "DELETE FROM $table WHERE question_fi = %s", - ['integer'], - [$question_id] - ); - } + if (strlen($table_name)) { + $this->db->manipulateF( + "DELETE FROM $table_name WHERE question_fi = %s", + [ilDBConstants::T_INTEGER], + [$question_id] + ); } } From 097b2a32b4aeab4b72795589150180e3a7d0dfa3 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 20 Jan 2026 08:44:23 +0100 Subject: [PATCH 009/333] BookingPool: Render Pool Without Booking Info See: https://mantis.ilias.de/view.php?id=46909 `ProcessUtilGUI::handleBookingSuccess()` only redirects to `displayPostInfo` when the bookable object has post text or a booking info file; otherwise, after the success message (and async return-URL handling when needed), it redirects to `ilObjBookingPoolGUI::render` so the pool view opens instead of stopping on an empty info step. Added an optional custom success message, split redirect logic into helpers, and aligned with/without-schedule booking flows (including `void` `confirmedBooking()` and translated text for multiple bookings). --- .../BookingProcess/class.ProcessUtilGUI.php | 70 ++++++++++++------- .../class.ilBookingProcessWithScheduleGUI.php | 11 +-- ...ass.ilBookingProcessWithoutScheduleGUI.php | 33 +++++---- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php index 584281ab09f1..1ddab4e7782f 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php @@ -37,6 +37,8 @@ class ProcessUtilGUI protected InternalGUIService $gui; protected InternalDomainService $domain; protected object $parent_gui; + protected \ilLanguage $lng; + protected \ilGlobalTemplateInterface $tpl; public function __construct( InternalDomainService $domain_service, @@ -53,6 +55,8 @@ public function __construct( $this->ctrl = $this->gui->ctrl(); $this->request = $this->gui->standardRequest(); $this->objects_manager = $domain_service->objects($pool->getId()); + $this->lng = $domain_service->lng(); + $this->tpl = $this->gui->mainTemplate(); } // Back to parent @@ -126,38 +130,50 @@ protected function checkPermission(string $a_perm) : void public function handleBookingSuccess( int $a_obj_id, string $post_info_cmd, - ?array $a_rsv_ids = null + ?array $a_rsv_ids = null, + ?string $message = null ): void { - $this->log->debug("handleBookingSuccess"); - $main_tpl = $this->gui->mainTemplate(); - $ctrl = $this->gui->ctrl(); - $lng = $this->domain->lng(); - $request = $this->gui->standardRequest(); - - $main_tpl->setOnScreenMessage('success', $lng->txt('book_reservation_confirmed'), true); + $this->log->debug('handleBookingSuccess'); + $this->tpl->setOnScreenMessage( + 'success', + $message ?: $this->lng->txt('book_reservation_confirmed'), + true + ); + + $this->redirectToReturnCmdIfAsynchronous(); + $this->redirectToBookingInformationIfAvailable($a_obj_id, $post_info_cmd, $a_rsv_ids); + $this->redirectToRender(); + } - // show post booking information? + public function redirectToBookingInformationIfAvailable(int $a_obj_id, string $post_info_cmd, ?array $a_rsv_ids = null): void + { $obj = new \ilBookingObject($a_obj_id); - $pfile = $this->objects_manager->getBookingInfoFilename($a_obj_id); - $ptext = $obj->getPostText(); + if (trim($obj->getPostText()) === '' && $this->objects_manager->getBookingInfoFilename($a_obj_id) === '') { + return; + } - if (trim($ptext) || $pfile) { - if (count($a_rsv_ids)) { - $ctrl->setParameter( - $this->parent_gui, - 'rsv_ids', - implode(";", $a_rsv_ids) - ); - } - $ctrl->redirect($this->parent_gui, $post_info_cmd); - } else { - if ($ctrl->isAsynch()) { - $this->gui->send(""); - } else { - $this->back(); - } + $a_rsv_ids ??= []; + if ($a_rsv_ids !== []) { + $this->ctrl->setParameter($this->parent_gui, 'rsv_ids', implode(';', $a_rsv_ids)); } + $this->ctrl->redirect($this->parent_gui, $post_info_cmd); + } + + private function redirectToReturnCmdIfAsynchronous(): void + { + if (!$this->ctrl->isAsynch()) { + return; + } + + $this->gui->send( + "" + ); + } + + private function redirectToRender(): void + { + $this->ctrl->saveParameterByClass(\ilObjBookingPoolGUI::class, 'ref_id'); + $this->ctrl->redirectByClass(\ilObjBookingPoolGUI::class, 'render'); } /** diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php index 79fe039b102f..696d42e5edc0 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php @@ -514,12 +514,13 @@ protected function bookAvailableItems(?int $recurrence = null, ?ilDateTime $unti $until, $message ); - if (count($booked) > 0) { - $this->util_gui->handleBookingSuccess($obj_id, "displayPostInfo", $booked); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); - $this->util_gui->back(); + if ($booked !== []) { + $this->util_gui->handleBookingSuccess($obj_id, 'displayPostInfo', $booked); + return; } + + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); + $this->util_gui->back(); } protected function getBookAvailableTarget( diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php index 384cb89f4dbc..1922cdee8e80 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php @@ -285,12 +285,12 @@ public function saveMultipleBookings(): void { $participants = $this->book_request->getParticipants(); $object_id = $this->book_request->getObjectId(); - if (count($participants) > 0 && $object_id > 0) { + if ($participants !== [] && $object_id > 0) { $this->book_obj_id = $object_id; } else { $this->util_gui->back(); } - $rsv_ids = array(); + $rsv_ids = []; foreach ($participants as $id) { if (!$this->access->canManageReservationForUser($this->pool->getRefId(), $id)) { $this->showNoPermission(); @@ -305,24 +305,29 @@ public function saveMultipleBookings(): void ); } - if (count($rsv_ids)) { - $this->tpl->setOnScreenMessage('success', "booking_multiple_succesfully"); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed_overbooked'), true); + if ($rsv_ids !== []) { + $this->util_gui->handleBookingSuccess( + $object_id, + 'displayPostInfo', + $rsv_ids, + $this->lng->txt('booking_multiple_succesfully') + ); + return; } + + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed_overbooked'), true); $this->util_gui->back(); } - // // Step 2: Confirmation // // Book object - either of type or specific - for given dates - public function confirmedBooking($message = ""): bool + public function confirmedBooking($message = ''): void { $success = false; - $rsv_ids = array(); + $rsv_ids = []; if ($this->book_obj_id > 0) { $object_id = $this->book_obj_id; @@ -349,12 +354,12 @@ public function confirmedBooking($message = ""): bool } if ($success) { - $this->util_gui->handleBookingSuccess($success, "displayPostInfo", $rsv_ids); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); - $this->ctrl->redirect($this, 'book'); + $this->util_gui->handleBookingSuccess($success, 'displayPostInfo', $rsv_ids); + return; } - return true; + + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); + $this->ctrl->redirect($this, 'book'); } public function displayPostInfo(): void From 829fe9cf26a6318d65cb83ed46273bef466541e5 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 16 Jan 2026 12:14:27 +0100 Subject: [PATCH 010/333] BookingPool: Allow Pool Filter in Course Resources See: https://mantis.ilias.de/view.php?id=46889 From the course resources tab, `ilBookingGatewayGUI` passed the course object id as reservation context, which pre-filtered the list and hid the booking pool filter. For courses, `context_obj_id` is now `null`, so `ilBookingReservationsGUI` does not restrict by context and the pool filter can be used. The constructor accepts `?int $context_obj_id` instead of defaulting to `0`. --- .../BookingService/class.ilBookingGatewayGUI.php | 6 +++++- .../Reservations/class.ilBookingReservationsGUI.php | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php b/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php index c6ad0f1f3255..ee95ae9ed325 100755 --- a/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php +++ b/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php @@ -174,7 +174,11 @@ public function executeCommand(): void case "ilbookingreservationsgui": $this->showPoolSelector("ilbookingreservationsgui"); $this->setSubTabs("reservations"); - $res_gui = new ilBookingReservationsGUI($this->pool, $this->help, $this->obj_id); + $res_gui = new ilBookingReservationsGUI( + $this->pool, + $this->help, + ilObject::_lookupType($this->obj_id) !== 'crs' ? $this->obj_id : null + ); $this->ctrl->forwardCommand($res_gui); break; diff --git a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php index a633795ca9cb..adacb94570a2 100755 --- a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php +++ b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php @@ -40,7 +40,7 @@ class ilBookingReservationsGUI protected array $raw_post_data; protected StandardGUIRequest $book_request; protected ilBookingHelpAdapter $help; - protected int $context_obj_id; + protected ?int $context_obj_id; protected ilCtrl $ctrl; protected ilGlobalTemplateInterface $tpl; protected ilLanguage $lng; @@ -53,7 +53,7 @@ class ilBookingReservationsGUI protected int $booked_user; protected ilUIService $ui_service; - public function __construct(ilObjBookingPool $pool, ilBookingHelpAdapter $help, int $context_obj_id = 0) + public function __construct(ilObjBookingPool $pool, ilBookingHelpAdapter $help, ?int $context_obj_id = null) { global $DIC; @@ -202,7 +202,7 @@ public function log(): void $this->access->canManageAllReservations($this->ref_id) || $this->pool->hasPublicLog(), $this->ui_service->filter()->getData($bookings_table->getFilter()) ?? [], null, - $this->context_obj_id > 0 ? [$this->context_obj_id] : null + $this->context_obj_id !== null ? [$this->context_obj_id] : null ); $reservations_table->getExportMode() > 0 && $reservations_table->exportData($reservations_table->getExportMode(), true); } From b49dc7263b59f9cce7bddc2991849482764a5fb4 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 17 Apr 2026 08:18:26 +0200 Subject: [PATCH 011/333] BookingPool: Disambiguate Info Stakeholders See: https://mantis.ilias.de/view.php?id=47260 Both `ilBookBookingInfoStakeholder` and `ilBookObjectInfoStakeholder` inherited the same `getConsumerNameForPresentation()` label from the parent, which made the two resource consumers hard to tell apart. Each class now appends a suffix (`/BookingInfo` or `/ObjectInfo`) to the parent name. --- .../Objects/class.ilBookBookingInfoStakeholder.php | 5 +++++ .../Objects/class.ilBookObjectInfoStakeholder.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php b/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php index 54413bf58529..bae67fbdfaf3 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php @@ -33,6 +33,11 @@ public function getOwnerOfNewResources(): int return $this->default_owner; } + public function getConsumerNameForPresentation(): string + { + return parent::getConsumerNameForPresentation() . '/BookingInfo'; + } + public function canBeAccessedByCurrentUser(ResourceIdentification $identification): bool { global $DIC; diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php b/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php index bcc4b6fac7d3..3b4efb35302f 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php @@ -34,6 +34,11 @@ public function getOwnerOfNewResources(): int return $this->default_owner; } + public function getConsumerNameForPresentation(): string + { + return parent::getConsumerNameForPresentation() . '/ObjectInfo'; + } + public function canBeAccessedByCurrentUser(ResourceIdentification $identification): bool { global $DIC; From a290618c4c38429a9c9a57c0cc8b402eebd0b9b7 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 27 Jan 2026 11:11:44 +0100 Subject: [PATCH 012/333] fix: missing logger dependency --- .../ILIAS/News/classes/class.ilNewsForContextBlockGUI.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php index 2d698667fb70..98149d3c2ab3 100755 --- a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php +++ b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php @@ -51,6 +51,7 @@ class ilNewsForContextBlockGUI extends ilBlockGUI protected ilHelpGUI $help; protected ilSetting $settings; protected ilTabsGUI $tabs; + protected ilLogger $logger; protected StandardGUIRequest $std_request; protected InternalDomainService $domain; @@ -70,6 +71,7 @@ public function __construct() $this->help = $DIC["ilHelp"]; $this->settings = $DIC->settings(); $this->tabs = $DIC->tabs(); + $this->logger = $DIC->logger()->news(); $locator = $DIC->news()->internal(); $this->std_request = $locator->gui()->standardRequest(); From 6bddf9499dae59ba58f8c6b99f8d57e64c9ac919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Jou=C3=9Fen?= Date: Fri, 26 Jun 2026 12:28:36 +0200 Subject: [PATCH 013/333] News: Fix duplicated property declaration in ContextBlockGUI --- components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php | 1 - 1 file changed, 1 deletion(-) diff --git a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php index 98149d3c2ab3..da2d994301f9 100755 --- a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php +++ b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php @@ -51,7 +51,6 @@ class ilNewsForContextBlockGUI extends ilBlockGUI protected ilHelpGUI $help; protected ilSetting $settings; protected ilTabsGUI $tabs; - protected ilLogger $logger; protected StandardGUIRequest $std_request; protected InternalDomainService $domain; From 9bec14a049dce3db64a39bf2185eab165f82753f Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 20 Feb 2026 12:53:46 +0100 Subject: [PATCH 014/333] BackgroundTasks: Add grp to folder-type whitelist See: https://mantis.ilias.de/view.php?id=42358 `ilCollectFilesJob` only treated `fold` and `crs` as folder-like objects, so selecting a group when collecting files skipped its contents. The job now recurses `grp` the same way, skips non-`ilObject` instances safely, and keeps single-file handling separate after folder traversal. --- .../classes/Jobs/class.ilCollectFilesJob.php | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php b/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php index 3dae62a64972..140b8fced1f6 100755 --- a/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php +++ b/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php @@ -97,21 +97,37 @@ public function run(array $input, Observer $observer): Value foreach ($object_ref_ids as $object_ref_id) { $object = ilObjectFactory::getInstanceByRefId($object_ref_id); + if (!$object instanceof ilObject) { + continue; + } + $object_type = $object->getType(); $object_name = $object->getTitle(); - $object_temp_dir = ""; // empty as content will be added in recurseFolder and getFileDirs + $object_temp_dir = ''; // empty as content will be added in recurseFolder and getFileDirs - if ($object_type === "fold" || $object_type === "crs") { + if (in_array($object_type, ['fold', 'crs', 'grp'], true)) { $num_recursions = 0; - $files_from_folder = $this->recurseFolder($object_ref_id, $object_name, $object_temp_dir, $num_recursions, $initiated_by_folder_action); + $files_from_folder = $this->recurseFolder( + $object_ref_id, + $object_name, + $object_temp_dir, + $num_recursions, + $initiated_by_folder_action + ); $files = array_merge($files, $files_from_folder); - } elseif (($object_type === "file") && (($file_dirs = $this->getFileDirs( - $object_ref_id, - $object_name, - $object_temp_dir - )) !== false)) { - $files[] = $file_dirs; + continue; } + + if ($object_type !== 'file') { + continue; + } + + $file_dirs = $this->getFileDirs($object_ref_id, $object_name, $object_temp_dir); + if (!is_array($file_dirs)) { + continue; + } + + $files[] = $file_dirs; } $this->logger->debug('Collected files:'); $this->logger->dump($files); From 241d08f47d8132a03871e9b976d1513de5aac3cf Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Wed, 24 Jun 2026 13:51:40 +0200 Subject: [PATCH 015/333] ItemGroup: Add migration for display fields See: https://mantis.ilias.de/view.php?id=31861 Item groups still stored presentation in legacy `hide_title` and `behaviour` columns after the new display model was introduced. `ilItemGroupDisplayMigration` maps those values to `display` and `toggleable_initially`, marks rows as migrated, and is registered on the ItemGroup setup `Agent` after `ilItemGroupDBUpdateSteps`. --- .../ItemGroup/classes/Setup/class.Agent.php | 7 + .../class.ilItemGroupDisplayMigration.php | 123 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php diff --git a/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php b/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php index 40a3967dc68d..2c3737c26f70 100755 --- a/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php +++ b/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php @@ -29,4 +29,11 @@ public function getUpdateObjective(?Setup\Config $config = null): Setup\Objectiv { return new \ilDatabaseUpdateStepsExecutedObjective(new ilItemGroupDBUpdateSteps()); } + + public function getMigrations(): array + { + return [ + new ilItemGroupDisplayMigration() + ]; + } } diff --git a/components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php b/components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php new file mode 100644 index 000000000000..f22fabe7cf1d --- /dev/null +++ b/components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php @@ -0,0 +1,123 @@ +db = $environment->getResource(Environment::RESOURCE_DATABASE); + } + + public function step(Environment $environment): void + { + $result = $this->db->queryF( + 'SELECT id, hide_title, behaviour FROM itgr_data WHERE hide_title <> %s AND behaviour <> %s LIMIT 1', + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [self::MIGRATED_MARKER, self::MIGRATED_MARKER] + ); + + $row = $this->db->fetchAssoc($result); + if ($row === null) { + return; + } + + [$display, $toggleable_initially] = $this->mapLegacyValues((int) $row['hide_title'], (int) $row['behaviour']); + + $this->db->update( + 'itgr_data', + [ + 'display' => [ilDBConstants::T_TEXT, $display], + 'toggleable_initially' => [ilDBConstants::T_TEXT, $toggleable_initially], + 'hide_title' => [ilDBConstants::T_INTEGER, self::MIGRATED_MARKER], + 'behaviour' => [ilDBConstants::T_INTEGER, self::MIGRATED_MARKER], + ], + [ + 'id' => [ilDBConstants::T_INTEGER, (int) $row['id']], + ] + ); + } + + public function getRemainingAmountOfSteps(): int + { + $result = $this->db->queryF( + 'SELECT COUNT(id) AS cnt FROM itgr_data WHERE hide_title <> %s AND behaviour <> %s', + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [self::MIGRATED_MARKER, self::MIGRATED_MARKER] + ); + + return (int) ($this->db->fetchObject($result)?->cnt ?? 0); + } + + /** + * @return array{0: string, 1: string} + */ + private function mapLegacyValues(int $hide_title, int $behaviour): array + { + return match (true) { + $hide_title === 1 => [ + ilItemGroupAR::DISPLAY_WITHOUT_TITLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_OPEN, + ], + $behaviour === ilItemGroupBehaviour::EXPANDABLE_CLOSED => [ + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_CLOSED, + ], + $behaviour === ilItemGroupBehaviour::EXPANDABLE_OPEN => [ + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_OPEN, + ], + default => [ + ilItemGroupAR::DISPLAY_WITH_TITLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_OPEN, + ] + }; + } +} From 1f31266bb291973b09a856e5fb7763b502ff8536 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Thu, 12 Mar 2026 08:03:33 +0100 Subject: [PATCH 016/333] Test: Full Width for Cloze Inputs in Scoring See: https://mantis.ilias.de/view.php?id=47413 Manual scoring cloze gaps render narrow text fields by default. Added a stylesheet rule for `.ilAssClozeTest` answer areas so `input[type=text]` spans the full row width, in both `_component_test.scss` and compiled `delos.css`. --- .../legacy/Modules/_component_test.scss | 12 +++++++++--- templates/default/delos.css | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/templates/default/070-components/legacy/Modules/_component_test.scss b/templates/default/070-components/legacy/Modules/_component_test.scss index ddb96549b42e..3d082718e745 100755 --- a/templates/default/070-components/legacy/Modules/_component_test.scss +++ b/templates/default/070-components/legacy/Modules/_component_test.scss @@ -172,9 +172,15 @@ $cons-scoring-bottom-fade-height: $il-padding-xxxlarge-vertical * 2; display: none; } - // fix for ordering question - .ilc_qanswer_Answer.solutionbox { - width: auto; + .ilc_qanswer_Answer { + // fix for ordering question + .solutionbox { + width: auto; + } + + .ilc_answers.answers.ilAssClozeTest div input[type=text] { + width: 100%; + } } } diff --git a/templates/default/delos.css b/templates/default/delos.css index 0b838ddd9024..9beac26ca5e9 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -16905,6 +16905,9 @@ div.ilc_Page.readonly textarea[disabled] { .c-consecutive-scoring__answer__body .ilc_qanswer_Answer.solutionbox { width: auto; } +.c-consecutive-scoring__answer__body .ilc_qanswer_Answer.ilc_answers.answers.ilAssClozeTest div input[type=text] { + width: 100%; +} .c-consecutive-scoring__answer__grade-card { width: 40%; border-left: 1px solid #dddddd; From a981ee45bd5f28b6ffc5b3363c9b80595c6cc965 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Mon, 16 Mar 2026 12:06:44 +0100 Subject: [PATCH 017/333] Test: Resize Mark Icon and Unify Summary Icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test player mark-question icon was 12×12px; it is now 28×28px in `_component_test_legacy.scss` and `delos.css`. In `QuestionsOfAttemptTable`, the marked column uses the standard checked/unchecked icons like the answered column instead of `marked.svg`. --- .../src/Questions/Presentation/QuestionsOfAttemptTable.php | 3 +-- .../070-components/legacy/Modules/_component_test_legacy.scss | 4 ++-- templates/default/delos.css | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php b/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php index 7ba58cbdaf2a..de061939dc99 100644 --- a/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php +++ b/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php @@ -139,7 +139,6 @@ protected function getColumns(): array $icon_factory = $this->ui_factory->symbol()->icon(); $icon_checked = $icon_factory->custom('assets/images/standard/icon_checked.svg', $this->lng->txt('yes')); $icon_unchecked = $icon_factory->custom('assets/images/standard/icon_unchecked.svg', $this->lng->txt('no')); - $icon_marked = $icon_factory->custom('assets/images/object/marked.svg', $this->lng->txt('tst_question_marked')); $columns = [ 'order' => $column_factory->number($this->lng->txt('tst_qst_order')), @@ -148,7 +147,7 @@ protected function getColumns(): array 'postponed' => $column_factory->boolean(ucfirst($this->lng->txt('postponed')), $this->lng->txt('yes'), ''), 'points' => $column_factory->number($this->lng->txt('tst_maximum_points'))->withUnit($this->lng->txt('points_short')), 'answered' => $column_factory->boolean($this->lng->txt('answered'), $icon_checked, $icon_unchecked), - 'marked' => $column_factory->boolean($this->lng->txt('tst_question_marker'), $icon_marked, ''), + 'marked' => $column_factory->boolean($this->lng->txt('tst_question_marker'), $icon_checked, $icon_unchecked), ]; $optional_columns = [ diff --git a/templates/default/070-components/legacy/Modules/_component_test_legacy.scss b/templates/default/070-components/legacy/Modules/_component_test_legacy.scss index fc89928f1838..5d0f70e7bd21 100755 --- a/templates/default/070-components/legacy/Modules/_component_test_legacy.scss +++ b/templates/default/070-components/legacy/Modules/_component_test_legacy.scss @@ -538,8 +538,8 @@ td.ilc_Page { } .ilTestMarkQuestionIcon { - width: 12px; - height: 12px; + width: 28px; + height: 28px; } .ilTestAnswerStatusIcon { diff --git a/templates/default/delos.css b/templates/default/delos.css index 9beac26ca5e9..c23d5b29f046 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -16623,8 +16623,8 @@ td.ilc_Page { } .ilTestMarkQuestionIcon { - width: 12px; - height: 12px; + width: 28px; + height: 28px; } .ilTestAnswerStatusIcon { From f127cd02c02938e1a8f3efe24fe34a8a19c6a3a0 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 26 Jun 2026 14:11:02 +0200 Subject: [PATCH 018/333] [Fix] glossary: make copying glossary against broken terms --- components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php | 7 +++++-- components/ILIAS/Glossary/classes/class.ilObjGlossary.php | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php b/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php index bd85e6f407da..411e3ab9a0a1 100755 --- a/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php +++ b/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php @@ -642,9 +642,12 @@ public static function _copyTerm( int $a_term_id, int $a_glossary_id ): int { - $old_term = new ilGlossaryTerm($a_term_id); - // copy the term + try { + $old_term = new ilGlossaryTerm($a_term_id); + } catch (Exception $e) { + return 0; + } $new_term = new ilGlossaryTerm(); $new_term->setTerm($old_term->getTerm()); $new_term->setLanguage($old_term->getLanguage()); diff --git a/components/ILIAS/Glossary/classes/class.ilObjGlossary.php b/components/ILIAS/Glossary/classes/class.ilObjGlossary.php index 8a1fb18b603c..e836f7c79a41 100755 --- a/components/ILIAS/Glossary/classes/class.ilObjGlossary.php +++ b/components/ILIAS/Glossary/classes/class.ilObjGlossary.php @@ -590,6 +590,9 @@ public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = $term_mappings = array(); foreach (ilGlossaryTerm::getTermList([$this->getRefId()]) as $term) { $new_term_id = ilGlossaryTerm::_copyTerm($term["id"], $new_obj->getId()); + if ($new_term_id === 0) { + continue; + } $term_mappings[$term["id"]] = $new_term_id; // copy tax node assignments From 3c55bf12dcaf177b1211b1d675e3bc905459e6a2 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 26 Jun 2026 14:50:23 +0200 Subject: [PATCH 019/333] copage: drag/drop array fix --- components/ILIAS/COPage/Page/class.PageCommandActionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php b/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php index 018d7fdc46a1..feb1801a63ca 100755 --- a/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php +++ b/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php @@ -184,7 +184,7 @@ protected function dragDropCommand(array $body): Server\Response $source = explode(":", $source); $target = explode(":", $target); - $updated = $page->moveContentAfter($source[0], $target[0], $source[1], $target[1]); + $updated = $page->moveContentAfter($source[0], $target[0], $source[1] ?? '', $target[1] ?? ''); return $this->sendPage($updated); } From 1fd99078a1792ae958cefb310562461b5a8ed8fc Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 09:55:24 +0200 Subject: [PATCH 020/333] blog: removed static calls --- .../ILIAS/Blog/Export/class.ilBlogDataSet.php | 2 +- .../Blog/Export/class.ilBlogImporter.php | 4 +++- .../Blog/Posting/class.ilBlogPosting.php | 21 ------------------- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 5 ++--- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php index b3aa993583a5..7e688a01008b 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php +++ b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php @@ -285,7 +285,7 @@ public function readData( // keywords foreach ($this->data as $idx => $item) { - $blog_id = ilBlogPosting::lookupBlogId($item["Id"]); + $blog_id = $this->service->domain()->posting()->lookupBlogId((int) $item["Id"]); $keywords = $this->service->domain()->posting()->getKeywords((int) $blog_id, (int) $item["Id"]); if ($keywords) { foreach ($keywords as $kidx => $keyword) { diff --git a/components/ILIAS/Blog/Export/class.ilBlogImporter.php b/components/ILIAS/Blog/Export/class.ilBlogImporter.php index 4481de258d39..60f8f93b4a01 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogImporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogImporter.php @@ -61,10 +61,12 @@ public function importXmlRepresentation( public function finalProcessing( ilImportMapping $a_mapping ): void { + global $DIC; + $blp_map = $a_mapping->getMappingsOfEntity("components/ILIAS/COPage", "pg"); foreach ($blp_map as $blp_id) { $blp_id = (int) substr($blp_id, 4); - $blog_id = ilBlogPosting::lookupBlogId($blp_id); + $blog_id = $DIC->blog()->internal()->domain()->posting()->lookupBlogId($blp_id); ilBlogPosting::_writeParentId("blp", $blp_id, (int) $blog_id); } diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php index 9e50d67ba854..86475aceafb7 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php @@ -254,27 +254,6 @@ public function unpublish(): void ); } - public static function lookupBlogId( - int $a_posting_id - ): ?int { - global $DIC; - return $DIC->blog()->internal()->domain()->posting()->lookupBlogId($a_posting_id); - } - - /** - * Checks whether a posting exists - */ - public static function exists( - int $a_blog_id, - int $a_posting_id - ): bool { - global $DIC; - return $DIC->blog()->internal()->domain()->posting()->exists( - $a_blog_id, - $a_posting_id - ); - } - /** * Set blog node id (needed for notification) */ diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 08e5be07df37..9ea6bf02de9c 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -103,6 +103,7 @@ public function __construct( $this->rbac_review = $domain->rbac()->review(); $this->rbacadmin = $domain->rbac()->admin(); $this->lng = $domain->lng(); + $this->posting_manager = $domain->posting(); $gui = $service->gui(); $this->gui = $gui; @@ -134,12 +135,10 @@ public function __construct( $blog_page = $this->blog_request->getBlogPage(); if ($blog_page > 0 && - ilBlogPosting::lookupBlogId($blog_page) !== $this->object->getId()) { + $this->posting_manager->lookupBlogId($blog_page) !== $this->object->getId()) { throw new ilException("Posting ID does not match blog."); } - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); - $blog_id = 0; if ($this->object) { // gather postings by month From a1ee21d87ec668fb33f5a2bd0fdeebdbfc641070 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 14:22:02 +0200 Subject: [PATCH 021/333] blog: removed static calls --- .../ILIAS/Blog/News/class.NewsManager.php | 6 +- .../Blog/Posting/Service/class.GUIService.php | 69 +++++++++++++++++++ .../Blog/Posting/class.ilBlogPosting.php | 5 +- .../Blog/Posting/class.ilBlogPostingGUI.php | 21 +++--- components/ILIAS/Blog/RSS/RSSGUI.php | 2 +- .../Blog/Service/class.InternalGUIService.php | 10 +++ .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 2 +- 7 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 components/ILIAS/Blog/Posting/Service/class.GUIService.php diff --git a/components/ILIAS/Blog/News/class.NewsManager.php b/components/ILIAS/Blog/News/class.NewsManager.php index b6b863237105..cc3c2d6b68ae 100644 --- a/components/ILIAS/Blog/News/class.NewsManager.php +++ b/components/ILIAS/Blog/News/class.NewsManager.php @@ -29,11 +29,15 @@ */ class NewsManager { + protected \ILIAS\Blog\Posting\Service\GUIService $posting_gui; + public function __construct( protected InternalDataService $data, protected InternalRepoService $repo, protected InternalDomainService $domain ) { + global $DIC; + $this->posting_gui = $DIC->blog()->internal()->gui()->posting(); } /** @@ -110,7 +114,7 @@ public function handle(\ilBlogPosting $page, bool $update = false): void $news_item->setContentTextIsLangVar(false); $news_item->setContent($content); - $snippet = \ilBlogPostingGUI::getSnippet($page->getId()); + $snippet = $this->posting_gui->getSnippet($page->getId()); $news_item->setContentLong($snippet); if (!$news_item->getId()) { diff --git a/components/ILIAS/Blog/Posting/Service/class.GUIService.php b/components/ILIAS/Blog/Posting/Service/class.GUIService.php new file mode 100644 index 000000000000..7fcd9217f95e --- /dev/null +++ b/components/ILIAS/Blog/Posting/Service/class.GUIService.php @@ -0,0 +1,69 @@ +postingGUI(0, null, $a_id); + return $bpgui->getSnippet( + $a_truncate, + $a_truncate_length, + $a_truncate_sign, + $a_include_picture, + $a_picture_width, + $a_picture_height, + $a_export_directory + ); + } +} diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php index 86475aceafb7..e30778d3d707 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php @@ -28,6 +28,7 @@ */ class ilBlogPosting extends ilPageObject { + protected \ILIAS\Blog\Posting\Service\GUIService $posting_gui; protected string $title = ""; protected ?ilDateTime $created = null; protected int $blog_node_id = 0; @@ -45,6 +46,8 @@ public function afterConstructor(): void $this->internal_data = $DIC->blog()->internal()->data(); $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); $this->blog_domain = $DIC->blog()->internal()->domain(); + $this->posting_gui = $DIC->blog()->internal()->gui()->posting(); + } public function getParentType(): string @@ -267,7 +270,7 @@ public function setBlogNodeId( public function getNotificationAbstract(): string { - $snippet = ilBlogPostingGUI::getSnippet($this->getId(), true); + $snippet = $this->posting_gui->getSnippet($this->getId(), true); // making things more readable $snippet = str_replace(array('
', '
', '

', ''), "\n", $snippet); diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index 967bf110d875..997bf8b97e62 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -735,8 +735,7 @@ public function saveKeywordsForm(): void /** * Get first text paragraph of page */ - public static function getSnippet( - int $a_id, + public function getSnippet( bool $a_truncate = false, int $a_truncate_length = 500, string $a_truncate_sign = "...", @@ -745,25 +744,23 @@ public static function getSnippet( int $a_picture_height = 144, ?string $a_export_directory = null ): string { - $bpgui = new self(0, null, $a_id); - // scan the full page for media objects $img = ""; if ($a_include_picture) { - $img = $bpgui->getFirstMediaObjectAsTag($a_picture_width, $a_picture_height, $a_export_directory); + $img = $this->getFirstMediaObjectAsTag($a_picture_width, $a_picture_height, $a_export_directory); } - $bpgui->setRawPageContent(true); - $bpgui->setAbstractOnly(true); + $this->setRawPageContent(true); + $this->setAbstractOnly(true); // #8627: export won't work - should we set offline mode? - $bpgui->setFileDownloadLink("."); - $bpgui->setFullscreenLink("."); - $bpgui->setSourcecodeDownloadScript("."); - $bpgui->setProfileBackUrl("."); + $this->setFileDownloadLink("."); + $this->setFullscreenLink("."); + $this->setSourcecodeDownloadScript("."); + $this->setProfileBackUrl("."); // render without title - $page = $bpgui->showPage(); + $page = $this->showPage(); if ($a_truncate) { $page = ilPageObject::truncateHTML($page, $a_truncate_length, $a_truncate_sign); diff --git a/components/ILIAS/Blog/RSS/RSSGUI.php b/components/ILIAS/Blog/RSS/RSSGUI.php index 8ed3896f0d09..646f492afb06 100644 --- a/components/ILIAS/Blog/RSS/RSSGUI.php +++ b/components/ILIAS/Blog/RSS/RSSGUI.php @@ -75,7 +75,7 @@ public function deliver(string $a_wsp_id): void } // #16434 - $snippet = strip_tags(\ilBlogPostingGUI::getSnippet($id), "

"); + $snippet = strip_tags($this->gui->posting()->getSnippet($id), "

"); $snippet = str_replace("&", "&", $snippet); $snippet = ""; diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index 3d616e8ef350..fad63aa0f514 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -25,6 +25,7 @@ use ILIAS\PermanentLink\PermanentLinkManager; use ILIAS\Blog\ReadingTime\GUIService; use ILIAS\Blog\RSS\RSSGUI; +use ILIAS\Blog\Posting\Service\GUIService as PostingGUIService; class InternalGUIService { @@ -114,6 +115,15 @@ public function readingTime(): GUIService ); } + public function posting(): PostingGUIService + { + return self::$instance["posting"] ??= new PostingGUIService( + $this->data_service, + $this->domain_service, + $this + ); + } + public function rss(): RSSGUI { return self::$instance["rss"] ??= new RSSGUI( diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 9ea6bf02de9c..099336228a0b 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -1254,7 +1254,7 @@ public function renderList( $wtpl->parseCurrentBlock(); } - $snippet = ilBlogPostingGUI::getSnippet( + $snippet = $this->gui->posting()->getSnippet( $item_id, $this->blog_settings->getAbstractShorten(), $this->blog_settings->getAbstractShortenLength(), From c6c4f54392a576f899710d43a9250e7184752ef9 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 14:25:03 +0200 Subject: [PATCH 022/333] blog: removed static calls --- components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index 997bf8b97e62..a5a648b4a515 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -805,7 +805,7 @@ protected function getFirstMediaObjectAsTag( // see ilCOPageHTMLExport::exportHTMLMOB() $mob_dir = "./mobs/mm_" . $mob_obj->getId(); } - $mob_res = self::parseImage( + $mob_res = $this->parseImage( $mob_size["width"], $mob_size["height"], $a_width, @@ -828,7 +828,7 @@ protected function getFirstMediaObjectAsTag( return ""; } - protected static function parseImage( + protected function parseImage( int $src_width, int $src_height, int $tgt_width, From d5815fcf71fd2711cd0191359d46723a589e9901 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 17:10:53 +0200 Subject: [PATCH 023/333] blog: improve DIC handling --- .../Blog/Posting/class.ilBlogPostingGUI.php | 21 +++++++------------ .../class.ilBlogDraftsDerivedTaskProvider.php | 5 +++-- .../classes/class.ilBlogNewsRendererGUI.php | 16 ++++++++------ .../ILIAS/Blog/classes/class.ilObjBlog.php | 5 +++-- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 6 ++---- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index a5a648b4a515..af3fc1ae46a3 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -44,6 +44,7 @@ class ilBlogPostingGUI extends ilPageObjectGUI protected ilLocatorGUI $locator; protected ilSetting $settings; protected LOMServices $lom_services; + protected ilToolbarGUI $toolbar; protected int $node_id; protected PostingManager $posting_manager; protected ?object $access_handler = null; @@ -114,6 +115,7 @@ public function __construct( $this->notes = $DIC->notes(); $this->profile_gui = $DIC->blog()->internal()->gui()->profile(); $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->toolbar = $DIC->toolbar(); } public function executeCommand(): string @@ -179,12 +181,11 @@ protected function checkAccess(string $a_cmd): bool public function preview( ?string $a_mode = null ): string { - global $DIC; $ilCtrl = $this->ctrl; $tpl = $this->tpl; $ilSetting = $this->settings; - $toolbar = $DIC->toolbar(); + $toolbar = $this->toolbar; $append = ""; $this->getBlogPosting()->increaseViewCnt(); @@ -628,9 +629,7 @@ public function activatePage(bool $a_to_list = false): void */ public function editKeywords(): void { - global $DIC; - - $renderer = $DIC->ui()->renderer(); + $renderer = $this->blog_gui->ui()->renderer(); $ilTabs = $this->tabs; $tpl = $this->tpl; @@ -652,9 +651,7 @@ public function editKeywords(): void */ protected function initKeywordsForm(): \ILIAS\UI\Component\Input\Container\Form\Standard { - global $DIC; - - $ui_factory = $DIC->ui()->factory(); + $ui_factory = $this->blog_gui->ui()->factory(); $keywords = $this->posting_manager->getKeywords( $this->getBlogPosting()->getBlogId(), @@ -681,7 +678,7 @@ protected function initKeywordsForm(): \ILIAS\UI\Component\Input\Container\Form\ $input_tag = $input_tag->withValue($keywords); } - $DIC->ctrl()->setParameter( + $this->ctrl->setParameter( $this, 'tags', 'tags_processing' @@ -689,7 +686,7 @@ protected function initKeywordsForm(): \ILIAS\UI\Component\Input\Container\Form\ $section = $ui_factory->input()->field()->section([$input_tag], $this->lng->txt("blog_edit_keywords"), ""); - $form_action = $DIC->ctrl()->getFormAction($this, "saveKeywordsForm"); + $form_action = $this->ctrl->getFormAction($this, "saveKeywordsForm"); return $ui_factory->input()->container()->form()->standard($form_action, ["tags" => $section]); } @@ -707,9 +704,7 @@ protected function getParentObjId(): int public function saveKeywordsForm(): void { - global $DIC; - - $request = $DIC->http()->request(); + $request = $this->blog_gui->http()->request(); $form = $this->initKeywordsForm(); if ($request->getMethod() === "POST" diff --git a/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php b/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php index 800be9b2ff13..49f0c0a3c3aa 100755 --- a/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php +++ b/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php @@ -37,8 +37,9 @@ public function __construct( ) { global $DIC; - $this->gui = $DIC->blog()->internal()->gui(); - $this->domain = $DIC->blog()->internal()->domain(); + $service = $DIC->blog()->internal(); + $this->gui = $service->gui(); + $this->domain = $service->domain(); $this->taskService = $taskService; $this->accessHandler = $accessHandler; $this->lng = $lng; diff --git a/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php b/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php index 3275e896e556..32dfa9f6ddc2 100755 --- a/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php +++ b/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php @@ -18,17 +18,21 @@ declare(strict_types=1); -/** - * Blog news renderer - * @author Alexander Killing - */ class ilBlogNewsRendererGUI extends ilNewsDefaultRendererGUI { - public function getObjectLink(): string + protected \ILIAS\Blog\InternalGUIService $blog_gui; + + public function __construct() { global $DIC; + parent::__construct(); + $service = $DIC->blog()->internal(); + $this->blog_gui = $service->gui(); + } - $pl = $DIC->blog()->internal()->gui()->permanentLink($this->getNewsRefId()); + public function getObjectLink(): string + { + $pl = $this->blog_gui->permanentLink($this->getNewsRefId()); $n = $this->getNewsItem(); $posting_id = 0; diff --git a/components/ILIAS/Blog/classes/class.ilObjBlog.php b/components/ILIAS/Blog/classes/class.ilObjBlog.php index 27da54a1f0b0..90092326f992 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlog.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlog.php @@ -44,8 +44,9 @@ public function __construct( ) { global $DIC; + $service = $DIC->blog()->internal(); $this->notes_service = $DIC->notes(); - $this->settings_manager = $DIC->blog()->internal()->domain()->blogSettings(); + $this->settings_manager = $service->domain()->blogSettings(); parent::__construct($a_id, $a_reference); $this->rbac_review = $DIC->rbac()->review(); @@ -57,7 +58,7 @@ public function __construct( if ($this->getId() > 0) { $this->blog_settings = $this->settings_manager->getByObjId($this->getId()); } - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->posting_manager = $service->domain()->posting(); } protected function initType(): void diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 099336228a0b..49b0f4b51f14 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -1810,10 +1810,8 @@ public function renderNavigation( } if (count($blocks)) { - global $DIC; - - $ui_factory = $DIC->ui()->factory(); - $ui_renderer = $DIC->ui()->renderer(); + $ui_factory = $this->ui->factory(); + $ui_renderer = $this->ui->renderer(); ksort($blocks); foreach ($blocks as $block) { From 4c20b8d80ccf22184ad28043210253155bdefc17 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 17:23:45 +0200 Subject: [PATCH 024/333] blog: improve DIC handling --- components/ILIAS/Blog/Export/BlogHtmlExport.php | 17 ++++++++++------- .../ILIAS/Blog/Export/class.ilBlogDataSet.php | 8 +++++--- .../ILIAS/Blog/Export/class.ilBlogExporter.php | 5 ++++- .../ILIAS/Blog/Export/class.ilBlogImporter.php | 3 ++- .../ILIAS/Blog/News/class.NewsManager.php | 6 +++--- .../Blog/PermanentLink/StaticUrlHandler.php | 3 ++- .../Service/class.InternalDomainService.php | 3 ++- .../Blog/classes/class.ilObjBlogListGUI.php | 5 ++--- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Blog/Export/BlogHtmlExport.php b/components/ILIAS/Blog/Export/BlogHtmlExport.php index b94f698f5158..e23f6447c2da 100755 --- a/components/ILIAS/Blog/Export/BlogHtmlExport.php +++ b/components/ILIAS/Blog/Export/BlogHtmlExport.php @@ -56,11 +56,13 @@ public function __construct( $this->blog_gui = $blog_gui; /** @var \ilObjBlog $blog */ $blog = $blog_gui->getObject(); + $this->blog = $blog; + + $blog_service = $DIC->blog()->internal(); + $this->collector = $DIC->export()->domain()->html()->collector($blog->getId()); $this->collector->init(); - $this->blog = $blog; - //$this->export_dir = $exp_dir; $this->sub_dir = $sub_dir; $this->target_dir = $exp_dir . "/" . $sub_dir; @@ -86,7 +88,7 @@ public function __construct( } else { $this->content_style_domain = $cs->domain()->styleForObjId($this->blog->getId()); } - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->posting_manager = $blog_service->domain()->posting(); } protected function init(): void { @@ -339,8 +341,6 @@ public function buildExportLink( protected function getInitialisedTemplate( string $a_back_url = "" ): \ilGlobalPageTemplate { - global $DIC; - $this->export_util->resetGlobalScreen(); $location_stylesheet = \ilUtil::getStyleSheetLocation(); @@ -350,13 +350,16 @@ protected function getInitialisedTemplate( ); \ilPCQuestion::resetInitialState(); - $tabs = $DIC->tabs(); + $tabs = $this->tabs; $tabs->clearTargets(); $tabs->clearSubTabs(); if ($a_back_url) { $tabs->setBackTarget($this->lng->txt("back"), $a_back_url); } - $tpl = new \ilGlobalPageTemplate($DIC->globalScreen(), $DIC->ui(), $DIC->http()); + + /** @var \ILIAS\DI\Container $DIC */ + global $DIC; + $tpl = new \ilGlobalPageTemplate($this->global_screen, $DIC->ui(), $DIC->http()); $this->co_page_html_export->getPreparedMainTemplate($tpl); diff --git a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php index 7e688a01008b..b8f8e9195cbe 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php +++ b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php @@ -44,13 +44,15 @@ public function __construct() { global $DIC; parent::__construct(); + + $blog_service = $DIC->blog()->internal(); + $this->content_style_domain = $DIC ->contentStyle() ->domain(); $this->notes = $DIC->notes(); - $this->reading_time = $DIC->blog()->internal()->domain()->readingTime(); - $this->blog_settings = $DIC->blog()->internal()->domain()->blogSettings(); - $this->service = $DIC->blog()->internal(); + $this->reading_time = $blog_service->domain()->readingTime(); + $this->blog_settings = $blog_service->domain()->blogSettings(); } public function getSupportedVersions(): array diff --git a/components/ILIAS/Blog/Export/class.ilBlogExporter.php b/components/ILIAS/Blog/Export/class.ilBlogExporter.php index ca20e3be90f0..71b49d3c5e4a 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogExporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogExporter.php @@ -34,10 +34,13 @@ public function init(): void $this->ds = new ilBlogDataSet(); $this->ds->setDSPrefix("ds"); + + $blog_service = $DIC->blog()->internal(); + $this->content_style_domain = $DIC ->contentStyle() ->domain(); - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->posting_manager = $blog_service->domain()->posting(); } public function getXmlExportTailDependencies( diff --git a/components/ILIAS/Blog/Export/class.ilBlogImporter.php b/components/ILIAS/Blog/Export/class.ilBlogImporter.php index 60f8f93b4a01..d5b1e2b0ed29 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogImporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogImporter.php @@ -62,11 +62,12 @@ public function finalProcessing( ilImportMapping $a_mapping ): void { global $DIC; + $blog_service = $DIC->blog()->internal(); $blp_map = $a_mapping->getMappingsOfEntity("components/ILIAS/COPage", "pg"); foreach ($blp_map as $blp_id) { $blp_id = (int) substr($blp_id, 4); - $blog_id = $DIC->blog()->internal()->domain()->posting()->lookupBlogId($blp_id); + $blog_id = $blog_service->domain()->posting()->lookupBlogId($blp_id); ilBlogPosting::_writeParentId("blp", $blp_id, (int) $blog_id); } diff --git a/components/ILIAS/Blog/News/class.NewsManager.php b/components/ILIAS/Blog/News/class.NewsManager.php index cc3c2d6b68ae..59227a090de8 100644 --- a/components/ILIAS/Blog/News/class.NewsManager.php +++ b/components/ILIAS/Blog/News/class.NewsManager.php @@ -34,10 +34,10 @@ class NewsManager public function __construct( protected InternalDataService $data, protected InternalRepoService $repo, - protected InternalDomainService $domain + protected InternalDomainService $domain, + \ILIAS\Blog\InternalGUIService $gui ) { - global $DIC; - $this->posting_gui = $DIC->blog()->internal()->gui()->posting(); + $this->posting_gui = $gui->posting(); } /** diff --git a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php index f2e3ea3b9f5c..5589c26e47e2 100644 --- a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php +++ b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php @@ -37,11 +37,12 @@ public function getNamespace(): string public function handle(Request $request, Context $context, Factory $response_factory): Response { global $DIC; + $blog_service = $DIC->blog()->internal(); $ctrl = $DIC->ctrl(); $access = $DIC->access(); $uri = null; - $blog_domain = $DIC->blog()->internal()->domain(); + $blog_domain = $blog_service->domain(); $id = $request->getReferenceId()?->toInt() ?? 0; $additional_params = $request->getAdditionalParameters() ?? []; diff --git a/components/ILIAS/Blog/Service/class.InternalDomainService.php b/components/ILIAS/Blog/Service/class.InternalDomainService.php index 55b2141439c3..8aa784d42057 100755 --- a/components/ILIAS/Blog/Service/class.InternalDomainService.php +++ b/components/ILIAS/Blog/Service/class.InternalDomainService.php @@ -119,7 +119,8 @@ public function news(): NewsManager return self::$instance["news"] ??= new NewsManager( $this->data, $this->repo, - $this + $this, + $this->dic->blog()->internal()->gui() ); } diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php index 6212950298f3..ffd04737cbe0 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php @@ -70,11 +70,10 @@ public function insertCommand( string $onclick = "" ): void { global $DIC; + $blog_service = $DIC->blog()->internal(); $tpl = $this->ui->mainTemplate(); - $export_possible = $DIC->blog() - ->internal() - ->domain() + $export_possible = $blog_service->domain() ->export() ->isCommentsExportPossible($this->obj_id); if ($cmd === "export" From 391b0683627f0b4b2ce27d827aaeae2bbc0ad82f Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 19:17:27 +0200 Subject: [PATCH 025/333] blog: improve DIC handling --- .../ILIAS/Blog/Export/class.ilBlogExporter.php | 14 +++++++++----- .../ILIAS/Blog/Export/class.ilBlogImporter.php | 17 +++++++++-------- .../Blog/PermanentLink/StaticUrlHandler.php | 17 ++++++++++++----- .../Blog/classes/class.ilObjBlogListGUI.php | 11 +++++++++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Blog/Export/class.ilBlogExporter.php b/components/ILIAS/Blog/Export/class.ilBlogExporter.php index 71b49d3c5e4a..4132a223fb5f 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogExporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogExporter.php @@ -28,13 +28,10 @@ class ilBlogExporter extends ilXmlExporter protected ilBlogDataSet $ds; protected \ILIAS\Style\Content\DomainService $content_style_domain; - public function init(): void + public function __construct() { global $DIC; - - $this->ds = new ilBlogDataSet(); - $this->ds->setDSPrefix("ds"); - + parent::__construct(); $blog_service = $DIC->blog()->internal(); $this->content_style_domain = $DIC @@ -43,6 +40,13 @@ public function init(): void $this->posting_manager = $blog_service->domain()->posting(); } + + public function init(): void + { + $this->ds = new ilBlogDataSet(); + $this->ds->setDSPrefix("ds"); + } + public function getXmlExportTailDependencies( string $a_entity, string $a_target_release, diff --git a/components/ILIAS/Blog/Export/class.ilBlogImporter.php b/components/ILIAS/Blog/Export/class.ilBlogImporter.php index d5b1e2b0ed29..d0c21290b127 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogImporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogImporter.php @@ -25,19 +25,23 @@ */ class ilBlogImporter extends ilXmlImporter { + protected \ILIAS\Blog\InternalService $blog_service; protected ilBlogDataSet $ds; protected \ILIAS\Style\Content\DomainService $content_style_domain; - public function init(): void + public function __construct() { global $DIC; - - $this->ds = new ilBlogDataSet(); - $this->ds->setDSPrefix("ds"); $this->content_style_domain = $DIC ->contentStyle() ->domain(); + $this->blog_service = $DIC->blog()->internal(); + } + public function init(): void + { + $this->ds = new ilBlogDataSet(); + $this->ds->setDSPrefix("ds"); $cop_config = $this->getImport()->getConfig("components/ILIAS/COPage"); $cop_config->setUpdateIfExists(true); } @@ -61,13 +65,10 @@ public function importXmlRepresentation( public function finalProcessing( ilImportMapping $a_mapping ): void { - global $DIC; - $blog_service = $DIC->blog()->internal(); - $blp_map = $a_mapping->getMappingsOfEntity("components/ILIAS/COPage", "pg"); foreach ($blp_map as $blp_id) { $blp_id = (int) substr($blp_id, 4); - $blog_id = $blog_service->domain()->posting()->lookupBlogId($blp_id); + $blog_id = $this->blog_service->domain()->posting()->lookupBlogId($blp_id); ilBlogPosting::_writeParentId("blp", $blp_id, (int) $blog_id); } diff --git a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php index 5589c26e47e2..2a0261e113f7 100644 --- a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php +++ b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php @@ -29,6 +29,15 @@ class StaticURLHandler extends BaseHandler implements Handler { + protected \ILIAS\Blog\InternalService $blog_service; + + public function __construct() + { + global $DIC; + $this->blog_service = $DIC->blog()->internal(); + parent::__construct(); + } + public function getNamespace(): string { return 'blog'; @@ -36,11 +45,9 @@ public function getNamespace(): string public function handle(Request $request, Context $context, Factory $response_factory): Response { - global $DIC; - $blog_service = $DIC->blog()->internal(); - - $ctrl = $DIC->ctrl(); - $access = $DIC->access(); + $blog_service = $this->blog_service; + $ctrl = $blog_service->gui()->ctrl(); + $access = $blog_service->domain()->access(); $uri = null; $blog_domain = $blog_service->domain(); diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php index ffd04737cbe0..d0437edddf09 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php @@ -27,6 +27,14 @@ class ilObjBlogListGUI extends ilObjectListGUI { private ?Modal $comment_modal = null; + protected \ILIAS\Blog\InternalService $blog_service; + + public function __construct(int $context = self::CONTEXT_REPOSITORY) + { + global $DIC; + parent::__construct($context); + $this->blog_service = $DIC->blog()->internal(); + } public function init(): void { @@ -69,8 +77,7 @@ public function insertCommand( string $cmd = "", string $onclick = "" ): void { - global $DIC; - $blog_service = $DIC->blog()->internal(); + $blog_service = $this->blog_service; $tpl = $this->ui->mainTemplate(); $export_possible = $blog_service->domain() From 42f193e1470a7ebf9ba4aaaaac46e7be576a556e Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 19:51:52 +0200 Subject: [PATCH 026/333] fixed copyright --- .../Blog/Posting/Service/class.GUIService.php | 16 +++++++++++++++- components/ILIAS/Blog/RSS/RSSGUI.php | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Blog/Posting/Service/class.GUIService.php b/components/ILIAS/Blog/Posting/Service/class.GUIService.php index 7fcd9217f95e..8e90ca73064a 100644 --- a/components/ILIAS/Blog/Posting/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Posting/Service/class.GUIService.php @@ -1,6 +1,20 @@ Date: Sat, 27 Jun 2026 21:54:29 +0200 Subject: [PATCH 027/333] blog: moved contributor code to subservice --- .../ILIAS/Blog/Contributor/ContributorGUI.php | 262 ++++++++++++++++++ .../Contributor/Service/class.GUIService.php | 11 + .../Blog/Service/class.InternalGUIService.php | 11 +- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 229 ++------------- 4 files changed, 297 insertions(+), 216 deletions(-) create mode 100644 components/ILIAS/Blog/Contributor/ContributorGUI.php diff --git a/components/ILIAS/Blog/Contributor/ContributorGUI.php b/components/ILIAS/Blog/Contributor/ContributorGUI.php new file mode 100644 index 000000000000..c2a277e0581e --- /dev/null +++ b/components/ILIAS/Blog/Contributor/ContributorGUI.php @@ -0,0 +1,262 @@ +gui->ctrl(); + $next_class = $ctrl->getNextClass($this); + $cmd = $ctrl->getCmd("contributors"); + + switch ($next_class) { + case strtolower(\ilRepositorySearchGUI::class): + $rep_search = new \ilRepositorySearchGUI(); + $rep_search->setTitle($this->domain->lng()->txt("blog_add_contributor")); + $rep_search->setCallback($this, 'addContributor', $this->blog->getAllLocalRoles($this->node_id)); + $ctrl->setReturn($this, 'contributors'); + $ctrl->forwardCommand($rep_search); + break; + + default: + if (method_exists($this, $cmd)) { + $this->$cmd(); + } + break; + } + } + + public function contributors(): void + { + $ilTabs = $this->gui->tabs(); + $ilToolbar = $this->gui->toolbar(); + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $tpl = $this->gui->ui()->mainTemplate(); + + $ilTabs->activateTab("contributors"); + + $local_roles = $this->blog->getAllLocalRoles($this->node_id); + + // add member + ilRepositorySearchGUI::fillAutoCompleteToolbar( + $this, + $ilToolbar, + array( + 'auto_complete_name' => $lng->txt('user'), + 'submit_name' => $lng->txt('add'), + 'add_search' => true, + 'add_from_container' => $this->node_id, + 'user_type' => $local_roles + ), + true + ); + + $other_roles = $this->blog->getRolesWithContributeOrRedact($this->node_id); + if ($other_roles) { + $tpl->setOnScreenMessage('info', sprintf($lng->txt("blog_contribute_other_roles"), implode(", ", $other_roles))); + } + + $table = $this->gui->contributor()->contributorTableBuilder( + $this->blog->getAllLocalRoles($this->node_id), + $this, + "contributors" + )->getTable(); + + if ($table->handleCommand()) { + return; + } + + $tpl->setContent($table->render()); + } + + /** + * Autocomplete submit + */ + public function addUserFromAutoComplete(): void + { + $lng = $this->domain->lng(); + $req = $this->gui->standardRequest(); + + $user_login = $req->getUserLogin(); + $user_type = $req->getUserType(); + + if (trim($user_login) === '') { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt('msg_no_search_string')); + $this->contributors(); + return; + } + $users = explode(',', $user_login); + + $user_ids = array(); + foreach ($users as $user) { + $user_id = ilObjUser::_lookupId($user); + + if (!$user_id) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt('user_not_known')); + $this->contributors(); + return; + } + + $user_ids[] = (int) $user_id; + } + + $this->addContributor($user_ids, $user_type); + } + + /** + * Centralized method to add contributors + */ + public function addContributor( + array $a_user_ids = array(), + ?string $a_user_type = null + ): void { + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $rbacreview = $this->domain->rbac()->review(); + $rbacadmin = $this->domain->rbac()->admin(); + $a_user_type = (int) $a_user_type; + + if (empty($a_user_ids)) { + $a_user_ids = $this->gui->standardRequest()->getIds(); + } + + if (!count($a_user_ids) || !$a_user_type) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("no_checkbox")); + $this->contributors(); + return; + } + + // get contributor role + $local_roles = array_keys($this->blog->getAllLocalRoles($this->node_id)); + if (!in_array($a_user_type, $local_roles)) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("missing_perm")); + $this->contributors(); + return; + } + + foreach ($a_user_ids as $user_id) { + $user_id = (int) $user_id; + $a_user_type = (int) $a_user_type; + if (!$rbacreview->isAssigned($user_id, $a_user_type)) { + $rbacadmin->assignUser($a_user_type, $user_id); + } + } + + $this->gui->ui()->mainTemplate()->setOnScreenMessage('success', $lng->txt("settings_saved"), true); + $ilCtrl->redirect($this, "contributors"); + } + + /** + * Used in ContributorTableBuilder + */ + public function confirmRemoveContributor(array $ids = []): void + { + if (empty($ids)) { + $ids = $this->gui->standardRequest()->getIds(); + } + if (count($ids) === 0) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $this->domain->lng()->txt("select_one"), true); + $this->gui->ctrl()->redirect($this, "contributors"); + } + + $confirm = new ilConfirmationGUI(); + $confirm->setHeaderText($this->domain->lng()->txt('blog_confirm_delete_contributors')); + $confirm->setFormAction($this->gui->ctrl()->getFormAction($this, 'removeContributor')); + $confirm->setConfirm($this->domain->lng()->txt('delete'), 'removeContributor'); + $confirm->setCancel($this->domain->lng()->txt('cancel'), 'contributors'); + + foreach ($ids as $user_id) { + $confirm->addItem( + 'id[]', + (string) $user_id, + \ilUserUtil::getNamePresentation($user_id, false, false, "", true) + ); + } + + $this->gui->ui()->mainTemplate()->setContent($confirm->getHTML()); + } + + public function removeContributor(): void + { + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $rbacadmin = $this->domain->rbac()->admin(); + + $ids = $this->gui->standardRequest()->getIds(); + + if (count($ids) === 0) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("select_one"), true); + $ilCtrl->redirect($this, "contributors"); + } + + // get contributor role + $local_roles = array_keys($this->blog->getAllLocalRoles($this->node_id)); + if (!$local_roles) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("missing_perm")); + $this->contributors(); + return; + } + + foreach ($ids as $user_id) { + foreach ($local_roles as $role_id) { + $rbacadmin->deassignUser($role_id, $user_id); + } + } + + $this->gui->ui()->mainTemplate()->setOnScreenMessage('success', $lng->txt("settings_saved"), true); + $this->gui->ctrl()->redirect($this, "contributors"); + } + + /** + * Used in ContributorTableBuilder + */ + public function addContributorContainerAction(array $ids = []): void + { + if (empty($ids)) { + $ids = $this->gui->standardRequest()->getIds(); + } + + // This would typically add contributors from a container + // For now, redirecting back to contributors as this seems to be a placeholder action + $this->gui->ctrl()->redirect($this, "contributors"); + } +} diff --git a/components/ILIAS/Blog/Contributor/Service/class.GUIService.php b/components/ILIAS/Blog/Contributor/Service/class.GUIService.php index ef8de2f00f64..8007e5d6d2d8 100755 --- a/components/ILIAS/Blog/Contributor/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Contributor/Service/class.GUIService.php @@ -57,4 +57,15 @@ public function contributorTableBuilder( $parent_cmd ); } + + public function contributorGUI(int $node_id, \ilObjBlog $blog): ContributorGUI + { + return new ContributorGUI( + $this->data_service, + $this->domain_service, + $this->gui, + $node_id, + $blog + ); + } } diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index fad63aa0f514..7fad2a4afa9c 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -67,11 +67,12 @@ public function standardRequest(): StandardGUIRequest public function contributor(): Contributor\GUIService { - return new Contributor\GUIService( - $this->data_service, - $this->domain_service, - $this - ); + return self::$instance["contributor"] ?? + self::$instance["contributor"] = new Contributor\GUIService( + $this->data_service, + $this->domain_service, + $this + ); } public function exercise(): Exercise\GUIService diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 49b0f4b51f14..6938914a27d5 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -30,15 +30,17 @@ use ILIAS\Blog\Settings\Settings; use ILIAS\Blog\ReadingTime\ReadingTimeManager; use ILIAS\Blog\Posting\PostingManager; +use ILIAS\Blog\Contributor\ContributorGUI; /** * @ilCtrl_Calls ilObjBlogGUI: ilBlogPostingGUI, ilWorkspaceAccessGUI * @ilCtrl_Calls ilObjBlogGUI: ilInfoScreenGUI, ilNoteGUI, ilCommonActionDispatcherGUI - * @ilCtrl_Calls ilObjBlogGUI: ilPermissionGUI, ilObjectCopyGUI, ilRepositorySearchGUI + * @ilCtrl_Calls ilObjBlogGUI: ilPermissionGUI, ilObjectCopyGUI * @ilCtrl_Calls ilObjBlogGUI: ilExportGUI, ilObjectContentStyleSettingsGUI, ilBlogExerciseGUI, ilObjNotificationSettingsGUI * @ilCtrl_Calls ilObjBlogGUI: ilObjectMetaDataGUI * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Settings\SettingsGUI * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Settings\BlockSettingsGUI + * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Contributor\ContributorGUI */ class ilObjBlogGUI extends ilObject2GUI implements ilDesktopItemHandling { @@ -295,7 +297,7 @@ protected function setTabs(): void $this->tabs_gui->addTab( "contributors", $lng->txt("blog_contributors"), - $this->ctrl->getLinkTarget($this, "contributors") + $this->ctrl->getLinkTargetByClass(ContributorGUI::class, "contributors") ); } @@ -508,14 +510,11 @@ public function executeCommand(): void $this->ctrl->forwardCommand($cp); break; - case 'ilrepositorysearchgui': + case strtolower(\ilRepositorySearchGUI::class): + $this->checkPermission("write"); $this->prepareOutput(); $ilTabs->activateTab("contributors"); - $rep_search = new ilRepositorySearchGUI(); - $rep_search->setTitle($this->lng->txt("blog_add_contributor")); - $rep_search->setCallback($this, 'addContributor', $this->object->getAllLocalRoles($this->node_id)); - $this->ctrl->setReturn($this, 'contributors'); - $this->ctrl->forwardCommand($rep_search); + $this->ctrl->forwardCommand($this->gui->contributor()->contributorGUI($this->node_id, $this->object)); break; case 'ilexportgui': @@ -581,6 +580,17 @@ public function executeCommand(): void $this->ctrl->forwardCommand($gui); break; + case strtolower(ContributorGUI::class): + $this->checkPermission("write"); + $this->prepareOutput(); + $ilTabs->activateTab("contributors"); + $gui = $this->gui->contributor()->contributorGUI( + $this->node_id, + $this->object + ); + $this->ctrl->forwardCommand($gui); + break; + case strtolower(\ILIAS\Blog\Settings\BlockSettingsGUI::class): $this->checkPermission("write"); $this->prepareOutput(); @@ -2127,209 +2137,6 @@ public function approve(): void } - // - // contributors - // - - public function contributors(): void - { - $ilTabs = $this->tabs; - $ilToolbar = $this->toolbar; - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $tpl = $this->tpl; - - if (!$this->checkPermissionBool("write")) { - return; - } - - $ilTabs->activateTab("contributors"); - - $local_roles = $this->object->getAllLocalRoles($this->node_id); - - // add member - ilRepositorySearchGUI::fillAutoCompleteToolbar( - $this, - $ilToolbar, - array( - 'auto_complete_name' => $lng->txt('user'), - 'submit_name' => $lng->txt('add'), - 'add_search' => true, - 'add_from_container' => $this->node_id, - 'user_type' => $local_roles - ), - true - ); - - $other_roles = $this->object->getRolesWithContributeOrRedact($this->node_id); - if ($other_roles) { - $this->tpl->setOnScreenMessage('info', sprintf($lng->txt("blog_contribute_other_roles"), implode(", ", $other_roles))); - } - - $table = $this->gui->contributor()->contributorTableBuilder( - $this->object->getAllLocalRoles($this->node_id), - $this, - "contributors" - )->getTable(); - - if ($table->handleCommand()) { - return; - } - - $tpl->setContent($table->render()); - } - - /** - * Autocomplete submit - */ - public function addUserFromAutoComplete(): void - { - $lng = $this->lng; - - $user_login = $this->blog_request->getUserLogin(); - $user_type = $this->blog_request->getUserType(); - - if (trim($user_login) === '') { - $this->tpl->setOnScreenMessage('failure', $lng->txt('msg_no_search_string')); - $this->contributors(); - return; - } - $users = explode(',', $user_login); - - $user_ids = array(); - foreach ($users as $user) { - $user_id = ilObjUser::_lookupId($user); - - if (!$user_id) { - $this->tpl->setOnScreenMessage('failure', $lng->txt('user_not_known')); - $this->contributors(); - return; - } - - $user_ids[] = (int) $user_id; - } - - $this->addContributor($user_ids, $user_type); - } - - /** - * Centralized method to add contributors - */ - public function addContributor( - array $a_user_ids = array(), - ?string $a_user_type = null - ): void { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $rbacreview = $this->rbac_review; - $rbacadmin = $this->rbacadmin; - $a_user_type = (int) $a_user_type; - - if (!$this->checkPermissionBool("write")) { - return; - } - - if (!count($a_user_ids) || !$a_user_type) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("no_checkbox")); - $this->contributors(); - return; - } - - // get contributor role - $local_roles = array_keys($this->object->getAllLocalRoles($this->node_id)); - if (!in_array($a_user_type, $local_roles)) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("missing_perm")); - $this->contributors(); - return; - } - - foreach ($a_user_ids as $user_id) { - $user_id = (int) $user_id; - $a_user_type = (int) $a_user_type; - if (!$rbacreview->isAssigned($user_id, $a_user_type)) { - $rbacadmin->assignUser($a_user_type, $user_id); - } - } - - $this->tpl->setOnScreenMessage('success', $lng->txt("settings_saved"), true); - $ilCtrl->redirect($this, "contributors"); - } - - /** - * Used in ContributorTableBuilder - */ - public function confirmRemoveContributor(array $ids = []): void - { - if (empty($ids)) { - $ids = $this->blog_request->getIds(); - } - if (count($ids) === 0) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt("select_one"), true); - $this->ctrl->redirect($this, "contributors"); - } - - $confirm = new ilConfirmationGUI(); - $confirm->setHeaderText($this->lng->txt('blog_confirm_delete_contributors')); - $confirm->setFormAction($this->ctrl->getFormAction($this, 'removeContributor')); - $confirm->setConfirm($this->lng->txt('delete'), 'removeContributor'); - $confirm->setCancel($this->lng->txt('cancel'), 'contributors'); - - foreach ($ids as $user_id) { - $confirm->addItem( - 'id[]', - (string) $user_id, - $this->profile_gui->getNamePresentation($user_id, false, "", true) - ); - } - - $this->tpl->setContent($confirm->getHTML()); - } - - public function removeContributor(): void - { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $rbacadmin = $this->rbacadmin; - - $ids = $this->blog_request->getIds(); - - if (count($ids) === 0) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("select_one"), true); - $ilCtrl->redirect($this, "contributors"); - } - - // get contributor role - $local_roles = array_keys($this->object->getAllLocalRoles($this->node_id)); - if (!$local_roles) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("missing_perm")); - $this->contributors(); - return; - } - - foreach ($ids as $user_id) { - foreach ($local_roles as $role_id) { - $rbacadmin->deassignUser($role_id, $user_id); - } - } - - $this->tpl->setOnScreenMessage('success', $lng->txt("settings_saved"), true); - $this->ctrl->redirect($this, "contributors"); - } - - /** - * Used in ContributorTableBuilder - */ - public function addContributorContainerAction(array $ids = []): void - { - if (empty($ids)) { - $ids = $this->blog_request->getIds(); - } - - // This would typically add contributors from a container - // For now, redirecting back to contributors as this seems to be a placeholder action - $this->ctrl->redirect($this, "contributors"); - } - public function deactivateAdmin(): void { if ($this->checkPermissionBool("write") && $this->apid > 0) { From dd7e52ff3981157dbd2747393a81bde2911a4bd3 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sun, 28 Jun 2026 09:58:31 +0200 Subject: [PATCH 028/333] blog: moved side block rendering to navigation subservice --- .../Navigation/Service/class.GUIService.php | 24 ++ .../Blog/Service/class.InternalGUIService.php | 9 +- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 369 ++---------------- 3 files changed, 53 insertions(+), 349 deletions(-) diff --git a/components/ILIAS/Blog/Navigation/Service/class.GUIService.php b/components/ILIAS/Blog/Navigation/Service/class.GUIService.php index e89279158f08..d420f56023c6 100755 --- a/components/ILIAS/Blog/Navigation/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Navigation/Service/class.GUIService.php @@ -43,4 +43,28 @@ public function toolbarNavigationRenderer( $this->gui ); } + + public function monthBlock(): MonthBlockGUI + { + return new MonthBlockGUI( + $this->domain, + $this->gui + ); + } + + public function authorBlock(): AuthorBlockGUI + { + return new AuthorBlockGUI( + $this->domain, + $this->gui + ); + } + + public function keywordBlock(): KeywordBlockGUI + { + return new KeywordBlockGUI( + $this->domain, + $this->gui + ); + } } diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index 7fad2a4afa9c..ddc5461168db 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -43,10 +43,11 @@ public function __construct( public function navigation(): Navigation\GUIService { - return new Navigation\GUIService( - $this->domain_service, - $this - ); + return self::$instance["navigation"] ?? + self::$instance["navigation"] = new Navigation\GUIService( + $this->domain_service, + $this + ); } public function presentation(): Presentation\GUIService diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 6938914a27d5..11e4381f6043 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -518,7 +518,10 @@ public function executeCommand(): void break; case 'ilexportgui': - $this->showExportGUI(); + $this->prepareOutput(); + $this->tabs->activateTab("export"); + $exp_gui = new ilExportGUI($this); + $this->ctrl->forwardCommand($exp_gui); break; case "ilobjectcontentstylesettingsgui": @@ -621,14 +624,6 @@ public function executeCommand(): void } } - protected function showExportGUI(): void - { - $this->prepareOutput(); - $this->tabs->activateTab("export"); - $exp_gui = new ilExportGUI($this); - $this->ctrl->forwardCommand($exp_gui); - } - protected function createExportFileWithComments(): void { $this->buildExportFile(true); @@ -1380,339 +1375,6 @@ protected function buildExportLink( } - /** - * Build navigation by date block - */ - protected function renderNavigationByDate( - array $a_items, - string $a_list_cmd = "render", - string $a_posting_cmd = "preview", - ?string $a_link_template = null, - bool $a_show_inactive = false, - int $a_blpg = 0 - ): string { - $ilCtrl = $this->ctrl; - - $blpg = ($a_blpg > 0) - ? $a_blpg - : $this->blpg; - - - // gather page active status - foreach ($a_items as $month => $postings) { - foreach (array_keys($postings) as $id) { - $active = ilBlogPosting::_lookupActive($id, "blp"); - if (!$a_show_inactive && !$active) { - unset($a_items[$month][$id]); - } - } - if (!count($a_items[$month])) { - unset($a_items[$month]); - } - } - - // list month (incl. postings) - if ($this->blog_settings->getNavMode() === ilObjBlog::NAV_MODE_LIST || $a_link_template) { - $max_months = $this->blog_settings->getNavModeListMonths(); - - $wtpl = new ilTemplate("tpl.blog_list_navigation_by_date.html", true, true, "components/ILIAS/Blog"); - - $ilCtrl->setParameter($this, "blpg", ""); - - $counter = $mon_counter = $last_year = 0; - foreach ($a_items as $month => $postings) { - if (!$a_link_template && $max_months && $mon_counter >= $max_months) { - break; - } - - $add_year = false; - $year = substr($month, 0, 4); - if (!$last_year || $year != $last_year) { - // #13562 - $add_year = true; - $last_year = $year; - } - - $mon_counter++; - - $month_name = ilCalendarUtil::_numericMonthToString((int) substr($month, 5)); - if (!$a_link_template) { - $ilCtrl->setParameter($this, "bmn", $month); - $month_url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - } else { - $month_url = $this->buildExportLink($a_link_template, "list", (string) $month); - } - - // list postings for month - //if($counter < $max_detail_postings) - if ($mon_counter <= $this->blog_settings->getNavModeListMonthsWithPostings()) { - if ($add_year) { - $wtpl->setCurrentBlock("navigation_year_details"); - $wtpl->setVariable("YEAR", $year); - $wtpl->parseCurrentBlock(); - } - - foreach ($postings as $id => $posting) { - //if($max_detail_postings && $counter >= $max_detail_postings) - //{ - // break; - //} - - $counter++; - - $caption = /* ilDatePresentation::formatDate($posting["created"], IL_CAL_DATETIME). - ", ".*/ $posting->getTitle(); - - if (!$a_link_template) { - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", $month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $id); - $url = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", $a_posting_cmd); - } else { - $url = $this->buildExportLink($a_link_template, "posting", (string) $id); - } - - if (!$posting->isActive()) { - $wtpl->setVariable("NAV_ITEM_DRAFT", $this->lng->txt("blog_draft")); - } elseif ($this->blog_settings->getApproval() && !$posting->isApproved()) { - $wtpl->setVariable("NAV_ITEM_APPROVAL", $this->lng->txt("blog_needs_approval")); - } - - $wtpl->setCurrentBlock("navigation_item"); - $wtpl->setVariable("NAV_ITEM_URL", $url); - $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("navigation_month_details"); - $wtpl->setVariable("NAV_MONTH", $month_name); - $wtpl->setVariable("URL_MONTH", $month_url); - } - // summarized month - else { - if ($add_year) { - $wtpl->setCurrentBlock("navigation_year"); - $wtpl->setVariable("YEAR", $year); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("navigation_month"); - $wtpl->setVariable("MONTH_NAME", $month_name); - $wtpl->setVariable("URL_MONTH", $month_url); - $wtpl->setVariable("MONTH_COUNT", count($postings)); - } - $wtpl->parseCurrentBlock(); - } - if (!$a_link_template) { - $this->ctrl->setParameterByClass(self::class, "bmn", null); - $url = $this->ctrl->getLinkTargetByClass(self::class, $a_list_cmd); - } else { - $url = "index.html"; - } - - $wtpl->setVariable( - "STARTING_PAGE", - $this->ui->renderer()->render( - $this->ui->factory()->link()->standard( - $this->lng->txt("blog_starting_page"), - $url - ) - ) - ); - } - // single month - else { - $wtpl = new ilTemplate("tpl.blog_list_navigation_month.html", true, true, "components/ILIAS/Blog"); - - $ilCtrl->setParameter($this, "blpg", ""); - - $month_options = array(); - foreach ($a_items as $month => $postings) { - $month_name = $this->gui->presentation()->util()->getMonthPresentation($month); - - $month_options[$month] = $month_name; - - if ($month == $this->month) { - if (!$a_link_template) { - $ilCtrl->setParameter($this, "bmn", $month); - $month_url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - } else { - $month_url = $this->buildExportLink($a_link_template, "list", (string) $month); - } - - foreach ($postings as $id => $posting) { - $caption = /* ilDatePresentation::formatDate($posting["created"], IL_CAL_DATETIME). - ", ".*/ $posting["title"]; - - if (!$a_link_template) { - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", $month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $id); - $url = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", $a_posting_cmd); - } else { - $url = $this->buildExportLink($a_link_template, "posting", (string) $id); - } - - if (!$posting->isActive()) { - $wtpl->setVariable("NAV_ITEM_DRAFT", $this->lng->txt("blog_draft")); - } elseif ($this->blog_settings->getApproval() && !$posting["approved"]) { - $wtpl->setVariable("NAV_ITEM_APPROVAL", $this->lng->txt("blog_needs_approval")); - } - - $wtpl->setCurrentBlock("navigation_item"); - $wtpl->setVariable("NAV_ITEM_URL", $url); - $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("navigation_month_details"); - if ($blpg > 0) { - $wtpl->setVariable("NAV_MONTH", $month_name); - $wtpl->setVariable("URL_MONTH", $month_url); - } - $wtpl->parseCurrentBlock(); - } - } - - if ($blpg === 0) { - $wtpl->setCurrentBlock("option_bl"); - foreach ($month_options as $value => $caption) { - $wtpl->setVariable("OPTION_VALUE", $value); - $wtpl->setVariable("OPTION_CAPTION", $caption); - if ($value == $this->month) { - $wtpl->setVariable("OPTION_SEL", ' selected="selected"'); - } - $wtpl->parseCurrentBlock(); - } - - $wtpl->setVariable("FORM_ACTION", $ilCtrl->getFormAction($this, $a_list_cmd)); - } - } - $ilCtrl->setParameter($this, "bmn", $this->month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", ""); - return $wtpl->get(); - } - - /** - * Build navigation by keywords block - */ - protected function renderNavigationByKeywords( - string $a_list_cmd = "render", - bool $a_show_inactive = false, - string $a_link_template = "", - int $a_blpg = 0 - ): string { - $ilCtrl = $this->ctrl; - - $blpg = ($a_blpg > 0) - ? $a_blpg - : $this->blpg; - - $keywords = $this->getKeywords($a_show_inactive, $blpg); - if ($keywords) { - $wtpl = new ilTemplate("tpl.blog_list_navigation_keywords.html", true, true, "components/ILIAS/Blog"); - - $max = max($keywords); - - $wtpl->setCurrentBlock("keyword"); - foreach ($keywords as $keyword => $counter) { - if (!$a_link_template) { - $ilCtrl->setParameter($this, "kwd", urlencode((string) $keyword)); // #15885 - $url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - $ilCtrl->setParameter($this, "kwd", ""); - } else { - $url = $this->buildExportLink($a_link_template, "keyword", (string) $keyword); - } - - $wtpl->setVariable("TXT_KEYWORD", $keyword); - $wtpl->setVariable("CLASS_KEYWORD", ilTagging::getRelevanceClass($counter, $max)); - $wtpl->setVariable("URL_KEYWORD", $url); - $wtpl->parseCurrentBlock(); - } - - return $wtpl->get(); - } - return ""; - } - - protected function renderNavigationByAuthors( - array $a_items, - string $a_list_cmd = "render", - bool $a_show_inactive = false - ): string { - $ilCtrl = $this->ctrl; - - $authors = array(); - foreach ($a_items as $month => $items) { - foreach ($items as $item) { - /** @var \ILIAS\Blog\Posting\Posting $item */ - $item_id = $item->getId(); - if (($a_show_inactive || ilBlogPosting::_lookupActive($item_id, "blp"))) { - $author_id = $item->getAuthor(); - if ($author_id) { - $authors[] = $author_id; - } - foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { - $editor_id = (int) $editor["user_id"]; - if ($editor_id !== $author_id) { - $authors[] = $editor_id; - } - } - } - } - } - - $authors = array_unique($authors); - - // filter out deleted users - $authors = array_filter($authors, function ($id) { - return ilObject::_lookupType($id) == "usr"; - }); - - if (count($authors) > 1) { - $list = array(); - foreach ($authors as $user_id) { - if ($user_id) { - $ilCtrl->setParameter($this, "ath", $user_id); - $url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - $ilCtrl->setParameter($this, "ath", ""); - - $base_name = ilUserUtil::getNamePresentation($user_id); - if (str_starts_with($base_name, "[")) { - $name = ilUserUtil::getNamePresentation($user_id, true); - $sort = $name; - } else { - $name = ilUserUtil::getNamePresentation( - $user_id, - true, - false, - "", - false, - true, - false - ); - $name_arr = ilObjUser::_lookupName($user_id); - $sort = $name_arr["lastname"] . " " . $name_arr["firstname"]; - } - - $idx = trim(strip_tags($sort)) . "///" . $user_id; // #10934 - $list[$idx] = array($name, $url); - } - } - ksort($list); - - $wtpl = new ilTemplate("tpl.blog_list_navigation_authors.html", true, true, "components/ILIAS/Blog"); - - $wtpl->setCurrentBlock("author"); - foreach ($list as $author) { - $wtpl->setVariable("TXT_AUTHOR", $author[0]); - $wtpl->setVariable("URL_AUTHOR", $author[1]); - $wtpl->parseCurrentBlock(); - } - - return $wtpl->get(); - } - return ""; - } - /** * Toolbar navigation */ @@ -1765,7 +1427,14 @@ public function renderNavigation( if (count($a_items)) { $blocks[$order["navigation"] ?? 0] = array( $this->lng->txt("blog_navigation"), - $this->renderNavigationByDate($a_items, $a_list_cmd, $a_posting_cmd, $a_link_template, $a_show_inactive, $a_blpg) + $this->gui->navigation()->monthBlock()->render( + $a_items, + $a_list_cmd, + $a_posting_cmd, + $a_link_template, + $a_show_inactive, + $a_blpg + ) ); } @@ -1776,7 +1445,13 @@ public function renderNavigation( $a_list_cmd !== "preview" && $a_list_cmd !== "gethtml" && !$a_link_template); - $keywords = $this->renderNavigationByKeywords($a_list_cmd, $a_show_inactive, (string) $a_link_template, $a_blpg); + $keywords = $this->gui->navigation()->keywordBlock()->render( + $a_items, + $a_list_cmd, + $a_show_inactive, + (string) $a_link_template, + $a_blpg + ); if ($keywords || $may_edit_keywords) { if (!$keywords) { $keywords = $this->lng->txt("blog_no_keywords"); @@ -1797,7 +1472,11 @@ public function renderNavigation( // authors if ($this->id_type === self::REPOSITORY_NODE_ID && $this->blog_settings->getAuthors()) { - $authors = $this->renderNavigationByAuthors($a_items, $a_list_cmd, $a_show_inactive); + $authors = $this->gui->navigation()->authorBlock()->render( + $a_items, + $a_list_cmd, + $a_show_inactive + ); if ($authors) { $blocks[$order["authors"] ?? 1] = array($this->lng->txt("blog_authors"), $authors); } From 9856b104c4254f624ee41c08182c2262cea7f50d Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sun, 28 Jun 2026 09:59:09 +0200 Subject: [PATCH 029/333] blog: moved side block rendering to navigation subservice --- .../ILIAS/Blog/Navigation/AuthorBlockGUI.php | 122 +++++++++ .../ILIAS/Blog/Navigation/KeywordBlockGUI.php | 138 ++++++++++ .../ILIAS/Blog/Navigation/MonthBlockGUI.php | 253 ++++++++++++++++++ 3 files changed, 513 insertions(+) create mode 100644 components/ILIAS/Blog/Navigation/AuthorBlockGUI.php create mode 100644 components/ILIAS/Blog/Navigation/KeywordBlockGUI.php create mode 100644 components/ILIAS/Blog/Navigation/MonthBlockGUI.php diff --git a/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php b/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php new file mode 100644 index 000000000000..6a88ce305357 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php @@ -0,0 +1,122 @@ +domain = $domain; + $this->gui = $gui; + } + + /** + * @param Posting[][] $items + */ + public function render( + array $items, + string $list_cmd = "render", + bool $show_inactive = false + ): string { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + + $authors = array(); + foreach ($items as $month => $month_items) { + foreach ($month_items as $item) { + $item_id = $item->getId(); + if (($show_inactive || \ilBlogPosting::_lookupActive($item_id, "blp"))) { + $author_id = $item->getAuthor(); + if ($author_id) { + $authors[] = $author_id; + } + foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { + $editor_id = (int) $editor["user_id"]; + if ($editor_id !== $author_id) { + $authors[] = $editor_id; + } + } + } + } + } + + $authors = array_unique($authors); + + // filter out deleted users + $authors = array_filter($authors, function ($id) { + return \ilObject::_lookupType($id) == "usr"; + }); + + if (count($authors) > 1) { + $list = array(); + foreach ($authors as $user_id) { + if ($user_id) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "ath", (string) $user_id); + $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "ath", ""); + + $base_name = \ilUserUtil::getNamePresentation($user_id); + if (str_starts_with($base_name, "[")) { + $name = \ilUserUtil::getNamePresentation($user_id, true); + $sort = $name; + } else { + $name = \ilUserUtil::getNamePresentation( + $user_id, + true, + false, + "", + false, + true, + false + ); + $name_arr = \ilObjUser::_lookupName($user_id); + $sort = $name_arr["lastname"] . " " . $name_arr["firstname"]; + } + + $idx = trim(strip_tags((string) $sort)) . "///" . $user_id; + $list[$idx] = array($name, $url); + } + } + ksort($list); + + $wtpl = new \ilTemplate("tpl.blog_list_navigation_authors.html", true, true, "components/ILIAS/Blog"); + + $wtpl->setCurrentBlock("author"); + foreach ($list as $author) { + $wtpl->setVariable("TXT_AUTHOR", $author[0]); + $wtpl->setVariable("URL_AUTHOR", $author[1]); + $wtpl->parseCurrentBlock(); + } + + return $wtpl->get(); + } + return ""; + } +} diff --git a/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php b/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php new file mode 100644 index 000000000000..7a177ae023da --- /dev/null +++ b/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php @@ -0,0 +1,138 @@ +domain = $domain; + $this->gui = $gui; + } + + /** + * @param Posting[][] $items + */ + public function render( + array $items, + string $list_cmd = "render", + bool $show_inactive = false, + string $link_template = "", + int $blpg = 0 + ): string { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + + $keywords = $this->getKeywords($items, $show_inactive, $blpg); + if ($keywords) { + $wtpl = new \ilTemplate("tpl.blog_list_navigation_keywords.html", true, true, "components/ILIAS/Blog"); + + $max = max($keywords); + + $wtpl->setCurrentBlock("keyword"); + foreach ($keywords as $keyword => $counter) { + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "kwd", urlencode((string) $keyword)); + $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "kwd", ""); + } else { + $url = $this->buildExportLink($link_template, "keyword", (string) $keyword); + } + + $wtpl->setVariable("TXT_KEYWORD", (string) $keyword); + $wtpl->setVariable("CLASS_KEYWORD", \ilTagging::getRelevanceClass((int) $counter, (int) $max)); + $wtpl->setVariable("URL_KEYWORD", $url); + $wtpl->parseCurrentBlock(); + } + + return $wtpl->get(); + } + return ""; + } + + /** + * @param Posting[][] $items + */ + protected function getKeywords( + array $items, + bool $show_inactive, + ?int $posting_id = null + ): array { + $keywords = array(); + $posting_manager = $this->domain->posting(); + $obj_id = \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()); + + if ($posting_id) { + foreach ($posting_manager->getKeywords($obj_id, $posting_id) as $keyword) { + if (isset($keywords[$keyword])) { + $keywords[$keyword]++; + } else { + $keywords[$keyword] = 1; + } + } + } else { + foreach ($items as $month => $month_items) { + foreach ($month_items as $item) { + $item_id = $item->getId(); + if ($show_inactive || \ilBlogPosting::_lookupActive($item_id, "blp")) { + foreach ($posting_manager->getKeywords($obj_id, $item_id) as $keyword) { + if (isset($keywords[$keyword])) { + $keywords[$keyword]++; + } else { + $keywords[$keyword] = 1; + } + } + } + } + } + } + + $tmp = array(); + foreach ($keywords as $keyword => $counter) { + $tmp[] = array("keyword" => $keyword, "counter" => $counter); + } + $tmp = \ilArrayUtil::sortArray($tmp, "keyword", "ASC"); + + $keywords = array(); + foreach ($tmp as $item) { + $keywords[(string) $item["keyword"]] = $item["counter"]; + } + return $keywords; + } + + protected function buildExportLink( + string $template, + string $type, + string $id + ): string { + $blog_export = new \ILIAS\Blog\Export\BlogHtmlExport($this->gui->standardRequest()->getRefId()); + return $blog_export->buildExportLink($template, $type, $id, []); + } +} diff --git a/components/ILIAS/Blog/Navigation/MonthBlockGUI.php b/components/ILIAS/Blog/Navigation/MonthBlockGUI.php new file mode 100644 index 000000000000..1c948d4031b7 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/MonthBlockGUI.php @@ -0,0 +1,253 @@ +domain = $domain; + $this->gui = $gui; + } + + /** + * @param Posting[][] $items + */ + public function render( + array $items, + string $list_cmd = "render", + string $posting_cmd = "preview", + ?string $link_template = null, + bool $show_inactive = false, + int $blpg = 0 + ): string { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $settings = $this->domain->blogSettings()->getByObjId( + \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()) + ); + + // gather page active status + foreach ($items as $month => $postings) { + foreach (array_keys($postings) as $id) { + $active = \ilBlogPosting::_lookupActive($id, "blp"); + if (!$show_inactive && !$active) { + unset($items[$month][$id]); + } + } + if (!count($items[$month])) { + unset($items[$month]); + } + } + + // list month (incl. postings) + if ($settings->getNavMode() === \ilObjBlog::NAV_MODE_LIST || $link_template) { + $max_months = $settings->getNavModeListMonths(); + + $wtpl = new \ilTemplate("tpl.blog_list_navigation_by_date.html", true, true, "components/ILIAS/Blog"); + + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", ""); + + $counter = $mon_counter = $last_year = 0; + foreach ($items as $month => $postings) { + if (!$link_template && $max_months && $mon_counter >= $max_months) { + break; + } + + $add_year = false; + $year = substr((string) $month, 0, 4); + if (!$last_year || $year != $last_year) { + $add_year = true; + $last_year = $year; + } + + $mon_counter++; + + $month_name = \ilCalendarUtil::_numericMonthToString((int) substr((string) $month, 5)); + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $month); + $month_url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + } else { + $month_url = $this->buildExportLink($link_template, "list", (string) $month); + } + + if ($mon_counter <= $settings->getNavModeListMonthsWithPostings()) { + if ($add_year) { + $wtpl->setCurrentBlock("navigation_year_details"); + $wtpl->setVariable("YEAR", $year); + $wtpl->parseCurrentBlock(); + } + + foreach ($postings as $id => $posting) { + $counter++; + $caption = $posting->getTitle(); + + if (!$link_template) { + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", $month); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $id); + $url = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $posting_cmd); + } else { + $url = $this->buildExportLink($link_template, "posting", (string) $id); + } + + if (!$posting->isActive()) { + $wtpl->setVariable("NAV_ITEM_DRAFT", $lng->txt("blog_draft")); + } elseif ($settings->getApproval() && !$posting->isApproved()) { + $wtpl->setVariable("NAV_ITEM_APPROVAL", $lng->txt("blog_needs_approval")); + } + + $wtpl->setCurrentBlock("navigation_item"); + $wtpl->setVariable("NAV_ITEM_URL", $url); + $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("navigation_month_details"); + $wtpl->setVariable("NAV_MONTH", $month_name); + $wtpl->setVariable("URL_MONTH", $month_url); + $wtpl->parseCurrentBlock(); + } + // summarized month + else { + if ($add_year) { + $wtpl->setCurrentBlock("navigation_year"); + $wtpl->setVariable("YEAR", $year); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("navigation_month"); + $wtpl->setVariable("MONTH_NAME", $month_name); + $wtpl->setVariable("URL_MONTH", $month_url); + $wtpl->setVariable("MONTH_COUNT", (string) count($postings)); + $wtpl->parseCurrentBlock(); + } + } + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", null); + $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + } else { + $url = "index.html"; + } + + $wtpl->setVariable( + "STARTING_PAGE", + $this->gui->ui()->renderer()->render( + $this->gui->ui()->factory()->link()->standard( + $lng->txt("blog_starting_page"), + $url + ) + ) + ); + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $this->gui->standardRequest()->getMonth()); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", ""); + return $wtpl->get(); + } + // single month + else { + $wtpl = new \ilTemplate("tpl.blog_list_navigation_month.html", true, true, "components/ILIAS/Blog"); + + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", ""); + + $month_options = array(); + foreach ($items as $month => $postings) { + $month_name = $this->gui->presentation()->util()->getMonthPresentation((string) $month); + + $month_options[(string) $month] = $month_name; + + if ($month == $this->gui->standardRequest()->getMonth()) { + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", (string) $month); + $month_url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + } else { + $month_url = $this->buildExportLink($link_template, "list", (string) $month); + } + + foreach ($postings as $id => $posting) { + $caption = $posting->getTitle(); + + if (!$link_template) { + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", (string) $month); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $id); + $url = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $posting_cmd); + } else { + $url = $this->buildExportLink($link_template, "posting", (string) $id); + } + + if (!$posting->isActive()) { + $wtpl->setVariable("NAV_ITEM_DRAFT", $lng->txt("blog_draft")); + } elseif ($settings->getApproval() && !$posting->isApproved()) { + $wtpl->setVariable("NAV_ITEM_APPROVAL", $lng->txt("blog_needs_approval")); + } + + $wtpl->setCurrentBlock("navigation_item"); + $wtpl->setVariable("NAV_ITEM_URL", $url); + $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("navigation_month_details"); + if ($blpg > 0) { + $wtpl->setVariable("NAV_MONTH", $month_name); + $wtpl->setVariable("URL_MONTH", $month_url); + } + $wtpl->parseCurrentBlock(); + } + } + + if ($blpg === 0) { + $wtpl->setCurrentBlock("option_bl"); + foreach ($month_options as $value => $caption) { + $wtpl->setVariable("OPTION_VALUE", $value); + $wtpl->setVariable("OPTION_CAPTION", $caption); + if ($value == $this->gui->standardRequest()->getMonth()) { + $wtpl->setVariable("OPTION_SEL", ' selected="selected"'); + } + $wtpl->parseCurrentBlock(); + } + + $wtpl->setVariable("FORM_ACTION", $ctrl->getFormActionByClass(\ilObjBlogGUI::class, $list_cmd)); + } + } + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $this->gui->standardRequest()->getMonth()); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", ""); + return $wtpl->get(); + } + + protected function buildExportLink( + string $template, + string $type, + string $id + ): string { + $blog_export = new \ILIAS\Blog\Export\BlogHtmlExport($this->gui->standardRequest()->getRefId()); + // Note: this might need adjustment since the original used $this->getKeywords(false) + // For now we assume keywords are not needed for these links or handled elsewhere + return $blog_export->buildExportLink($template, $type, $id, []); + } +} From bbb17a76f1657e1e5a02a8fe632c331b4f6b2df4 Mon Sep 17 00:00:00 2001 From: Ahmed Hamouda Date: Fri, 26 Jun 2026 10:47:02 +0200 Subject: [PATCH 030/333] add Http StatusCode::HTTP_UNPROCESSABLE_ENTITY --- components/ILIAS/HTTP/src/StatusCode.php | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ILIAS/HTTP/src/StatusCode.php b/components/ILIAS/HTTP/src/StatusCode.php index cdac27a2a57f..0d46f3a347a5 100755 --- a/components/ILIAS/HTTP/src/StatusCode.php +++ b/components/ILIAS/HTTP/src/StatusCode.php @@ -71,6 +71,7 @@ interface StatusCode public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; public const HTTP_EXPECTATION_FAILED = 417; + public const HTTP_UNPROCESSABLE_ENTITY = 422; public const HTTP_TOO_MANY_REQUESTS = 429; // [Server Error 5xx] From 93b4439231778b1ac2bdedcbc7e65c8a3d55f8d6 Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 30 Jun 2026 09:49:47 +0200 Subject: [PATCH 031/333] [FIX] Auth: Skip login-attempt counting for accounts without local auth See: https://mantis.ilias.de/view.php?id=47987 --- .../classes/Frontend/class.ilAuthFrontend.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Authentication/classes/Frontend/class.ilAuthFrontend.php b/components/ILIAS/Authentication/classes/Frontend/class.ilAuthFrontend.php index 8423441e903c..472b0723d3ef 100755 --- a/components/ILIAS/Authentication/classes/Frontend/class.ilAuthFrontend.php +++ b/components/ILIAS/Authentication/classes/Frontend/class.ilAuthFrontend.php @@ -404,7 +404,19 @@ protected function handleLoginAttempts(): void $usr_id_candidates = []; foreach (array_filter($auth_modes) as $auth_mode) { if ((int) $auth_mode === ilAuthUtils::AUTH_LOCAL) { - $usr_id_candidates[] = ilObjUser::_lookupId($this->getCredentials()->getUsername()); + $local_usr_id = ilObjUser::_lookupId($this->getCredentials()->getUsername()); + // Mantis #47987: A failed local login must only count against an + // account that can actually be authenticated locally. Without this + // check, external accounts (e.g., Shibboleth/SAML) whose login name + // is entered in the local login form get their login attempts + // incremented and are eventually deactivated - even though a local + // login is impossible for them because "Allow Local Authentication" + // is disabled. This mirrors the gate in ilAuthProviderDatabase. + if (is_int($local_usr_id) && $local_usr_id > 0 && ilAuthUtils::isLocalPasswordEnabledForAuthMode( + (int) ilAuthUtils::_getAuthMode(ilObjUser::_lookupAuthMode($local_usr_id)) + )) { + $usr_id_candidates[] = $local_usr_id; + } continue; } From eee6e8155249aa5906607d3b587145fdc796c819 Mon Sep 17 00:00:00 2001 From: Chris Potter Date: Wed, 1 Jul 2026 10:41:34 +0200 Subject: [PATCH 032/333] Changed commons lang variable to sentence case as it seems to always be used to the right of a checkbox. --- lang/ilias_en.lang | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 512dd28c41af..dc21913a16ce 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -3950,7 +3950,7 @@ common#:#default_skin_style#:#Default Skin / Style common#:#default_style#:#Default Style common#:#defaults#:#Defaults common#:#delete#:#Delete -common#:#delete_existing_file#:#Delete Existing File +common#:#delete_existing_file#:#Delete existing file common#:#delete_inactivated_user_accounts#:#Delete inactivated user accounts common#:#delete_inactivated_user_accounts_desc#:#If enabled, user accounts will be deleted %s days after their inactivation. common#:#delete_inactivated_user_accounts_include_roles#:#Considered roles From 21c255ead9b948e8dc607f5d13d92bf2579bba76 Mon Sep 17 00:00:00 2001 From: Matthias Kunkel Date: Wed, 1 Jul 2026 15:59:23 +0200 Subject: [PATCH 033/333] Updated links in Readme file and fixed some typos --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2ebeb496c0d8..6b40284da0a9 100755 --- a/README.md +++ b/README.md @@ -3,24 +3,24 @@ # ILIAS -ILIAS is a powerful Open Source Learning Management System for developing and realising web-based e-learning. The software was developed to reduce the costs of using new media in education and further training and to ensure the maximum level of customer influence in the implementation of the software. ILIAS is published under the General Public Licence and free of charge. +ILIAS is a powerful Open Source Learning Management System for developing and realising web-based e-learning. The software was developed to reduce the costs of using new media in education and further training and to ensure the maximum level of customer influence in the implementation of the software. ILIAS is published under the General Public Licence 3.0 and free of charge. ### Features -[see all features on our official website](https://www.ilias.de/en/about-ilias/) or [read our booklet](http://www.ilias.de/docu/goto_docu_file_1854_download.html) +[Read more about ILIAS features on our official website](https://www.ilias.de/en/about-ilias/) or [have a look in our booklet (de)](https://docu.ilias.de/goto_docu_file_4712_download.html) ### Installation -Installation of ILIAS is well documented on [our official Installation manual](http://www.ilias.de/docu/goto_docu_lm_367.html) and in the documentation contained inside this repo: [/docs/configuration/install.md](/docs/configuration/install.md) +Installation of ILIAS is well documented on [our official Installation manual](https://docu.ilias.de/go/lm/367) and in the documentation contained inside this repo: [/docs/configuration/install.md](/docs/configuration/install.md) ### Plugins -ILIAS can be extended with a lot of Plugins. You find the complete list in the [Plugin Repository](http://www.ilias.de/docu/goto.php?target=cat_1442&client_id=docu) +ILIAS can be extended with a lot of Plugins. You find a list in the [Plugin Data Collection](https://docu.ilias.de/go/dcl/3342) ### Community -We have a big [community](http://www.ilias.de/docu/goto.php?target=cat_1444&client_id=docu) and you can get a member of [ILIAS Society](http://www.ilias.de/docu/goto.php?target=cat_1669&client_id=docu). -You may even join us at one of our regular [ILIAS Conferences](http://www.ilias.de/docu/goto.php?target=cat_2255&client_id=docu). +We have a big [community](https://www.ilias.de/en/ilias-society/) and you can get a member of the [ILIAS Society](https://www.ilias.de/en/join-ilias-society/). +You may even join us at one of our regular [ILIAS Conferences](https://www.ilias-conference.org/en/). ### Development From ab7637eb6dde6507845dfb1201131438a07a6a04 Mon Sep 17 00:00:00 2001 From: Matthias Kunkel Date: Wed, 1 Jul 2026 17:42:33 +0200 Subject: [PATCH 034/333] Update security.md Improved headline of the security.md file --- docs/development/security.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/security.md b/docs/development/security.md index 9881338c2452..bff91440cf39 100755 --- a/docs/development/security.md +++ b/docs/development/security.md @@ -1,4 +1,4 @@ -# ILIAS Security Group +# ILIAS Security Policy ## Table of Contents * [Reporting Security Issues](#reporting-security-issues) From 192b7cba8912e9681971a0299c428ab37d26effc Mon Sep 17 00:00:00 2001 From: Fabian Schmid Date: Thu, 2 Jul 2026 13:15:50 +0200 Subject: [PATCH 035/333] Add IRSS storage audit report Component-by-component map of file storage relative to the IRSS (MIGRATED / PARTIAL / LEGACY / N/A / INFRA) with file:line evidence, shared legacy substrates, a migration-priority plan, and a per-release migration history (ILIAS 7 to trunk). AI-generated, verify before use. --- .../ResourceStorage/IRSS_STORAGE_AUDIT.md | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 components/ILIAS/ResourceStorage/IRSS_STORAGE_AUDIT.md diff --git a/components/ILIAS/ResourceStorage/IRSS_STORAGE_AUDIT.md b/components/ILIAS/ResourceStorage/IRSS_STORAGE_AUDIT.md new file mode 100644 index 000000000000..f4d8c4aab330 --- /dev/null +++ b/components/ILIAS/ResourceStorage/IRSS_STORAGE_AUDIT.md @@ -0,0 +1,358 @@ +# IRSS Storage Audit — Components Still Storing Files Outside the IRSS + +**Goal:** map which ILIAS components still persist/read files by legacy means (`ilFileSystemGUI`, +`ilFileUtils`/`ilUtil`, raw PHP `fopen`/`file_put_contents`/`mkdir`, or `ILIAS\Filesystem` used +*directly*) instead of routing all binary content through the **IRSS** (ILIAS Resource Storage Service), +which is meant to become the single storage entry point. + +**Scope:** all 182 dirs under `components/ILIAS/`. Generated by a per-component grep signal matrix +followed by a domain-grouped read of every component with a file-storage signal. Branch: `trunk` +(`ILIAS 12.0_alpha`). Date: 2026-07-02. + +> **⚠ Note:** This report was AI-generated and may deviate from reality. Component classifications, +> verdicts, effort estimates and `file:line` references reflect an automated audit of the code at the time +> of writing and can contain errors, omissions or stale citations. Verify against the current source before +> acting on any finding. + +--- + +## How to read this + +- **IRSS** = target. Markers: `resourceStorage()`, `ResourceStorage\`, `IRSSServices`, `ResourceBuilder`, + `StorableResource`, `AbstractResourceStakeholder`, a persisted `rid` column. +- **Legacy** = anything else that touches disk for *persistent domain content*: + 1. `ilFileSystemGUI` (legacy file browser) + 2. `ilFileUtils::` / `ilUtil::` file helpers (`makeDir`, `moveUploadedFile`, `getWebspaceDir`, `delDir`, `zip`, …) + 3. raw PHP fs (`fopen`, `fwrite`, `file_put_contents`, `move_uploaded_file`, `mkdir`, `copy`, `unlink`, …) + 4. `ILIAS\Filesystem` service used **directly** (`$DIC->filesystem()->web()/storage()/temp()`) — note IRSS + uses this service *internally*, so calling it directly is a bypass. + +### Architectural ground truth + +> IRSS never touches disk itself. `ResourceStorage` orchestrates; it injects a `Filesystem $fs` into its +> storage handlers (`ResourceStorage/src/StorageHandler/FileSystemBased/AbstractFileSystemStorageHandler.php:54`), +> which wraps `league/flysystem`'s `LocalFilesystemAdapter` +> (`Filesystem/src/Provider/FlySystem/FlySystemLocalFilesystemFactory.php:68`). So **Filesystem is the backend, +> ResourceStorage is IRSS, FileUpload is ingest, VirusScanner is a scan preprocessor, FileDelivery is egress, +> WebAccessChecker is legacy egress being retired.** Everything else is a consumer. + +### Verdict legend + +| Verdict | Meaning | +|---|---| +| **MIGRATED** | Persistent domain files stored via IRSS. Remaining disk I/O is only transient (temp/zip/export). | +| **PARTIAL** | Some persistent content on IRSS, some still on legacy disk. | +| **LEGACY** | Persistent domain content stored on disk, no IRSS. | +| **N/A** | No persistent file storage of its own (DB-backed, delegates to another component, or pure infra/temp). | +| **INFRA** | Part of the storage stack itself (backend / ingest / egress / scan / setup). | + +--- + +## Master table + +### Still to migrate — LEGACY (persistent content on disk, no IRSS) + +| Component | What persists on disk (legacy) | Mechanism | ~sites | Effort | +|---|---|---|---|---| +| **Course** | course file attachments (info tab), member exports, info HTML | `ilFSStorageCourse` / `ilFileDataCourse` (`ilFileSystemAbstractionStorage`) + raw fs | ~20 | high | +| **ScormAicc** | SCORM/AICC package tree `public/data/…/lm_data/lm_`, manifest | base `ilObjSAHSLearningModule::getDataDirectory` + **`ilFileSystemGUI`** + raw fs | ~30 | high | +| **Scorm2004** | SCORM 2004 package (webspace), imsmanifest, per-user tracking logs on disk | inherits SAHS dir + `fopen`/`fwrite`/`file_put_contents`; **`ilFileSystemGUI`** wired | ~30 | high | +| **LearningModule** | `lm_data/lm_` import/export/offline tree, subtitles | `ilFileUtils::makeDir/delDir/zip` + `fopen`/`fwrite` | ~25 | medium | +| **SurveyQuestionPool** | question images + material uploads (`CLIENT_WEB_DIR/survey/…`) | `ilFileUtils::moveUploadedFile` + `getImagePathWeb` | ~29 | medium | +| **Survey** | survey export/import dirs (`svy_data`), uploaded import files | `ilFileUtils` dir tree + `fopen` | ~24 | medium | +| **CmiXapi** | cmi5/xAPI content extracted into webspace object dir | `$DIC->filesystem()->web()` **direct** + `ilFileUtils` | ~10 | medium | +| **Verification** | certificate verification PDFs | `ilVerificationStorageFile extends ilFileSystemAbstractionStorage` + `file_put_contents`/`mkdir` | ~4 | medium | +| **Language** | `.lang` files in `CLIENT_DATA_DIR/lang_data` | `fopen`/`fwrite` + `ilFileUtils::makeDir` | ~9 | medium | +| **Container** | container header/custom icons `webspace/container_data/obj_` | `ilFileUtils::getWebspaceDir/makeDir` + Object custom-icon layer | ~6 | medium | +| **DidacticTemplate** | template icons (user-uploaded SVG) in web dir | `ILIAS\Filesystem` **direct** (`webDirectory->write/copy`) | ~4 | medium | +| **LearningSequence** | LSO intro/extro images | `move_uploaded_file` → `STORAGE_WEB` | ~2 | medium | +| **Saml** | SimpleSAMLphp `config.php`/`authsources.php`, IdP metadata XML, discovery | `file_put_contents` + `ILIAS\Filesystem` direct (lib expects real paths) | ~7 | high | +| **OpenIdConnect** | one login-page element image | `$DIC->filesystem()->web()` **direct** | ~4 | low | +| **LTIConsumer** | LTI provider icons (`lti_data/provider_icon/`) | `$DIC->filesystem()->web()->put` **direct** | ~9 | low–med | +| **Authentication** | apache-auth allowed-domains `.txt` | `file_put_contents`/`file_get_contents` | ~6 | low | +| **Chatroom** | chat-server config file only (not user content) | `fopen`/`fwrite` + `ilFileUtils::getDataDir` | ~4 | low | +| **Category** | category-import staging dir only | `ilFileUtils::getDataDir()."/cat_import"` | ~1 | low | +| **WebServices** | transient REST/ECS payloads (`ilRestFileStorage` small durable store) | `ilTempnam`/`fopen`/`fwrite` | ~13 | low | + +### PARTIAL (IRSS for the primary payload, legacy remnants) + +| Component | On IRSS | Still legacy | ~sites | Effort | +|---|---|---|---|---| +| **Mail** | compose-time upload handler (bridge only) | **canonical attachment store on disk** (`ilFileDataMail`/`ilFSStorageMail`), ZIP delivery, cron cleanup | ~20 | high | +| **Export** | finished export **artifact** + registry (`export_files` table) + download + public-access → IRSS (`export_handler` stakeholder); one central pipeline, no runtime fallback (see appendix) | zip **assembly** on a disk run-dir (then ingested), import **extraction** on data dir; per-component `ilExporter` reused unchanged; `ilImportDirectory` uses Filesystem direct | ~25 | high | +| **Test** | export / results / PDF-archive final zips | assessment question images, `tst_data` dirs, participant answer dirs, `ilTestArchiver` builds tree on disk | ~64 | high | +| **TestQuestionPool** | only `assFileUpload` answer files | question **image uploads** across ~7 question classes, suggested solutions, qpl export dirs; even `assFileUpload` keeps legacy preview uploads | ~122 | high | +| **Style** | content styles + content-style images (IRSS container) | `ilObjStyleSheet` legacy `/sty` image+export dirs; whole system-style/skin subsystem on disk | ~35 | high | +| **Certificate** | template files, template zips, bg/tile images | **portfolio certificate PDFs** on `$DIC->filesystem()->storage()` direct; bulk-zip + upload helpers | ~10 | medium | +| **User** | profile pictures (`usr_data.rid`), new-account-mail attachment | user-list exports (CSV/XML/Excel), personal-data export ZIP, HTML export, legacy-avatar cleanup remnant | ~18 | medium | +| **Badge** | badge + template images (`image_rid`) | `pub_badges/` public OpenBadges publishing tree, residual `getImagePath()` disk paths | ~13 | medium | +| **StudyProgramme** | programme **type** icons | object custom icon (Object service), member-export (`ilFSStoragePRG`) | ~5 | medium | +| **DataCollection** | record **file fields** (`rid`) | MOB field media (delegated), XLSX/content export temp | ~7 | low–med | +| **OrgUnit** | **type** icons | import XML reads, export zip | ~5 | low | +| **Calendar** | appointment attachments (read from IRSS) | temp/zip staging for download bundles, ICS import staging | ~13 | low | +| **ILIASObject** | TileImage via IRSS stakeholder | import upload + custom-icon staging on disk first | ~6 | medium | +| **COPage** | (content is DB-native XML) | HTML/offline export builder, TeX-render image cache in `webspace/output`, layout import scratch | ~45 | medium | + +### MIGRATED (persistent content on IRSS; only transient disk left) + +| Component | Notes | +|---|---| +| **File** (`ilObjFile`) | versions/revisions + icons + previews all IRSS. Legacy only in XML import/export interchange, setup default-icon scan, transient chunked upload. | +| **MediaObjects** | mob payload is an IRSS container (`mob_data.rid`) since ILIAS 10 (`ilMobMigration`); writes + reads route through the IRSS-backed manager/repo. Legacy tail: `exportFiles()` rCopy, multi-SRT staging, empty-dir `createDirectory()`, WAC disk fallback. See appendix. | +| **Exercise** | submissions, tutor(team) feedback, instruction files, sample solutions, peer-review criteria all IRSS (+ setup migrations). Legacy = zip/download/temp only. | +| **Forum** | post + draft attachments IRSS (`ResourceCollection`); old `ilFileDataForum` is now a delegating wrapper. Legacy = export dir plumbing. | +| **HTMLLearningModule** | whole HTML package is an IRSS container zip (`file_based_lm.rid`) + `ilHTLMMigration`. Reference pattern for the SCORM family. | +| **AdvancedMetaData** | record file-fields + record-XML exports IRSS (`Record/File/`). Legacy = import staging + one parse temp. | +| **Bibliographic** | source `.bib`/`.ris` in IRSS. Minor direct-Filesystem copy + dataset export leftovers. | +| **IndividualAssessment** | grading files IRSS; legacy `ilIndividualAssessmentFileStorage` marked `@deprecated … only used for migration`. | +| **Poll** | poll images + thumbnails in IRSS with crop flavour + `ilPollImagesMigration`. | +| **WOPI** | pure IRSS consumer (Collabora/OnlyOffice); stores nothing itself. Good reference. | +| **WebDAV** | protocol layer; content flows through IRSS (`IRSSStreamHandler`). Legacy `StreamHandler` fallback remains. | + +### N/A (no own persistent file storage) + +| Component | Why | +|---|---| +| ContentPage, Glossary, Wiki, Blog, Portfolio | content = COPage (DB); media = MediaObjects (IRSS); only transient exports touch disk | +| MediaCast, MediaPool | delegate all media to MediaObjects (IRSS); only transient download/import zips | +| News | media delegated to MediaObjects (IRSS); no direct fs | +| Contact, Notes, OnScreenChat | DB-backed; no file content | +| PersonalWorkspace, WorkspaceFolder | wrap `ilObjFile`; only temp export assembly | +| WebResource | web links in DB; only on-the-fly `bookmarks.html` | +| soap | protocol/adapter layer (+ third-party nuSOAP) | +| Repository | *provides* the Repository-level IRSS facade (`IRSSWrapper`) | +| Group, Excel, Form, RTE | export/temp staging, framework upload plumbing, editor config | +| BackgroundTasks_, Database, Logging, Html, Xml, Init, GlobalScreen, Search, Refinery, UI, UICore, UIComponent | infra/temp/cache/asset/read-only | +| Migration | DB-update helper, never touches fs | + +### INFRA (the storage stack itself) + +| Component | Role | +|---|---| +| **ResourceStorage** | IS IRSS | +| **Filesystem** | IRSS backend (flysystem local adapter); still also ships legacy `ilFileSystemGUI`, `ilFileData`, `ilUploadFiles` | +| **FileUpload** | upload ingest → `UploadResult` consumed by IRSS | +| **VirusScanner** | upload `PreProcessor` (scan scratch only) | +| **FileDelivery** | egress; new `src` is stream/IRSS-aligned, legacy `FileDeliveryTypes` remain (PARTIAL) | +| **WebAccessChecker** | legacy secure `/data` egress, being retired as IRSS makes it obsolete | +| **FileServices** | **hosts the legacy `ilFileUtils` toolbox** (`classes/class.ilFileUtils.php`) used repo-wide + IRSS upload/policy/sanitizer services | +| **Setup** | provisions & validates the on-disk directory layout | + +--- + +## Shared legacy substrates (fix once, many benefit) + +These non-IRSS storage layers are leaned on by multiple components — migrating them unblocks several: + +1. **`ilFileSystemAbstractionStorage`** — object-id-keyed on-disk tree. + Used by: **Course** (`ilFSStorageCourse`), **StudyProgramme** (`ilFSStoragePRG`), **Verification** + (`ilVerificationStorageFile`), Mail (`ilFSStorageMail`), Group (`ilFSStorageGroup`). +2. **Object custom-icon layer** (`webspace/container_data/obj_`, `object.customicons.factory`). + Used by: **Container**, **Category**, **StudyProgramme** (object icon). +3. **`ilFileUtils` toolbox** (`FileServices/classes/class.ilFileUtils.php`) — every `ilFileUtils::` call + in every component resolves here. It is infrastructure, not a per-component store, but retiring it is + the endgame. +4. **Export/Import zip build/extract on data dir** (`ilExport`/`ilExportContainer`/`ilImport`/`ilImportDirectory`) + — core infra; the disk-based assembly step sits under many components' export features. + +**Already covered:** MediaObjects payload (`mobs/mm_`) lives in IRSS since ILIAS 10, so the media of +Glossary / Wiki / Blog / Portfolio / News / MediaCast / MediaPool already sits in IRSS. Those components only +need residual `_getDirectory` / `_getURL` call-site cleanup, not a data migration. + +--- + +## Suggested migration priority + +**Tier 1 — real user content, high value, self-contained-ish** +- **Mail** attachments (deeply disk-coupled, but pure user content) +- **Course** attachments +- **SCORM family** (ScormAicc/Scorm2004/CmiXapi) — follow the **HTMLLearningModule** IRSS-container pattern + (migration already scaffolded on branch `feature/12/scorm-irss`) + +**Tier 2 — clean, bounded, low effort (good "Poll-style" stakeholder migrations)** +- **OpenIdConnect** login image +- **LTIConsumer** provider icons +- **Verification** cert PDFs +- **LearningSequence** intro/extro images +- **DidacticTemplate** icons +- **Badge** `pub_badges/` + +**Tier 3 — finish the PARTIALs** +- **TestQuestionPool** / **Test** question images + archiver (largest surface, ~186 sites combined) +- **Style** `ilObjStyleSheet` legacy path +- **Certificate** portfolio PDF · **User** exports +- **StudyProgramme** / **OrgUnit** object icons + member export + +**Tier 4 — shared substrates & infra** +- Retire `ilFileSystemAbstractionStorage`, the Object custom-icon layer, and eventually the `ilFileUtils` + toolbox; stream-ify Export/Import zip handling; drop `WebAccessChecker` and legacy `FileDeliveryTypes`. + +**Not IRSS targets (leave as-is):** Saml (bundled lib needs real paths), Language files, Chatroom server +config, Authentication domains file, setup probes, DB dumps, logs, caches, and all transient export/zip/temp +scaffolding. + +--- + +## Appendix: MediaObjects — detailed IRSS state + +MediaObjects payload is stored in IRSS, migrated in the ILIAS 10 cycle +(`components/ILIAS/MediaObjects/classes/Setup/class.ilMobMigration.php`, commit `2d78e5a9628`, +2024-10-23, registered in `class.ilMediaObjectSetupAgent.php`). The `mobs/mm_` dir API remains in the code +but is no longer the store of record. + +### What the migration does +- `ilMobMigration::step()` moves `CLIENT_WEB_DIR/mobs/mm_` into an **IRSS container resource** via + `ilResourceStorageMigrationHelper::moveDirectoryToContainerResource()`, persists the `rid` in + `mob_data.rid`, then **deletes the source dir** (`recursiveRmDir`). Empty/absent dirs get `rid = '-'`. + +### Runtime is IRSS-first +- **Writes** — every ingest path routes to the IRSS-backed manager/repo: + `addMediaItemFromUpload/FromLocalFile/FromLegacyUpload`, `replaceMediaItemFromUpload`, + `uploadAdditionalFile` → `MediaObjectManager::addFile*` → `MediaObjectRepository` (IRSS container). +- **Reads** — `MediaObjectManager::getLocalSrc()` → `repo->getLocalSrc()` → + `irss->getContainerUri($rid, $location)` (`MediaObjectRepository.php:186`). Falls back to a WAC-signed + legacy `_getURL()` path only when the container returns nothing (un-migrated / offline). +- `ilObjMediaObject::getXML()` `IL_MODE_OUTPUT` is dual-mode: the `// pre irss file` branch + (`class.ilObjMediaObject.php:522-536`) uses `ilWACSignedPath::signFile()` on the legacy disk file **only + if it still exists**, otherwise `manager->getLocalSrc()` (IRSS). Correct back-compat, not a leak. + +### Residual legacy call-sites (cleanup, ~6) +| Site | `path:line` | Note | +|---|---|---| +| `exportFiles()` | `class.ilObjMediaObject.php:736-737` | `rCopy` from `mobs/mm_`. The central XML export no longer uses it (mob DataSet emits an `rscontainer` field, streamed IRSS→IRSS), but it is **still called by other components' standalone exporters** (Test `ExportImport/Export.php:253,262`, Forum `ilForumXMLWriter.php:204`, Survey `ilSurveyExport.php:123,131`, LearningModule `ilObjContentObject.php:1457,1472`, TestQuestionPool `ilQuestionpoolExport.php:180`). After `ilMobMigration` deletes `mobs/mm_`, those `rCopy` from a missing source → embedded media silently dropped. **Latent cross-component gap.** | +| multi-SRT upload | `getMultiSrtUploadDir():1735`, `uploadMultipleSubtitleFile():1751-1755`, `getMultiSrtFiles():1775` | stages + unzips the multi-VTT zip inside `mobs/mm_/srt/tmp` on disk, then reads it back — bypasses IRSS | +| `createDirectory()` | `:433-440`, called at `:1323` + `ilMediaCreationGUI.php:414/604` + `ilObjMediaPoolGUI.php:1493` | creates an **empty** legacy dir; content is written via the IRSS manager, so the dir/`$mob_dir`/`$file` locals (`:1324-1326`) are vestigial | +| `getVideoPreviewPic(true)` | `:1695-1711` | filename-only lookup still `is_file()`-probes the legacy dir; primary `getVideoPreviewPic()` uses `thumbs->getPreviewSrc()` (IRSS) | +| static path helpers | `_getDirectory():378`, `_getRelativeDirectory():387`, `_getURL():395`, `_lookupItemPath():404`, `getDataDirectory():1301` | still return `mobs/mm_` strings; kept for the fallback + the sites above | + +### Downstream delegators +Glossary / Wiki / Blog / Portfolio / News / MediaCast / MediaPool store their media **as media objects**, so +that content lives in IRSS. Their remaining `_getDirectory` / `_getURL` references +(e.g. `Blog/Posting/class.ilBlogPostingGUI.php:788,806`, `News/classes/class.ilNewsItem.php:1820`, +`MediaCast/classes/class.ilObjMediaCastAccess.php:154` `dirsize`, `MediaPool/classes/class.ilObjMediaPoolGUI.php:1493` +`createDirectory`) hit the fallback/vestigial path — call-site cleanup, not a data migration. + +**Net state:** MediaObjects payload is on IRSS since ILIAS 10; the only functional gap is `exportFiles()` + +multi-SRT staging still touching the now-removed webspace dir. + +--- + +## Appendix: Export component — detailed IRSS state + +**On IRSS:** the finished export **artifact** (the completed zip) plus its **registry, download and +public-access** live in IRSS as a container resource owned by the `export_handler` stakeholder +(`ExportHandler/Repository/Stakeholder/Handler.php:37`), created via +`manageContainer()->containerFromStream()` (`ExportHandler/Repository/Wrapper/IRSS/Handler.php:62`) and +served with `consume()->download()` (`ExportHandler/Repository/Element/Wrapper/IRSS/Handler.php:202-209`). +The old disk-scanned `export_file_info` registry was replaced by table `export_files(object_id, rid, +owner_id, timestamp)`; a one-off migration ingests each existing on-disk zip into IRSS +(`Setup/…/ilExportFilesToIRSSMigration.php:97-125`, registered `Setup/ilExportSetupAgent.php:51`). + +**Still on disk:** the zip is **assembled on a data-dir "export run dir" first**, then streamed into the +IRSS container and deleted (`ExportHandler/Manager/Handler.php:180-204`: +`makeDirParents` → exporters write in → `writeDirectoryRecursive` → `delDir`). Even `createEmptyContainer` +builds a temp zip on disk before ingest (`Repository/Wrapper/IRSS/Handler.php:52-64`). Import extraction is +still fully on the data dir (`ilImport`/`ilImportDirectory`, the latter injecting `ILIAS\Filesystem` directly). + +**One central pipeline, no fallback.** `ilExportGUI` always delegates to the IRSS `ExportHandler` — +`class.ilExportGUI.php:285-286` (`createXMLExport`) and `:333-356` (`createXMLContainerExport`). There is no +runtime `if(legacy) … else …` branch and no feature flag. The legacy `ilExport::exportObject()/exportEntity()` +survive only for a few direct non-GUI callers: Style (`class.ilObjStyleSheetGUI.php:414`), COPage layout admin +(`class.ilPageLayoutAdministrationGUI.php:371`), LearningModule entity export +(`class.ilContObjectExport.php:175`). + +**Per-component exporters are reused, not rewritten.** Both the new and legacy paths resolve the same +`ilExporter` (`ilXmlExporter` subclass) via `ilImportExportFactory::getExporterClass` +(`class.ilImportExportFactory.php:29-65`; new path at `ExportHandler/Manager/Handler.php:73`) and call +`getXmlRepresentation()` + head/tail dependency recursion. Binary payloads are emitted by `ilDataSet` as +typed fields (`Export/DataSet/class.ilDataSet.php:307-348`): +- **`rscollection` / `rscontainer`** → copied **IRSS-container-to-IRSS-container, no disk** + (`Consumer/ExportWriter/Handler.php:161-165`). This is how migrated components (e.g. MediaObjects, + File, Forum) export their binaries. +- legacy **`directory`** / `exportFiles()` output → still written into the disk run-dir, then ingested. + +**Standalone exports** (plugged into `ilExportGUI` as export *options* but managing their own files, mostly +disk via `ExportHandler/Consumer/ExportOption/BasicLegacyHandler.php:56,87` + `deliverFileLegacy`, or fully +independent): HTML exports (`ilCOPageHTMLExport`, `ilWikiUserHTMLExport`, `ilSystemStyleHTMLExport`, +`ilHTLMExportOptionHTML`), Test (`ilTestExportOptionARC`, `ilTestExportOptionXMLRES`), DataCollection +(`ilDataCollectionExportOptionsXLSX`), member exports (`StudyProgramme`, `Membership`). + +**Cross-component gap:** `ilObjMediaObject::exportFiles()` still `rCopy`s from the deleted `mobs/mm_` dir +and is called by the standalone/on-disk exporters of Test, Forum, Survey, LearningModule and TestQuestionPool +— so embedded media in *those* exports can be silently dropped after the mob migration. The central pipeline +is unaffected (it uses the `rscontainer` field). + +--- + +## Migration history by release + +When each component's IRSS storage **first shipped**. Resolved by finding the commit that introduced the +component's earliest IRSS anchor (a `*Stakeholder` class or a Setup IRSS migration) — following file renames +across the pre-ILIAS-10 `Modules/`+`Services/` → `components/ILIAS/` restructure via +`git log --follow -S "class "` — then mapping that commit to the earliest release tag that contains it +(`git tag --contains … | grep '^v(7|8|9|1[0-9])\.[0-9]+$' | sort -V | head -1`; the `N>=7` filter drops a +spurious `v3.8` SVN-import tag). The **release is authoritative** (via `tag --contains`); the **date** is the +commit's author date, which routinely precedes the release by 1–2 years because features land on trunk long +before the release is cut. For components with several stakeholders across releases, this is the *earliest* +one; PARTIAL components added further pieces later and still carry legacy remnants (see the tables above). + +IRSS launched in **ILIAS 7** with `ilObjFile` as the pilot, then spread release by release. + +### ILIAS 7 (v7.0, ~2021) + +| Component | Migrated content | Anchor | Commit | +|---|---|---|---| +| File | file object versions/revisions | `class.ilObjFileStakeholder.php` ("Implemented File Object Migration") | 2020-11-12 | + +### ILIAS 8 (v8.1, ~2023) + +| Component | Migrated content | Anchor | Commit | +|---|---|---|---| +| Bibliographic | source `.bib`/`.ris` file | `class.ilObjBibliographicStakeholder.php` | 2021-07-28 | +| IndividualAssessment | grading files | `class.ilIndiviualAssessmentGradingStakeholder.php` | 2022-01-04 | + +### ILIAS 9 (v9.0, ~2024) + +| Component | Migrated content | Anchor | Commit | +|---|---|---|---| +| Forum | post + draft attachments | `class.ilForumPostingFileStakeholder.php` | 2022-09-14 | +| User | profile pictures / avatars | `class.ilUserProfilePictureStakeholder.php` | 2023-02-28 | +| DataCollection | record file fields | `class.ilDataCollectionStorageMigration.php` | 2023-03-18 | +| Exercise | submissions, feedback, instructions, solutions, criteria | `class.ilExcInstructionFilesStakeholder.php` | 2023-08-29 | +| TestQuestionPool | `assFileUpload` answer files | `class.ilTestQuestionPoolFileUploadQuestionMigration.php` | 2023-10-06 | + +### ILIAS 10 (v10.0, ~2025) + +| Component | Migrated content | Anchor | Commit | +|---|---|---|---| +| StudyProgramme | programme type icons | `ilStudyProgrammeTypeStakeholder.php` | 2023-11-24 | +| OrgUnit | type icons | `ilOrgUnitTypeStakeholder.php` | 2023-11-28 | +| Calendar | appointment attachments *(consumer — reads IRSS)* | `class.ilCalendarCopyFilesToTempDirectoryJob.php` | 2023-12-14 | +| WOPI | edited office documents | `WOPIStakeholder` | 2024-05-02 | +| HTMLLearningModule | HTML package container | `class.ilHTLMMigration.php` | 2024-09-13 | +| Style | content styles + images | `class.ilContentStyleStakeholder.php` | 2024-10-22 | +| Poll | poll images | `class.ilPollImagesMigration.php` | 2024-10-22 | +| MediaObjects | mob payload → IRSS container | `class.ilMobMigration.php` | 2024-10-23 | +| AdvancedMetaData | record file-fields + record-XML | `Record/File/.../Stakeholder`, `RecordFilesMigration.php` | 2024-10-24 | +| Certificate | template files + background/tile images | `ilCertificateTemplateStakeholder.php` | 2024-10-24 | +| Badge | badge + template images | `ilBadgeTemplatesFilesMigration.php` | 2024-10-25 | + +### ILIAS 11 (v11.0, ~2026) + +| Component | Migrated content | Anchor | Commit | +|---|---|---|---| +| Export | export HTML + finished artifact/registry | `class.ilExportHTMLStakeholder.php`, `ilExportFilesToIRSSMigration.php` | 2025-01-07 | +| Test | results / export / PDF-archive artifacts | `ResultsExportStakeholder.php` | 2025-03-22 | +| ILIASObject | TileImage | `Properties/CoreProperties/TileImage/Stakeholder.php` | 2025-03-25 | +| Mail | compose-time attachment upload (bridge) | `class.ilMailAttachmentStakeholder.php` | 2025-06-16 | + +### trunk (unreleased — ILIAS 12) + +| Component | Migrated content | Anchor | Commit | +|---|---|---|---| +| WebDAV | routes object content through IRSS *(consumer)* | `src/Objects/IRSSStreamHandler.php` | 2026-05-01 | + +**Not yet migrated (no release):** every component in the LEGACY table above (Course, ScormAicc, Scorm2004, +LearningModule, Survey, SurveyQuestionPool, CmiXapi, Verification, Language, Container, DidacticTemplate, +LearningSequence, Saml, OpenIdConnect, LTIConsumer, Authentication, Chatroom, Category, WebServices) has no +IRSS migration. From 60ef11cfaf9202b32c7f7f731aed6bb462147642 Mon Sep 17 00:00:00 2001 From: Fabian Schmid Date: Tue, 30 Jun 2026 09:56:15 +0200 Subject: [PATCH 036/333] [FIX] 0045055: Cannot overwrite a feedback file by uploading a new version, instead 2 files are created --- .../Service/IRSS/CollectionWrapperGUI.php | 6 +- .../Collections/View/Configuration.php | 6 +- .../classes/Collections/View/OnDuplicate.php | 54 +++++ .../classes/Collections/View/Request.php | 4 +- .../classes/Collections/View/UploadStorer.php | 89 ++++++++ .../class.ilResourceCollectionGUI.php | 32 +-- .../Collections/View/ConfigurationTest.php | 72 ++++++ .../Collections/View/UploadStorerTest.php | 211 ++++++++++++++++++ 8 files changed, 447 insertions(+), 27 deletions(-) create mode 100644 components/ILIAS/ResourceStorage/classes/Collections/View/OnDuplicate.php create mode 100644 components/ILIAS/ResourceStorage/classes/Collections/View/UploadStorer.php create mode 100644 components/ILIAS/ResourceStorage/tests/Collections/View/ConfigurationTest.php create mode 100644 components/ILIAS/ResourceStorage/tests/Collections/View/UploadStorerTest.php diff --git a/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php b/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php index f1e68d09e6b8..389d2f7af737 100755 --- a/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php +++ b/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php @@ -23,6 +23,7 @@ use ILIAS\ResourceStorage\Stakeholder\ResourceStakeholder; use ILIAS\components\ResourceStorage\Collections\View\Configuration; use ILIAS\components\ResourceStorage\Collections\View\Mode; +use ILIAS\components\ResourceStorage\Collections\View\OnDuplicate; class CollectionWrapperGUI { @@ -38,7 +39,8 @@ public function getResourceCollectionGUI( ResourceStakeholder $stakeholder, string $rcid, string $caption, - bool $write = false + bool $write = false, + OnDuplicate $on_duplicate = OnDuplicate::REPLACE ): \ilResourceCollectionGUI { if ($rcid === "") { throw new \LogicException("No resource collection ID given."); @@ -53,7 +55,7 @@ public function getResourceCollectionGUI( 100, $write, $write, - true + $on_duplicate ) ); } diff --git a/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php b/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php index fe8d979cad08..3b63cd13dd23 100755 --- a/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php +++ b/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php @@ -36,7 +36,7 @@ public function __construct( private int $items_per_page = 100, private bool $user_can_upload = false, private bool $user_can_administrate = false, - private bool $append_duplicate_filenames_as_revision = false + private OnDuplicate $on_duplicate = OnDuplicate::ALLOW ) { } @@ -80,8 +80,8 @@ public function canUserAdministrate(): bool return $this->user_can_administrate; } - public function preventDuplicates(): bool + public function getOnDuplicate(): OnDuplicate { - return $this->append_duplicate_filenames_as_revision; + return $this->on_duplicate; } } diff --git a/components/ILIAS/ResourceStorage/classes/Collections/View/OnDuplicate.php b/components/ILIAS/ResourceStorage/classes/Collections/View/OnDuplicate.php new file mode 100644 index 000000000000..2c4d40f6aede --- /dev/null +++ b/components/ILIAS/ResourceStorage/classes/Collections/View/OnDuplicate.php @@ -0,0 +1,54 @@ + + */ +enum OnDuplicate: int +{ + /** + * Allow duplicates: the file is always stored as a new, separate resource, + * even if another resource with the same name already exists. + */ + case ALLOW = 1; + + /** + * On duplicate, overwrite the existing resource with a new revision and + * delete all previous revisions (no history is kept). + */ + case REPLACE = 2; + + /** + * On duplicate, overwrite the existing resource by appending a new revision + * while keeping the previous revisions as history. + */ + case APPEND_REVISION = 3; + + /** + * On duplicate, reject the uploaded file: the existing resource is left + * untouched and the new file is not stored. + */ + case REJECT = 4; +} diff --git a/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php b/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php index ddc3cdb2144a..b4ed099c887a 100755 --- a/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php +++ b/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php @@ -198,8 +198,8 @@ public function canUserAdministrate(): bool return $this->view_configuration->canUserAdministrate(); } - public function preventDuplicates(): bool + public function getOnDuplicate(): OnDuplicate { - return $this->view_configuration->preventDuplicates(); + return $this->view_configuration->getOnDuplicate(); } } diff --git a/components/ILIAS/ResourceStorage/classes/Collections/View/UploadStorer.php b/components/ILIAS/ResourceStorage/classes/Collections/View/UploadStorer.php new file mode 100644 index 000000000000..a648b2cc6547 --- /dev/null +++ b/components/ILIAS/ResourceStorage/classes/Collections/View/UploadStorer.php @@ -0,0 +1,89 @@ + + */ +final readonly class UploadStorer +{ + public function __construct( + private Manager $manage, + private Collections $collections + ) { + } + + /** + * @return ResourceIdentification|null the identification of the affected + * resource, or null if the upload was rejected (OnDuplicate::REJECT) + * and therefore not stored. + */ + public function store( + ResourceCollection $collection, + ResourceStakeholder $stakeholder, + OnDuplicate $on_duplicate, + UploadResult $result + ): ?ResourceIdentification { + // an existing resource with the same name is only relevant if duplicates + // are not simply allowed + $existing_rid = $on_duplicate === OnDuplicate::ALLOW + ? null + : $this->collections->findIdentificationByNameIn($collection, $result->getName()); + + if ($existing_rid === null) { + // no name clash (or duplicates allowed): store as a new, separate resource + $rid = $this->manage->upload($result, $stakeholder); + $collection->add($rid); + return $rid; + } + + switch ($on_duplicate) { + case OnDuplicate::REJECT: + // leave the existing resource untouched, do not store the upload + return null; + case OnDuplicate::REPLACE: + // overwrite with a new revision and drop all previous revisions + $this->manage->replaceWithUpload($existing_rid, $result, $stakeholder); + return $existing_rid; + case OnDuplicate::APPEND_REVISION: + // overwrite by appending a new revision while keeping the previous ones as history + $this->manage->appendNewRevision($existing_rid, $result, $stakeholder); + return $existing_rid; + } + + // OnDuplicate::ALLOW never reaches this point ($existing_rid is null above) + return $existing_rid; + } +} diff --git a/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php b/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php index 3d5afb0d39a1..f6d83838aeb1 100755 --- a/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php +++ b/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php @@ -29,6 +29,7 @@ use ILIAS\FileUpload\Handler\FileInfoResult; use ILIAS\components\ResourceStorage\Collections\View\Configuration; use ILIAS\components\ResourceStorage\Collections\View\Request; +use ILIAS\components\ResourceStorage\Collections\View\UploadStorer; use ILIAS\components\ResourceStorage\Collections\View\ViewFactory; use ILIAS\components\ResourceStorage\Collections\DataProvider\TableDataProvider; use ILIAS\components\ResourceStorage\BinToHexSerializer; @@ -225,30 +226,21 @@ public function upload(): void return; } $collection = $this->view_request->getCollection(); + $stakeholder = $this->view_configuration->getStakeholder(); + $on_duplicate = $this->view_request->getOnDuplicate(); + $storer = new UploadStorer($this->irss->manage(), $this->irss->collection()); + $rid = null; foreach ($this->upload->getResults() as $result) { if (!$result->isOK()) { continue; } - // if activated, prevent duplicate files by checking filenames. in thjis case a new revision gets appended - if ($this->view_request->preventDuplicates()) { - $existing_rid = $this->irss->collection()->findIdentificationByNameIn( - $this->view_request->getCollection(), - $result->getName() - ); - if ($existing_rid !== null) { - $this->irss->manage()->appendNewRevision( - $existing_rid, - $upload_result, - $this->view_configuration->getStakeholder() - ); - } - } - $rid = $existing_rid ?? $this->irss->manage()->upload( - $result, - $this->view_configuration->getStakeholder() - ); - $collection->add($rid); + $stored_rid = $storer->store($collection, $stakeholder, $on_duplicate, $result); + if ($stored_rid === null) { + // the upload was rejected (OnDuplicate::REJECT), nothing was stored + continue; + } + $rid = $stored_rid; // ensure flavour $this->irss->flavours()->ensure( @@ -260,7 +252,7 @@ public function upload(): void $upload_result = new BasicHandlerResult( self::P_RESOURCE_ID, BasicHandlerResult::STATUS_OK, - $rid->serialize(), + $rid?->serialize() ?? '', '' ); $response = $this->http->response()->withBody(Streams::ofString(json_encode($upload_result))); diff --git a/components/ILIAS/ResourceStorage/tests/Collections/View/ConfigurationTest.php b/components/ILIAS/ResourceStorage/tests/Collections/View/ConfigurationTest.php new file mode 100644 index 000000000000..0e6d6f1751e9 --- /dev/null +++ b/components/ILIAS/ResourceStorage/tests/Collections/View/ConfigurationTest.php @@ -0,0 +1,72 @@ + + */ +final class ConfigurationTest extends TestCase +{ + public function testDefaultOnDuplicateIsAllow(): void + { + $configuration = new Configuration( + $this->createStub(ResourceCollection::class), + $this->createStub(ResourceStakeholder::class), + 'title' + ); + + $this->assertSame(OnDuplicate::ALLOW, $configuration->getOnDuplicate()); + } + + #[DataProvider('onDuplicateProvider')] + public function testOnDuplicateIsPassedThrough(OnDuplicate $on_duplicate): void + { + $configuration = new Configuration( + $this->createStub(ResourceCollection::class), + $this->createStub(ResourceStakeholder::class), + 'title', + Mode::DATA_TABLE, + 100, + true, + true, + $on_duplicate + ); + + $this->assertSame($on_duplicate, $configuration->getOnDuplicate()); + } + + public static function onDuplicateProvider(): \Iterator + { + yield 'allow' => [OnDuplicate::ALLOW]; + yield 'replace' => [OnDuplicate::REPLACE]; + yield 'append revision' => [OnDuplicate::APPEND_REVISION]; + yield 'reject' => [OnDuplicate::REJECT]; + } +} diff --git a/components/ILIAS/ResourceStorage/tests/Collections/View/UploadStorerTest.php b/components/ILIAS/ResourceStorage/tests/Collections/View/UploadStorerTest.php new file mode 100644 index 000000000000..1c6a2e1e3a0f --- /dev/null +++ b/components/ILIAS/ResourceStorage/tests/Collections/View/UploadStorerTest.php @@ -0,0 +1,211 @@ + + */ +final class UploadStorerTest extends TestCase +{ + private const FILE_NAME = 'feedback.pdf'; + + private Manager&MockObject $manage; + private Collections&MockObject $collections; + private ResourceCollection&MockObject $collection; + private ResourceStakeholder&MockObject $stakeholder; + private UploadResult $result; + private UploadStorer $storer; + + protected function setUp(): void + { + parent::setUp(); + $this->manage = $this->createMock(Manager::class); + $this->collections = $this->createMock(Collections::class); + $this->collection = $this->createMock(ResourceCollection::class); + $this->stakeholder = $this->createMock(ResourceStakeholder::class); + $this->result = new UploadResult( + self::FILE_NAME, + 123, + 'application/pdf', + new EntryLockingStringMap(), + new ProcessingStatus(ProcessingStatus::OK, 'ok'), + 'dummy/path' + ); + $this->storer = new UploadStorer($this->manage, $this->collections); + } + + // ALLOW: never dedupes, always stores a new, separate resource + + public function testAllowStoresNewResourceWithoutLookupEvenWhenNameExists(): void + { + $new_rid = new ResourceIdentification('new'); + + // ALLOW must not even ask whether a same-name resource exists + $this->collections->expects($this->never())->method('findIdentificationByNameIn'); + $this->manage->expects($this->never())->method('replaceWithUpload'); + $this->manage->expects($this->never())->method('appendNewRevision'); + + $this->manage->expects($this->once()) + ->method('upload') + ->with($this->result, $this->stakeholder) + ->willReturn($new_rid); + $this->collection->expects($this->once())->method('add')->with($new_rid); + + $this->assertSame( + $new_rid, + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::ALLOW, $this->result) + ); + } + + // REJECT: on a name clash nothing is stored, the existing resource is untouched + + public function testRejectLeavesExistingResourceUntouchedAndStoresNothing(): void + { + $existing_rid = new ResourceIdentification('existing'); + $this->givenExistingResource($existing_rid); + + $this->manage->expects($this->never())->method('upload'); + $this->manage->expects($this->never())->method('replaceWithUpload'); + $this->manage->expects($this->never())->method('appendNewRevision'); + $this->collection->expects($this->never())->method('add'); + + $this->assertNull( + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::REJECT, $this->result) + ); + } + + public function testRejectStoresNewResourceWhenNoNameClash(): void + { + $new_rid = new ResourceIdentification('new'); + $this->givenNoExistingResource(); + + $this->manage->expects($this->never())->method('replaceWithUpload'); + $this->manage->expects($this->never())->method('appendNewRevision'); + $this->manage->expects($this->once())->method('upload')->willReturn($new_rid); + $this->collection->expects($this->once())->method('add')->with($new_rid); + + $this->assertSame( + $new_rid, + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::REJECT, $this->result) + ); + } + + // REPLACE: on a name clash overwrite the existing resource, drop the history + + public function testReplaceOverwritesExistingResource(): void + { + $existing_rid = new ResourceIdentification('existing'); + $this->givenExistingResource($existing_rid); + + $this->manage->expects($this->never())->method('upload'); + $this->manage->expects($this->never())->method('appendNewRevision'); + $this->collection->expects($this->never())->method('add'); + $this->manage->expects($this->once()) + ->method('replaceWithUpload') + ->with($existing_rid, $this->result, $this->stakeholder) + ->willReturn($this->createStub(Revision::class)); + + $this->assertSame( + $existing_rid, + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::REPLACE, $this->result) + ); + } + + public function testReplaceStoresNewResourceWhenNoNameClash(): void + { + $new_rid = new ResourceIdentification('new'); + $this->givenNoExistingResource(); + + $this->manage->expects($this->never())->method('replaceWithUpload'); + $this->manage->expects($this->once())->method('upload')->willReturn($new_rid); + $this->collection->expects($this->once())->method('add')->with($new_rid); + + $this->assertSame( + $new_rid, + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::REPLACE, $this->result) + ); + } + + // APPEND_REVISION: on a name clash append a new revision, keep the history + + public function testAppendRevisionAddsRevisionToExistingResource(): void + { + $existing_rid = new ResourceIdentification('existing'); + $this->givenExistingResource($existing_rid); + + $this->manage->expects($this->never())->method('upload'); + $this->manage->expects($this->never())->method('replaceWithUpload'); + $this->collection->expects($this->never())->method('add'); + $this->manage->expects($this->once()) + ->method('appendNewRevision') + ->with($existing_rid, $this->result, $this->stakeholder) + ->willReturn($this->createStub(Revision::class)); + + $this->assertSame( + $existing_rid, + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::APPEND_REVISION, $this->result) + ); + } + + public function testAppendRevisionStoresNewResourceWhenNoNameClash(): void + { + $new_rid = new ResourceIdentification('new'); + $this->givenNoExistingResource(); + + $this->manage->expects($this->never())->method('appendNewRevision'); + $this->manage->expects($this->once())->method('upload')->willReturn($new_rid); + $this->collection->expects($this->once())->method('add')->with($new_rid); + + $this->assertSame( + $new_rid, + $this->storer->store($this->collection, $this->stakeholder, OnDuplicate::APPEND_REVISION, $this->result) + ); + } + + private function givenExistingResource(ResourceIdentification $existing_rid): void + { + $this->collections->method('findIdentificationByNameIn') + ->with($this->collection, self::FILE_NAME) + ->willReturn($existing_rid); + } + + private function givenNoExistingResource(): void + { + $this->collections->method('findIdentificationByNameIn') + ->with($this->collection, self::FILE_NAME) + ->willReturn(null); + } +} From 652a8d98d72e612e25ce3dd37610eb15779d8a5b Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Fri, 3 Jul 2026 10:28:35 +0200 Subject: [PATCH 037/333] EmployeeTalk: fix deletion process (47934) --- .../Talk/class.ilEmployeeTalkAppointmentGUI.php | 6 ++++++ .../classes/Talk/class.ilObjEmployeeTalkGUI.php | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/EmployeeTalk/classes/Talk/class.ilEmployeeTalkAppointmentGUI.php b/components/ILIAS/EmployeeTalk/classes/Talk/class.ilEmployeeTalkAppointmentGUI.php index 5fe1af5663db..3d3e754e819f 100755 --- a/components/ILIAS/EmployeeTalk/classes/Talk/class.ilEmployeeTalkAppointmentGUI.php +++ b/components/ILIAS/EmployeeTalk/classes/Talk/class.ilEmployeeTalkAppointmentGUI.php @@ -46,6 +46,7 @@ final class ilEmployeeTalkAppointmentGUI implements ControlFlowCommandHandler private Refinery $refinery; private ilTabsGUI $tabs; protected NotificationHandlerInterface $notif_handler; + private ilTree $tree; private ilObjEmployeeTalk $talk; public function __construct( @@ -56,6 +57,7 @@ public function __construct( Refinery $refinery, ilTabsGUI $tabs, NotificationHandlerInterface $notif_handler, + ilTree $tree, ilObjEmployeeTalk $talk ) { $this->template = $template; @@ -65,6 +67,7 @@ public function __construct( $this->refinery = $refinery; $this->tabs = $tabs; $this->notif_handler = $notif_handler; + $this->tree = $tree; $this->talk = $talk; $this->language->loadLanguageModule('cal'); @@ -534,7 +537,10 @@ private function getPendingTalksInSeries(ilObjEmployeeTalkSeries $series): array private function deleteTalks(array $talks): void { foreach ($talks as $talk) { + $ref_id = $talk->getRefId(); + $node_data = $this->tree->getNodeData($talk->getRefId()); $talk->delete(); + $this->tree->deleteNode($node_data['tree'], $ref_id); } } diff --git a/components/ILIAS/EmployeeTalk/classes/Talk/class.ilObjEmployeeTalkGUI.php b/components/ILIAS/EmployeeTalk/classes/Talk/class.ilObjEmployeeTalkGUI.php index 96d014ec8df9..caa7f9a29375 100755 --- a/components/ILIAS/EmployeeTalk/classes/Talk/class.ilObjEmployeeTalkGUI.php +++ b/components/ILIAS/EmployeeTalk/classes/Talk/class.ilObjEmployeeTalkGUI.php @@ -144,6 +144,7 @@ public function executeCommand(): void $this->refinery, $this->tabs_gui, $this->notif_handler, + $this->tree, $this->object ); $this->ctrl->forwardCommand($appointmentGUI); @@ -224,17 +225,20 @@ public function confirmedDeleteObject(): void return; } + $ref_ids = []; if ($this->post_wrapper->has("interruptive_items")) { - $ref_id = $this->post_wrapper->retrieve( + $ref_ids = $this->post_wrapper->retrieve( "interruptive_items", $this->refinery->kindlyTo()->listOf($this->refinery->kindlyTo()->int()) ); - $saved_post = array_unique(array_merge(ilSession::get('saved_post') ?? [], $ref_id)); - ilSession::set('saved_post', $saved_post); + } + + if ($ref_ids === []) { + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('no_checkbox'), true); + $this->redirectToParentGUI(); } $ru = new ilRepositoryTrashGUI($this); - $ref_ids = ilSession::get("saved_post"); $talks = []; foreach ($ref_ids as $refId) { From d175660ec11e29145eb1dd32613f954dd93f72c0 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 3 Jul 2026 12:13:28 +0200 Subject: [PATCH 038/333] [FIX] 47851: Evaluation statement text is not saved after < sign (KS textarea) --- .../Service/Form/TagsSpaceTransformation.php | 45 +++++++++++++++++++ .../Service/Form/class.FormAdapterGUI.php | 8 +++- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 components/ILIAS/Repository/Service/Form/TagsSpaceTransformation.php diff --git a/components/ILIAS/Repository/Service/Form/TagsSpaceTransformation.php b/components/ILIAS/Repository/Service/Form/TagsSpaceTransformation.php new file mode 100644 index 000000000000..efc1061a17d0 --- /dev/null +++ b/components/ILIAS/Repository/Service/Form/TagsSpaceTransformation.php @@ -0,0 +1,45 @@ +values[$key] = $value; - $field = $this->ui->factory()->input()->field()->text($title, $description); + $field = $this->ui->factory()->input()->field()->text($title, $description) + ->withoutStripTags() + ->withAdditionalTransformation(new TagsSpaceTransformation()); if ($max_length > 0) { $field = $field->withMaxLength($max_length); } @@ -253,7 +255,9 @@ public function textarea( ?string $value = null ): self { $this->values[$key] = $value; - $field = $this->ui->factory()->input()->field()->textarea($title, $description); + $field = $this->ui->factory()->input()->field()->textarea($title, $description) + ->withoutStripTags() + ->withAdditionalTransformation(new TagsSpaceTransformation()); if (!is_null($value)) { $field = $field->withValue($value); } From 7b491d03171c60fcf6c397edf34cbc1bbdefabca Mon Sep 17 00:00:00 2001 From: lscharmer <52695099+lscharmer@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:58:33 +0200 Subject: [PATCH 039/333] [FIX] UI: remove unused `moment-with-locales.min.js` include (#11716) --- .../ILIAS/UI/src/Implementation/Component/Button/Renderer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/components/ILIAS/UI/src/Implementation/Component/Button/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Button/Renderer.php index 39cdf90d444e..f52c0e294aa1 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Button/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Button/Renderer.php @@ -177,7 +177,6 @@ public function registerResources(ResourceRegistry $registry): void { parent::registerResources($registry); $registry->register('assets/js/button.js'); - $registry->register("./assets/js/moment-with-locales.min.js"); } protected function renderClose(Component\Button\Close $component): string From c99e49f19c406bb13d9b11d0bb05cd39556b6e99 Mon Sep 17 00:00:00 2001 From: iszmais <45942348+iszmais@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:03:35 +0200 Subject: [PATCH 040/333] fix text sorting in datacollection (#11722) --- .../Fields/Text/class.ilDclTextFieldModel.php | 2 +- .../Text/class.ilDclTextRecordFieldModel.php | 4 +-- .../class.ilDataCollectionDBUpdateSteps10.php | 27 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextFieldModel.php b/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextFieldModel.php index 9e0fd02ac6a1..69c0d86fbd71 100755 --- a/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextFieldModel.php +++ b/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextFieldModel.php @@ -46,8 +46,8 @@ public function checkValidityFromForm(ilPropertyFormGUI &$form, ?int $record_id) { if ($this->getProperty(ilDclBaseFieldModel::PROP_URL)) { $value = [ - 'link' => $form->getInput("field_" . $this->getId()), 'title' => $form->getInput("field_" . $this->getId() . "_title"), + 'link' => $form->getInput("field_" . $this->getId()), ]; } else { $value = $form->getInput('field_' . $this->getId()); diff --git a/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextRecordFieldModel.php b/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextRecordFieldModel.php index 63ee41fde0c1..09d956f935c7 100755 --- a/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextRecordFieldModel.php +++ b/components/ILIAS/DataCollection/classes/Fields/Text/class.ilDclTextRecordFieldModel.php @@ -26,8 +26,8 @@ public function setValueFromForm(ilPropertyFormGUI $form): void { if ($this->getField()->hasProperty(ilDclBaseFieldModel::PROP_URL)) { $value = [ - "link" => $form->getInput("field_" . $this->getField()->getId()), "title" => $form->getInput("field_" . $this->getField()->getId() . '_title'), + "link" => $form->getInput("field_" . $this->getField()->getId()), ]; } else { $value = $form->getInput("field_" . $this->getField()->getId()); @@ -116,7 +116,7 @@ public function getValueFromExcel(ilExcel $excel, int $row, int $col) if ($excel->getCell(1, $col + 1) == $this->getField()->getTitle() . '_title') { $title = $excel->getCell($row, $col + 1); } - $value = ['link' => $value, 'title' => $title]; + $value = ['title' => $title, 'link' => $value]; } if ($value) { diff --git a/components/ILIAS/DataCollection/classes/Setup/class.ilDataCollectionDBUpdateSteps10.php b/components/ILIAS/DataCollection/classes/Setup/class.ilDataCollectionDBUpdateSteps10.php index b530c8186c50..b4b6d9a2a69e 100644 --- a/components/ILIAS/DataCollection/classes/Setup/class.ilDataCollectionDBUpdateSteps10.php +++ b/components/ILIAS/DataCollection/classes/Setup/class.ilDataCollectionDBUpdateSteps10.php @@ -56,4 +56,31 @@ public function step_1(): void ['text_area'] ); } + + public function step_2(): void + { + $stmt = $this->db->queryF( + 'SELECT il_dcl_stloc1_value.* FROM il_dcl_stloc1_value ' . + 'INNER JOIN il_dcl_record_field ON il_dcl_record_field.id = il_dcl_stloc1_value.record_field_id ' . + 'INNER JOIN il_dcl_field ON il_dcl_field.id = il_dcl_record_field.field_id ' . + 'WHERE il_dcl_field.datatype_id = %s AND il_dcl_stloc1_value.value LIKE %s', + [ilDBConstants::T_INTEGER, ilDBConstants::T_TEXT], + [ilDclDatatype::INPUTFORMAT_TEXT, "{%"] + ); + + while ($row = $this->db->fetchAssoc($stmt)) { + $old = json_decode($row['value'], true); + if (isset($old['title']) && isset($old['link'])) { + $value = json_encode([ + 'title' => $old['title'], + 'link' => $old['link'], + ]); + $this->db->update( + 'il_dcl_stloc1_value', + ['value' => [ilDBConstants::T_TEXT, $value]], + ['id' => [ilDBConstants::T_INTEGER, $row['id']]], + ); + } + } + } } From 8b48c5b42bcbb79a48e02e963dde130ee79e0e39 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 3 Jul 2026 16:46:41 +0200 Subject: [PATCH 041/333] [Fix] Exercise, #47925: Action Evaluation by File does not update evaluation date for Team Uploads --- .../TutorFeedbackFileManager.php | 28 ++++++++++------- .../TutorFeedbackFileRepository.php | 13 +++++--- .../TutorFeedbackFileRepositoryInterface.php | 2 +- .../TutorFeedbackFileTeamRepository.php | 30 +++++++++++++++---- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileManager.php b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileManager.php index 3f3d1e45a94a..a3b8f05fa5b7 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileManager.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileManager.php @@ -69,6 +69,8 @@ public function getStakeholder(): ResourceStakeholder public function addObserver(): void { + $log = $this->domain->log(); + $log->debug("------------- addObserver ---------------"); $this->domain->resourceStorage()->events()->attach( $this->file_observer, Event::COLLECTION_RESOURCE_ADDED @@ -78,6 +80,7 @@ public function addObserver(): void public function sendNotification(string $rcid, string $rid): void { $log = $this->domain->log(); + $log->debug("------------- sendNotification ---------------"); $log->debug("Ass id: " . $this->ass_id); $exc_id = \ilExAssignment::lookupExerciseId($this->ass_id); @@ -91,21 +94,26 @@ public function sendNotification(string $rcid, string $rid): void $log->debug("Get notification"); $notification = $this->domain->notification($ref_id); $log->debug("Get participant"); + // this might be a team id (or a user id) $part_id = $this->repo->getParticipantIdForRcid($this->ass_id, $rcid); + // this is a user id (might be first from team) + $user_id = $this->repo->getUserIdForRcid($this->ass_id, $rcid); $log->debug("Get filename"); - $filename = $this->repo->getFilenameForRid($this->ass_id, $part_id, $rid); + $filename = $this->repo->getFilenameForRid($this->ass_id, $user_id, $rid); $log->debug("Get assignment"); $ass = new \ilExAssignment($this->ass_id); $log->debug("Get submission"); - $submission = new \ilExSubmission($ass, $part_id); + $log->debug("Part id: " . $part_id); + $log->debug("User id: " . $user_id); + $submission = new \ilExSubmission($ass, $user_id); $feedback_id = $submission->getFeedbackId(); $noti_rec_ids = $submission->getUserIds(); $log->debug("Feedback id: " . $feedback_id); if ($feedback_id) { if ($noti_rec_ids) { - foreach ($noti_rec_ids as $user_id) { - $member_status = $ass->getMemberStatus($user_id); + foreach ($noti_rec_ids as $note_user_id) { + $member_status = $ass->getMemberStatus($note_user_id); $member_status->setFeedback(true); $member_status->update(); } @@ -177,27 +185,27 @@ public function deleteCollection(int $participant_id): void ); } - public function getFiles(int $participant_id): array + public function getFiles(int $user_id): array { $files = []; - if ($this->repo->hasCollection($this->ass_id, $participant_id)) { + if ($this->repo->hasCollection($this->ass_id, $user_id)) { $files = array_map(function (ResourceInformation $info): string { return $info->getTitle(); - }, iterator_to_array($this->repo->getCollectionResourcesInfo($this->ass_id, $participant_id))); + }, iterator_to_array($this->repo->getCollectionResourcesInfo($this->ass_id, $user_id))); } return $files; } - public function deliver(int $participant_id, string $file): void + public function deliver(int $user_id, string $file): void { $assignment = $this->domain->assignment()->getAssignment($this->ass_id); if ($assignment->notStartedYet()) { return; } - if ($this->repo->hasCollection($this->ass_id, $participant_id)) { + if ($this->repo->hasCollection($this->ass_id, $user_id)) { // IRSS - $this->repo->deliverFile($this->ass_id, $participant_id, $file); + $this->repo->deliverFile($this->ass_id, $user_id, $file); } } diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php index 53db9beb92bc..01b0acc01b36 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php @@ -102,10 +102,10 @@ public function count(int $ass_id, int $user_id): int return 0; } - public function deliverFile($ass_id, $participant_id, $file): void + public function deliverFile($ass_id, $user_id, $file): void { /** @var ResourceInformation $info */ - foreach ($this->getCollectionResourcesInfo($ass_id, $participant_id) as $info) { + foreach ($this->getCollectionResourcesInfo($ass_id, $user_id) as $info) { if ($file === $info->getTitle()) { $this->wrapper->deliverFile($info->getRid()); } @@ -113,9 +113,9 @@ public function deliverFile($ass_id, $participant_id, $file): void throw new \ilExerciseException("Resource $file not found."); } - public function getFilenameForRid(int $ass_id, int $part_id, string $rid): string + public function getFilenameForRid(int $ass_id, int $user_id, string $rid): string { - foreach ($this->getCollectionResourcesInfo($ass_id, $part_id) as $info) { + foreach ($this->getCollectionResourcesInfo($ass_id, $user_id) as $info) { if ($rid === $info->getRid()) { return $info->getTitle(); } @@ -135,6 +135,11 @@ public function getParticipantIdForRcid(int $ass_id, string $rcid): int return (int) ($rec["usr_id"] ?? 0); } + public function getUserIdForRcid(int $ass_id, string $rcid): int + { + return $this->getParticipantIdForRcid($ass_id, $rcid); + } + /** * @return \Iterator */ diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepositoryInterface.php b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepositoryInterface.php index 6c73ef068813..f5a8a997c740 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepositoryInterface.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepositoryInterface.php @@ -46,6 +46,6 @@ public function deleteCollection( public function getParticipantIdForRcid(int $ass_id, string $rcid): int; - public function getFilenameForRid(int $ass_id, int $part_id, string $rid): string; + public function getFilenameForRid(int $ass_id, int $user_id, string $rid): string; } diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileTeamRepository.php b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileTeamRepository.php index 319e09d3e608..18f2313bd025 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileTeamRepository.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileTeamRepository.php @@ -24,6 +24,7 @@ use ILIAS\ResourceStorage\Collection\ResourceCollection; use ILIAS\ResourceStorage\Stakeholder\ResourceStakeholder; use ILIAS\Repository\IRSS\ResourceInformation; +use ilLoggerFactory; class TutorFeedbackFileTeamRepository implements TutorFeedbackFileRepositoryInterface { @@ -53,6 +54,20 @@ protected function getTeamId(int $ass_id, int $user_id): int return 0; } + protected function getOneUserIdOfTeam(int $ass_id, int $team_id): int + { + $set = $this->db->queryF( + "SELECT user_id FROM il_exc_team " . + " WHERE ass_id = %s AND id = %s", + ["integer", "integer"], + [$ass_id, $team_id] + ); + if ($rec = $this->db->fetchAssoc($set)) { + return (int) $rec["user_id"]; + } + return 0; + } + public function createCollection(int $ass_id, int $user_id): void { $team_id = $this->getTeamId($ass_id, $user_id); @@ -83,6 +98,12 @@ public function getParticipantIdForRcid(int $ass_id, string $rcid): int return (int) ($rec["id"] ?? 0); } + public function getUserIdForRcid(int $ass_id, string $rcid): int + { + $team_id = $this->getParticipantIdForRcid($ass_id, $rcid); + return $this->getOneUserIdOfTeam($ass_id, $team_id); + } + public function getIdStringForAssIdAndUserId(int $ass_id, int $user_id): string { @@ -123,10 +144,10 @@ public function count(int $ass_id, int $user_id): int return 0; } - public function deliverFile($ass_id, $participant_id, $file): void + public function deliverFile($ass_id, $user_id, $file): void { /** @var ResourceInformation $info */ - foreach ($this->getCollectionResourcesInfo($ass_id, $participant_id) as $info) { + foreach ($this->getCollectionResourcesInfo($ass_id, $user_id) as $info) { if ($file === $info->getTitle()) { $this->wrapper->deliverFile($info->getRid()); } @@ -134,11 +155,10 @@ public function deliverFile($ass_id, $participant_id, $file): void throw new \ilExerciseException("Resource $file not found."); } - public function getFilenameForRid(int $ass_id, int $part_id, string $rid): string + public function getFilenameForRid(int $ass_id, int $user_id, string $rid): string { - foreach ($this->getCollectionResourcesInfo($ass_id, $part_id) as $info) { + foreach ($this->getCollectionResourcesInfo($ass_id, $user_id) as $info) { if ($rid === $info->getRid()) { - $this->wrapper->deliverFile($info->getRid()); return $info->getTitle(); } } From 1426df995cc0221413b0ef275caff2b0203b247c Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 3 Jul 2026 17:24:58 +0200 Subject: [PATCH 042/333] [FIX] Exercise #47850: Team uploads: Only 1st team member can see evaluation statement --- .../class.ilExerciseSubmissionFeedbackGUI.php | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/components/ILIAS/Exercise/Submission/class.ilExerciseSubmissionFeedbackGUI.php b/components/ILIAS/Exercise/Submission/class.ilExerciseSubmissionFeedbackGUI.php index fd7958d69f36..4c7b397f3f72 100644 --- a/components/ILIAS/Exercise/Submission/class.ilExerciseSubmissionFeedbackGUI.php +++ b/components/ILIAS/Exercise/Submission/class.ilExerciseSubmissionFeedbackGUI.php @@ -135,17 +135,21 @@ protected function validateAndSubmitFeedbackForm(): void $ass_id = $request->getAssId(); $ass = $this->domain->assignment()->getAssignment($ass_id); $comment = $form->getData("comment"); - $member_status = $ass->getMemberStatus($user_id); - $member_status->setComment($comment); - $member_status->setFeedback(true); - $member_status->update(); - if (trim($comment) !== '' && trim($comment) !== '0') { - $this->notification->sendFeedbackNotification( - $ass_id, - [$user_id], - "", - true - ); + $submission = new ilExSubmission($ass, $user_id); + $user_ids = $submission->getUserIds(); + foreach ($user_ids as $user_id) { + $member_status = $ass->getMemberStatus($user_id); + $member_status->setComment($comment); + $member_status->setFeedback(true); + $member_status->update(); + if (trim($comment) !== '' && trim($comment) !== '0') { + $this->notification->sendFeedbackNotification( + $ass_id, + [$user_id], + "", + true + ); + } } $cmd = $request->getParticipantId() ? "showParticipant" From 91a7ff843436b4e231cdde0a6782c64d701bb59b Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sun, 5 Jul 2026 20:03:25 +0200 Subject: [PATCH 043/333] blog: introduced link builder; separated presentation from editing gui; separated month, author and keyword block --- components/ILIAS/Blog/Editing/EditingGUI.php | 314 +++++ .../Blog/Editing/Service/class.GUIService.php | 58 + .../ILIAS/Blog/Export/BlogHtmlExport.php | 33 +- .../ILIAS/Blog/Navigation/AuthorBlockGUI.php | 50 +- .../ILIAS/Blog/Navigation/KeywordBlockGUI.php | 63 +- .../Navigation/Link/EditingLinkBuilder.php | 49 + .../Navigation/Link/ExportLinkBuilder.php | 65 ++ .../Blog/Navigation/Link/LinkBuilder.php | 52 + .../Navigation/Link/PermanentLinkBuilder.php | 62 + .../Link/PresentationLinkBuilder.php | 123 ++ .../ILIAS/Blog/Navigation/MonthBlockGUI.php | 99 +- .../Blog/Navigation/PresentationHeaderGUI.php | 142 +++ .../Navigation/Service/class.GUIService.php | 89 +- .../ILIAS/Blog/Navigation/SideBarGUI.php | 172 +++ .../Navigation/ToolbarNavigationRenderer.php | 38 +- .../Blog/Permission/PermissionManager.php | 5 + components/ILIAS/Blog/Posting/PostingList.php | 203 ++++ .../ILIAS/Blog/Posting/PostingListGUI.php | 350 ++++++ .../Blog/Posting/Service/class.GUIService.php | 20 + .../Blog/Presentation/PresentationGUI.php | 231 ++++ .../Presentation/Service/class.GUIService.php | 33 +- .../Service/class.InternalDomainService.php | 13 + .../Blog/Service/class.InternalGUIService.php | 12 +- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 1035 +++-------------- .../Blog/classes/class.ilObjBlogListGUI.php | 22 + .../ILIASObject/classes/class.ilObjectGUI.php | 1 + 26 files changed, 2239 insertions(+), 1095 deletions(-) create mode 100644 components/ILIAS/Blog/Editing/EditingGUI.php create mode 100644 components/ILIAS/Blog/Editing/Service/class.GUIService.php create mode 100644 components/ILIAS/Blog/Navigation/Link/EditingLinkBuilder.php create mode 100644 components/ILIAS/Blog/Navigation/Link/ExportLinkBuilder.php create mode 100644 components/ILIAS/Blog/Navigation/Link/LinkBuilder.php create mode 100644 components/ILIAS/Blog/Navigation/Link/PermanentLinkBuilder.php create mode 100644 components/ILIAS/Blog/Navigation/Link/PresentationLinkBuilder.php create mode 100644 components/ILIAS/Blog/Navigation/PresentationHeaderGUI.php create mode 100644 components/ILIAS/Blog/Navigation/SideBarGUI.php create mode 100644 components/ILIAS/Blog/Posting/PostingList.php create mode 100644 components/ILIAS/Blog/Posting/PostingListGUI.php create mode 100644 components/ILIAS/Blog/Presentation/PresentationGUI.php diff --git a/components/ILIAS/Blog/Editing/EditingGUI.php b/components/ILIAS/Blog/Editing/EditingGUI.php new file mode 100644 index 000000000000..a65691edc7f4 --- /dev/null +++ b/components/ILIAS/Blog/Editing/EditingGUI.php @@ -0,0 +1,314 @@ +blog_request = $gui->standardRequest(); + $this->blog = $parent_gui->getObject(); + $this->blog_settings = $this->domain->blogSettings()->getByObjId($this->blog->getId()); + $this->month = $this->blog_request->getMonth(); + } + + public function executeCommand(): void + { + $next_class = $this->gui->ctrl()->getNextClass($this); + $cmd = $this->gui->ctrl()->getCmd("render"); + + switch ($next_class) { + case strtolower(ilBlogPostingGUI::class): + $this->forwardPosting(); + break; + + default: + $this->$cmd(); + break; + } + } + + protected function getLinkBuilder(): EditingLinkBuilder + { + return $this->gui->navigation()->editingLink(); + } + + protected function forwardPosting(): void + { + $ilCtrl = $this->gui->ctrl(); + $tpl = $this->gui->ui()->mainTemplate(); + $lng = $this->domain->lng(); + $req = $this->gui->standardRequest(); + + $ilCtrl->saveParameter($this, "user_page"); + $tpl->loadStandardTemplate(); + + if (!$this->perm->mayContribute()) { + $tpl->setOnScreenMessage('info', $lng->txt("no_permission")); + return; + } + + $style_sheet_id = $this->content_style_domain->getEffectiveStyleId(); + + $bpost_gui = new ilBlogPostingGUI( + $this->node_id, + $this->perm->getAccessHandler(), + $req->getBlogPage(), + $req->getOldNr(), + $this->blog->getNotesStatus(), + $this->perm->mayEditPosting($req->getBlogPage()), + $style_sheet_id + ); + + $this->parent_gui->setContentStyleSheet(); + + $ilCtrl->setParameterByClass(ilBlogPostingGUI::class, "blpg", $req->getBlogPage()); + $this->gui->tabs()->addNonTabbedLink( + "preview", + $lng->txt("blog_preview"), + $ilCtrl->getLinkTargetByClass(ilBlogPostingGUI::class, "previewFullscreen") + ); + $ilCtrl->setParameterByClass(ilBlogPostingGUI::class, "blpg", ""); + + $ret = $ilCtrl->forwardCommand($bpost_gui); + + if ($ret != "") { + $is_owner = $this->perm->mayContribute(); + $is_active = $bpost_gui->getBlogPosting()->getActive(); + + // do not show inactive postings + $cmd = $ilCtrl->getCmd(); + if (($cmd === "previewFullscreen") + && !$is_owner && !$is_active) { + $ilCtrl->redirect($this->parent_gui, "preview"); + } + + // infos about draft status / snippet + $info = array(); + if (!$is_active) { + $info[] = $lng->txt("blog_draft_info_contributors"); + } + $public_action = false; + if ($cmd !== "history" && $cmd !== "edit" && $is_active && empty($info)) { + $info[] = $lng->txt("blog_new_posting_info"); + $public_action = true; + } + if ($this->blog->getNotesStatus() && + $this->blog_settings->getApproval() && + !$bpost_gui->getBlogPosting()->isApproved()) { + // #9737 + $info[] = $lng->txt("blog_posting_edit_approval_info"); + } + if ($public_action) { + $tpl->setOnScreenMessage('success', implode("
", $info)); + } else { + if (count($info) > 0) { + $tpl->setOnScreenMessage('info', implode("
", $info)); + } + } + + // revert to edit cmd to avoid confusion + $tpl->setContent($ret); + /* + if ($cmd !== "edit") { + $nav = $this->gui->navigation()->sideBar( + $this->perm, + $this->getLinkBuilder() + )->render( + $this->parent_gui, + $this->parent_gui->getItems(), + $is_owner + ); + $tpl->setRightContent($nav); + } else { + $this->gui->tabs()->setBackTarget("", ""); + }*/ + } + + if (!$this->gui->tabs()->back_target) { + $ilCtrl->setParameter($this, "bmn", ""); + $this->gui->tabs()->setBackTarget( + $lng->txt("back"), + $ilCtrl->getLinkTarget($this, "") + ); + } + } + + public function render(): void + { + $tpl = $this->gui->ui()->mainTemplate(); + $ilTabs = $this->gui->tabs(); + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $ilToolbar = new ilToolbarGUI(); + + if (!$this->parent_gui->checkPermissionBool("read")) { + $tpl->setOnScreenMessage('info', $lng->txt("no_permission")); + return; + } + + $ilTabs->activateTab("content"); + + // toolbar + if ($this->perm->mayContribute()) { + $ilToolbar->setFormAction($ilCtrl->getFormActionByClass(self::class, "createPosting")); + + $title = new ilTextInputGUI($lng->txt("title"), "title"); + $title->setSize(30); + $ilToolbar->addStickyItem($title, true); + $tpl->addOnLoadCode(" + document.getElementById('title').setAttribute('data-blog-input', 'posting-title'); + document.getElementById('title').setAttribute('placeholder', ' '); + "); + + $this->gui->button( + $lng->txt("blog_add_posting"), + "createPosting" + )->submit()->toToolbar(true, $ilToolbar); + + + // #18763 + $items = $this->parent_gui->getItems(); + $keys = array_keys($items); + $first = array_shift($keys); + if ($first != $this->month) { + $ilToolbar->addSeparator(); + + $ilCtrl->setParameter($this->parent_gui, "bmn", $first); + $url = $ilCtrl->getLinkTarget($this->parent_gui, ""); + $ilCtrl->setParameter($this->parent_gui, "bmn", $this->month); + + $ilToolbar->addComponent( + $this->gui->ui()->factory()->button()->standard( + $lng->txt("blog_show_latest"), + $url + ) + ); + } + + // print/pdf + $print_view = $this->parent_gui->getPrintView(); + $modal_elements = $print_view->getModalElements( + $ilCtrl->getLinkTarget( + $this->parent_gui, + "printViewSelection" + ) + ); + $ilToolbar->addSeparator(); + $ilToolbar->addComponent($modal_elements->button); + $ilToolbar->addComponent($modal_elements->modal); + } + + $is_owner = $this->perm->mayContribute(); + + $list_items = $this->parent_gui->getListItems($is_owner); + + $list = $nav = ""; + if ($list_items) { + $list = $this->gui->posting()->postingList( + $this->parent_gui, + $this->perm, + $this->current_month, + $this->node_id, + $this->id_type + )->render( + $list_items, + "preview", + "", + $is_owner + ); + $nav = $this->gui->navigation()->sideBar( + $this->perm, + $this->getLinkBuilder(), + $this->blog_settings, + $this->node_id, + $this->id_type + )->render( + $this->parent_gui, + $this->parent_gui->getItems(), + $is_owner + ); + } + + $this->parent_gui->setContentStyleSheet(); + + $tpl->setContent($ilToolbar->getHTML() . $list); + $tpl->setRightContent($nav); + } + + /** + * Create new posting + */ + public function createPosting(): void + { + $ctrl = $this->gui->ctrl(); + $user = $this->domain->user(); + $mt = $this->gui->ui()->mainTemplate(); + $lng = $this->domain->lng(); + $title = $this->blog_request->getTitle(); + if ($title) { + // create new posting + $posting = new \ilBlogPosting(); + $posting->setTitle($title); + $posting->setBlogId($this->blog->getId()); + $posting->setActive(false); + $posting->setAuthor($user->getId()); + $posting->create(false); + + // switch month list to current month (will include new posting) + $ctrl->setParameter($this, "bmn", date("Y-m")); + + $ctrl->setParameterByClass("ilblogpostinggui", "blpg", $posting->getId()); + $ctrl->redirectByClass("ilblogpostinggui", "edit"); + } else { + $mt->setOnScreenMessage('failure', $lng->txt("msg_no_title"), true); + $ctrl->redirect($this, "render"); + } + } + +} diff --git a/components/ILIAS/Blog/Editing/Service/class.GUIService.php b/components/ILIAS/Blog/Editing/Service/class.GUIService.php new file mode 100644 index 000000000000..48eb53b951dc --- /dev/null +++ b/components/ILIAS/Blog/Editing/Service/class.GUIService.php @@ -0,0 +1,58 @@ +data, + $this->domain, + $this->gui, + $node_id, + $id_type, + $perm, + $month, + $content_style_domain, + $parent_gui + ); + } +} diff --git a/components/ILIAS/Blog/Export/BlogHtmlExport.php b/components/ILIAS/Blog/Export/BlogHtmlExport.php index e23f6447c2da..7289e35c1934 100755 --- a/components/ILIAS/Blog/Export/BlogHtmlExport.php +++ b/components/ILIAS/Blog/Export/BlogHtmlExport.php @@ -25,6 +25,8 @@ class BlogHtmlExport { + protected ?\ILIAS\Blog\Settings\Settings $settings; + protected \ILIAS\Blog\InternalGUIService $gui; protected \ILIAS\Blog\Posting\PostingManager $posting_manager; protected \ILIAS\components\Export\HTML\ExportCollector $collector; protected \ilObjBlog $blog; @@ -47,6 +49,7 @@ class BlogHtmlExport public function __construct( \ilObjBlogGUI $blog_gui, + protected bool $is_repository, string $exp_dir, string $sub_dir, bool $set_export_key = true @@ -59,6 +62,8 @@ public function __construct( $this->blog = $blog; $blog_service = $DIC->blog()->internal(); + $this->gui = $blog_service->gui(); + $this->settings = $blog_service->domain()->blogSettings()->getByObjId($blog->getId()); $this->collector = $DIC->export()->domain()->html()->collector($blog->getId()); $this->collector->init(); @@ -83,7 +88,7 @@ public function __construct( } $cs = $DIC->contentStyle(); - if ($this->blog_gui->getIdType() === \ilObject2GUI::REPOSITORY_NODE_ID) { + if ($this->is_repository) { $this->content_style_domain = $cs->domain()->styleForRefId($this->blog->getRefId()); } else { $this->content_style_domain = $cs->domain()->styleForObjId($this->blog->getId()); @@ -178,7 +183,17 @@ public function exportHTMLPages( // lists // global nav - $nav = $this->blog_gui->renderNavigation("", "", $a_link_template); + $nav = $this->gui->navigation()->sideBar( + null, + $this->gui->navigation()->exportLink( + $a_link_template, + [] + ), + $this->settings + )->render( + $this->blog_gui, + $this->items + ); // month list $has_index = false; @@ -256,10 +271,16 @@ public function exportHTMLPages( : ""; // posting nav - $nav = $this->blog_gui->renderNavigation( - "", - "", - $a_link_template, + $nav = $this->gui->navigation()->sideBar( + null, + $this->gui->navigation()->exportLink( + $a_link_template, + [] + ), + $this->settings + )->render( + $this->blog_gui, + $this->items, false, $page_id ); diff --git a/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php b/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php index 6a88ce305357..2ecc6fb64c09 100644 --- a/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php +++ b/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php @@ -23,51 +23,27 @@ use ILIAS\Blog\InternalDomainService; use ILIAS\Blog\InternalGUIService; use ILIAS\Blog\Posting\Posting; +use ILIAS\Blog\Navigation\Link\LinkBuilder; class AuthorBlockGUI { - protected InternalDomainService $domain; - protected InternalGUIService $gui; - public function __construct( - InternalDomainService $domain, - InternalGUIService $gui + protected InternalDomainService $domain, + protected InternalGUIService $gui, + protected LinkBuilder $link_builder ) { - $this->domain = $domain; - $this->gui = $gui; } - /** - * @param Posting[][] $items - */ public function render( - array $items, - string $list_cmd = "render", bool $show_inactive = false ): string { - $ctrl = $this->gui->ctrl(); - $lng = $this->domain->lng(); - - $authors = array(); - foreach ($items as $month => $month_items) { - foreach ($month_items as $item) { - $item_id = $item->getId(); - if (($show_inactive || \ilBlogPosting::_lookupActive($item_id, "blp"))) { - $author_id = $item->getAuthor(); - if ($author_id) { - $authors[] = $author_id; - } - foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { - $editor_id = (int) $editor["user_id"]; - if ($editor_id !== $author_id) { - $authors[] = $editor_id; - } - } - } - } - } - - $authors = array_unique($authors); + $obj_id = \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()); + $posting_list = $this->domain->postingList( + $obj_id, + $this->domain->blogSettings()->getByObjId($obj_id), + $show_inactive + ); + $authors = $posting_list->getAuthors(); // filter out deleted users $authors = array_filter($authors, function ($id) { @@ -78,9 +54,7 @@ public function render( $list = array(); foreach ($authors as $user_id) { if ($user_id) { - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "ath", (string) $user_id); - $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "ath", ""); + $url = $this->link_builder->forAuthor($user_id); $base_name = \ilUserUtil::getNamePresentation($user_id); if (str_starts_with($base_name, "[")) { diff --git a/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php b/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php index 7a177ae023da..157b4f5590bc 100644 --- a/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php +++ b/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php @@ -23,34 +23,22 @@ use ILIAS\Blog\InternalDomainService; use ILIAS\Blog\InternalGUIService; use ILIAS\Blog\Posting\Posting; +use ILIAS\Blog\Navigation\Link\LinkBuilder; class KeywordBlockGUI { - protected InternalDomainService $domain; - protected InternalGUIService $gui; - public function __construct( - InternalDomainService $domain, - InternalGUIService $gui + protected InternalDomainService $domain, + protected InternalGUIService $gui, + protected LinkBuilder $link_builder ) { - $this->domain = $domain; - $this->gui = $gui; } - /** - * @param Posting[][] $items - */ public function render( - array $items, - string $list_cmd = "render", bool $show_inactive = false, - string $link_template = "", int $blpg = 0 ): string { - $ctrl = $this->gui->ctrl(); - $lng = $this->domain->lng(); - - $keywords = $this->getKeywords($items, $show_inactive, $blpg); + $keywords = $this->getKeywords($show_inactive, $blpg); if ($keywords) { $wtpl = new \ilTemplate("tpl.blog_list_navigation_keywords.html", true, true, "components/ILIAS/Blog"); @@ -58,13 +46,7 @@ public function render( $wtpl->setCurrentBlock("keyword"); foreach ($keywords as $keyword => $counter) { - if (!$link_template) { - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "kwd", urlencode((string) $keyword)); - $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "kwd", ""); - } else { - $url = $this->buildExportLink($link_template, "keyword", (string) $keyword); - } + $url = $this->link_builder->forKeyword($keyword); $wtpl->setVariable("TXT_KEYWORD", (string) $keyword); $wtpl->setVariable("CLASS_KEYWORD", \ilTagging::getRelevanceClass((int) $counter, (int) $max)); @@ -77,11 +59,7 @@ public function render( return ""; } - /** - * @param Posting[][] $items - */ protected function getKeywords( - array $items, bool $show_inactive, ?int $posting_id = null ): array { @@ -98,16 +76,20 @@ protected function getKeywords( } } } else { - foreach ($items as $month => $month_items) { + $posting_list = $this->domain->postingList( + $obj_id, + $this->domain->blogSettings()->getByObjId($obj_id), + $show_inactive + ); + $all_items = $posting_list->getPostingsGroupedByMonth(); + foreach ($all_items as $month => $month_items) { foreach ($month_items as $item) { $item_id = $item->getId(); - if ($show_inactive || \ilBlogPosting::_lookupActive($item_id, "blp")) { - foreach ($posting_manager->getKeywords($obj_id, $item_id) as $keyword) { - if (isset($keywords[$keyword])) { - $keywords[$keyword]++; - } else { - $keywords[$keyword] = 1; - } + foreach ($posting_manager->getKeywords($obj_id, $item_id) as $keyword) { + if (isset($keywords[$keyword])) { + $keywords[$keyword]++; + } else { + $keywords[$keyword] = 1; } } } @@ -126,13 +108,4 @@ protected function getKeywords( } return $keywords; } - - protected function buildExportLink( - string $template, - string $type, - string $id - ): string { - $blog_export = new \ILIAS\Blog\Export\BlogHtmlExport($this->gui->standardRequest()->getRefId()); - return $blog_export->buildExportLink($template, $type, $id, []); - } } diff --git a/components/ILIAS/Blog/Navigation/Link/EditingLinkBuilder.php b/components/ILIAS/Blog/Navigation/Link/EditingLinkBuilder.php new file mode 100644 index 000000000000..932a7e9d554c --- /dev/null +++ b/components/ILIAS/Blog/Navigation/Link/EditingLinkBuilder.php @@ -0,0 +1,49 @@ + [ + \ilObjBlogGUI::class, + EditingGUI::class + ], + self::VIEW_POSTING => [ + \ilObjBlogGUI::class, + EditingGUI::class, + \ilBlogPostingGUI::class + ], + }; + } + + protected function getCmdForView(string $view): string + { + return match ($view) { + self::VIEW_MAIN => "render", + self::VIEW_POSTING => "edit", + }; + } +} diff --git a/components/ILIAS/Blog/Navigation/Link/ExportLinkBuilder.php b/components/ILIAS/Blog/Navigation/Link/ExportLinkBuilder.php new file mode 100644 index 000000000000..feb3706874f4 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/Link/ExportLinkBuilder.php @@ -0,0 +1,65 @@ +template); + } + + public function forMonth(string $month): string + { + return $this->build("m", $month); + } + + public function forPosting(int $posting_id, string $month = ""): string + { + return $this->build("p", (string) $posting_id); + } + + public function forKeyword(string $keyword): string + { + $id = (string) ($this->keyword_map[$keyword] ?? ""); + return $this->build("k", $id); + } + + public function forAuthor(int $user_id): string + { + // Currently not supported in HTML export, but could be added if needed. + return ""; + } + + public function forMainList(): string + { + return "index.html"; + } +} diff --git a/components/ILIAS/Blog/Navigation/Link/LinkBuilder.php b/components/ILIAS/Blog/Navigation/Link/LinkBuilder.php new file mode 100644 index 000000000000..27f231f074b5 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/Link/LinkBuilder.php @@ -0,0 +1,52 @@ +manager->getPermanentLink($posting_id); + } + + public function forKeyword(string $keyword): string + { + // Not supported by permanent links + return ""; + } + + public function forAuthor(int $user_id): string + { + // Not supported by permanent links + return ""; + } + + public function forMainList(): string + { + return $this->manager->getPermanentLink(); + } +} diff --git a/components/ILIAS/Blog/Navigation/Link/PresentationLinkBuilder.php b/components/ILIAS/Blog/Navigation/Link/PresentationLinkBuilder.php new file mode 100644 index 000000000000..afad32ce8ac8 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/Link/PresentationLinkBuilder.php @@ -0,0 +1,123 @@ + [ + \ilObjBlogGUI::class, + PresentationGUI::class + ], + self::VIEW_POSTING => [ + \ilObjBlogGUI::class, + PresentationGUI::class, + \ilBlogPostingGUI::class + ], + }; + } + + protected function getCmdForView(string $view): string + { + return match ($view) { + self::VIEW_MAIN => "preview", + self::VIEW_POSTING => "previewFullscreen", + }; + } + + protected function setParameter( + string $view, + string $par, + string $value + ): void { + $path = $this->getPathForView($view); + $class = end($path); + $this->ctrl->setParameterByClass($class, $par, $value); + } + + protected function getLinkForView(string $view): string + { + return $this->ctrl->getLinkTargetByClass( + $this->getPathForView($view), + $this->getCmdForView($view) + ); + } + + public function forMonth(string $month): string + { + $this->setParameter(self::VIEW_MAIN, self::PAR_MONTH, $month); + $this->setParameter(self::VIEW_MAIN, self::PAR_POSTING, ""); + return $this->getLinkForView(self::VIEW_MAIN); + } + + public function forPosting(int $posting_id, string $month = ""): string + { + $this->setParameter(self::VIEW_POSTING, self::PAR_MONTH, $month); + $this->setParameter(self::VIEW_POSTING, self::PAR_POSTING, (string) $posting_id); + return $this->getLinkForView(self::VIEW_POSTING); + } + + public function forKeyword(string $keyword): string + { + $this->setParameter(self::VIEW_MAIN, self::PAR_KEYWORD, urlencode($keyword)); + $this->setParameter(self::VIEW_MAIN, self::PAR_POSTING, ""); + $link = $this->getLinkForView(self::VIEW_MAIN); + $this->setParameter(self::VIEW_MAIN, self::PAR_KEYWORD, ""); + return $link; + } + + public function forAuthor(int $user_id): string + { + $this->setParameter(self::VIEW_MAIN, self::PAR_AUTHOR, (string) $user_id); + $this->setParameter(self::VIEW_MAIN, self::PAR_POSTING, ""); + $link = $this->getLinkForView(self::VIEW_MAIN); + $this->setParameter(self::VIEW_MAIN, self::PAR_AUTHOR, ""); + return $link; + } + + public function forMainList(): string + { + $this->setParameter(self::VIEW_MAIN, self::PAR_MONTH, ""); + $this->setParameter(self::VIEW_MAIN, self::PAR_POSTING, ""); + return $this->getLinkForView(self::VIEW_MAIN); + } +} diff --git a/components/ILIAS/Blog/Navigation/MonthBlockGUI.php b/components/ILIAS/Blog/Navigation/MonthBlockGUI.php index 1c948d4031b7..d8f294d1bcb0 100644 --- a/components/ILIAS/Blog/Navigation/MonthBlockGUI.php +++ b/components/ILIAS/Blog/Navigation/MonthBlockGUI.php @@ -23,18 +23,21 @@ use ILIAS\Blog\InternalDomainService; use ILIAS\Blog\InternalGUIService; use ILIAS\Blog\Posting\Posting; +use ILIAS\Blog\Navigation\Link\LinkBuilder; +use ILIAS\Blog\Navigation\Link\ExportLinkBuilder; class MonthBlockGUI { - protected InternalDomainService $domain; - protected InternalGUIService $gui; - public function __construct( - InternalDomainService $domain, - InternalGUIService $gui + protected InternalDomainService $domain, + protected InternalGUIService $gui, + protected LinkBuilder $link_builder ) { - $this->domain = $domain; - $this->gui = $gui; + } + + protected function isExport(): bool + { + return $this->link_builder instanceof ExportLinkBuilder; } /** @@ -42,33 +45,21 @@ public function __construct( */ public function render( array $items, - string $list_cmd = "render", - string $posting_cmd = "preview", - ?string $link_template = null, bool $show_inactive = false, int $blpg = 0 ): string { $ctrl = $this->gui->ctrl(); $lng = $this->domain->lng(); - $settings = $this->domain->blogSettings()->getByObjId( - \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()) - ); - - // gather page active status - foreach ($items as $month => $postings) { - foreach (array_keys($postings) as $id) { - $active = \ilBlogPosting::_lookupActive($id, "blp"); - if (!$show_inactive && !$active) { - unset($items[$month][$id]); - } - } - if (!count($items[$month])) { - unset($items[$month]); - } + $obj_id = \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()); + $settings = $this->domain->blogSettings()->getByObjId($obj_id); + + if (!$show_inactive) { + $items = $this->domain->postingList($obj_id, $settings, false)->getPostingsGroupedByMonth(); } // list month (incl. postings) - if ($settings->getNavMode() === \ilObjBlog::NAV_MODE_LIST || $link_template) { + if ($settings->getNavMode() === \ilObjBlog::NAV_MODE_LIST || + $this->isExport()) { $max_months = $settings->getNavModeListMonths(); $wtpl = new \ilTemplate("tpl.blog_list_navigation_by_date.html", true, true, "components/ILIAS/Blog"); @@ -77,7 +68,7 @@ public function render( $counter = $mon_counter = $last_year = 0; foreach ($items as $month => $postings) { - if (!$link_template && $max_months && $mon_counter >= $max_months) { + if (!$this->isExport() && $max_months && $mon_counter >= $max_months) { break; } @@ -91,12 +82,7 @@ public function render( $mon_counter++; $month_name = \ilCalendarUtil::_numericMonthToString((int) substr((string) $month, 5)); - if (!$link_template) { - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $month); - $month_url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); - } else { - $month_url = $this->buildExportLink($link_template, "list", (string) $month); - } + $month_url = $this->link_builder->forMonth($month); if ($mon_counter <= $settings->getNavModeListMonthsWithPostings()) { if ($add_year) { @@ -106,17 +92,8 @@ public function render( } foreach ($postings as $id => $posting) { - $counter++; $caption = $posting->getTitle(); - - if (!$link_template) { - $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", $month); - $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $id); - $url = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $posting_cmd); - } else { - $url = $this->buildExportLink($link_template, "posting", (string) $id); - } - + $url = $this->link_builder->forPosting($posting->getId()); if (!$posting->isActive()) { $wtpl->setVariable("NAV_ITEM_DRAFT", $lng->txt("blog_draft")); } elseif ($settings->getApproval() && !$posting->isApproved()) { @@ -149,12 +126,7 @@ public function render( $wtpl->parseCurrentBlock(); } } - if (!$link_template) { - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", null); - $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); - } else { - $url = "index.html"; - } + $url = $this->link_builder->forMainList(); $wtpl->setVariable( "STARTING_PAGE", @@ -182,23 +154,11 @@ public function render( $month_options[(string) $month] = $month_name; if ($month == $this->gui->standardRequest()->getMonth()) { - if (!$link_template) { - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", (string) $month); - $month_url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); - } else { - $month_url = $this->buildExportLink($link_template, "list", (string) $month); - } + $month_url = $this->link_builder->forMonth($month); foreach ($postings as $id => $posting) { $caption = $posting->getTitle(); - - if (!$link_template) { - $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", (string) $month); - $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $id); - $url = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $posting_cmd); - } else { - $url = $this->buildExportLink($link_template, "posting", (string) $id); - } + $url = $this->link_builder->forPosting($id); if (!$posting->isActive()) { $wtpl->setVariable("NAV_ITEM_DRAFT", $lng->txt("blog_draft")); @@ -221,6 +181,7 @@ public function render( } } + /* if ($blpg === 0) { $wtpl->setCurrentBlock("option_bl"); foreach ($month_options as $value => $caption) { @@ -234,20 +195,10 @@ public function render( $wtpl->setVariable("FORM_ACTION", $ctrl->getFormActionByClass(\ilObjBlogGUI::class, $list_cmd)); } + */ } $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $this->gui->standardRequest()->getMonth()); $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", ""); return $wtpl->get(); } - - protected function buildExportLink( - string $template, - string $type, - string $id - ): string { - $blog_export = new \ILIAS\Blog\Export\BlogHtmlExport($this->gui->standardRequest()->getRefId()); - // Note: this might need adjustment since the original used $this->getKeywords(false) - // For now we assume keywords are not needed for these links or handled elsewhere - return $blog_export->buildExportLink($template, $type, $id, []); - } } diff --git a/components/ILIAS/Blog/Navigation/PresentationHeaderGUI.php b/components/ILIAS/Blog/Navigation/PresentationHeaderGUI.php new file mode 100644 index 000000000000..ee7058843182 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/PresentationHeaderGUI.php @@ -0,0 +1,142 @@ +addHeaderActionForCommand (called in editing, presentation and bloggui) + * -> addHeaderActionForCommandInternal + * -> initHeaderAction + * -> $this->initHeaderAction (ilObjectGUI) + * -> insertHeaderAction + */ +class PresentationHeaderGUI +{ + protected \ilObjUser $user; + + public function __construct( + protected InternalDomainService $domain, + protected InternalGUIService $gui, + protected \ilObjBlog $blog, + protected PermissionManager $perm, + ) { + $this->user = $this->domain->user(); + } + + public function addHeaderAction( + ?\ilObjectListGUI $list_gui + ): void { + $user = $this->domain->user(); + + // notification + if ($user->getId() !== ANONYMOUS_USER_ID) { + $this->insertHeaderAction($list_gui); + } + } + + public function get( + ?\ilObjectListGUI $lg, + int $posting_id = 0 + ): ?ilObjectListGUI { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + if ($posting_id > 0) { + if ($this->blog->getNotesStatus()) { + $lg->enableComments(true); + } + $lg->enableNotes(true); + } + $lg->enableTags(false); + + if (\ilNotification::hasNotification( + \ilNotification::TYPE_BLOG, + $this->user->getId(), + $this->blog->getId() + ) + ) { + $ctrl->setParameterByClass( + PresentationGUI::class, + "ntf", + "1" + ); + $link = $ctrl->getLinkTargetByClass( + PresentationGUI::class, + "setNotification" + ); + $ctrl->setParameter($this, "ntf", ""); + if (\ilNotification::hasOptOut($this->blog->getId())) { + $lg->addCustomCommand($link, "blog_notification_toggle_off"); + } + + $lg->addHeaderIcon( + "not_icon", + \ilUtil::getImagePath("object/notification_on.svg"), + $lng->txt("blog_notification_activated") + ); + } else { + $ctrl->setParameterByClass(PresentationGUI::class, "ntf", 2); + $link = $ctrl->getLinkTargetByClass(PresentationGUI::class, "setNotification"); + $ctrl->setParameterByClass(PresentationGUI::class, "ntf", ""); + $lg->addCustomCommand($link, "blog_notification_toggle_on"); + + $lg->addHeaderIcon( + "not_icon", + \ilUtil::getImagePath("object/notification_off.svg"), + $lng->txt("blog_notification_deactivated") + ); + } + + // #11758 + if ($this->perm->mayContribute()) { + $edit_path = [ + \ilObjBlogGUI::class, + EditingGUI::class + ]; + $ctrl->setParameterByClass(EditingGUI::class, "bmn", ""); + $ctrl->setParameterByClass(EditingGUI::class, "blpg", ""); + $link = $ctrl->getLinkTargetByClass($edit_path, ""); + $lg->addCustomCommand($link, "blog_edit"); // #11868 + + $posting_path = [ + \ilObjBlogGUI::class, + EditingGUI::class, + \ilBlogPostingGUI::class, + ]; + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", $posting_id); + + if ($posting_id && $this->perm->mayEditPosting($posting_id)) { + $link = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, "edit"); + $lg->addCustomCommand($link, "blog_edit_posting"); + } + } + + $ctrl->setParameterByClass(PresentationGUI::class, "ntf", ""); + + return $lg; + } +} diff --git a/components/ILIAS/Blog/Navigation/Service/class.GUIService.php b/components/ILIAS/Blog/Navigation/Service/class.GUIService.php index d420f56023c6..0212241be6a9 100755 --- a/components/ILIAS/Blog/Navigation/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Navigation/Service/class.GUIService.php @@ -22,6 +22,12 @@ use ILIAS\Blog\InternalDomainService; use ILIAS\Blog\InternalGUIService; +use ILIAS\Blog\Permission\PermissionManager; +use ILIAS\Blog\Navigation\Link\PresentationLinkBuilder; +use ILIAS\Blog\Navigation\Link\EditingLinkBuilder; +use ILIAS\Blog\Navigation\Link\ExportLinkBuilder; +use ILIAS\Blog\Navigation\Link\LinkBuilder; +use ILIAS\Blog\Settings\Settings; class GUIService { @@ -37,34 +43,97 @@ public function __construct( } public function toolbarNavigationRenderer( + LinkBuilder $link_builder ): ToolbarNavigationRenderer { return new ToolbarNavigationRenderer( $this->domain, - $this->gui + $this->gui, + $link_builder ); } - public function monthBlock(): MonthBlockGUI - { + public function monthBlock( + LinkBuilder $link_builder + ): MonthBlockGUI { return new MonthBlockGUI( $this->domain, - $this->gui + $this->gui, + $link_builder ); } - public function authorBlock(): AuthorBlockGUI - { + public function authorBlock( + LinkBuilder $link_builder + ): AuthorBlockGUI { return new AuthorBlockGUI( $this->domain, - $this->gui + $this->gui, + $link_builder ); } - public function keywordBlock(): KeywordBlockGUI - { + public function keywordBlock( + LinkBuilder $link_builder + ): KeywordBlockGUI { return new KeywordBlockGUI( $this->domain, - $this->gui + $this->gui, + $link_builder + ); + } + + public function sideBar( + ?PermissionManager $perm, + LinkBuilder $link_builder, + Settings $settings, + ?int $node_id = null, + int $id_type = \ilObjBlogGUI::REPOSITORY_NODE_ID + ): SideBarGUI { + return new SideBarGUI( + $this->domain, + $this->gui, + $perm, + $link_builder, + $settings, + $node_id, + $id_type + ); + } + + public function presentationHeader( + \ilObjBlog $blog, + PermissionManager $perm, + ): PresentationHeaderGUI { + return new PresentationHeaderGUI( + $this->domain, + $this->gui, + $blog, + $perm ); } + + public function presentationLink(): PresentationLinkBuilder + { + return new PresentationLinkBuilder( + $this->gui->ctrl(), + ); + } + + public function editingLink(): EditingLinkBuilder + { + return new EditingLinkBuilder( + $this->gui->ctrl(), + ); + } + + public function exportLink( + string $link_template = "", + array $keyword_map = [] + ): ExportLinkBuilder { + return new ExportLinkBuilder( + $link_template, + $keyword_map + ); + } + } diff --git a/components/ILIAS/Blog/Navigation/SideBarGUI.php b/components/ILIAS/Blog/Navigation/SideBarGUI.php new file mode 100644 index 000000000000..2000fc33fe67 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/SideBarGUI.php @@ -0,0 +1,172 @@ +domain = $domain; + $this->gui = $gui; + } + + protected function mayEditPosting(int $blpg): bool + { + if ($this->link_builder instanceof EditingLinkBuilder && + !is_null($this->perm) && + $this->perm->mayEditPosting($blpg)) { + return true; + } + return false; + } + + protected function isExport(): bool + { + return $this->link_builder instanceof ExportLinkBuilder; + } + + protected function isPresentation(): bool + { + return $this->link_builder instanceof PresentationLinkBuilder; + } + + /** + * Build navigation blocks + */ + public function render( + \ilObjBlogGUI $gui_obj, + array $items, + bool $show_inactive = false, + int $blpg = 0 + ): string { + $lng = $this->domain->lng(); + $ui = $this->gui->ui(); + if ($this->settings->getOrder()) { + $order = array_flip($this->settings->getOrder()); + } else { + $order = array( + "navigation" => 0, + "keywords" => 2, + "authors" => 1 + ); + } + + $wtpl = new ilTemplate("tpl.blog_list_navigation.html", true, true, "components/ILIAS/Blog"); + + $blocks = array(); + + // by date + if (count($items)) { + $blocks[$order["navigation"] ?? 0] = array( + $lng->txt("blog_navigation"), + $this->gui->navigation()->monthBlock($this->link_builder)->render( + $items, + $show_inactive, + $blpg + ) + ); + } + + if ($this->settings->getKeywords()) { + // keywords + $may_edit_keywords = ($blpg > 0 && + $this->mayEditPosting($blpg)); + + $keywords = $this->gui->navigation()->keywordBlock($this->link_builder)->render( + $show_inactive, + $blpg + ); + if ($keywords || $may_edit_keywords) { + if (!$keywords) { + $keywords = $lng->txt("blog_no_keywords"); + } + $cmd = null; + $blocks[$order["keywords"] ?? 2] = array( + $lng->txt("blog_keywords"), + $keywords, + $cmd + ? array($cmd, $lng->txt("blog_edit_keywords")) + : null + ); + } + } + + // is not part of (html) export + if (!$this->isExport()) { + // authors + if ($this->id_type === \ilObjBlogGUI::REPOSITORY_NODE_ID && + $this->settings->getAuthors()) { + $authors = $this->gui->navigation()->authorBlock($this->link_builder)->render( + $show_inactive + ); + if ($authors) { + $blocks[$order["authors"] ?? 1] = array($lng->txt("blog_authors"), $authors); + } + } + + // rss + if ($this->settings->getRSS() && + $this->domain->settings()->get('enable_global_profiles') && + $this->isPresentation()) { + // #10827 + $blog_id = (string) $this->node_id; + if ($this->id_type !== \ilObjBlogGUI::WORKSPACE_NODE_ID) { + $blog_id .= "_cll"; + } + $url = ILIAS_HTTP_PATH . "/feed.php?blog_id=" . $blog_id . + "&client_id=" . rawurlencode(CLIENT_ID); + + $wtpl->setVariable("RSS_BUTTON", ilRSSButtonGUI::get(ilRSSButtonGUI::ICON_RSS, $url)); + } + } + + if (count($blocks)) { + $ui_factory = $ui->factory(); + $ui_renderer = $ui->renderer(); + + ksort($blocks); + foreach ($blocks as $block) { + $title = $block[0]; + $content = $block[1]; + + $secondary_panel = $ui_factory->panel()->secondary()->legacy($title, $ui_factory->legacy()->content($content)); + + if (isset($block[2]) && is_array($block[2])) { + $link = $ui_factory->button()->shy($block[2][1], $block[2][0]); + $secondary_panel = $secondary_panel->withFooter($link); + } + + $wtpl->setCurrentBlock("block_bl"); + $wtpl->setVariable("BLOCK", $ui_renderer->render($secondary_panel)); + $wtpl->parseCurrentBlock(); + } + } + + return $wtpl->get(); + } +} diff --git a/components/ILIAS/Blog/Navigation/ToolbarNavigationRenderer.php b/components/ILIAS/Blog/Navigation/ToolbarNavigationRenderer.php index aca2ab2cf074..8ce022685e73 100755 --- a/components/ILIAS/Blog/Navigation/ToolbarNavigationRenderer.php +++ b/components/ILIAS/Blog/Navigation/ToolbarNavigationRenderer.php @@ -24,9 +24,12 @@ use ILIAS\Blog\InternalGUIService; use ILIAS\Blog\Permission\PermissionManager; use ILIAS\Blog\Posting\Posting; +use ILIAS\Blog\Navigation\Link\LinkBuilder; +use ILIAS\Blog\Editing\EditingGUI; class ToolbarNavigationRenderer { + protected Link\EditingLinkBuilder $edit_link_builder; protected array $items; protected InternalGUIService $gui; protected int $portfolio_page; @@ -39,11 +42,13 @@ class ToolbarNavigationRenderer public function __construct( InternalDomainService $domain, - InternalGUIService $gui + InternalGUIService $gui, + protected LinkBuilder $pres_link_builder ) { $this->domain = $domain; $this->gui = $gui; $this->util = $gui->presentation()->util(); + $this->edit_link_builder = $gui->navigation()->editingLink(); } public function renderToolbarNavigation( @@ -62,21 +67,19 @@ public function renderToolbarNavigation( $this->blog_page = $blog_page; $this->portfolio_page = $portfolio_page; - $cmd = "previewFullscreen"; - if ($single_posting) { // single posting view $next_posting = $this->getNextPosting($blog_page); if ($next_posting > 0) { - $this->renderPreviousButton($this->getPostingTarget($next_posting, $cmd)); + $this->renderPreviousButton($this->getPostingTarget($next_posting)); } else { $this->renderPreviousButton(""); } - $this->renderPostingDropdown($cmd); + $this->renderPostingDropdown(); $prev_posting = $this->getPreviousPosting($blog_page); if ($prev_posting > 0) { - $this->renderNextButton($this->getPostingTarget($prev_posting, $cmd)); + $this->renderNextButton($this->getPostingTarget($prev_posting)); } else { $this->renderNextButton(""); } @@ -117,13 +120,7 @@ protected function renderActionDropdown(bool $single_posting): void $ctrl = $this->ctrl; $actions = []; if ($this->blog_access->mayContribute()) { - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "prvm", ""); - - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", ""); - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", ""); - $link = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, ""); - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", $this->blog_page); - $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $this->current_month); + $link = $this->edit_link_builder->forMainList(); $actions[] = $f->button()->shy( $lng->txt("blog_edit"), $link @@ -131,8 +128,7 @@ protected function renderActionDropdown(bool $single_posting): void } if ($single_posting && $this->blog_access->mayContribute() && $this->blog_access->mayEditPosting($this->blog_page)) { - $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", $this->blog_page); - $link = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, "edit"); + $link = $this->edit_link_builder->forPosting($this->blog_page); $actions[] = $f->button()->shy( $lng->txt("blog_edit_posting"), $link @@ -204,16 +200,14 @@ protected function getPreviousPosting( return $prev_blpg; } - protected function getPostingTarget(int $posting, string $cmd): string + protected function getPostingTarget(int $posting): string { - $this->ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $posting); - return $this->ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $cmd); + return $this->pres_link_builder->forPosting($posting); } protected function getMonthTarget(string $month): string { - $this->ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $month); - return $this->ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, "preview"); + return $this->pres_link_builder->forMonth($month); } protected function renderMonthDropdown(): void @@ -296,7 +290,7 @@ protected function renderNavButton(string $dir, string $href = ""): void $toolbar->addStickyItem($b); } - protected function renderPostingDropdown(string $cmd): void + protected function renderPostingDropdown(): void { $toolbar = $this->gui->toolbar(); $f = $this->gui->ui()->factory(); @@ -321,7 +315,7 @@ protected function renderPostingDropdown(string $cmd): void $label = str_pad("", 12, " ") . $label; $m[] = $f->link()->standard( $label, - $this->getPostingTarget((int) $item->getId(), $cmd) + $this->getPostingTarget((int) $item->getId()) ); } } diff --git a/components/ILIAS/Blog/Permission/PermissionManager.php b/components/ILIAS/Blog/Permission/PermissionManager.php index fcf834b4cd31..c1def58786f3 100755 --- a/components/ILIAS/Blog/Permission/PermissionManager.php +++ b/components/ILIAS/Blog/Permission/PermissionManager.php @@ -117,4 +117,9 @@ public function isActive(int $posting_id): bool return (\ilBlogPosting::_lookupActive($posting_id, "blp")); } + public function getAccessHandler(): \ilAccessHandler|\ilWorkspaceAccessHandler + { + return $this->access; + } + } diff --git a/components/ILIAS/Blog/Posting/PostingList.php b/components/ILIAS/Blog/Posting/PostingList.php new file mode 100644 index 000000000000..72011ae6c3a1 --- /dev/null +++ b/components/ILIAS/Blog/Posting/PostingList.php @@ -0,0 +1,203 @@ + 0) { + return $this->getByAuthor($author_id); + } + if ($keyword !== "") { + return $this->getByKeyword($keyword); + } + + $max = $this->settings->getOverviewPostings(); + if ($month === "" && $max > 0) { + $list_items = []; + $all_items = $this->getPostingsGroupedByMonth(); + foreach ($all_items as $postings) { + foreach ($postings as $id => $item) { + $list_items[$id] = $item; + if (count($list_items) >= $max) { + break(2); + } + } + } + return $list_items; + } + + return $this->getByMonth($month); + } + + /** + * @return Posting[] + */ + protected function getPostings(): array + { + if ($this->postings === null) { + $this->postings = $this->posting_manager->getAllPostings($this->obj_id); + if (!$this->include_inactive) { + $this->postings = array_filter($this->postings, function (Posting $posting) { + return $posting->isActive(); + }); + } + } + return $this->postings; + } + + /** + * @return Posting[] + */ + public function getByMonth(string $month): array + { + $res = []; + foreach ($this->getPostings() as $posting) { + if (substr($posting->getCreated()->get(IL_CAL_DATE), 0, 7) === $month) { + $res[$posting->getId()] = $posting; + } + } + return $res; + } + + /** + * @return Posting[] + */ + public function getByAuthor(int $author_id): array + { + $res = []; + foreach ($this->getPostings() as $posting) { + if ($posting->getAuthor() === $author_id) { + $res[$posting->getId()] = $posting; + continue; + } + foreach (\ilPageObject::getPageContributors("blp", $posting->getId()) as $editor) { + if ((int) $editor["user_id"] === $author_id) { + $res[$posting->getId()] = $posting; + break; + } + } + } + return $res; + } + + /** + * @return Posting[] + */ + public function getByKeyword(string $keyword): array + { + $res = []; + foreach ($this->getPostings() as $posting) { + if (in_array( + $keyword, + $this->posting_manager->getKeywords($this->obj_id, $posting->getId()), + true + )) { + $res[$posting->getId()] = $posting; + } + } + return $res; + } + + /** + * @return array> [month => [posting_id => Posting]] + */ + public function getPostingsGroupedByMonth(): array + { + $items = []; + foreach ($this->getPostings() as $posting) { + $month = substr($posting->getCreated()->get(IL_CAL_DATE), 0, 7); + $items[$month][$posting->getId()] = $posting; + } + return $items; + } + + /** + * @return string[] months (YYYY-MM) + */ + public function getMonthsWithPostings(): array + { + $months = []; + foreach ($this->getPostings() as $posting) { + $month = substr($posting->getCreated()->get(IL_CAL_DATE), 0, 7); + if (!in_array($month, $months, true)) { + $months[] = $month; + } + } + rsort($months); + return $months; + } + + /** + * @return int[] user ids + */ + public function getAuthors(): array + { + $authors = []; + foreach ($this->getPostings() as $posting) { + $author_id = $posting->getAuthor(); + if ($author_id > 0 && !in_array($author_id, $authors, true)) { + $authors[] = $author_id; + } + + foreach (\ilPageObject::getPageContributors("blp", $posting->getId()) as $editor) { + $editor_id = (int) $editor["user_id"]; + if ($editor_id > 0 && !in_array($editor_id, $authors, true)) { + $authors[] = $editor_id; + } + } + } + return $authors; + } + + public function hasAuthorPostings(int $user_id): bool + { + foreach ($this->getPostings() as $posting) { + if ($posting->getAuthor() === $user_id) { + return true; + } + foreach (\ilPageObject::getPageContributors("blp", $posting->getId()) as $editor) { + if ((int) $editor["user_id"] === $user_id) { + return true; + } + } + } + return false; + } +} diff --git a/components/ILIAS/Blog/Posting/PostingListGUI.php b/components/ILIAS/Blog/Posting/PostingListGUI.php new file mode 100644 index 000000000000..fda49602609f --- /dev/null +++ b/components/ILIAS/Blog/Posting/PostingListGUI.php @@ -0,0 +1,350 @@ +notes = $DIC->notes(); + $this->posting_manager = $domain->posting(); + $this->reading_time_manager = $domain->readingTime(); + $this->blog_settings = + $domain->blogSettings()->getByObjId($parent_gui->getObject()->getId()); + $req = $gui->standardRequest(); + $this->blog = $parent_gui->getObject(); + $this->keyword = $req->getKeyword(); + $this->author = $req->getAuthor(); + + } + + public function render( + array $items, + string $a_cmd = "preview", + string $a_link_template = "", + bool $a_show_inactive = false, + string $a_export_directory = "" + ): string { + $lng = $this->domain->lng(); + $ilCtrl = $this->gui->ctrl(); + $ui_factory = $this->gui->ui()->factory(); + $ui_renderer = $this->gui->ui()->renderer(); + + $wtpl = new ilTemplate("tpl.blog_list.html", true, true, "components/ILIAS/Blog"); + + $is_admin = $this->perm->canManage(); + + $last_month = null; + $is_empty = true; + foreach ($items as $item) { + /** @var Posting $item */ + $item_id = $item->getId(); + $author = $item->getAuthor(); + $created = $item->getCreated(); + $approved = $item->isApproved(); + // only published items + $is_active = ilBlogPosting::_lookupActive($item_id, "blp"); + if (!$is_active && !$a_show_inactive) { + continue; + } + + $is_empty = false; + + $month = ""; + if (!$this->keyword && !$this->author) { + $month = substr($created->get(IL_CAL_DATE), 0, 7); + } + + if (!$last_month || $last_month != $month) { + if ($last_month) { + $wtpl->setCurrentBlock("month_bl"); + $wtpl->parseCurrentBlock(); + } + + // title according to current "filter"/navigation + if ($this->keyword) { + $title = $lng->txt("blog_keyword") . ": " . $this->keyword; + } elseif ($this->author) { + $title = $lng->txt("blog_author") . ": " . \ilUserUtil::getNamePresentation($this->author); + } else { + $title = $this->gui->presentation()->util()->getMonthPresentation($month); + $last_month = $month; + } + + $wtpl->setVariable("TXT_CURRENT_MONTH", $title); + } + + if (!$a_link_template) { + $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", $this->current_month); + $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $item_id); + $preview = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", $a_cmd); + } else { + $preview = $this->parent_gui->buildExportLink($a_link_template, "posting", (string) $item_id); + } + $more_link = $preview; + + // actions + $posting_edit = $this->perm->mayEditPosting($item_id, $author); + if (($posting_edit || $is_admin) && !$a_link_template && $a_cmd === "preview") { + $actions = []; + + if ($is_active && $this->blog_settings->getApproval() && !$approved) { + if ($is_admin) { + $ilCtrl->setParameter($this->parent_gui, "apid", $item_id); + $actions[] = $ui_factory->link()->standard( + $lng->txt("blog_approve"), + $ilCtrl->getLinkTarget($this->parent_gui, "approve") + ); + $ilCtrl->setParameter($this->parent_gui, "apid", ""); + } + + $wtpl->setVariable("APPROVAL", $lng->txt("blog_needs_approval")); + } + + if ($posting_edit) { + $actions[] = $ui_factory->link()->standard( + $lng->txt("edit_content"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edit") + ); + $more_link = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edit"); + + // #11858 + if ($is_active) { + $actions[] = $ui_factory->link()->standard( + $lng->txt("blog_toggle_draft"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "deactivatePageToList") + ); + } else { + $actions[] = $ui_factory->link()->standard( + $lng->txt("blog_toggle_final"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "activatePageToList") + ); + } + + $actions[] = $ui_factory->link()->standard( + $lng->txt("rename"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edittitle") + ); + + if ($this->blog_settings->getKeywords()) { // #13616 + $actions[] = $ui_factory->link()->standard( + $lng->txt("blog_edit_keywords"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "editKeywords") + ); + } + + $actions[] = $ui_factory->link()->standard( + $lng->txt("blog_edit_date"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "editdate") + ); + + $actions[] = $ui_factory->link()->standard( + $lng->txt("delete"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "deleteBlogPostingConfirmationScreen") + ); + } elseif ($is_admin) { + // #10513 + if ($is_active) { + $ilCtrl->setParameter($this->parent_gui, "apid", $item_id); + $actions[] = $ui_factory->link()->standard( + $lng->txt("blog_toggle_draft_admin"), + $ilCtrl->getLinkTarget($this->parent_gui, "deactivateAdmin") + ); + $ilCtrl->setParameter($this->parent_gui, "apid", ""); + } + + $actions[] = $ui_factory->link()->standard( + $lng->txt("delete"), + $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "deleteBlogPostingConfirmationScreen") + ); + } + + $dd = $ui_factory->dropdown()->standard($actions)->withLabel($lng->txt("actions")); + + $wtpl->setCurrentBlock("actions"); + $wtpl->setVariable("ACTION_SELECTOR", $ui_renderer->render($dd)); + $wtpl->parseCurrentBlock(); + } + + // comments + if ($this->blog->getNotesStatus() && !$a_link_template) { + // count (public) notes + $notes_context = $this->notes + ->data() + ->context( + $this->blog->getId(), + (int) $item_id, + "blp" + ); + $count = $this->notes + ->domain() + ->getNrOfCommentsForContext($notes_context); + + if ($a_cmd !== "preview") { + $wtpl->setCurrentBlock("comments"); + $wtpl->setVariable("TEXT_COMMENTS", $lng->txt("blog_comments")); + $wtpl->setVariable("URL_COMMENTS", $preview); + $wtpl->setVariable("COUNT_COMMENTS", $count); + $wtpl->parseCurrentBlock(); + } + } + + // permanent link + if ($this->node_id !== null && + $a_cmd !== "preview") { + if ($this->id_type === ilObjBlogGUI::WORKSPACE_NODE_ID) { + $goto = $this->gui->permanentLink(0, (int) $this->node_id)->getPermanentLink((int) $item_id); + } else { + $goto = $this->gui->permanentLink((int) $this->node_id)->getPermanentLink((int) $item_id); + } + $wtpl->setCurrentBlock("permalink"); + $wtpl->setVariable("URL_PERMALINK", $goto); + $wtpl->setVariable("TEXT_PERMALINK", $lng->txt("blog_link")); + $wtpl->parseCurrentBlock(); + } + + $snippet = $this->gui->posting()->getSnippet( + $item_id, + $this->blog_settings->getAbstractShorten(), + $this->blog_settings->getAbstractShortenLength(), + "…", + $this->blog_settings->getAbstractImage(), + $this->blog_settings->getAbstractImageWidth(), + $this->blog_settings->getAbstractImageHeight(), + $a_export_directory + ); + + if ($snippet) { + $wtpl->setCurrentBlock("more"); + $wtpl->setVariable("URL_MORE", $more_link); + $wtpl->setVariable("TEXT_MORE", $lng->txt("blog_list_more")); + $wtpl->parseCurrentBlock(); + } + + if (!$is_active) { + $wtpl->setCurrentBlock("draft_text"); + $wtpl->setVariable("DRAFT_TEXT", $lng->txt("blog_draft_text")); + $wtpl->parseCurrentBlock(); + $wtpl->setVariable("DRAFT_CLASS", " ilBlogListItemDraft"); + } + + // reading time + $reading_time = $this->reading_time_manager->getReadingTime( + $this->blog->getId(), + $item_id + ); + if (!is_null($reading_time)) { + $lng->loadLanguageModule("copg"); + $wtpl->setCurrentBlock("reading_time"); + $wtpl->setVariable( + "READING_TIME", + $lng->txt("copg_est_reading_time") . ": " . + sprintf($lng->txt("copg_x_minutes"), $reading_time) + ); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("posting"); + + $author_str = ""; + if ($this->id_type === ilObjBlogGUI::REPOSITORY_NODE_ID) { + $authors = array(); + + // primary author + if ($author) { + $authors[] = \ilUserUtil::getNamePresentation($author); + } + + // additional editors + foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { + $editor_id = (int) $editor["user_id"]; + if ($editor_id !== $author) { + $authors[] = \ilUserUtil::getNamePresentation($editor_id); + } + } + + if ($authors) { + $author_str = implode(", ", $authors) . " - "; + } + } + + // title + $wtpl->setVariable("URL_TITLE", $preview); + $wtpl->setVariable("TITLE", $item->getTitle()); + + $kw = $this->posting_manager->getKeywords($this->blog->getId(), $item_id); + natcasesort($kw); + $keywords = (count($kw) > 0) + ? "
" . $lng->txt("keywords") . ": " . implode(", ", $kw) + : ""; + + $wtpl->setVariable("DATETIME", $author_str . + ilDatePresentation::formatDate($created) . $keywords); + + // content + $wtpl->setVariable("CONTENT", $snippet); + + $wtpl->parseCurrentBlock(); + } + + // permalink + if ($a_cmd === "previewFullscreen") { + $ref_id = ($this->id_type === ilObjBlogGUI::WORKSPACE_NODE_ID) + ? 0 + : $this->node_id; + $wsp_id = ($this->id_type === ilObjBlogGUI::WORKSPACE_NODE_ID) + ? $this->node_id + : 0; + $this->gui->permanentLink($ref_id, $wsp_id)->setPermanentLink(); + } + + if (!$is_empty || $a_show_inactive) { + return $wtpl->get(); + } + return ""; + } +} diff --git a/components/ILIAS/Blog/Posting/Service/class.GUIService.php b/components/ILIAS/Blog/Posting/Service/class.GUIService.php index 8e90ca73064a..3c95e5b4fc31 100644 --- a/components/ILIAS/Blog/Posting/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Posting/Service/class.GUIService.php @@ -23,6 +23,7 @@ use ILIAS\Blog\InternalDataService; use ILIAS\Blog\InternalDomainService; use ILIAS\Blog\InternalGUIService; +use ILIAS\Blog\Permission\PermissionManager; /** * GUI service for blog postings @@ -56,6 +57,25 @@ public function postingGUI( ); } + public function postingList( + \ilObjBlogGUI $parent_gui, + PermissionManager $perm, + ?string $current_month = null, + ?int $node_id = null, + int $id_type = \ilObjBlogGUI::REPOSITORY_NODE_ID + ): \ILIAS\Blog\Posting\PostingListGUI { + return new \ILIAS\Blog\Posting\PostingListGUI( + $this->data, + $this->domain, + $this->gui, + $parent_gui, + $perm, + $current_month, + $node_id, + $id_type + ); + } + /** * Get first text paragraph of page */ diff --git a/components/ILIAS/Blog/Presentation/PresentationGUI.php b/components/ILIAS/Blog/Presentation/PresentationGUI.php new file mode 100644 index 000000000000..a1999d2f3740 --- /dev/null +++ b/components/ILIAS/Blog/Presentation/PresentationGUI.php @@ -0,0 +1,231 @@ +user = $this->domain->user(); + $this->ctrl = $this->gui->ctrl(); + $this->blog = $this->parent_gui->getObject(); + $this->obj_id = $this->blog->getId(); + $req = $this->gui->standardRequest(); + $this->blpg = $req->getBlogPage(); + $this->user_page = $req->getUserPage(); + $this->ntf = $req->getNotification(); + $this->blog_settings = $domain->blogSettings()->getByObjId($this->obj_id); + $this->nav_renderer = $this->gui->navigation()->toolbarNavigationRenderer( + $this->getLinkBuilder(), + ); + } + + protected function getLinkBuilder(): LinkBuilder + { + return $this->gui->navigation()->presentationLink(); + } + + public function executeCommand(): void + { + $next_class = $this->gui->ctrl()->getNextClass($this); + $cmd = $this->gui->ctrl()->getCmd("preview"); + + switch ($next_class) { + case strtolower(ilBlogPostingGUI::class): + $this->forwardPosting(); + break; + + default: + $this->$cmd(); + break; + } + } + + /** + * Toolbar navigation + */ + public function renderToolbarNavigation( + array $a_items, + bool $single_posting = false + ): void { + $nav_renderer = $this->gui->navigation()->toolbarNavigationRenderer( + $this->getLinkBuilder(), + ); + $nav_renderer->renderToolbarNavigation( + $this->perm, + $a_items, + $this->blpg, + $single_posting, + $this->current_month, + $this->user_page + ); + } + + protected function forwardPosting(): void + { + $ilCtrl = $this->gui->ctrl(); + $req = $this->gui->standardRequest(); + + if ($this->id_type === \ilObjBlogGUI::REPOSITORY_NODE_ID) { + //$this->parent_gui->setLocator(); + } + + $style_sheet_id = $this->content_style_domain->getEffectiveStyleId(); + + $bpost_gui = new ilBlogPostingGUI( + $this->node_id, + $this->perm->getAccessHandler(), + $req->getBlogPage(), + $req->getOldNr(), + $this->blog->getNotesStatus(), + $this->perm->mayEditPosting($req->getBlogPage()), + $style_sheet_id + ); + + $ilCtrl->setParameter($this, "prvm", "fsc"); + + $this->renderToolbarNavigation($this->parent_gui->getItems(), true); + + $ret = $ilCtrl->forwardCommand($bpost_gui); + + if ($ret != "") { + $is_owner = $this->perm->mayContribute(); + $is_active = $bpost_gui->getBlogPosting()->getActive(); + + // do not show inactive postings + $cmd = $ilCtrl->getCmd(); + if (($cmd === "previewFullscreen") + && !$is_owner && !$is_active) { + $ilCtrl->redirect($this->parent_gui, "preview"); + } + + if ($cmd === "previewFullscreen") { + $this->parent_gui->addPresentationHeaderAction(); + $this->parent_gui->filterInactivePostings(); + $nav = $this->gui->navigation()->sideBar( + $this->perm, + $this->getLinkBuilder(), + $this->blog_settings, + $this->node_id, + $this->id_type + )->render( + $this->parent_gui, + $this->parent_gui->getItems(), + ); + $this->parent_gui->renderFullScreen($ret, $nav); + } + } + } + + /** + * Render fullscreen presentation + */ + public function preview(): void + { + $lng = $this->domain->lng(); + $tpl = $this->gui->ui()->mainTemplate(); + + if (!$this->parent_gui->checkPermissionBool("read")) { + $tpl->setOnScreenMessage('info', $lng->txt("no_permission")); + return; + } + + $this->parent_gui->filterInactivePostings(); + + $list_items = $this->parent_gui->getListItems(); + + $list = $nav = ""; + if ($list_items) { + $list = $this->parent_gui->renderList($list_items, "previewFullscreen"); + $nav = $this->gui->navigation()->sideBar( + $this->perm, + $this->getLinkBuilder(), + $this->blog_settings, + $this->node_id, + $this->id_type + )->render( + $this->parent_gui, + $this->parent_gui->getItems(), + ); + $this->renderToolbarNavigation($this->parent_gui->getItems()); + } + + $this->parent_gui->renderFullScreen($list, $nav); + } + + protected function setNotification(): void + { + $ilUser = $this->user; + $ilCtrl = $this->ctrl; + switch ($this->ntf) { + case 1: + \ilNotification::setNotification( + \ilNotification::TYPE_BLOG, + $ilUser->getId(), + $this->obj_id, + false + ); + break; + + case 2: + \ilNotification::setNotification( + \ilNotification::TYPE_BLOG, + $ilUser->getId(), + $this->obj_id, + true + ); + break; + } + + $ilCtrl->redirect($this, ""); + } + +} diff --git a/components/ILIAS/Blog/Presentation/Service/class.GUIService.php b/components/ILIAS/Blog/Presentation/Service/class.GUIService.php index 87ee13853935..8200bc7b70a6 100755 --- a/components/ILIAS/Blog/Presentation/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Presentation/Service/class.GUIService.php @@ -20,24 +20,43 @@ namespace ILIAS\Blog\Presentation; +use ILIAS\Blog\InternalDataService; use ILIAS\Blog\InternalDomainService; use ILIAS\Blog\InternalGUIService; +use ILIAS\Blog\Permission\PermissionManager; class GUIService { - protected InternalGUIService $gui; - protected InternalDomainService $domain; - public function __construct( - InternalDomainService $domain, - InternalGUIService $gui + protected InternalDataService $data, + protected InternalDomainService $domain, + protected InternalGUIService $gui ) { - $this->domain = $domain; - $this->gui = $gui; } public function util(): Util { return new Util(); } + + public function presentationGUI( + \ilObjBlogGUI $parent_gui, + PermissionManager $perm, + \ILIAS\Style\Content\Object\ObjectFacade $content_style_domain, + string $current_month, + ?int $node_id = null, + int $id_type = \ilObjBlogGUI::REPOSITORY_NODE_ID + ): PresentationGUI { + return new PresentationGUI( + $this->data, + $this->domain, + $this->gui, + $parent_gui, + $perm, + $content_style_domain, + $current_month, + $node_id, + $id_type + ); + } } diff --git a/components/ILIAS/Blog/Service/class.InternalDomainService.php b/components/ILIAS/Blog/Service/class.InternalDomainService.php index 8aa784d42057..4480081ecc63 100755 --- a/components/ILIAS/Blog/Service/class.InternalDomainService.php +++ b/components/ILIAS/Blog/Service/class.InternalDomainService.php @@ -114,6 +114,19 @@ public function posting(): PostingManager ); } + public function postingList( + int $obj_id, + Settings\Settings $settings, + bool $include_inactive = true + ): Posting\PostingList { + return new Posting\PostingList( + $obj_id, + $this->posting(), + $settings, + $include_inactive + ); + } + public function news(): NewsManager { return self::$instance["news"] ??= new NewsManager( diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index ddc5461168db..bc9d853621f7 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -52,7 +52,17 @@ public function navigation(): Navigation\GUIService public function presentation(): Presentation\GUIService { - return new Presentation\GUIService( + return self::$instance["presentation"] ??= new Presentation\GUIService( + $this->data_service, + $this->domain_service, + $this + ); + } + + public function editing(): Editing\GUIService + { + return self::$instance["editing"] ??= new Editing\GUIService( + $this->data_service, $this->domain_service, $this ); diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 11e4381f6043..7d44a01e4e8f 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -31,9 +31,10 @@ use ILIAS\Blog\ReadingTime\ReadingTimeManager; use ILIAS\Blog\Posting\PostingManager; use ILIAS\Blog\Contributor\ContributorGUI; +use ILIAS\Blog\Editing\EditingGUI; /** - * @ilCtrl_Calls ilObjBlogGUI: ilBlogPostingGUI, ilWorkspaceAccessGUI + * @ilCtrl_Calls ilObjBlogGUI: ilWorkspaceAccessGUI * @ilCtrl_Calls ilObjBlogGUI: ilInfoScreenGUI, ilNoteGUI, ilCommonActionDispatcherGUI * @ilCtrl_Calls ilObjBlogGUI: ilPermissionGUI, ilObjectCopyGUI * @ilCtrl_Calls ilObjBlogGUI: ilExportGUI, ilObjectContentStyleSettingsGUI, ilBlogExerciseGUI, ilObjNotificationSettingsGUI @@ -41,6 +42,8 @@ * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Settings\SettingsGUI * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Settings\BlockSettingsGUI * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Contributor\ContributorGUI + * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Editing\EditingGUI + * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Presentation\PresentationGUI */ class ilObjBlogGUI extends ilObject2GUI implements ilDesktopItemHandling { @@ -71,11 +74,9 @@ class ilObjBlogGUI extends ilObject2GUI implements ilDesktopItemHandling protected int $old_nr = 0; protected int $ppage = 0; protected int $user_page = 0; - protected string $prvm; //preview mode (fsc|emb) protected int $ntf = 0; protected int $apid = 0; protected string $new_type = ""; - protected bool $disable_notes = false; protected ContextServices $tool_context; protected \ILIAS\DI\UIServices $ui; protected \ILIAS\Style\Content\GUIService $content_style_gui; @@ -87,7 +88,6 @@ public function __construct( int $a_parent_node_id = 0 ) { global $DIC; - // other services $cs = $DIC->contentStyle(); $this->tool_context = $DIC->globalScreen()->tool()->context(); @@ -126,8 +126,6 @@ public function __construct( $this->ppage = $req->getPPage(); $this->user_page = $req->getUserPage(); $this->new_type = $req->getNewType(); - $this->prvm = $req->getPreviewMode(); - $this->ntf = $req->getNotification(); $this->apid = $req->getApId(); $this->month = $req->getMonth(); $this->keyword = $req->getKeyword(); @@ -143,6 +141,17 @@ public function __construct( $blog_id = 0; if ($this->object) { + $this->content_style_gui = $cs->gui(); + if (is_object($this->object)) { + if ($this->id_type !== self::REPOSITORY_NODE_ID) { + $this->content_style_domain = $cs->domain()->styleForObjId($this->object->getId()); + } else { + $this->content_style_domain = $cs->domain()->styleForRefId($this->object->getRefId()); + } + $this->blog_settings = + $domain->blogSettings()->getByObjId($this->object->getId()); + } + // gather postings by month $this->items = $this->buildPostingList($this->object->getId()); if ($this->items) { @@ -159,18 +168,6 @@ public function __construct( } $this->lng->loadLanguageModule("blog"); - $this->ctrl->saveParameter($this, "prvm"); - - $this->content_style_gui = $cs->gui(); - if (is_object($this->object)) { - if ($this->id_type !== self::REPOSITORY_NODE_ID) { - $this->content_style_domain = $cs->domain()->styleForObjId($this->object->getId()); - } else { - $this->content_style_domain = $cs->domain()->styleForRefId($this->object->getRefId()); - } - $this->blog_settings = - $domain->blogSettings()->getByObjId($this->object->getId()); - } $this->reading_time_gui = $gui->readingTime()->settingsGUI($blog_id); $this->reading_time_manager = $domain->readingTime(); @@ -267,12 +264,15 @@ protected function setTabs(): void $ilHelp->setScreenIdComponent("blog"); - if ($this->checkPermissionBool("read")) { + if ($this->perm->mayContribute()) { $this->ctrl->setParameterByClass(self::class, "bmn", null); $this->tabs_gui->addTab( "content", $lng->txt("content"), - $this->ctrl->getLinkTarget($this, "") + $this->ctrl->getLinkTargetByClass( + EditingGUI::class, + "" + ) ); } if ($this->checkPermissionBool("read")) { @@ -323,7 +323,13 @@ protected function setTabs(): void $this->tabs_gui->addNonTabbedLink( "preview", $lng->txt("blog_preview"), - $this->ctrl->getLinkTarget($this, "preview") + $this->ctrl->getLinkTargetByClass( + [ + self::class, + \ILIAS\Blog\Presentation\PresentationGUI::class + ], + "preview" + ) ); } parent::setTabs(); @@ -350,138 +356,16 @@ public function executeCommand(): void if (($this->id_type === self::REPOSITORY_NODE_ID) && !$this->getCreationMode() && $this->getAccessHandler()->checkAccess("read", "", $this->node_id)) { // see #22067 - $link = $ilCtrl->getLinkTargetByClass(["ilrepositorygui", "ilObjBlogGUI"], "preview"); + $link = $ilCtrl->getLinkTargetByClass([ + ilRepositoryGUI::class, + ilObjBlogGUI::class, + \ILIAS\Blog\Presentation\PresentationGUI::class + ], "preview"); $ilNavigationHistory->addItem($this->node_id, $link, "blog"); } switch ($next_class) { - case 'ilblogpostinggui': - $this->ctrl->saveParameter($this, "user_page"); - $tpl->loadStandardTemplate(); - - if (!$this->checkPermissionBool("read")) { - $this->tpl->setOnScreenMessage('info', $lng->txt("no_permission")); - return; - } - - // #9680 - if ($this->id_type === self::REPOSITORY_NODE_ID) { - $this->setLocator(); - } - - $style_sheet_id = $this->content_style_domain->getEffectiveStyleId(); - - $bpost_gui = new ilBlogPostingGUI( - $this->node_id, - $this->getAccessHandler(), - $this->blpg, - $this->old_nr, - ($this->object->getNotesStatus() && !$this->disable_notes), - $this->perm->mayEditPosting($this->blpg), - $style_sheet_id - ); - - // keep preview mode through notes gui (has its own commands) - switch ($cmd) { - // blog preview - case "previewFullscreen": - $ilCtrl->setParameter($this, "prvm", "fsc"); - break; - - default: - $this->setContentStyleSheet(); - - - $this->ctrl->setParameterByClass("ilblogpostinggui", "blpg", $this->blpg); - $this->tabs_gui->addNonTabbedLink( - "preview", - $lng->txt("blog_preview"), - $this->ctrl->getLinkTargetByClass("ilblogpostinggui", "previewFullscreen") - ); - $this->ctrl->setParameterByClass("ilblogpostinggui", "blpg", ""); - break; - } - - // keep preview mode through notes gui - if ($this->prvm) { - $cmd = "previewFullscreen"; - } - if ($cmd === "previewFullscreen") { - $this->renderToolbarNavigation($this->items, true); - } - $ret = $ilCtrl->forwardCommand($bpost_gui); - if (!$ilTabs->back_target) { - $ilCtrl->setParameter($this, "bmn", ""); - $ilTabs->setBackTarget( - $lng->txt("back"), - $ilCtrl->getLinkTarget($this, "") - ); - } - - if ($ret != "") { - // $is_owner = $this->object->getOwner() == $ilUser->getId(); - $is_owner = $this->perm->mayContribute(); - $is_active = $bpost_gui->getBlogPosting()->getActive(); - - // do not show inactive postings - if (($cmd === "previewFullscreen") - && !$is_owner && !$is_active) { - $this->ctrl->redirect($this, "preview"); - } - - switch ($cmd) { - // blog preview - case "previewFullscreen": - $this->addHeaderActionForCommand($cmd); - $this->filterInactivePostings(); - $nav = $this->renderNavigation("preview", $cmd); - $this->renderFullScreen($ret, $nav); - break; - - default: - // infos about draft status / snippet - $info = array(); - if (!$is_active) { - // single author blog (owner) in personal workspace - if ($this->id_type === self::WORKSPACE_NODE_ID) { - $info[] = $lng->txt("blog_draft_info"); - } else { - $info[] = $lng->txt("blog_draft_info_contributors"); - } - } - $public_action = false; - if ($cmd !== "history" && $cmd !== "edit" && $is_active && empty($info)) { - $info[] = $lng->txt("blog_new_posting_info"); - $public_action = true; - } - if ($this->blog_settings->getApproval() && !$bpost_gui->getBlogPosting()->isApproved()) { - // #9737 - $info[] = $lng->txt("blog_posting_edit_approval_info"); - } - if ($public_action) { - $this->tpl->setOnScreenMessage('success', implode("
", $info)); - } else { - if (count($info) > 0) { - $this->tpl->setOnScreenMessage('info', implode("
", $info)); - } - } - - // revert to edit cmd to avoid confusion - $tpl->setContent($ret); - if ($cmd !== "edit") { - $this->addHeaderActionForCommand("render"); - $nav = $this->renderNavigation("render", $cmd, "", $is_owner); - $tpl->setRightContent($nav); - } else { - $this->tabs->setBackTarget("", ""); - } - break; - } - } - break; - case "ilinfoscreengui": $this->prepareOutput(); - $this->addHeaderActionForCommand("render"); $this->infoScreenForward(); break; @@ -583,6 +467,34 @@ public function executeCommand(): void $this->ctrl->forwardCommand($gui); break; + case strtolower(\ILIAS\Blog\Editing\EditingGUI::class): + $this->prepareOutput(); + $this->addHeaderAction(); + $gui = $this->gui->editing()->editingGUI( + $this->node_id, + $this->id_type, + $this->perm, + $this->month, + $this->content_style_domain, + $this + ); + $this->ctrl->forwardCommand($gui); + break; + + case strtolower(\ILIAS\Blog\Presentation\PresentationGUI::class): + $this->prepareOutput(); + $this->initHeaderAction(null, null, true); + $gui = $this->gui->presentation()->presentationGUI( + $this, + $this->perm, + $this->content_style_domain, + $this->month, + $this->node_id, + $this->id_type, + ); + $this->ctrl->forwardCommand($gui); + break; + case strtolower(ContributorGUI::class): $this->checkPermission("write"); $this->prepareOutput(); @@ -607,18 +519,24 @@ public function executeCommand(): void break; default: + if ($cmd === "preview") { + $this->ctrl->setCmdClass(\ILIAS\Blog\Presentation\PresentationGUI::class); + $this->executeCommand(); + return; + } + if ($cmd === "" || $cmd === "render") { + $this->ctrl->setCmdClass(\ILIAS\Blog\Editing\EditingGUI::class); + $this->ctrl->setCmd("render"); + $this->executeCommand(); + return; + } + if ($cmd !== "gethtml") { // desktop item handling, must be toggled before header action if ($cmd === "addToDesk" || $cmd === "removeFromDesk") { $this->{$cmd . "Object"}(); - if ($this->prvm) { - $cmd = "preview"; - } else { - $cmd = "render"; - } - // $ilCtrl->setCmd($cmd); } - $this->addHeaderActionForCommand($cmd); + $this->addHeaderAction(); } parent::executeCommand(); } @@ -702,164 +620,25 @@ public function infoScreenForward(): void $this->ctrl->forwardCommand($info); } - /** - * Create new posting - */ - public function createPosting(): void - { - $ilCtrl = $this->ctrl; - $ilUser = $this->user; - - $title = $this->blog_request->getTitle(); - if ($title) { - // create new posting - $posting = new ilBlogPosting(); - $posting->setTitle($title); - $posting->setBlogId($this->object->getId()); - $posting->setActive(false); - $posting->setAuthor($ilUser->getId()); - $posting->create(false); - - // switch month list to current month (will include new posting) - $ilCtrl->setParameter($this, "bmn", date("Y-m")); - - $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $posting->getId()); - $ilCtrl->redirectByClass("ilblogpostinggui", "edit"); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt("msg_no_title"), true); - $ilCtrl->redirect($this, "render"); - } - } - - /** - * Render object context - */ - public function render(): void - { - $tpl = $this->tpl; - $ilTabs = $this->tabs; - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $ilToolbar = new ilToolbarGUI(); - - if (!$this->checkPermissionBool("read")) { - $this->tpl->setOnScreenMessage('info', $lng->txt("no_permission")); - return; - } - - $ilTabs->activateTab("content"); - - // toolbar - if ($this->perm->mayContribute()) { - $ilToolbar->setFormAction($ilCtrl->getFormAction($this, "createPosting")); - - $title = new ilTextInputGUI($lng->txt("title"), "title"); - $title->setSize(30); - $ilToolbar->addStickyItem($title, true); - $tpl->addOnLoadCode(" - document.getElementById('title').setAttribute('data-blog-input', 'posting-title'); - document.getElementById('title').setAttribute('placeholder', ' '); - "); - - $this->gui->button( - $lng->txt("blog_add_posting"), - "createPosting" - )->submit()->toToolbar(true, $ilToolbar); - - // #18763 - $keys = array_keys($this->items); - $first = array_shift($keys); - if ($first != $this->month) { - $ilToolbar->addSeparator(); - - $ilCtrl->setParameter($this, "bmn", $first); - $url = $ilCtrl->getLinkTarget($this, ""); - $ilCtrl->setParameter($this, "bmn", $this->month); - - $this->gui->link( - $lng->txt("blog_show_latest"), - $url - )->emphasised()->toToolbar(true, $ilToolbar); - } - - // print/pdf - $print_view = $this->getPrintView(); - $modal_elements = $print_view->getModalElements( - $this->ctrl->getLinkTarget( - $this, - "printViewSelection" - ) - ); - $ilToolbar->addSeparator(); - $ilToolbar->addComponent($modal_elements->button); - $ilToolbar->addComponent($modal_elements->modal); - } - - // $is_owner = ($this->object->getOwner() == $ilUser->getId()); - $is_owner = $this->perm->mayContribute(); - - $list_items = $this->getListItems($is_owner); - - $list = $nav = ""; - if ($list_items) { - $list = $this->renderList($list_items, "preview", "", $is_owner); - $nav = $this->renderNavigation("render", "edit", "", $is_owner); - } - - $this->setContentStyleSheet(); - - $tpl->setContent($ilToolbar->getHTML() . $list); - $tpl->setRightContent($nav); - } - /** * Filter blog postings by month, keyword or author */ - protected function getListItems( + public function getListItems( bool $a_show_inactive = false ): array { - if ($this->author) { - $list_items = array(); - foreach ($this->items as $month => $items) { - foreach ($items as $id => $item) { - /** @var \ILIAS\Blog\Posting\Posting $item */ - $author_id = $item->getAuthor(); - $editors = []; - foreach (\ilPageObject::getPageContributors("blp", $item->getId()) as $editor) { - if ($editor["user_id"] != $author_id) { - $editors[] = (int) $editor["user_id"]; - } - } - if ($author_id === $this->author || in_array($this->author, $editors, true)) { - $list_items[$id] = $item; - } - } - } - } elseif ($this->keyword) { - $list_items = $this->filterItemsByKeyword($this->items, $this->keyword); - } else { - $max = $this->blog_settings->getOverviewPostings(); - if ($this->month_default && $max) { - $list_items = array(); - foreach ($this->items as $month => $postings) { - foreach ($postings as $id => $item) { - if (!$a_show_inactive && - !ilBlogPosting::_lookupActive($id, "blp")) { - continue; - } - $list_items[$id] = $item; + return $this->getListItemsInternal($a_show_inactive); + } - if (count($list_items) >= $max) { - break(2); - } - } - } - } else { - $list_items = $this->items[$this->month] ?? []; - } - } - return $list_items; + protected function getListItemsInternal( + bool $a_show_inactive = false + ): array { + return $this->domain->postingList($this->obj_id, $this->blog_settings, $a_show_inactive) + ->getPostingsForView( + $this->author ?? 0, + $this->keyword ?? "", + $this->month ?? "" + ); } /** @@ -867,26 +646,9 @@ protected function getListItems( */ public function preview(): void { - $lng = $this->lng; - $toolbar = $this->toolbar; - - if (!$this->checkPermissionBool("read")) { - $this->tpl->setOnScreenMessage('info', $lng->txt("no_permission")); - return; - } - - $this->filterInactivePostings(); - - $list_items = $this->getListItems(); - - $list = $nav = ""; - if ($list_items) { - $list = $this->renderList($list_items, "previewFullscreen"); - $nav = $this->renderNavigation("preview", "previewFullscreen"); - $this->renderToolbarNavigation($this->items); - } - - $this->renderFullScreen($list, $nav); + $this->ctrl->setCmdClass(\ILIAS\Blog\Presentation\PresentationGUI::class); + $this->ctrl->setCmd("preview"); + $this->executeCommand(); } /** @@ -939,7 +701,7 @@ public function renderFullScreen( $this->ctrl->setParameterByClass("ilblogpostinggui", "blpg", $this->blpg); $back = $this->ctrl->getLinkTargetByClass("ilblogpostinggui", "preview"); } - $this->ctrl->setParameter($this, "prvm", $this->prvm); + //$this->ctrl->setParameter($this, "prvm", $this->prvm); } $back_caption = $this->lng->txt("blog_back_to_blog_owner"); @@ -1041,33 +803,13 @@ public function renderFullscreenHeader( protected function buildPostingList( int $a_obj_id ): array { - $author_found = false; - - $items = array(); - foreach ($this->posting_manager->getAllPostings($a_obj_id) as $posting) { - // author filter pre-check - if ($this->author) { - $author_id = $posting->getAuthor(); - $editors = []; - foreach (\ilPageObject::getPageContributors("blp", $posting->getId()) as $editor) { - if ($editor["user_id"] != $author_id) { - $editors[] = (int) $editor["user_id"]; - } - } - if ($author_id === $this->author || in_array($this->author, $editors, true)) { - $author_found = true; - } - } + $posting_list = $this->domain->postingList($a_obj_id, $this->blog_settings); - $month = substr($posting->getCreated()->get(IL_CAL_DATE), 0, 7); - $items[$month][$posting->getId()] = $posting; - } - - if ($this->author && !$author_found) { + if ($this->author && !$posting_list->hasAuthorPostings($this->author)) { $this->author = null; } - return $items; + return $posting_list->getPostingsGroupedByMonth(); } /** @@ -1080,448 +822,41 @@ public function renderList( bool $a_show_inactive = false, string $a_export_directory = "" ): string { - $lng = $this->lng; - $ilCtrl = $this->ctrl; - $ilUser = $this->user; - $ui_factory = $this->ui->factory(); - $ui_renderer = $this->ui->renderer(); - - $wtpl = new ilTemplate("tpl.blog_list.html", true, true, "components/ILIAS/Blog"); - - $is_admin = $this->perm->canManage(); - - $last_month = null; - $is_empty = true; - foreach ($items as $item) { - /** @var \ILIAS\Blog\Posting\Posting $item */ - $item_id = $item->getId(); - $author = $item->getAuthor(); - $created = $item->getCreated(); - $approved = $item->isApproved(); - // only published items - $is_active = ilBlogPosting::_lookupActive($item_id, "blp"); - if (!$is_active && !$a_show_inactive) { - continue; - } - - $is_empty = false; - - $month = ""; - if (!$this->keyword && !$this->author) { - $month = substr($created->get(IL_CAL_DATE), 0, 7); - } - - if (!$last_month || $last_month != $month) { - if ($last_month) { - $wtpl->setCurrentBlock("month_bl"); - $wtpl->parseCurrentBlock(); - } - - // title according to current "filter"/navigation - if ($this->keyword) { - $title = $lng->txt("blog_keyword") . ": " . $this->keyword; - } elseif ($this->author) { - $title = $lng->txt("blog_author") . ": " . $this->profile_gui->getNamePresentation($this->author); - } else { - $title = $this->gui->presentation()->util()->getMonthPresentation($month); - $last_month = $month; - } - - $wtpl->setVariable("TXT_CURRENT_MONTH", $title); - } - - if (!$a_link_template) { - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", $this->month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $item_id); - $preview = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", $a_cmd); - } else { - $preview = $this->buildExportLink($a_link_template, "posting", (string) $item_id); - } - $more_link = $preview; - - // actions - $posting_edit = $this->perm->mayEditPosting($item_id, $author); - if (($posting_edit || $is_admin) && !$a_link_template && $a_cmd === "preview") { - $actions = []; - - if ($is_active && $this->blog_settings->getApproval() && !$approved) { - if ($is_admin) { - $ilCtrl->setParameter($this, "apid", $item_id); - $actions[] = $ui_factory->link()->standard( - $lng->txt("blog_approve"), - $ilCtrl->getLinkTarget($this, "approve") - ); - $ilCtrl->setParameter($this, "apid", ""); - } - - $wtpl->setVariable("APPROVAL", $lng->txt("blog_needs_approval")); - } - - if ($posting_edit) { - $actions[] = $ui_factory->link()->standard( - $lng->txt("edit_content"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edit") - ); - $more_link = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edit"); - - // #11858 - if ($is_active) { - $actions[] = $ui_factory->link()->standard( - $lng->txt("blog_toggle_draft"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "deactivatePageToList") - ); - } else { - $actions[] = $ui_factory->link()->standard( - $lng->txt("blog_toggle_final"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "activatePageToList") - ); - } - - $actions[] = $ui_factory->link()->standard( - $lng->txt("rename"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edittitle") - ); - - if ($this->blog_settings->getKeywords()) { // #13616 - $actions[] = $ui_factory->link()->standard( - $lng->txt("blog_edit_keywords"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "editKeywords") - ); - } - - $actions[] = $ui_factory->link()->standard( - $lng->txt("blog_edit_date"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "editdate") - ); - - $actions[] = $ui_factory->link()->standard( - $lng->txt("delete"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "deleteBlogPostingConfirmationScreen") - ); - } elseif ($is_admin) { - // #10513 - if ($is_active) { - $ilCtrl->setParameter($this, "apid", $item_id); - $actions[] = $ui_factory->link()->standard( - $lng->txt("blog_toggle_draft_admin"), - $ilCtrl->getLinkTarget($this, "deactivateAdmin") - ); - $ilCtrl->setParameter($this, "apid", ""); - } - - $actions[] = $ui_factory->link()->standard( - $lng->txt("delete"), - $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "deleteBlogPostingConfirmationScreen") - ); - } - - $dd = $ui_factory->dropdown()->standard($actions)->withLabel($this->lng->txt("actions")); - - $wtpl->setCurrentBlock("actions"); - $wtpl->setVariable("ACTION_SELECTOR", $ui_renderer->render($dd)); - $wtpl->parseCurrentBlock(); - } - - // comments - if ($this->object->getNotesStatus() && !$a_link_template && !$this->disable_notes) { - // count (public) notes - $notes_context = $this->notes - ->data() - ->context( - $this->obj_id, - (int) $item_id, - "blp" - ); - $count = $this->notes - ->domain() - ->getNrOfCommentsForContext($notes_context); - - if ($a_cmd !== "preview") { - $wtpl->setCurrentBlock("comments"); - $wtpl->setVariable("TEXT_COMMENTS", $lng->txt("blog_comments")); - $wtpl->setVariable("URL_COMMENTS", $preview); - $wtpl->setVariable("COUNT_COMMENTS", $count); - $wtpl->parseCurrentBlock(); - } - } - - // permanent link - if ($this->node_id !== null && - $a_cmd !== "preview") { - if ($this->id_type === self::WORKSPACE_NODE_ID) { - $goto = $this->gui->permanentLink(0, (int) $this->node_id)->getPermanentLink((int) $item_id); - } else { - $goto = $this->gui->permanentLink((int) $this->node_id)->getPermanentLink((int) $item_id); - } - $wtpl->setCurrentBlock("permalink"); - $wtpl->setVariable("URL_PERMALINK", $goto); - $wtpl->setVariable("TEXT_PERMALINK", $lng->txt("blog_link")); - $wtpl->parseCurrentBlock(); - } - - $snippet = $this->gui->posting()->getSnippet( - $item_id, - $this->blog_settings->getAbstractShorten(), - $this->blog_settings->getAbstractShortenLength(), - "…", - $this->blog_settings->getAbstractImage(), - $this->blog_settings->getAbstractImageWidth(), - $this->blog_settings->getAbstractImageHeight(), - $a_export_directory - ); - - if ($snippet) { - $wtpl->setCurrentBlock("more"); - $wtpl->setVariable("URL_MORE", $more_link); - $wtpl->setVariable("TEXT_MORE", $lng->txt("blog_list_more")); - $wtpl->parseCurrentBlock(); - } - - - - if (!$is_active) { - $wtpl->setCurrentBlock("draft_text"); - $wtpl->setVariable("DRAFT_TEXT", $lng->txt("blog_draft_text")); - $wtpl->parseCurrentBlock(); - $wtpl->setVariable("DRAFT_CLASS", " ilBlogListItemDraft"); - } - - // reading time - $reading_time = $this->reading_time_manager->getReadingTime( - $this->object->getId(), - $item_id - ); - if (!is_null($reading_time)) { - $this->lng->loadLanguageModule("copg"); - $wtpl->setCurrentBlock("reading_time"); - $wtpl->setVariable( - "READING_TIME", - $this->lng->txt("copg_est_reading_time") . ": " . - sprintf($this->lng->txt("copg_x_minutes"), $reading_time) - ); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("posting"); - - $author_str = ""; - if ($this->id_type === self::REPOSITORY_NODE_ID) { - $authors = array(); - - // primary author - if ($author) { - $authors[] = $this->profile_gui->getNamePresentation($author); - } - - // additional editors - foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { - $editor_id = (int) $editor["user_id"]; - if ($editor_id !== $author) { - $authors[] = $this->profile_gui->getNamePresentation($editor_id); - } - } - - if ($authors) { - $author_str = implode(", ", $authors) . " - "; - } - } - - // title - $wtpl->setVariable("URL_TITLE", $preview); - $wtpl->setVariable("TITLE", $item->getTitle()); - - $kw = $this->posting_manager->getKeywords($this->obj_id, $item_id); - natcasesort($kw); - $keywords = (count($kw) > 0) - ? "
" . $this->lng->txt("keywords") . ": " . implode(", ", $kw) - : ""; - - $wtpl->setVariable("DATETIME", $author_str . - ilDatePresentation::formatDate($created) . $keywords); - - // content - $wtpl->setVariable("CONTENT", $snippet); - - $wtpl->parseCurrentBlock(); - } - - // permalink - if ($a_cmd === "previewFullscreen") { - $ref_id = ($this->id_type === self::WORKSPACE_NODE_ID) - ? 0 - : $this->node_id; - $wsp_id = ($this->id_type === self::WORKSPACE_NODE_ID) - ? $this->node_id - : 0; - $this->gui->permanentLink($ref_id, $wsp_id)->setPermanentLink(); - } - - if (!$is_empty || $a_show_inactive) { - return $wtpl->get(); - } - return ""; + return $this->gui->posting()->postingList( + $this, + $this->perm, + $this->month, + $this->node_id, + $this->id_type, + )->render( + $items, + $a_cmd, + $a_link_template, + $a_show_inactive, + $a_export_directory + ); } - /** - * Build export link - */ - protected function buildExportLink( + public function buildExportLink( string $a_template, string $a_type, string $a_id ): string { - $blog_export = new BlogHtmlExport($this, "", ""); - return $blog_export->buildExportLink($a_template, $a_type, $a_id, $this->getKeywords(false)); - } - - - /** - * Toolbar navigation - */ - public function renderToolbarNavigation( - array $a_items, - bool $single_posting = false - ): void { - $nav_renderer = $this->gui->navigation()->toolbarNavigationRenderer(); - $nav_renderer->renderToolbarNavigation( - $this->perm, - $a_items, - $this->blpg, - $single_posting, - $this->month, - $this->user_page - ); + return $this->buildExportLinkInternal($a_template, $a_type, $a_id); } - /** - * Build navigation blocks - */ - public function renderNavigation( - string $a_list_cmd = "render", - string $a_posting_cmd = "preview", - ?string $a_link_template = null, - bool $a_show_inactive = false, - int $a_blpg = 0 + protected function buildExportLinkInternal( + string $a_template, + string $a_type, + string $a_id ): string { - $ilSetting = $this->settings; - $a_items = $this->items; - $blpg = ($a_blpg > 0) - ? $a_blpg - : $this->blpg; - - if ($this->blog_settings->getOrder()) { - $order = array_flip($this->blog_settings->getOrder()); - } else { - $order = array( - "navigation" => 0 - ,"keywords" => 2 - ,"authors" => 1 - ); - } - - $wtpl = new ilTemplate("tpl.blog_list_navigation.html", true, true, "components/ILIAS/Blog"); - - $blocks = array(); - - // by date - if (count($a_items)) { - $blocks[$order["navigation"] ?? 0] = array( - $this->lng->txt("blog_navigation"), - $this->gui->navigation()->monthBlock()->render( - $a_items, - $a_list_cmd, - $a_posting_cmd, - $a_link_template, - $a_show_inactive, - $a_blpg - ) - ); - } - - if ($this->blog_settings->getKeywords()) { - // keywords - $may_edit_keywords = ($blpg > 0 && - $this->perm->mayEditPosting($blpg) && - $a_list_cmd !== "preview" && - $a_list_cmd !== "gethtml" && - !$a_link_template); - $keywords = $this->gui->navigation()->keywordBlock()->render( - $a_items, - $a_list_cmd, - $a_show_inactive, - (string) $a_link_template, - $a_blpg - ); - if ($keywords || $may_edit_keywords) { - if (!$keywords) { - $keywords = $this->lng->txt("blog_no_keywords"); - } - $cmd = null; - $blocks[$order["keywords"] ?? 2] = array( - $this->lng->txt("blog_keywords"), - $keywords, - $cmd - ? array($cmd, $this->lng->txt("blog_edit_keywords")) - : null - ); - } - } - - // is not part of (html) export - if (!$a_link_template) { - // authors - if ($this->id_type === self::REPOSITORY_NODE_ID && - $this->blog_settings->getAuthors()) { - $authors = $this->gui->navigation()->authorBlock()->render( - $a_items, - $a_list_cmd, - $a_show_inactive - ); - if ($authors) { - $blocks[$order["authors"] ?? 1] = array($this->lng->txt("blog_authors"), $authors); - } - } - - // rss - if ($this->blog_settings->getRSS() && - $ilSetting->get('enable_global_profiles') && - $a_list_cmd === "preview") { - // #10827 - $blog_id = $this->node_id; - if ($this->id_type !== self::WORKSPACE_NODE_ID) { - $blog_id .= "_cll"; - } - $url = ILIAS_HTTP_PATH . "/feed.php?blog_id=" . $blog_id . - "&client_id=" . rawurlencode(CLIENT_ID); - - $wtpl->setVariable("RSS_BUTTON", ilRSSButtonGUI::get(ilRSSButtonGUI::ICON_RSS, $url)); - } - } - - if (count($blocks)) { - $ui_factory = $this->ui->factory(); - $ui_renderer = $this->ui->renderer(); - - ksort($blocks); - foreach ($blocks as $block) { - $title = $block[0]; - - $content = $block[1]; - - $secondary_panel = $ui_factory->panel()->secondary()->legacy($title, $ui_factory->legacy()->content($content)); - - if (isset($block[2]) && is_array($block[2])) { - $link = $ui_factory->button()->shy($block[2][1], $block[2][0]); - $secondary_panel = $secondary_panel->withFooter($link); - } - - $wtpl->setCurrentBlock("block_bl"); - $wtpl->setVariable("BLOCK", $ui_renderer->render($secondary_panel)); - $wtpl->parseCurrentBlock(); - } - } - - return $wtpl->get(); + $blog_export = new BlogHtmlExport( + $this, + $this->id_type === self::REPOSITORY_NODE_ID, + "", + "" + ); + return $blog_export->buildExportLink($a_template, $a_type, $a_id, $this->getKeywords(false)); } /** @@ -1595,52 +930,29 @@ public function buildExportFile( $subdir .= "print"; } - $blog_export = new BlogHtmlExport($this, "", $subdir); + $blog_export = new BlogHtmlExport( + $this, + $this->id_type === self::REPOSITORY_NODE_ID, + "", + $subdir + ); $blog_export->setPrintVersion($print_version); $blog_export->includeComments($a_include_comments); $blog_export->exportHTML(); return $blog_export; } - public function getNotesSubId(): int - { - return $this->blpg; - } - public function disableNotes(bool $a_value = false): void + public function addPresentationHeaderAction(): void { - $this->disable_notes = $a_value; - } - - protected function addHeaderActionForCommand( - string $a_cmd - ): void { - $ilUser = $this->user; - $ilCtrl = $this->ctrl; - // preview? - if ($a_cmd === "preview" || $a_cmd === "previewFullscreen" || $this->prvm) { - // notification - if ($ilUser->getId() !== ANONYMOUS_USER_ID) { - if (!$this->prvm) { - $ilCtrl->setParameter($this, "prvm", "fsc"); - } - $this->insertHeaderAction($this->initHeaderAction(null, null, true)); - if (!$this->prvm) { - $ilCtrl->setParameter($this, "prvm", ""); - } - } - } else { - $this->addHeaderAction(); - } + $this->insertHeaderAction($this->initHeaderAction(null, null, true)); } protected function initHeaderAction( ?string $sub_type = null, ?int $sub_id = null, - bool $is_preview = false + bool $presenation = false ): ?ilObjectListGUI { - $ilUser = $this->user; - $ilCtrl = $this->ctrl; if (!$this->obj_id) { return null; } @@ -1656,83 +968,13 @@ protected function initHeaderAction( } $lg->enableComments(false); $lg->enableNotes(false); - - if ($is_preview) { - if ($this->blpg > 0) { - if (($this->object->getNotesStatus() && !$this->disable_notes)) { - $lg->enableComments(true); - } - $lg->enableNotes(true); - } - $lg->enableTags(false); - - if (ilNotification::hasNotification(ilNotification::TYPE_BLOG, $ilUser->getId(), $this->obj_id)) { - $ilCtrl->setParameter($this, "ntf", 1); - $link = $ilCtrl->getLinkTarget($this, "setNotification"); - $ilCtrl->setParameter($this, "ntf", ""); - if (ilNotification::hasOptOut($this->obj_id)) { - $lg->addCustomCommand($link, "blog_notification_toggle_off"); - } - - $lg->addHeaderIcon( - "not_icon", - ilUtil::getImagePath("object/notification_on.svg"), - $this->lng->txt("blog_notification_activated") - ); - } else { - $ilCtrl->setParameter($this, "ntf", 2); - $link = $ilCtrl->getLinkTarget($this, "setNotification"); - $ilCtrl->setParameter($this, "ntf", ""); - $lg->addCustomCommand($link, "blog_notification_toggle_on"); - - $lg->addHeaderIcon( - "not_icon", - ilUtil::getImagePath("object/notification_off.svg"), - $this->lng->txt("blog_notification_deactivated") - ); - } - - // #11758 - if ($this->perm->mayContribute()) { - $ilCtrl->setParameter($this, "prvm", ""); - - $ilCtrl->setParameter($this, "bmn", ""); - $ilCtrl->setParameter($this, "blpg", ""); - $link = $ilCtrl->getLinkTarget($this, ""); - $ilCtrl->setParameter($this, "blpg", $sub_id); - $ilCtrl->setParameter($this, "bmn", $this->month); - $lg->addCustomCommand($link, "blog_edit"); // #11868 - - if ($sub_id && $this->perm->mayEditPosting($sub_id)) { - $link = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", "edit"); - $lg->addCustomCommand($link, "blog_edit_posting"); - } - - $ilCtrl->setParameter($this, "prvm", "fsc"); - } - - $ilCtrl->setParameter($this, "ntf", ""); - } - - return $lg; - } - - protected function setNotification(): void - { - $ilUser = $this->user; - $ilCtrl = $this->ctrl; - - switch ($this->ntf) { - case 1: - ilNotification::setNotification(ilNotification::TYPE_BLOG, $ilUser->getId(), $this->obj_id, false); - break; - - case 2: - ilNotification::setNotification(ilNotification::TYPE_BLOG, $ilUser->getId(), $this->obj_id, true); - break; + if (!$presenation) { + return $lg; } - - $ilCtrl->redirect($this, "preview"); + return $this->gui->navigation()->presentationHeader( + $this->object, + $this->perm, + )->get($lg, $this->blpg); } /** @@ -1753,7 +995,16 @@ public static function lookupSubObjectTitle( /** * Filter inactive items from items list */ - protected function filterInactivePostings(): void + public function checkPermissionBool( + string $perm, + string $cmd = "", + string $type = "", + ?int $ref_id = null + ): bool { + return parent::checkPermissionBool($perm, $cmd, $type, $ref_id); + } + + public function filterInactivePostings(): void { foreach ($this->items as $month => $postings) { foreach ($postings as $id => $item) { diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php index d0437edddf09..e34cf3507a32 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php @@ -147,4 +147,26 @@ public function getModalTemplate(): array return $modalt; } + public function getCommandLink(string $cmd): string + { + switch ($cmd) { + case "render": + $this->ctrl->setParameterByClass(ilObjBlogGUI::class, "ref_id", $this->ref_id); + return $this->ctrl->getLinkTargetByClass( + [ilObjBlogGUI::class, \ILIAS\Blog\Editing\EditingGUI::class], + "" + ); + break; + case "preview": + $this->ctrl->setParameterByClass(ilObjBlogGUI::class, "ref_id", $this->ref_id); + return $this->ctrl->getLinkTargetByClass( + [ilObjBlogGUI::class, \ILIAS\Blog\Presentation\PresentationGUI::class], + "" + ); + break; + } + return parent::getCommandLink($cmd); + } + + } diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php index aac6cefae523..c2569917934a 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php @@ -306,6 +306,7 @@ protected function assignObject(): void public function prepareOutput(bool $show_sub_objects = true): bool { + $this->tpl->loadStandardTemplate(); $base_class = $this->request_wrapper->retrieve("baseClass", $this->refinery->kindlyTo()->string()); if (strtolower($base_class) == "iladministrationgui") { From 7c023d400dada3a1bb85fbacab54e552c01381bd Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sun, 5 Jul 2026 20:21:44 +0200 Subject: [PATCH 044/333] blog: fixed header; added change log entries --- components/ILIAS/Blog/CHANGELOG.md | 6 ++++++ components/ILIAS/Blog/Export/BlogHtmlExport.php | 8 +++++--- components/ILIAS/Blog/Navigation/SideBarGUI.php | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Blog/CHANGELOG.md b/components/ILIAS/Blog/CHANGELOG.md index 48a1c83b8815..3df2ee827bc8 100755 --- a/components/ILIAS/Blog/CHANGELOG.md +++ b/components/ILIAS/Blog/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## ILIAS 12 +- Separated presentation and editing GUI +- Moved month, author and keyword block to separate classes +- Removed all static function declarations (that are not enforced by other components) +- Removed all DIC access outside of constructors + ## ILIAS 11 - Data/Repository/Domain classes for Postings - Refactored news and notification related code diff --git a/components/ILIAS/Blog/Export/BlogHtmlExport.php b/components/ILIAS/Blog/Export/BlogHtmlExport.php index 7289e35c1934..328252e40d0b 100755 --- a/components/ILIAS/Blog/Export/BlogHtmlExport.php +++ b/components/ILIAS/Blog/Export/BlogHtmlExport.php @@ -378,9 +378,11 @@ protected function getInitialisedTemplate( $tabs->setBackTarget($this->lng->txt("back"), $a_back_url); } - /** @var \ILIAS\DI\Container $DIC */ - global $DIC; - $tpl = new \ilGlobalPageTemplate($this->global_screen, $DIC->ui(), $DIC->http()); + $tpl = new \ilGlobalPageTemplate( + $this->global_screen, + $this->gui->ui(), + $this->gui->http() + ); $this->co_page_html_export->getPreparedMainTemplate($tpl); diff --git a/components/ILIAS/Blog/Navigation/SideBarGUI.php b/components/ILIAS/Blog/Navigation/SideBarGUI.php index 2000fc33fe67..71f594a269c5 100644 --- a/components/ILIAS/Blog/Navigation/SideBarGUI.php +++ b/components/ILIAS/Blog/Navigation/SideBarGUI.php @@ -1,6 +1,20 @@ Date: Mon, 6 Jul 2026 08:26:42 +0200 Subject: [PATCH 045/333] [Fix] LDAP: Correctly display user synchronization cron status See: https://mantis.ilias.de/view.php?id=48023 --- .../LDAP/classes/class.ilLDAPCronSynchronization.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/LDAP/classes/class.ilLDAPCronSynchronization.php b/components/ILIAS/LDAP/classes/class.ilLDAPCronSynchronization.php index 0df6facc2439..ccd3462fff4d 100755 --- a/components/ILIAS/LDAP/classes/class.ilLDAPCronSynchronization.php +++ b/components/ILIAS/LDAP/classes/class.ilLDAPCronSynchronization.php @@ -165,10 +165,10 @@ private function deactivateUsers(ilLDAPServer $server, array $a_ldap_users): voi public function addToExternalSettingsForm(int $a_form_id, array &$a_fields, bool $a_is_active): void { if ($a_form_id === ilAdministrationSettingsFormHandler::FORM_LDAP) { - $a_fields["ldap_user_sync_cron"] = [$a_is_active ? - $this->lng->txt("enabled") : - $this->lng->txt("disabled"), - ilAdministrationSettingsFormHandler::VALUE_BOOL]; + $a_fields["ldap_user_sync_cron"] = [ + $a_is_active, + ilAdministrationSettingsFormHandler::VALUE_BOOL + ]; } } } From 89f9a519c49a8a75631bec011ee9c9d5445a845f Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 6 Jul 2026 10:13:06 +0200 Subject: [PATCH 046/333] [FIX] #47986 UI: update `Text` and `Textarea` default value (#11714) These `UI\Component\Input\Field` components cannot handle `null` as default value. Their constraint for requirement and default operations rely on working with a string. Hence the default value is changed to an empty string (`""`). * Fix https://mantis.ilias.de/view.php?id=47986 --- .../UI/src/Implementation/Component/Input/Field/Text.php | 5 +++++ .../UI/src/Implementation/Component/Input/Field/Textarea.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Text.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Text.php index 46c1e5a765a0..749e8dc3911e 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Text.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Text.php @@ -67,6 +67,11 @@ public function getMaxLength(): ?int return $this->max_length; } + public function withValue($value): self + { + return parent::withValue($value ?? ""); + } + /** * @inheritdoc */ diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Textarea.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Textarea.php index 9a9682c28352..2dbab9a14c46 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Textarea.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Textarea.php @@ -101,6 +101,11 @@ public function getMinLimit(): ?int return $this->min_limit; } + public function withValue($value): self + { + return parent::withValue($value ?? ""); + } + /** * @inheritdoc */ From dd9316b0db0f2e979fd300dbf15b6ae05c54bebf Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 6 Jul 2026 11:32:28 +0200 Subject: [PATCH 047/333] [FEATURE] UI: add `Listing\Inline` and `Listing\Entity\Grid` components (#11444) Breaking changes: - For plugins that provide a custom implementation of `ILIAS\UI\Component\Listing\Entity\Factory` and/or `ILIAS\UI\Component\Listing\Factory` by using the according exchange mechanism. - For plugins that provide a custom implementation of the following UI components and their respective interface: - `ILIAS\UI\Component\Entity\Entity` - `ILIAS\UI\Component\Listing\Property` - `ILIAS\UI\Component\Symbol\Glyph` - For skins that replace one or more of the following templates: - `Entity/tpl.entity.html` - `Listing/tpl.propertylisting.html` Given the novelty status of the entity (and associated) UI component(s), such cases should be rare, and any breaking change could be fixed in a backwards compatible manner. We therefore consider these breaking changes acceptable, as their value outweighs their harm. --- .../Notification/BaseNotificationSetUpTBD.php | 16 +- .../LearningSequence/tests/IliasMocks.php | 4 +- .../Scoring/Settings/ScoreSettingsTest.php | 12 +- components/ILIAS/UI/UI.php | 2 +- .../Image/mountains_widescreen-thumbnail.jpg | Bin 0 -> 120908 bytes .../sanfrancisco_widescreen-thumbnail.jpg | Bin 0 -> 262511 bytes .../images/Image/ski_widescreen-thumbnail.jpg | Bin 0 -> 122016 bytes .../ILIAS/UI/src/Component/Entity/Entity.php | 25 +- .../src/Component/Listing/Entity/Factory.php | 29 +- .../UI/src/Component/Listing/Entity/Grid.php | 25 + .../UI/src/Component/Listing/Factory.php | 67 ++- .../ILIAS/UI/src/Component/Listing/Inline.php | 24 + .../UI/src/Component/Listing/Property.php | 7 +- .../UI/src/Component/Symbol/Glyph/Glyph.php | 5 + .../Component/Entity/Entity.php | 42 +- .../Component/Entity/Renderer.php | 37 +- .../Component/Listing/Entity/Factory.php | 5 + .../Component/Listing/Entity/Grid.php | 27 ++ .../Component/Listing/Entity/Renderer.php | 22 +- .../Component/Listing/Factory.php | 8 + .../Component/Listing/Inline.php | 26 + .../Component/Listing/Property.php | 14 +- .../Component/Listing/Renderer.php | 112 +++-- .../Component/Symbol/Glyph/Factory.php | 127 ++--- .../Component/Symbol/Glyph/Renderer.php | 3 +- .../UI/src/examples/Entity/Standard/base.php | 12 +- .../Entity/Standard/semantic_groups.php | 30 +- .../examples/Entity/Standard/video_object.php | 111 +++++ .../Standard/with_open_in_new_viewport.php | 27 ++ .../src/examples/Listing/Entity/Grid/base.php | 128 +++++ .../examples/Listing/Entity/Standard/base.php | 5 +- .../UI/src/examples/Listing/Inline/base.php | 30 ++ .../Listing/Inline/property_listing.php | 46 ++ .../UI/src/examples/Listing/Property/base.php | 40 +- .../templates/default/Entity/tpl.entity.html | 76 ++- .../Listing/tpl.entitylistinggrid.html | 5 + .../templates/default/Listing/tpl.inline.html | 5 + .../default/Listing/tpl.propertylisting.html | 12 +- .../Component/Card/RepositoryObjectTest.php | 5 +- .../Counter/CounterClientHtmlTest.php | 4 +- .../UI/tests/Component/Entity/EntityTest.php | 140 ++++-- .../Container/Filter/FilterInputTest.php | 4 +- .../Container/Filter/StandardFilterTest.php | 4 +- .../Input/Field/DateTimeInputTest.php | 9 +- .../Input/Field/DurationInputTest.php | 10 +- .../Component/Input/Field/FileInputTest.php | 3 +- .../Input/Field/HasOptionFilterTestHelper.php | 11 +- .../Input/ViewControl/ViewControlTestBase.php | 11 +- .../Item/ItemNotificationClientHtmlTest.php | 11 +- .../Component/Item/ItemNotificationTest.php | 11 +- .../Component/Launcher/LauncherInlineTest.php | 11 +- .../Listing/Entity/GridEntityListingTest.php | 177 +++++++ .../tests/Component/Listing/ListingTest.php | 34 ++ .../Listing/Property/PropertyListingTest.php | 107 +++- .../Component/MainControls/MainBarTest.php | 15 +- .../Component/MainControls/MetaBarTest.php | 13 +- .../Component/MainControls/ModeInfoTest.php | 11 +- .../MainControls/Slate/DrilldownSlateTest.php | 11 +- .../Slate/NotificationSlateTest.php | 11 +- .../Component/MainControls/SystemInfoTest.php | 11 +- .../Menu/Drilldown/DrilldownTest.php | 13 +- .../Panel/PanelSecondaryLegacyTest.php | 11 +- .../Panel/PanelSecondaryListingTest.php | 11 +- .../UI/tests/Component/Panel/PanelTest.php | 11 +- .../Component/Symbol/Glyph/GlyphTest.php | 4 +- .../Component/Table/PresentationTest.php | 11 +- .../Component/Table/TableRendererTestBase.php | 12 +- .../Component/ViewControl/PaginationTest.php | 11 +- components/ILIAS/UI/tests/InitUIFramework.php | 4 +- components/ILIAS/UI/tests/LanguageStubs.php | 46 ++ lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + templates/default/030-tools/_index.scss | 3 + .../030-tools/_tool_focus-outline.scss | 14 +- .../_tool_text-more-less-toggle.scss | 45 ++ .../050-layout/_layout_element-bar.scss | 16 +- .../050-layout/_layout_grid-auto-columns.scss | 15 + .../default/060-elements/_elements_media.scss | 9 +- .../Entity/_ui-component_entity.scss | 134 +++-- .../Listing/_ui-component_entitylisting.scss | 33 +- .../Listing/_ui-component_inline.scss | 19 + .../Listing/_ui-component_properties.scss | 10 +- templates/default/070-components/_index.scss | 1 + templates/default/delos.css | 458 +++++++++++++----- templates/default/delos.scss | 3 +- 85 files changed, 2198 insertions(+), 488 deletions(-) create mode 100644 components/ILIAS/UI/resources/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg create mode 100644 components/ILIAS/UI/resources/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg create mode 100644 components/ILIAS/UI/resources/ui-examples/images/Image/ski_widescreen-thumbnail.jpg create mode 100644 components/ILIAS/UI/src/Component/Listing/Entity/Grid.php create mode 100644 components/ILIAS/UI/src/Component/Listing/Inline.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Grid.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php create mode 100644 components/ILIAS/UI/src/examples/Entity/Standard/video_object.php create mode 100644 components/ILIAS/UI/src/examples/Link/Standard/with_open_in_new_viewport.php create mode 100644 components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php create mode 100644 components/ILIAS/UI/src/examples/Listing/Inline/base.php create mode 100644 components/ILIAS/UI/src/examples/Listing/Inline/property_listing.php create mode 100644 components/ILIAS/UI/src/templates/default/Listing/tpl.entitylistinggrid.html create mode 100644 components/ILIAS/UI/src/templates/default/Listing/tpl.inline.html create mode 100644 components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php create mode 100644 components/ILIAS/UI/tests/LanguageStubs.php create mode 100644 templates/default/030-tools/_index.scss create mode 100644 templates/default/030-tools/_tool_text-more-less-toggle.scss create mode 100644 templates/default/050-layout/_layout_grid-auto-columns.scss create mode 100644 templates/default/070-components/UI-framework/Listing/_ui-component_inline.scss diff --git a/components/ILIAS/GlobalScreen/tests/Notification/BaseNotificationSetUpTBD.php b/components/ILIAS/GlobalScreen/tests/Notification/BaseNotificationSetUpTBD.php index 7f597cf1797e..03d9ecba7368 100644 --- a/components/ILIAS/GlobalScreen/tests/Notification/BaseNotificationSetUpTBD.php +++ b/components/ILIAS/GlobalScreen/tests/Notification/BaseNotificationSetUpTBD.php @@ -78,8 +78,16 @@ protected function setUp(): void public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { - public function item(): I\Item\Factory + $language_mock = $this->createMock(\ILIAS\Language\Language::class); + $language_mock->method('txt')->willReturnArgument(0); + + $factory = new class ($language_mock) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + + public function item(): ILIAS\UI\Component\Item\Factory { return new I\Item\Factory(); } @@ -88,7 +96,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } @@ -102,7 +110,7 @@ public function mainControls(): I\MainControls\Factory new Factory(), new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ) ) diff --git a/components/ILIAS/LearningSequence/tests/IliasMocks.php b/components/ILIAS/LearningSequence/tests/IliasMocks.php index 3e4ae624ccce..51d7f0f619d4 100755 --- a/components/ILIAS/LearningSequence/tests/IliasMocks.php +++ b/components/ILIAS/LearningSequence/tests/IliasMocks.php @@ -55,10 +55,12 @@ protected function mockUIFactory(): UIFactory }); $ui_factory->method('link') ->willReturn(new CImpl\Link\Factory()); + $language_mock = $this->createMock(\ILIAS\Language\Language::class); + $language_mock->method('txt')->willReturnArgument(0); $ui_factory->method('symbol') ->willReturn(new CImpl\Symbol\Factory( new CImpl\Symbol\Icon\Factory(), - new CImpl\Symbol\Glyph\Factory(), + new CImpl\Symbol\Glyph\Factory($language_mock), new CImpl\Symbol\Avatar\Factory() )); diff --git a/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php b/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php index 62f8e4d68bdd..8ab4e626f326 100755 --- a/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php +++ b/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php @@ -223,12 +223,20 @@ public function testScoreSettingsSectionScoring(): void public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + $language_mock = $this->createMock(\ILIAS\Language\Language::class); + $language_mock->method('txt')->willReturnArgument(0); + + return new class ($language_mock) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function symbol(): S\Factory { return new S\Factory( new S\Icon\Factory(), - new S\Glyph\Factory(), + new S\Glyph\Factory($this->language), new S\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 403eb59ea7e8..ca489d6225c0 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -444,7 +444,7 @@ public function init( $internal[UI\Implementation\Component\Symbol\Icon\Factory::class] = static fn() => new UI\Implementation\Component\Symbol\Icon\Factory(); $internal[UI\Implementation\Component\Symbol\Glyph\Factory::class] = static fn() => - new UI\Implementation\Component\Symbol\Glyph\Factory(); + new UI\Implementation\Component\Symbol\Glyph\Factory($use[Language\Language::class]); $internal[UI\Implementation\Component\Symbol\Avatar\Factory::class] = static fn() => new UI\Implementation\Component\Symbol\Avatar\Factory(); diff --git a/components/ILIAS/UI/resources/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg b/components/ILIAS/UI/resources/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c62994e2ddab55be9840a6a99a45917d40de3288 GIT binary patch literal 120908 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 b1K2dU0@U!4FYon1t)<7B823U^WM7W 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


eC_@G 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
<*EACNiDNNGF1vVIzg%*hsz)l>`~M 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�Q&@x z6$g>$6wW&cfrHRniodNlY{Dc1hfBkyhzm~#n6=^Bt@y}ho zwOUqVk%1>=-B^LS{x7Ib)s*zd>9<+>q3&KgNa*}+EPgh-3;LH(zDcOn!%&EOw&!~it3-cDJ@T3wn<|p2@>f7$n8U&q-yKQKgZ*s)YoxX7C2* zMnYsp}3`E+Vyyj(NhYzbb(9etU9R+JFm}xd@)CPo z3C6g4Jl6-sTZOQoQd8IVlLz<;_#IpTLUxL&| zw7i3=%8W_*{0)bV`j$tQ)8$z@sahp!DsZW4_m-lf3f0t@5y%Jv!v^C{Ab&jKrgJyI zX#u)k&7@Cc(|wCd4K@kxp(;_^bxAwo7rXUw_JvQq2A&4 zqb;sG^Q!b_lal^VEuGhwDo~=4%Phi4r*{Zfhcmk|8>rlp0N<{c_a{%{zr%mhmapv| zg2_Ujs(ZVm!aVB`WS}I(& zfTY({7Q<;b356t=7C*pN7}oPf>Qi(J!VP?3t|}i0-xu&!}$d;xjJxd!?SQm9A#OQ=e^{5k^ha zk@ni2Gbh}tyr_4niDE}h#ul~PQALlIy@j_}SwOTP1q-}tN%;rv+@H@?leV_f z4&&%7l?xJAvrk;%rxBoG%Z@_32J%2Z1fHILt1MB6zxuT5C9zJ+7$XzWbYyizF{>XW z^7{w6 z8)2`r)C9JHZK718b*fZuaKPe`?RyMmrFt5n#JnsyxlO)5vLHq&ybyI1_X(E~K+?vF5#zY@L%~_%T54FUC4(II~f4^5*MxVFl zNk8_<^8Q0O(s;QFJU_zkuYa*DUwc>9{a+Si)oqMa{-L|03^nh4T$z|{O6e?iQvi9T!lEj%vMcV9zJU#gSYP6eXo`*o?OvN#*Qj{D0_Lsn?3_nS&hBvM#v zx8W~OD1aGG-+*@C{{UX8-*hV?%e0@zP5%Ho1~xiINBtu4LuOCiX5KURZ!7W^!RMrX zZ;z9{S|*h{)i6(D#PHgJmQs2YvH(<@{#AtNz=&h+&Ad@y0^rW$yvU9zlfLt4eiv-T?86iP>69EE-=(tCh_cPIY<^6}6o zZM8-p8`|Fb*v@2e6yrKC8wFc(A}wotzto}L}e()z==`Oe7p%SvW! z&sS3>rh#8KxyMBvQx(Y!l1cVP7CQ2gAOq*i4yksiz~PoY@8I-BwbXR9zKCctIa*b$ zR;3^r{3(X=EYU{jq$7}$AaSwr)jLlz=MSccFYk2&9N@k{KM+Tu$4RNm>l5uY%-KyV z?8UXEDzp_NFG`e=;sTRAhM{>QAFRJX_eu0==_IUyM3{?S<8B!4d()+X`1#xo$^QLD z`nr=y_s{%9FkQ2KAFDNYaT7~o`){DM2+z>L>CE-$PD1tUjk#7vnc$8)xaAAV2$b4y zsG1#uw-)^ve~KkN38j5L_5#*`%H`qhY!Lvc;Lm`rzxidNbBt%Yu#7bxVhYs`qAylmAz8nVG;eYPiX-pZM=P| z(5epK{{XcA0N<~~A9s6i+!?igGr5|I&Z9_mJX%vO*b5Y^SfDZYaTvCTHEP1lB(9Uq zH?stC!m>{CJFnP0e%*Un@CAg zlqLx$ehKQ$K|>?zWBf<7E|}AqoNs=0{{X1tr?ek>A+XU$sZk=z3Fe!ou2Oc&+o>8?a0Lt=X9rMj`t-hR;FwSTBC;$vvv)7d*6$;ncOU*#_xmM>mjx2Yc- zgy$+(X>{f$7nUV?iam)0ivIvrUJz_0yZ- z(FT{%I!6~J%-PA!6Fc#Wl=rLNwvx{@ts+mSD?Sb7?J8mKteNKw5%O z&ZrG!E;!=Sl9tM}YEN!;6qN#?kl+dm zN<}?TS=Bw0?(RpiouC-p4c#BxrN-bj(ZN>6Xzd{++Ir3GrdHIpS+UxG4^`&JM)TPw zOB3W)Hl9#@wcoE0*sN&U$@bp~ zY9~cu?^yP-nI@OnYjzr}Y8@m5pW_MZ-%NUy@5Zyy9j)%Q{Y}{n2BXG`pX9p#0HkRh zT_qfKQD$r5>#)koF0HugJnpchXr%$zvEmM;(m6bSmZz4-O%01V+^I<^)v;J+se6tZ zI`$cVr}yj}1n`HKU4z-bvRu8kQJ?4IQk+_rhVBNEr4(x?yk#U3or#4KW-;=YwV(8v z&sw2tg7{E&E}@3nsi+aDZUm&d0003FYPknNDnb$f3dD);4!X)}kMqBFvXgfYT{Fu< zhId;b)w5fYQzVmExh=J2atSTGrM6%R@#CoM&RbPrXD37M@PcNraf5j}erj8a5ZBt@5$GO^^6fh1%yDeBg19>{7PJ%YKPIUd|i zk-c1LZBdZYz0$zzonfo%NQc<7jK|ZtBASr!8oq-iIvO0qoq&xorq;)2u z!PM0Ftt)vlRCF$;#682UtVDm{;#_+NNtQ`OY!XEaa)exuFU^(}6FLv-5#d*8`4jQD=;tk<`-i16lpwJ}^XA4H z49=&;W~+%Yx^6tYQrmHAyojPEjv0$EbIKv|sz-)?n96puxz;b~zS(xqR%A7vn$t38 z^Lbr8EVIQdc=~T0i?}*V1jZO(o=Wqxzqsy@>^SmnCj|&hox0&Z$=4O28xB|B$OC@9 z6N=HV^aYyv9F7-JVBo~+IuzrzZ7j6#%#Nre5LH(8z-dO!#aHbceDxcM&1;Hy{9L9> z+MWy@HhMVPwtrQZPQfCQVlJx87h*XA4#UCgraqnfW8BXA%R^4)^my-9yVUq>hL8OZ zQ0Gkuv^{FpAr&)OD$!bo@78;dVhCWJ14_u85TZ8K6Vw`L-qH7#{({q(OL}(RF8sP< zSmU(qlea7sc5f@n-WKm~+bvsMTb9J}*wgl4wWehERn^lakFgkyc#`Tw zhl$*H-)@DQN(#p@tL`%u+KuT70^T1R}&^eBu4p^VhVCT=&%<2qns9KgWmmaZZ zA&zIXvMH=3hSbB{o<<@^8=>M@?Xqpx-h+J~r|{AZRRPtxr?QdI(|Otu$EPmVRDXHp zvK}^2Hi9tFo_}+l(`7voWV`%2A>itQrWP1_C9vc{t!z^%5Jf#tM z@QrzwcThJzQ>F@u?v7^!LjZ&!1eKV91M}YASH)SYXHHVp*V46kv9Zx-MI-Jb@&^4~ zF7IFLhibJnx=u|atb29boDN=O*BCpJQ`Fc_+gX9O{W~3I3o}b`l#}jKmP8rt)v0F1NN9bWnWuuv@Bn_z!)@ej zK00RU{db+wIk_|1eyDr%Sql!K!AbOXl~f4jS&(uL0{y&LY{Py2dR_Miy?WnPMTo@1 zVtqS_!&3bx9Y-HZwk8jz-PoZK#;+$fc^tIN?o0V&<7G^AZO>fIOD?G-N8S-la6vRJ zxZDB0$LHsv6_t$&yy2A7D^PXCL`Tb+BNs$uw5Cr})4e4`r}Z;7V=o08Fykz{@6F^8 zx_e$+fCsgI3$r=9uRBKVLmP+z1 ztPY@ozUx&o>f1U32JyN8;n{8=ZublHiqK zFFd5XDw1U1ZhL@w{&+9cTI2l3^*1SBk0*$;DVi%$nCe)tL-97JfyV00q=ktN-gnsT z(56d4Wpx%h?3J;(S)h!@^m24+JoKUs$b#xQ4qFD@0rBU5ow}aH<21CkaoS@W21h-a zwqaa;4;fIIo=HB?eYJ>?HzkM1Q8zo!Ja9pWYS{bu+LY29W-AOYAa`=I zG@?kFB{OnW-;y3IppEwObgVWY3t=Ut#}}zMkGY1#VKj{lR%;n{T&-(Q4suNF5zv~9 zttZ=Bq{hHc#n~|1IJ7Z7jp6~e-g1}G+pMQ!5m}IPfBWyZ@zI`37h(p(eFXrOYb;YG zij6i)-){t#l&>-Sj{6g~`+t7Dr<#ri;?j~5M=kk%dSSDI7*@$w)HNV@toss0YM8J@ z*0+8AvaEca{{WA{Kc0?te`z)Ln9}%u^HR|ontt4ByRVFVli=o)PX|d_DI%8RAdVO0 zgmEpo2gd&Z9V9h}>kA`6j-hWMPFT^Pv4ME)Q>aRok-e82ley!$+hPx%vg5n`+=_Z~ zeFrYD)#pYvZ~iBtqq8g+T$9&fBrPpfc8S8sSn}8%$?L=s-e?aQpO=mor*E`M)bfc0 z7yym=PlqGSZ-Y+pX@1*bFPt1ADKOaSc`4yk@JR}R$T5(9HU+wk z#`hBZ^dL&}rF^r|{{Z4zgG*SE>E&V!cte$^^SN zgK0^=JfW#Sejz|xe*qWx_}?4al9*Vo<}sXOxp7!aB`D;%g4y=jzMbFg2lMTd{r2bw zHKr-qr=~Qx#?t*l&Hn(^a#@;DagsIh?Fga$@%SyvazXL7-g?u{=j>@LoYrBfVh9Y3 zwQA&NT%JA2@z2{a?dN^e1O4_Vp$eM1DJN^@J26ij;nI3nq(K^j30@?5Q{;u*ZTxh| zST=Cs)+wq&1C5yjW6oj#-(f$n!Cos`>fYGr>|ro=vzSV8w2KV*%Ssm?b;KQffJi`)AzgVJPS@Vv zL}OpnA8hN^*7N4{=zDO*DH7EBfnPBbsxKojkPW~gj^n{2ti6lRU#&K3Ox7+txg46w zgU2>ZMM{L9O%qQfa%{nxR#i`JN6Fu(W^$a`nrsBn=SF)WcO3Y1$>wV<+6xe75v#Wl zc7ns+vD!Mf;Kg?s%k1Vf@VPLP{sQ3m=PHQ| zF~}JI08f3s-mj*+(->a&X!^Q8M9Vd3@%v;ub*gdoYo<20*G8>t50}LD^@%B2NEx9Q9%&;lJfT&# zk;oo4>i%}mTJ|@$x;IMVH3xI5>zzm2TwNP_X0D&5CMQj0@)2D&8jfzPku{0p5S|Sz zp4%${_W(BPwbhhqa3qy-9}WF&^Tp;|+8t8ql(hClT#^ReCJxv4W?*lQN$Y;t=d@>Y zqS6_7Dd?QGI-*#yl9axYh2U_kAKQQEObc)J#t38JdFYF~x&u_>J5i=Pn%PY~s&uBH z%Fm9+&yBAd=kd1atYYj}F>az|UNR|;!fg}rVfZ~?uK8>6-aMCZ^V+`)rdM2RoW>p) zG(B4}NvEr0nmIqEjk+BcCOcOKHejkqktpyNiZKj#&gySJty$adzaz|E4U9gNPDp>}+B4jJMoY(1Lf zJz~Ud#z@Rfx$>vR{CV!igS*=d&5GF3YD_GaJ;uj743q^KSKMV`xeEUP9!D=91FN0b zpHkX?2;^#9dd*RZS2+1&#pdZskd6@hHf0MXcj5`$^55fbj(eG^`*+-~= zp1aScaCrQln9F5sQK_h9TCa_JGt$OO#yJFXN(wX}LgY8~7na`yZhAh-XpJ?haFjB- zS1XIbYTCBm-X}GVy(TR(76Yq9lgecX?Zw9tBL4uW`9A`#>m3=VFFe*Ux~CZp<7}&~ z)M_tbM(U3oy9oC?`31NB=d9XSA>6$iT`ggx$Y!xM-+Opn>*g@o00=_xM=53xxF2+T zk+()xu_ad+8Eu6_j4w*R!l@pB^TnT~>Iy9_V=>zf?`b?T{*^l==<8zJDS@|rL5$ve zF!Fe$UHOKuxGV=R18%CWw$RxOwn%GE(b>0_!^J;3v}MXBb0JNVFz?Pe5hQMG9lSyS z<+kT-`aaa!{{Tg6x>d!>kzn(1ud#kJixh3#1xm^8qHW8|H=o`1j+dEz9&zm!cSYvu zB`1_YjE0rlc$~c_D504dquoF!&cu(NyxIYf1cT7rV9mYQl)wmA{7D5Qbn70miB8da zjjN<{g9qIl6->60)HW8Lti|Q?{v6`9f=zbUxGZ3o}uwp^Ljn(%cd`8Dd4hk1uIh_%D7};%@vr`{lR#h znbkx(h=WwoKS0Hb)_K9VgxBgoDeHT6yEHsx;McHZWpp@_;Y ze9ibNRI!}JMzU_-NoFw=@w#zhvNMsm0FJHk*SJkvqca%&E8HD5++8n}k{w8$xbF=n zpHa{-%8L|Dkx3$PXNWPCfwm!t=1C;c>s{5jxjMNWOCCyAYGEtbY89Tvlu@!5m)lby zKP8`$_}`{>n9~>yZwlx1g{iRGrx#h@CyHlUbHf`jbg=`WWo2-GZy&XY@H&dtg|Akx zmGP7|<4wSCOzc50dW)0tz=Rr;9;-*_^0edO5Si&&QIEA7f;SOz`^NtO!2bPi{<)E- zc58_9V_xH@+ovZvp=nRYBZowwb8j+enAzFMwjVZAx~YumiMPI5``QzBl>) z{Y_WNTcc{j*19BydY?r#dowKTQ~(r`6dk|^?*9PStCToFm2Et}zatpeT%&7brcXKa z*n#<{%weDUwD1E3OXB=o)1Y=AXQ@mPu#2@$0x#M=9A0qgxRRdMH%DROB;69lJ?TK?P*1Z0m5 zA#e3>{k(OxgS2m@(ImL}tU#P$t!br6C13$|U*r+sZ`5ujQfhG-a^%e#w+I#!VcY@= zs3nR20I(bNF%Ci)VslM4C$r^-9X|!y5h%FgT7!bFkmzrmhwYJ_g=DQ@xeS;}w3RC8y-K z8d!b~zT`gePDmX~Z6Yfbo>GHzLsa-lWjmfv|JBh?Zm<&9y&={=M!D?DU zOO!1Hr6eSczL-c`jDUQCzk&x|j&2C5*urG7b>oGQLiYah=jVtgzyxjbM^PD^^}M&g zle>>>b}o&B1!%p)`i}yxQZ8fxAW^U~fEa8%4fg0#ZsE_4LltC>8Z9W2t+{~6-5h^( zY)JU?{;mH2Jpj0rUty0mOHoilN_7z&_C5r2@QgGt*l|j{YlW{~%=5-%mdy!dlEk3- zB3PZc9s~VD=dXDjmOoEbdpR4ABeZ3siBsw8#>4I86&rFv8*FwSet(svT|thsBzZBD zu~J~C-I$ff)^|QiAyzB_BliL0scCW%YAToMt!W^FHCZCH8Vm9~o2zg2EI}ac z-5o0lL?iRZjT^xoymy~02-nqw$&^x`EW&Z%>sDL$ay8?G`+nxUa3;zJG zP+1LOdnPMZ_w7-uD{UlX3eZ?h_a;Oeox$7!-yhGOhNjb5GdYqrgeri=UUBXr*nExm z@ISxjs9V*2$-UpKuQairZ;WC_a-i%tKkwu7^U~{a=)Nv#sAyyTg#kp+FV zaUaOrayK9K{(9SEG^`Y(Y=}=mkWHu*HdODF6d}8?6g6LX*r4kf|Fp zPLQ-9j~fnKj|cebGuw=BG*_Oh##vZ{?xcWj03?6zI>QDs)NmtSoIw`uN^yPI!Pwxt&0m9 zuIK$J^SIlnY_2MucQA9SDF_^x!Q#wFVmTj!oPC@O1YAw&g@AmD#^VB6v zSnO%`q^ln{4apjD#Dlimu<{T0=zESZOwNvy9f{xM;aL`-Y zporLyt-m}HrQEz?I(Y29NWmJEi6-{uRpdSsZv%gg_WX2bptM(U`?0LBdLK{WZs+q^ z3o+BL39golG>)hV;zJR;F3L9~><2?yV-;&t=J5D^C6CBl)EOz(l?stll38bwz7LJY z$`0i1w%v9!EvWR*_-6K^wl@uP390>b_hU{a@oB^tDtdLpU5&QL*dUSmy@f6Gb=ZGF zS*CHn0NTe8XKrPsx+)}gwa(e5<^he?v-n#?f}`R9l14Ya4dDc5t9v)j71(6-&}tvk z-V^S$(6C$X!3am1l?h28<5J$xK7{*K++R-JvY#28^+S>EmHk8B9_C^%M+NTE8B(+Z zPU9v8kLpie+=ASQN7JYDhVISBfi~*c$ID~csT^_GEFt8^!^R09A0OMR1KQ{_89hPV z3zssGUdZRWZ>eXv*D{9wLn$*;d$L)cL%X}X#Lmvdl1U`_=`n5`v*Xry8bH&?H&|# z7Vys_M21NM6fNy;fympC@_JcS%BDhU^|AQbAk9x{Qym6A?VAIUBV^*tPah|3j^Lh; zD|Y)Ss5>W7Vy`5*I-S`I6$){*w&eP(Kp~jzxFpv;Ha{J8gVHaid z-Ce9JX&i>6>}O`Pl@{dMs^%<3C+hc5g3)7Y%JHkpcK77|lJfJob8P*rf`M+hHLXi1 zLt-^8YBosdFC|+fNjPUD;Hs`S&uQ4CR)$kf+mAhRhM8>;R;b_kWFc(F+mznQRZqw>LHIbHC8*WV_wTE&~$#y*edZgDHuetr-)LQ=l zSm&e2YK>=G8>upvEizWxTbRm|5gPBsAyV8DHQ7;A1poolPeb6dId4lBZ%rjB(-@lV zWie$Sn;_AcG|a(7EP0Sh9l{16dHCyCqRG{l<&>F?dbQQKw#wDW0F$M(bV3Iqx}`?u z-8QF+Ri1HXd5~o-_Bm4&^+{==J>?hL?H^phQ?8InC*4MrCIU>*JtIAswO#^vO?V!b zp`~z_*WbRnnA)jW!8LIlQ>!I$J0;72u|K=u4YvLVNh)`0 z!FH&Y!NvPlVn1GZObJO;j!Vbj5%bgM*4@l-?;Y2UY+g!PaVAqE#?{%GuUugJ@gE_9 z*!8s47~(2LPVKrrZJcwgm1d*ao_2w4HB?HEG*qV>KAm+HXuBip2Onk_h?BNhJ3!OQpRrKc3GMP%_B94Ej_d~i0BK-G-^W#d86>iWf;cP>ba3W~#dq7vuKxhf_WpW? zTIm_4Vvsj+v;P3WYxc%Cs);XfpAIcXG}FGHg>6c&{{WlnanYYu8sZ5%znEK9+O|U0 zmD84uoxSSE-P5?Y-JBi%4&(j0ipJ%WC+P#fh6>O-!Q9OmU`Y&%?rugxKebWQwN_(<|f_~WP3jtxa9%&ExZy1ER1nhp(_WbzYqh8r$eVW)6XPA-KfhkU^m#X@V#k>&gEeB-XAMdO=j>dr*af7 z>U$dMoy{euFx2n+W~`U3Hxi;)rALl0-zOkPgTGVV#q~w5yGyIP^`QG1sq@w`+NZiR z<#oQHnOd00+NW;vR*w-_uODUxnnh-n{{X102}wL`&0=#W^JP7h6cwqYgq6um1^29* zn=0D^dE-1iO_8gE5kman1vRi`_B1UQ@pOoW*nmR%v#7_a^%lfN=7li8dC>`JHsf{4c+iTkMBu65|lk)0p>f_01{OI6TeF0?lPy(_?nj>;e5C^NSbX) zgatgcy+9#LP?I`B5=kTtGF6}envtOY0K+@I{kp~WC)DPzz+kjKi_dEc_Hj6kQ>^Lf z&29A6=Z;mjFw-(vYSy<{YDl72voYZscr34vL!OE~8TW6t-Tm(`s-*Q9lE~=<$?2^n z+AheZD+im^nWdJaRg%6=b)VgTZu)=2*hE0lNF9hO2YP*Mb_=t5+f4d;^zZ5OLDJD( zzx36ts_P3{he%1mk-k$old>7v{{TgG$E=)Cy~k7vEug4&-_ttd zI}_SDYwM2ecx-{l%q^E6tOGRNRuFi5||_cnFM3B`z`hkbF6`;c)^vTlowWIW_v|8 z$9XD9PJ|?(Nl=BAB}HiV#C-l9eHAi=QjojUFdm*Z{&uz2O6dyRQTOUh<}Q|oyP+wkZzIC! znIzTKBJGcO@hwV@Qr1r!gRwNW%(U!C(p}UQqg8%Q1lFZzgvHoi)ONC!k67BvEqdEn zrFSpf1Z+%I)*6ik{VCdd?GrZpfU6%Lv}{2i8+G&l02%RCVZ`)Qh|M$#wpbfNj4eQ> zZcrphFcopCLGF*NcyEVtFK2Z5o@GLV6$`0$4|pYPrM}TA3e-wmSqdcS)9$1|3OvE@ z*Q%b@`d!tF*bdt1D5ln!*j%m}SQox6vuC5o?XMOGII{IHVeulHLRo_tID{72x8tTOzc~U4WBSd9^Vq_}8kzZmrE4N(R zgS&dFwqi@U8Dp0-6~~twn2BrBzd}dfkG2v&Wl=s>HBLX+ZP(S_%Q!D1py8Sv*)-`@ zL+rNWi&&KvDIr@CU^fGOx(NK#_Vw*oj5FRj%yNgT){0tZoNW!ZvJwC=brhcZm3>l3 znXtUU<8e7HGpwS<>X|E1!`r(ROvjn7V>^Iiux zrnJ_c#^7*MSE-wtIdmY*>kCK30-w}V_W2{E4BDRdQztpfHBD1K zPu!O!CSnh3Ig*vhG8ehH-M4MS_}gK)->qe~)aDO@!{4g=eLQO=#@`fDr<$yB!zHPo>ZFonhwqV!#}|RRqeph9US^`vI?uhi8(qBZ7OQEl zW3{HJk0Tu|Rw<5iRmMjIa$1^KJP}+Vi<7Gvc!ETZb6e@{Nu(@xi?TX7>fof+390fz zQ)4NwbtURmW{2yq-#@l5Z{LzS5rYg|oc+v870OcMg-bf3AQ51Bkb3#xov+!M2}sj3 zFixcl5Dn~@HrO6>%E@->UrT+V&+5+R;rnk#yIIUGD=UP|rl8fAG$xb8TdMK>4rdc- z<42ms=pvp->v3xy&8`Tsy0fhj??-;MUvf2O15;Ik3KewLHE_C9>0+GR(zPp0(owHQ z>2qOYug_@7en2SPley|^O8)>t8(-%9ww}pg_rv((IT%1@K9B_PqQ6iUcdOMQ~g;FV9jjruxd zI$WEZd^|QDp14!DL18Tv*m8@XGw&U&2_PM#Y(#3#!FrtTPi6ItTEn(_h6)<`u5qTtcc`7=K}oWJ-VrJV*k30K;o3 ztLN?}r?r~9a~o{rXzk&BE6wd-YOb$yRyk%44iKH-u1!W(RMU1O58) zu1>BpMqHGQEm=TCTO?UEIkw;<`5>qpac}3WhcujE(kLBJb{;eE=jnnzc5)T%u=-eQ zQ)6x~skkAELd(Ah60C?0f(^boTevKD_LfQ#1NL4;7JzQEA`u)}hATSA992 z#WW>kr#w(RHWB0`OYg%pQwbtgSMk+hyA16E%@m>P+K|a!(s-5^=6EA01c2Woakq}Q zYe9I~f`6rx*oZw^_I;+3Ij4|p#a#a81J6TI*578Wj{Z1pga%fZl5Q?JLHgW$a93G0 z9=N-5wL7`(A5z?^u=}&kt7u~P6)nS&Fz zmLc02iLovE@#N{xUI|(_GBHOlv1^IhWkq#a2QW7QkP*L-IvrJ-rGT~C3k%4v_2ND_ zk>sxX568#kboavcTT-eT0)4T$85fL2 zPp07Q{jiO(p|To_GmCYStkPr^1omR^-yr71|alEQwk8tK;n5 zd{X4HGC>m#Xc}QOtbe9RBXPOki94N-MN(3x`!lh?6I}aiO4~O`wYDJ5zF)Q&-QCwY zU2kV0ul2@vGy1Mh%4McSqNz_iG?M+0$v?QSAXO46*bt#rK?BE0-DllQ{PEkGE0V4Z z(TfvT88{|N72#)itJRWF(ky7wIate)vGc!C`jb??KO>BL7xDRv72~ck89T5XwAclO zU457X{{TzchQ9kXv$>r42iIhkv=7L1tOb*iZz6{^E&-bqqRb;2~I#D!W} z*_fvk<-tOR?uqU9Z~JqY!D>x!scz?gM_0XTNa^WFYcIjd(-%0l`;s>?ro`@`_$|_p zOi7)p3(>I+=89s=373UpskkMH`*s_AADxeb)P>A!S-Y=pU#+E<0c1g1Uur3fAG>)u z007(_{1RLE=!hu^k}rt7`^s~(*A0$91N&PHsum{hHg+0VnQqmVtU|V)!KEryWr#F| zy{OA6Ea0&mk0&Q!50TN2cDos`doM1T{{Y6g#*WPD$}{CL*j%KDy0QwfG$2oUoJHq> zB=?trl@VWzoEi8w$^8SUB+|N{5goIXzlf)hTUoq*GIJhFF zE#!gIPq=V)qq}{=)_R*Asx!JbO6IIfcNwSibq&n(*(wAymPpI5wG#lL*@GC-L2dV9 zf3&Jnlt+&K8(;J|Srp6wwKz{LFh#eLd!~8_A|_1jg~qefxU60J_Vjk7Ug7DPqk!V= z%^dV%dF6o`c$yOJC-kLeU)(ta1aaSp6~pJJ#9Vpq*i(?HGenU^W@sX!7!AV5%+&lrf^W^S&W@GX8W`aDOLrUtyy+xj+ zCbKh-1j`=Z1MvO_Qws@9s|!@qrIH(10F9%cNtwPUvI8IbYGUZDDIKOe+Q!#QYU;zt z41V_&%NSgyf1a_|cI-{AE#TBVE>fnv|+DotY7)K;xd zW}}8*Z;^JL7?4V{?`v)iyG=tL0)?VN|IbDtkt+CFLLrNRimD7IoWy zJu$OaZ^e49MmcTdD~J~#deyvnyN|FUEA4wAG2K*c#2?A|9V+$eN1C2?#aLW)Gh9DX zlB+>tWdIa!4pnyF$WyQ#!h<@Y3)&risIL| z`o>T6sV<*r*^&jTBJz0D#>jXT=H*WQSFM>q)weHd%qCgolEO52?Jac{o=#MO#9vJi z@{ALABYn0B=W=?hI87Z5rV&A!f*HNUlH3zpdiR-0C75yrPX0D;@BR8T$LnTWl(F)~ zN;ok&HG+(3ZDvL)-dVq&aFAGo`RV|rCNO7d%9KND)HgPdmKfI-mO8s;H1cYUCPG4E zk{B&V1t=%;#g=H;0z(ff!>H&xsjV=D;naBK%6s#RlY(H5#efFOH@XKBNG-pe+w;EN zF?7yy_8U7(RN2I18D8?5^=&nJ&0-`)jyL3thwWBRf=D&3=IH8&*L zO2H&sg5ZYm;O;gfp!-o#nPsy|ER8wjwp27K(YXpiKz3pnuHcV9$4s=~60A?`7>-ub!gunON|_ zKAywVvwownSoqpmKJO8~k3Kf}+u(TU%XumgudTUaZPPfE!VS7d2o z6FlDRuaa8%h!8mc080vaf~lzd}}VSvVkhY0|qqX7c|4 z#?^^$M*#%ont zWt9-k5k};yRPDN-;Q8~^4yM*w1j!R>tV=YJ554O>bzT1edLu@|{{X+y3(*Qu+r#t0 zTcT3j(8w1Yj>qGTZ9yJRmQfg1u&}g&5@A`Eq{>d*gE-^|`*ccwTE;*|T3RcQYQo+g z=#c$Yo&-;Sdy15-wDV&X{+RHI22Ho5Tza5qux zO>zitOfArP7Fpw-N35ri+uTMf!-|4@0oUF`5Hz#p8oh|+$6cyouR5$*FAh1CnRxHS z{BN)$$Bvhh;XAfpvbTa9 zs15%BRvY-~b-YQ1OyKKb#KM_{B<=)Xo&j5v*5zj9cq~T~wH3Od@$930;~|fNd~5*f zLquvEmIpD8&FR={W3tsP2KspND<(40ZybHMJXJ^@RGp6gI^E5tA0bwEqPG^j()*?< zg3nqr{{YZ8AoxGq_L=ZNdKl-=!^l z)u`B&y!`grnWXzq_icw#@#-tln8MkmC|>}qF63|d`1$_;eyy8zurXf!1wlGNu=B%$ zwr+e<%>)Jgjshr^-Ku3%XZZxP@k00X$+^Z47xQ`Isyt%)PZlN%Qd9vIlg7(Pk&KO6ow`RRFAQ_f|I z%v9AF9hyG{q*LI7;1B)w=#~vzi;{Rny4UIvCng4QY6Kg3;~3lH@$=DJP?;*e6n>H6 zV%(59Z{>zvT%6UB14v~eJ`uw(5JrH2SZ(FJ505@~>T;!=Zcvp1+A_vq-UTv62|wsx zj|6~8{B)$Vs4>>v*)PiwGH{EPU?VH!FC_eIKaQp_O+tIs>&s#~KJ4mV$+FSI?fet% zgXHb8Bl~|n7&uC_o8qR7XnRBWAiB>Hr; zmBf&{C;I&T-hbb%jBJ#%5Wy64<0`^SDSHttW&!)#@K2u`>_5}c)}W=pj7pKGsRCbg zC?|eH^f&qX;1z3CmF0JlQ6L4srWbwp9u$0?`j)2CO&V69)&hH;NrjOkJ~t$P-_Q5z zUR=f#88Aw9qsTI`0@O7DuEZZK3w-bRBlzonEkPXiQ`+SC&# zu~kfOCr~Z zLZ(6k30M{c9rpwN{{ZyrYAr?ZtukwqApm?2KZN}8mnf5o-kmfO1(TeXV}zxrU^e7Q z`5XQZTljjGsmv|gmxzpg(4aC%PTTU^lOtm>2XlKp<-$0DxfHCepru&{@#8+jm5r=6QNz2PKlT_CbCYf;Li8N~B9+ z-AOwJ-(p7n2G4Wdlq{9!Ss0!TVnNJ)0V=2S=cx?F0`(cao3^V#I>*ex9Hz{OKh?S4 zasL3{>NcDbi9{99#Ik*+ehcq7K6lt|-;err36&5=Ea}p~Mzq{~vA4?)0FAF$nQWgX z2>G-?e+=Kx_Um??$GdVmxLSVf(@v06tqK`snb_>3f0Oa?@z%6h#G1+~8xK5-=bdti zIpxF%_d65hZ{&RS)o#l6>$aYWyHiWmr>MLgO-bCnMJZoR{fx@*UVTq~F}mk+v3w>X zp=6buZY(fZkdjYSd_kPk;p{A1>+*#QoYycNhjgpyB#^=G@BT>cm&NPMg zRE#IIseNF_M->huH@k~{;>61-28kw4#;-O z`XfWz+-!$$^G0^L*@6$d>1w!BJc_JHMPt`5`|+30XM5w_jYX_-?|UaOT_GgdxtS!(=?Wb+wWU851O+(!fv zrvBA5T0;o|##tcAE`(LjDJn<6*x; zG_^HK@Fa+G6Tu~89E~eRL_lm*s*pmr`*}O`GucgPZy(&P7PUjfE#*!=S!R$*R9J#o zHrRvb`ls>Ls`ORqlD~O!`R?`i(^fZZ{WA7gN2t4#+zVB}TP8V3sf5Ksb{29sq@SbDEbaWXt3N<=jsF|=z zJ34ZutjGyM08*5qU;uYEH`CHKo6!BZ^!2NKQ+k4rKZWg1me#bd>3u(0D}3_O%MJIYFu*Cr74fO6T+s$k`wE6R(dajb|n2j{+(TBC{l8! zwCpu5i%pIM%Hoz;(5dd=wJl^tLM})yP9J`+*>@J=C__ACKexy13r2@d=Xd zRwBpURNT^4=Vp<;l|0FpefK1T&*#U-RAOC2G=rNgOu;xJje?IJ2p`AD>fd^>)OK?j z{Rw7dF;Ub;s=#k#p;VHzZC4hs=9qPg3oZP*h>=t^Mf+$S3XOK@t(kLQQS<4Z(#^j%lucxK2 z@W|A8OvZ)Oz4^vxAjD^#vJ-ZD0^dcil9ENvocVigx?|(8`!S4zI>wI7v4XADB~Eui zl4h08R;mf?%2F<+LuO!F!%3AA5;>8LnuCHkhmLq>Af?FJ)DW7KxZ?Y*HVs9J)?{B; z(vTFjUqVzwVSj<_ma6WDetUhFrKhjzZ7-+$bD(uEaPoKRm~@_-r4An))>kEda4V^3 z;%~K^cjibo+;^=w@i-sZiB0X7whUuX;6$6_gaPuctKhYR2kS z2DCM#Xw^iBjpPq>RgYC(w=|!p{BD%(CtNUkPKuV?h#I`Bza7i+0u zWXszv6lG*{A_PX>PbNOOwC2A@z%Z3)!8j|tETPtI)zHtpi0o<>etU>UMh4d z!ixoZz_D2CS6#;@@JZjT5uO^)_}-S6Fs?sm%Tf}6iSIZ&M1V*l4_&kf1a_kM`U##b z;Gbj}&S{fm$L-ns#-Zh`q;_m6G|nrtNj>rSkYM(tDM>wyl%3A~`nT@4sf}TBw0XN) zTei7O%rxw2%JxilwS`e~27<CZ8S5fFJ)k=|jlgOtjr&;{J+2X5mVlZ#&1Ig%hC%O^_ky0MU72Kj7M7Px5SCKe zNl`j)t+a!;c=%1?6Sx@Q8D3e%KGtdL7n->1l}mlKBqciHS*TD<0|-i#Qj$VRMy+cW zCjcI!`(=^U(sr}4IyR?h^;V$JQ08=gKK}rx?38-5PikeX^rhoAM!4aSys}PWw{jSo zNv4Nl%A=QEVgCRQuSY%A?dPYBU!pxtW%bMH4E#;1==x{VxeEB|dUBmfqsu{+)HZ6x zC9wqd6OvJ#=)vi|^OydA?iinymXcsZHU(l&)5WOfZWhET{!QgpVr zL2QL9UuFY^K$Rj=`p^2F&g#qtkk?vEOeMV~2B)#xeFJ+;>6SNS)wASar>81la8by9 znuThxAXth?ieE0Ht0&xXEX6EGDg-C8u7^-a;=*eTu8-5*gsi4(DkY6Q?{Lg&^m)Fqdhr~)1w{$f&ej0T9-ReZos4&=jmvv3railRd?|Q1uQm9pyIFHm#Wy>*o{{V@$vFw+o zuTmYt>{oDc^I^R{`hvm1+*ioytaP~xeVNpmHl6OI^ zH7PHmj-Z_^2UFRjvs$Er+THuKx}+ruAS|UxIN{^R#6x*Cg8UG4{Q zeKzXNBXj)Cei*Sh&u%*QlBN}Th*rASsVRpaUO2)s@JRUiAGD6sApZdDsMRc-n0Z4n>%Ay>Ox7=^w{k^)2@%(#xZ~gkX_T!UM<@`$3RU)*Z z)|vqzX-ZPsw1ANU0iC=t&b^N3^_kbR2dVQU5c@xF)}=36)Lcs{@2X6c+)p%-OcH=)YfgZb!Cpdcn(_L^_pS#GnN2`Q?OPee~qgSl>Y!IW3q3k zDqTw7aJX6Ubnolk?|(nSQL9PpMA&oOJe~o__IaId3~irWslD z7YIb(@HZdz{B=_1lOaOXv09CJyB0C|iZ|p~ZtOPX!^h+2ue11FVH`J?fIr+d{{SL! z`H}4nZYz#6f|Gx>RDbiAII^09xq5nyD>31;ewkZ#*(AqHM;{tgjBPT^EX^u3?%&h^ zb3YM{_wmxsZsF$ItvdMpieoI+lW0{b#|l8(lOM9%c_5R=bGPxgO85C; zx0=`8z}0u*$ZCyIkkoU>0!0l>y|#FujwC3a+704_1v@IDsQxzS3M*q0Z#8Y9e8)QG zL1p5O2Q9OH!ULVR3Z#+b?hjuyG9Jz<+>UC)6_ydTg_2dU0{-TSSAPWf-H+#fu_%rC zN&vA+6%&9skz8&_9Eke~-(o=j0B(j(#A0a*DwF-=Z-zdxf*68F_43k;f-M+TEm^dA z5F(3lylQ_erGGz;veMGlY-B9hfVx8>v9C=eR*phW;4c3F=^i-|{B$p7Xua03qIjkf zaYv&kCWmvdD*pf{z##Y^j~x+GwHi)?q{GFWgB>uiuR-1!C0B64fKa@IT(&9C$pm!TwcK^oZ+{HdvSUJK zYz&rj1%|~`tdQ7-2ZpqUB~_2ySmc-n05Yu2=gH|Y?n5+^OB&C10}r;$_S6{8{yyRe zU5WnyS=jIM*WH>1i?yhd{BT*4KdVTUedv^(!|bG~+=IDR@(Alap@`*yr2$c^-}rpF zV@+t%ha%$}Mu9^0f~JYnH>Hv^ z)LPFUYav@tDdhhES85vZncR=r!WAv?{q_>ikV zZbf2aDj>(gk%DPy`qjlB2#w!?mny%mnfIk%?rIZTDDly+smmA=^h z;)ysK(bhramL-V|xC#ItJDq7>>SMI?)c*i1VW6O7t!Ei+J;`h}s3t~PkjdnzD%?*F zEdGA&x7`O6xocHgM9FIkm>Eq^sqY)#!wkF**}*(y&W zsg@~Oq6ad;PeRxacxP0#AzupI(1GLQJq0YJvIUs)bmIqNQ4sGVai&YN4Tuu zrzd0i>(-&xy1P?cg2tlF+tlBr!ZFlE`J}wm;Mn_g>%{G}a7D?2M;Qu$?4Wh-oiQTW zNYnVDN9Db_VouJX%4M?epUdX+Ijfc}v$efLWwICYkMQo(Ibg(-;2rkrRrUT^^!iN1Q5!k z0=s_$Z}&YhQhh3DMQblo&0BZYRiz#^soMI5L_6A%z0OJ*xTr#l@K4{i#$pH%aJfsG zq~LcGV$xnS*UH+)+V>ga$<(y2(dM9CWxpS!@=e03vJQ)ei~ zm+^OpDI7&2%@SIW`;qZ}D(Y299Cu^nZQ%9bpat<4TX5Xic=}>bBY6b(65U=i&-Q&g zQi`pLvXkXTz$X9$eI~fVeq>K0B}5*@3|!S{yHVkNu%oIeuD#=zo>A-8%^%y za$0S4oro;S7<-V%Tiu&zb=^fg7&3@+lm@u z&)v@-G*b%-s;Apz*cA!^Z;kwczt2ui-Dqhwc5a?8FPm&8J?smz%XV1?D;rX}H!F6f zKWuP9rO9Udh}kV^HaTk3G+)EdXN7)@c8(-<8i zUr_06PNJ_ZO;1NXZx?Spj;dSgWRvP*QwP@oWf8nWNJ&)l#P1!dPNAWwH8zT`rSZM9 zI;L9r`nVkC8Y(u`T@caY1_B>ur^{JSr3|e|kXDfI@)WTlVa` z>;UR1IwDN@;zS1&N<>e~t@n&O;@q7%3;d1&2#*u_LeNiTn_=1F(l11Qn*6ouBwYZ<`~5?e0lF z-+{lMK6mSKBSt)sys}G5OIBuQLjbQzHb*Cy5#Ywd?Eq{#iLaEmR~PM6yerh1*&Is& zLY!M-%;fL62llT1cj`7<)+Mzi3eL7E;@Ra@l2OL8NZaf~4fY|vPxI7EvA!26Qj|y^ zUO4)>s`BvPy694_J}W-ir+&Y>CssZj!@2TH9y=4Z!>B8o#kLNCgsHf1Lcn*4&RN&-9>7h2{xR_ z+>{kNAH#3?=(1wQ*k!XztR+J6`*>f^8@94`s>NOz z-n3HWG2c~KyaiI?k~k8fxSfvvb{p;B_3k{W4PF+3ZOsH@+j#g`Ha9z;w_>0VA0Hcj zJoF`)hw88uR;7vm08q!TyKZRYWRe({ZVMkF$S%iyyli*h=dB$%ov}k9N#&B!mJ`le z)@q2QXv&oZWK*$X4*Rg^g}|X0m)U`2xShn?>#iQ@sxqu&uUW1|K_Um}SOkqo^TU7I zRGrBKe;qK^{{U|<;xf4>%Geod6=F9q*xY!rVS@qgOzkGhV^j9+7Vg{p0q3gGYdo%H zn;DeN9lF8^7dOI5qHo?%-@u=bFV75JMXEIYYs-mmYT>b7CVYfd%}ML2AiO{l zdGQBtByLB>{VO$|nyqG_%j4xs2_lIpSgSX*>#+rrPzc@rKF~iuC#ajg<`6+bdh^D+ zC~LCV5;7}nz$gw|U_TxS=z|ZbO&|Dmt&Ye=Jco^2HR6&*;tCVRypUCn!;avPdLSf= z;e@oRVjZU2yc_4 zjyoe(q=zAme069dzx5`fyqw?K1%4TU@Vk7Tm)9iHwj{YbW3JYT43TXk1MC9K71@XI zLht?m0PEB}G)ngCoOWI)r-ek&nH}r7P4*ytz7Nm)j=Wc-T;j!Su<{HfrCa_-mrOY7 z!88KY2TIe;K-B~gtWixB>RY%#Nme_!+i|!fpg40g>AN@KwMQXYUfS4>jzXWg3bOnb z421l6*zLF;dYi~!p<^C$6l_?NBNS~N$zYRc{?o)2BVs)7^ZfK5b2|jBMP?=P-cU6zW1wWeo%#7Bk*G8(H5l&39XF%Vd=k~3UN zi|)w8DG>x_;;IPwP(R=C(&pO4sd;NpkeEgyxm^?PdhS3nfPiu;M*D5xcejd++UN8z?_g$=il{d)PqX&mlm+_qw!D^XaCw;ipliNj(&v_N@pu-E_(Qq-b* z_<5G8*>AM%Ab9nWVS0xyBVhQI;RiYnl}<(dr@kMUXCW5N`;n5 zIEUoNhzfvt==-=ja}}VmnOqc_+fQoDh92TYjMTbP(_Y49E3)nU3fjKTr=?#XcBN|w?8}UGE5`F_1VaSX`@*kmTF}9JpM!>L7 z17cKd{`>WZYFA^iHq~rKgX~#YEQn9Sf_#tr^mAT4HJzQ()tgmQi!3%`K_W^(+lkl^ z0Uw|3(Dp;O(Ftd4E#gUv4 zeY&m#Zp*iVN&N5Ap+Q{CV$ihmQveUk8)!;yO4az-$18iRH~xI>)=@IH%1cJvc6I~X z2mQA5)&rCc8i%I=RrBI7$A94`Tqc1E~wH_qDQCnIEaRn($1?rsTD&ODF4c9n`Z) z4=8@dGALOF{s>S9lkxum;p#1yFa2@-%yuk2`NiVSY3;zJ${@qQr1z95nB%E>w?SKXI@<{Xl0DiXx zcoozLmSC(AmN=0_M2{P8eE$G;KOID{@H)!z1ynK+5|n&DwlO7~q=Gd^;}8LwY1ep{ zet)@5k~FfOF~GB}WZ@<>9n zkUtIh>s91yGM9NzBd{yEc}O01t+vrV0`sOI|W*Y;tuKVsjHtAmshyGgeN0aUwfYN;U z-0$PAmij{UzocquOsA_qR8!?N{{W|*0c}$siHf(io9^~ycY2z85Y7N+nbAbje%Pbr z+x|sHZ=KTQ)JC?dO~O)?iCWg92|^RPmZM+?i7){;+W2FF>-euXMp2e1Bm}8zTF#c4 zX#!U3s)$Muo04V#Jwd|0Y>*V90dIPA?FVoBPuh%stRGeMZQ1Sq7VMO}FWO@3UoYPL z>|@+o@F(tCFffhn+F`{|6$G&W)8AHI``4bNGFlV6J-~W$XEPX_o-Q12Iulb)>3Vds zwi1>z7hL_Y)~y3U8Y2Ag+>%K>4Em+@ORoJ(_a1FwsaGF>!C`4Go$bGF@LR>Ddv%3_ z?W>H(L-00Gtu*8YnodmLAO#+zx%FMB&=yz*nuep!<0JvM99?nNQ*w9wyq-U?>s-t7 zYNK9`BF~69g2_loGKL!|SKT1&RBT;e<5tj+k&cC%aTZeELtDVS8VJeSLRO~~G`m>( zUS?_?5h+LoL56?bwnEB={VELCf-VJIE~!#1Nhs2{eQn!*S0>NJA>+vxxJ_D&u?kPZzG$~ zbR@~-Y&oll)70rbh;C#r*>4cRS~$JQByS3%=g8{%x~iIleX^}#Eg>o@Q7`}yN%?+* z8fHh9S7z+Fl;zErUvb8CAtFkZDnJ9(ew&*~2Ofg`FY2#Redxi@Ow_@4`?ohC*B#E+ zS=ahKFQc&3rAM`9^^b+!PqvYQURQNR1fK<7xqVsbzUAG*_6xDN+gc;ipJOuiA<(*0 ziKUe824z^skk%T6C;Z!%og7z*_Z4f9ia9fM?)8J;eGS^4*LH)_7o|;Jb3KNGQH@32 z9_Cl3lro+A)L1;JrqVRYvz#S4#O9rM@gZfE!TWaPp1V^Xvel6p5tIDKcn&{mANZMe z@H}jH>rlzsbBJ?9@g`u|O9=`T?I>M3R#IJBPy5ARKq6a7{{Rt@)3}*P;7%A<1LZF2 zs#4IZ$C5!x`$mxBSY<#@VvrJCI*4_9LPQ%e-YkPjuRThLJ+xevJU)3AHBq>h#wx&l z&z~a6JO2Rnbg%3M#%}TGw0Y4@*>7XplNKy&V=lq~{C(W^{yy%DdxDgW@2Lo9G*o2{{Y)= zot>D5n)cIJ!)9oM$K1_5WQgGm=(aHWo}nIocjgA&B(+tzV!-IQAxD+z)X8J!X&e*= z9G!v!kK_7>&-dwr+Kkd;v~SW$sf;CtVmv2 z1ASxW=)zn$;A@Oh;=RF6vV0YuSNac^=YenaJpixO*^46dIJjp})d6HmNIvf|uHzkVh4?>%IA7V_PIc`K^$yj>is_kk3v}rvY^6Z~MXhqF3MpBX zf?Be$f1`an_M^}bYrBiwCDJtY4z#_8u`ZR=xvf~3-tFpaJ$S3hZE+Qc6sZ9*ypm5+ zCOl<{49AOa&rHv(zVGTQ-lAizCUS1vcbguV9T|ns=j5k1Z7W4-U)|wMM+N&b)oV~n zwn2=&m9PPp7>Klfp!CmXbk}nr)4C(R+Sk>OcqZ%yr=jW-yiYphJ4ch$Vvo66hJ&>< z5M8y7uM)_T8DWh%stCC#ZK$;g?{-5+r4df}WMsSzS?a@H%Di+fS9l zSWIojh2HAir}Ys#d%_TcH&E>TTI5_iLqkyYZgM`;R_Sbku`whB_R1Q9bZU{LMCm{l z#=FLxbD8J4B^F)5zxq2WW9f>Mh3dBSxD6n~zW9>cH6!I4w66b{Z&SNSf5SjEc!IilvvUO3K7o>0Ed1 zPcGq{xsDyFXt|lVptMt>Zl2f#0!V^XL|6b$!0nCa+4r$b--$E&-)I5UHr)?JPCh@G>y77m6(340@;3cd$O z>Wf-$C32vrhAHpGdJ9q0ne%z*X8O1#63{UFa)Demm#Lj^-O9AvRCm9zx??8>M4P zDAe@@Da#^TqiQ}-+JW|v3E^f^S7XG2I_tj9^1Pca%-&95>OYBu5|+qLq_#|JY{}G> zL=_z++RcR4mWB$`~5goJpaad>-W<520bLf%`XU!-o> z_NUd}vhCMmyXV~=@>cyS)HqJh>1{S@%cbpe2YU zdgvGUg@17wS8+F!wJPb~y)fwEj04o0`0B4TU*sXMnJSX+FXT4YL8Z{eMKh z&T$^5yPsvY3k|JyL3aBT4MCA zyD8v&+LJP6O;iOxsVYfIq_oJoRR#}^KMB63Z)30eg6{*^hDaxmxq5<1@w;rZNOKoP z>Zj*&3E%V8VYN|bl38b+nP3em;-Emjci2dOaVKvJ@CR4>yZYO?U552h-3!{YLSc15 z#P<(14sW&{;nVV7qkfg^HwV#}-)Z3!SG1DFJS-VGB#7I5*ZoJL^dE9pJE(gW`BgQN z3-fkoKF2GpF~-}-Zb65Y5DV3+6;xM0*K{G7JT~81aLdX609WV{xa=D50tojLYkqrU z^Sjyw8kNQQtEp5g?Nt((g9X(PdJu25?K=^Q;{!^rRn+<&)OZKQ)QB^&8>mH1zQuPc2e+W!E= z4I2-z4UdnL{0^~v9wB&mKy-RcVn?Uw(-Bu zT6sf>vcBM}^tR&wMIxS1Ps`8qxL|hpJ9Pzay@rMp6iAYkGMQ}4xtOT|i6jxf@CRNN z##>;A#zIcu zf=>KFC#-D)5)1(5iMSmwVM<9W+LcgKYSK!vqenBU$lwQW8|-}SPWz7o$3__Gr=fjb z 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 XwR`x1@^MT+#HnW!cJ^(>{hR+E-36=Q literal 0 HcmV?d00001 diff --git a/components/ILIAS/UI/resources/ui-examples/images/Image/ski_widescreen-thumbnail.jpg b/components/ILIAS/UI/resources/ui-examples/images/Image/ski_widescreen-thumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8f31cce2aa878bcf8070dd7183b0800277162466 GIT binary patch literal 122016 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 + +
+
+
+
+ +
{REACTIONS}
+ +
+
+
+ + + +
+
+
+ + diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.entitylistinggrid.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.entitylistinggrid.html new file mode 100644 index 000000000000..18f073d65d9b --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.entitylistinggrid.html @@ -0,0 +1,5 @@ +
    + +
  • {ENTITY}
  • + +
diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.inline.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.inline.html new file mode 100644 index 000000000000..04e9b86140b3 --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.inline.html @@ -0,0 +1,5 @@ +
    + +
  • {ITEM}
  • + +
diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.propertylisting.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.propertylisting.html index ac941ceb9b36..52b4c8e05011 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.propertylisting.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.propertylisting.html @@ -2,9 +2,17 @@
- {LABEL} +
{LABEL}
- {VALUE} + + +
{LONG_VALUE}
+ + +
{SHORT_VALUE}
+

)P&cv(hT~Ci1nezrc=AX}ua@FoZs6M_d#yX}KFRdV=hlvE zQ`W80Led^Xiw(EAUL#Zfs2NY!{mi>MFiCGpyuW7I3WYy(!WPQ%Mj#U(6GC3(OTGL;R+q9ate^Oe{#Hz|QL({}lUVQr>bC}) z?E6vEJPVyeKww&K0p(h{qU&RVaIfiGpG4@+letR>c6BgAC+D0U$CK2_%1 zeNSCVTUF0h>#nw8OAOwj*1lj#{HdxQdi`N`y-lbzK$R=+iA%}Ho)z+}3c50UDktMLc(Ab1K@Op-2G_>& z5)=oa30OSIce@bx8?^H0}?MJF^SpW>$g+j1Dd@|l3P3r6l>kS;ilwp98okm{bEa7^5~S}%%Xdm{2VwvbTEAass&uQ@ z+0~q4;3Y*3{pQ(31e{?*1boNBx42&jr|WdKq`ZzqkATn3WLI%IhO;mRgU;WdC2G{V z-%z(vw$iSWEeXpiR2Gc?03kels-LK8H`>dg>!(!p!V$F&DcE@3f6^$ zq@hzS+o^2FqKL$L)rQ%vLE?qlDaTxmp|snOl}~BnX1jEBuu}l$0e=4g<#pH3B~Bbk z9p7b(OkEfb+%2+<5~oz52g)X#w_=BY0`cbn;5g!nTO*Ptj<(Zv$B?ACpucpq5ER%~ z-N^ZoOs%v%QNgjz9_GnJ?eNhGxCVU56Y+|p>Y6jCW5j>DmOyXppKODMQ>1s6mGu z@b>wDNYB=pp~g^2@enr%KgS9cGPK)t1gz=$iV~qR>plv;X~;Aw)9lbmZrSa|Wh=8$ z#N#pjXaY#yd7>EJ4#>dek&?Tn(4)Xps!#N+!}jf*J`^Ld+$N*8((LXo+fBnuZIqB! zvP%gdMkg^_7gc}R>zWVwU5=S|WvPG<`GZa-^py_#!N@*QOU+Osk*sSR>+fJJJWueA z6b^eF{Y@6yoi#1=THF2|Um(d>Yik*(1n8g8&qjzLNTjy|$^HR$fNFC*rdzz;5l`_WO@UoSW^O>WV z)wd)E*iZw4hRTzJ$u(JO)#nO_+}TtnU|bfD>-{KKop#WGrY-z_kRa>xQsE6TEzL%}~*3b@}_%{{VJeEDEqJI%jL4W+gJ*0r}T<`gOyv zB)XBb5=h;(DxA`f9oEw=TzH_Ff|A;}?TVag*GZJ_N@PI_k&1y7pjnns(#ze-+{5;E z3fk?6L_kv1km^MEX%Nz{QVQ6J2$+k(DTGOx>+{vVJl zOasL(>QWu3-u`76R5scG9^~uqcPUoXxn`t)F7&7@c??l){rPgY9=GsRWkIJ&BmV%2 zq>bE!?c9bOo~ec*R=ai_^8~swNjv|)MiOS ziU-tFW;ClCgbkX5tt%3j6jq#eC;n+ah=eHkej?+A0#bfriDm1h;9fUw$r1yJNczRwp&(?36o$y{ z-m-Ozw-S=#EhqghEiy5fB-FM>w%ubw;Elu`1oBAQv~j{!dihMrhKf``&j5F(X7fu8 zqTLK!6a3CN@Du653NlIeWRchrSSdv$ zd*vs9nDGhzvCTnqk7cBfci5>Bup77o@B5|$PSDbg$rt-h*Fj3u8~{p^uoezK)CAC)98KQh zN0|Qr{Gy!zS)c>te(7VEig1*rmeda_bV*M>WB~gIM@Q=^VgK#84M~ae3;EenY7R+$dWFTI{aB`U)R`JyZuKv*nx=pZP ztLL98AN=L}a=p7(Io~Bi;3ZK@RN9a|&k{~f`PzL+n&WKl{76%W$sX?SnX3q4w!n`? zPPSXv6)TtSIuC%7+NMcJ4V?V*L7R)@xhrwfVj(1}bpBLxj5?(4@L+x+2?VFmnwH&{ zQa$#Q1V}JGljm0*TN`TyQY3TiNkebc-AEuf5IIn8BjP}=+qk)53rm+zl94DJHfPLv z(d$u6CK0xO3b9=9tpYc#Ed`vxe91kC6-`58+$)wHwIvR&_U00usuD7UY*)}^*J`zS zv;{n;*iji-Za*atDqz@1FsGVGIRK_qJhK?AZY~$MXt@h1I7lVLk1S1FSuWDhET~!8 z)bo@k_sWo)T|lSZg`1=Vqw7UJhb`Jlh5ePogk>!LA@Zf==M=r}I22AeGM`W1m0=)) zcLbhzBz&q%FTUPi!i{3q8~Luj`E-H;x2XCc5NGF6t5Ld00s-fgR{nCMITN_NYsjtIC`RDjky`{Ai|KYyy&? zPyN&9U8>=fk`%7e0#Y`gsG{Iuj8!C)a!~0_(trtlT;vrHPu87k6- z&Q=nFWGOG?VQ4^(-HmpeaAJHyA~$W_p3_y7>iV>h<5uV(lAV(2{P9JjQ@wF%NVpyt z{6#DnPp>$l8hNd2fD|@%6M$33t%Q$t!2{lul;XIgwt}tBtclx+5HZaN)5hT?KY3Uw zFhZ@wtLSIxLcaQ%cqF*u(Y6YFEr1`4=jlUiQaRdgh+Y5%7U59B5*NNb>6!Ud>G2vt zJ6mgVV3!m~`cNu)WhwqAf-o`y2^AvHrJ%YVLYe>qK|6WoYD);PDg(h?hZ{qQLcQfH z`I8Aq^%8Z(_c>5xnV$$!k5tEk?8@o+X2CFqZ=Z5M?CjQ$*Q=15U3anA>AMRAjoY|;n6?4``b+z-Rf=yNvj}mN zU654}TT8){KZs&tyJhFS?+uwqG2%FYsq~Xrt;GW{B=P`G&zKZOusDXeUdosM04AtE z%r5Q`xlSe2Ap7rvJT%V$o_lu`!GglUcJ*j=l0i1f3EaPezm)mVJ5)(pSZFp(xSs_p zLgO+)!Kqbv8Qgy9Y_`=2QVLeWIm`r+lU#7cnBQ{XBxU6Xa+@?y)4=w$X)e3QA9gx%e25O0@m^NxZfa<+Ba4-qx)?=%+3>XZh4(K6O^PQ$!VR z8xmwCDukr^#Y|3fj+Zp!zW)G+;;QyFg6(z>av67NT1wq6`Pf= zRHY?t#t(*1z<_xA(CG%@)o)WsLCKg(m+&Js#aj*X(MgH^BWfWj_>)cmz$})#oNr`G zUr|yP(v-rIWh+5ZaVHbN!1>TuOh0s@vKv~l3vd*vL)BgX06L9}XBjJ7DIsQeJdzc= z;;0lg^SXji6K-wN^ubNQQj)IRr1KSKfvjn=hY}f(yZf6-Bs$6#r9dNg6RvQgXl0%m7F!>{EEh}PJ3iO^038`7?52;REGVQXI zH>B9xGNdomr-ShX8m~EI+LEOdnL^Gt4-An$LmX6M#r1?1?jN#PJDb^XwgK%j2<9q+ z7B$^t9d9xD@>UX;4G(X){(RN4%G$>5foFMfagUkkUu)fy(6apOFDB` zkF7-b#^DPLgX)k6ra(1@kA14yskUC%ycUX~PF*EeC`A7N-?c`g>Q1(>>D#Mo7j;X0 zNAD*$O=-WuVN-w}Qc_RClOmnMdeKD>+H0D7X|!mLCz(&F99Fo~ zdYA9lO1!Yt9%=T!8Cxh(5<7yUxJPm-@1<&YI^z#p+<%c>T?s-ScI~SRr$l}dl>`zq z*po?BQ%cQUaXL+}k_bMc-s>jm=Y{%Oz%^#rz5U>M%t5i1<>gMSCQdldAOB-L7sCxBSA{Ls4c9J>e!kB{;7>YgYqHzPPh)!7X@e zJ|lMbfy;)~GJl4+DjumbL0fA}xZP~uh|`2mjMHQk4rORsR-qBt3bQy1bQvI#?Q5)X z^M5_hxk=*IA~~4SMl#X%3H`rB^%k1St!?^erd4hdN8T+QQ!fuPK}n9;&1(DQ^tVmw zj<>F&#=}rbrER1+ZsC*2AZ%FmiK3kLh1ZuW+Woep2$d-5&7>5-p6bZw&Z>94Lhn*M zr!H(8yt%d|D>~Jf3Pf`=9$fm?og@{FasA7NlfIH~>PbIEnNvM60MfLAHq<^=va=&y z=_`dO*mW(}trrjPlu^)>B8EZ88p%Dol=o6O6Oa3Dv}ttxzp~MSeJYTpF#D4pg{Q1tU2uO zQEgitb*CDX_(AnH>W`>6p;6Pemu?Ya>cc8f*j>8;Xa+GLf6Z_jVs9*DTf8Y9c>}AE z5POQWX&qgoFxtP(wA8(Xe)NM4~V$df$?OIv^%sX`~|LDL1y)VC+QRlW$M z$m>Ti9(MYpG`(`_;@wTyUg~KL$U}E8IcDO7@$QlNQ>#zEJn|mCY4^Yfb85YGC0>M- zk@KM*)Kh-Za@p%`HDy2a*g|&zC;34DeQ6?p+#3t2KIv}Ywp%~=ApoC| ztw_t418;|IV{++HpS(QsoE&lz6e`Zm-%*eoN;I1^2njwDt`CqPP^6*_wnwS|0CKL% zkNJMT!nljum2twpjzmJ)*@NiBt?L*hm$ zi%Ri81p#=OX&Fz(r}hy8Z~*cOqPAC^B$9kqz#7UGxdB&BK!T;*tp5P9qb>C&c7&)d zf0V6qbN>Kz)e*D?!Gh{HN4le*lzb@GR)UhC;`a^#5TC69)X{-(jM*eMzUug$N=OI7 zMqq9y^%XF>cxAT;CoqQqPtHiHZMErT&wFd22-=_)W6mhF-ZtSd;v~ogEP_&df@Xl~ zeqby0&O4Q8POMO);-$^N`^!l20r|~z?x}450JuirOiQUud^1U#O{v14JCvR_?nx)% zMWLjafE6Fi03+rI{l!N=^+{PV=^eDi{ zEDfxzjbCXDks51g7$l`f#2<U0uDW-5@EJhT+*s_mlLh*enC?ta+9~sREK^S$xb^OXFTH@V0MZ7GG3h;| z6hX7OTVyNUZT8`D<+uu)?@lDA#%&G|Kbat?{0#zZmeBW(4iCHxAD8cn;b}yG0(S#D zj!*RcD0X9w?zyOgwUaNuCwAppq^A;~HUmW>>9^FBB|AysDOm^o={afa4XYvv*p*0B zPHUyM>w7|&S^*hR82RJ(HB%c70ap?{Y@L}w+Lb6ScBrbjqB&x?{UG6150bmPR4&zD)~-Qu=s`XE&2f5}Wk4}* z$}y6+1Jm9NicD1DM$ok(K;-TO{$iy`NJ!q{Dk5a40)Aqt)Q#PhJ!v>@J;|cATeu_^ z!BhfTNkId)D2rDO$d_(dWGKe_fI^H%7^Nw3B}A!3LU05GiaY$t^rbBj!r3Q>5|z)? zO;PNyvm0WJ$DQ18k8s+csV(X@Ya{W5?g!>1npn5n3lOw8kf;g-q#vzFtIH5)?iRbF z-YvDTtI|jIq0P;*$PH;spv1{}U;*&1F{BHSehMD;-s~gdi5C{CFrtt-jfx;o!xUEQ zL`g{q_e7M4=i{1Kt*Sv-Wwc7B5Va5^=kTb|w$wz!aUvN{Id|AVmKF4Fx3phCY=aghGibE(HjcB&H+E zqpM}GTI*#TpcX>MxOgGI!5)*E z>vEF>IlDLp0?+rM%A5iVZw=T!;h&L;&5sf`?G0vktQF6x70n7K=034?ZR@A3HkzDI0#uVv>$GIvxN!+MPBw|+b z0)JYj>-QF#dP_>XX}gC9@S6m{G-AF2fyVQKo^_5#x$j?&okNcYqS zl*G`kjXi~+1o)1FDp++&Gv5(KGKN(4@^3;HfRIka`9^2yN|!aQ4gllxYoq;QfIgs_9U|O0g5}f7Ucyw$nh8Fl1u#-l(J5hzw+fZGlWw4xJ*4^8 zD^0-(Hr9?PAI8vcV~6IWLl3_^dJp9jN#=vRd8U91N=lHJ8#q!Lfl%9pixmm7Hq zR0Jn-Hz7(weFW2#+BUGK9Me?lkYQNyKoRl%Dhr0x7rDJPcIirn;#?^U9)f%S07||1dP3sL_7w1Wn*aaO89 zZr;-8$4sfg0StO=Cp5Ha>0Bw|!z!5t)fJc{f!cmlXE~1_)W969Ka?QX#%#=!=CW_B zt`e=Uhf6K06L6#@EiO(nCYt^I?LbRv2tX6LNf5pW_oFnC>#Jr0t=J3{oRzi$NIZ!I za~@Ryab=PmyC@|R(om$Rpux#KhsvbE3#{nbq^k9j3`qN@k3D+nfcs3QHj%msf!N}> z#+<8-%5m_eOv0V~UVj>|HFBT|O4OjxlJb|i3LddVc%j6H?a5j;Z6PWC8oB4nxMD3D zLARpdkT~Ag^;Ustb8dp7!nRJ(ksE%W)};*B!s1B zbExKN*UZFIlcshucz)A-B?`pN?@=E|HtGk3~I)X6RPz z*=1fn(Y)XSnfj0KS-9F=kfvBO!ow&*pCbZjr6Fo>dY0cf5McVrsM$-4P;bLRmZ=E= zV9%vczx;_r&_*q~9@W3PIv!~#ZSn4h0La0>#Z3FJTrBv@L02*1fRpFIteisHFyIIy z@d99{_Y@gk^^~p%>{I;TI)U4Ev%h+Q*zAl1F}9VU2P0~?BqXRL`hK)RtSnSXyCuEA z%F9LcB>BM2WKs+e0zl4G38nA4=w8(B+^k7i+I~@rz258xS#r%|Zd-JVSJADmlIsm7 z-u<;5rD$w|lkiu6TGTfkE7sa|>#x}9*O%IQ7rmC7Y$N~=;X7cO-)vgA`GOuf|Oh#*D=ly)}4chO-=nc>_NfR{3Ru#j-$5VMYgx|$y7A^ z4O>#MXx-`eZM}l41zVx_5_=ID{N|h7S@m9;TexRO)a=S3Ox(F@urbE$AEgy-f7A^v zE40;@W|XVld1%|Tj@yE9TboGslVfRY%RNhQ=BIF!fh%!4RT-3=9^K}%E9)yEA<;tF z2mBzL{H&j&q^6V+B!z@?AXxlXuF>wDzqxJgU!=Nn{_+CceRj*UxO<6|gOl+UcGYz5 zmDaT10_m%5GgCqdwbPYqi*ykjAc!P$24=QHUe-OHBqwVP*xVHDCg#~IP%+4G0~M-z z^H=EZqEfXhEkXNjO)1*b?W{j)uOLuJkCav{brqFWP1jENK-hvv_iP7})#_>Bfxt@H z3GcA@?|(I;Z|vLBwG}YCXI8@1w1(Xc6!{VkK9z!zXL#ytpGCXP6Yd+(m)J=^>g11& zEWK{&_f)Iv=PpoGQUgJ@RtIn8;D1_LjRmLtMw`EJOC%QBaBgw4Jnl+|t#?)i=mtmR z4aN8GU0LAV^-a5bl3uFzZLcH5z3GdsKHlV?5tfm1;_VbXwt`^#f+~`}v+ItQO{SaG zHZHQ}@)=w9V(Qh|@^vW4KB_0uy%VH+I@VsY2J5P=(`p2?-+~tNSmSigJ~U(99jU!< zb){-ORNk|tK7!J19n;XV&>|!&DxV?^OE|?lZ3Ro2BSG$PccHNd%i^KjE@z&;oH{am zTW?Wu?;hpnhx*;CyI0iyhh*i!Zc%XC<)%S{24XvL>BUg2x`SA{TAzNwrpg>5P4RuV zQVGIBRUp6|fyWhp)xCptrikU!+6&rFy`rd>KI-ePHv1&z3RDy)JjnS~3d5x}yGNg0 z$BfGXXYYw_#YGk*au$G22l|BkMQ_th!rd8lER7?~+<04(!=64U*&51*IlhD6eqmP~ z_MvvQEe-WmwXjFJ8`x6e2ht{!FQ~PP^{VE|@r^}}9$m|cv>#ChJ|eYaI)M+g&ZniW z(Z?0FZ#`tBzTdr*x?>W4m9gwJZ3^upX}fXNrbG7+C5Bs`#Ur1c7}HbJi?zQ2^s-r% zbJU$Wz9b)w)NbEuY`7n1S7P6C^2MFXpGr@B5%JAitaZI+;gG96YglV)KyfJAmKBVh zrYL0eR+Qzo_O+G^=~i2o(9!f6=lfDUM@zTx(Qf#?S`dBLzV?QiN2*}Wa5@=410;AW z2Gm3ee#ib}^xNF7j6J8Yr9FA6TB+45_=*HA(s448^QNx#){moGq3todrCy;zRQpOc z&j|7ZkBw`lbgrGTM~LpQwJBVI{{XpB#Kvcjjb^{{mynRlmpgyf-Kc^;`0?NAQecKd z+qrL*g0re-V=Ubu`P{9!Z|!$ixn%_w*6ov@;IN0#cv|iS?lhdMbAZ zv49joEp+V5zN@V+{_HH4U0t|Qf}wPT4_Pysc9*YQSzq2lR|A zI*2lx~H&A`|y_zWlKskSRF?M*xD3 z%|X{7gz>A(mdvk7v~*`{i@Vt!_^BOz=i~;FXq6|HloSWdL8|i(TquRE>HD=n2}9Q9 zlqNlA_cY}ew~7HR?noyi#5nr%{?rFBFoJA7{{UaIFm|Dgk3>Un+%}}6V@`l2DlG0< zj`BqmT(~OS>n(yn%ErOv*a6z1ZHvanANP`Ae-IKsD#=0cLY{Okc#r^&GeWyb#g%|I z=88%fQ2?b5I0peiJbrXad=|#lZAM4Bl@Ldz2o%bK2E?e20!CDSIw`AFC`)WDDNzPg zKtTSrQf-1%>COT@yPHIj4Ey0DJA%*1e|luw*H;M$Ww!PzC4LY>{zhqYcFnd*oM{d& zebnwF`_bWG0FiFWK)_0vlOGyg1|wqmei^GROwJo z1FBFQDM%_K^PtV!K}p>T0PQ1bgY>Qo=2S<4ZnQzlgP8c_O>l?1Hd5Cyoc?K-im*Z4 zT=ghOBjH^x=}tm?G^D2iK`KhS^9BtFg}ma)QBXl3atzOG(M^)pcBx4m4gji_f$a>d zYAzTy3T9j1_?FT~ed_h}&-_yY#j!$m-85EmQk2LaGC`%=R{8e=ga|1{Pd`tkOq)a| zONk_rFjW9hoJTWdcH;nF#TtebapT*VAB9`J?)|H@3zYHU88BfWr{z$gv=~e!Bj!l@ zeCdN~OHJWT5DrwN4gUZWIiS7P+vJD_r5P|uaVsjAPzlk{SSPww0=SkgR!Wlg#H;;ll|=bg zekx2Tg+gP1AW`<}wOAf2F03fTrAZrpRRzv(Yy~YBHYW*bw{kG)QqBoedrD8G5Y@A# zvZcO~LHsFBv=!Vs!ifMS)P*PW?L`|44pXQwrFKYUnK+IOENXxjvG*g2 za&{x$s|Aacfwc-Cj3}Lm`kLr7bYv`{F_0GKKebq0w0S_KmslwP5xKlwjfyq;4Y7tY0-f$$e)wzK(5(2(CC-SRx#AE=fA|za;4K;BJJ0yd_Ad%}?hHh1r z7SMr=g0F!+slf4(C&B`GR0T(@QAurUg*q-yFjB;#ex8*_OyS5P2uCB7kt{7KK`$^B zJBo26d@DZ*Yfvgq;U4UN41FRf_pOz@Z6QeRRMTiD%uH0E+c2G~P14Cf%Gh!}BkA<2 z8424e-CJ+kz(XaJAuy{&DZ&O)zJ6c6YbM|DlvDRq1jHX2WPNc(m9pSUn=ZgU@NuNJ zr4K2Y6^piDkhGhY_h)LCxj!kYhk^)rRxJPxDkWYAeaKW4#?%rK^MMsDvYRRZyr#^= z1jtVr&-L@Ef)Lt4bcJmngf=JQXg$GeQq$p~0zglR;W(;*1AzcoE?(Aiifp2kH?=oS zT&@g)m|908iVar{0Uj9&N}>XZ1n00OmHp5>wW&%0DIbJ5RIWHCqYXI0A9r!)q5)bC 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 E*@j?(6951J literal 0 HcmV?d00001 diff --git a/components/ILIAS/UI/src/Component/Entity/Entity.php b/components/ILIAS/UI/src/Component/Entity/Entity.php index 1fd4b1140b26..1ed2beaef8d5 100755 --- a/components/ILIAS/UI/src/Component/Entity/Entity.php +++ b/components/ILIAS/UI/src/Component/Entity/Entity.php @@ -21,14 +21,14 @@ namespace ILIAS\UI\Component\Entity; use ILIAS\UI\Component\Component; -use ILIAS\UI\Component\Image\Image; -use ILIAS\UI\Component\Symbol\Symbol; use ILIAS\UI\Component\Symbol\Glyph\Glyph; +use ILIAS\UI\Component\Button\Standard as StandardButton; use ILIAS\UI\Component\Button\Shy; use ILIAS\UI\Component\Button\Tag; use ILIAS\UI\Component\Legacy\Content; use ILIAS\UI\Component\Listing\Property as PropertyListing; use ILIAS\UI\Component\Link\Standard as StandardLink; +use ILIAS\UI\Component\Listing\Workflow; /** * This describes an Entity @@ -70,15 +70,14 @@ public function withMainDetails( * Another way of distinguishing Reactions might be the availability/significance * for everybody in contrast to the current user (e.g. rating vs. my favorite) */ - public function withPrioritizedReactions(Glyph | Tag ...$prio_reactions): self; - + public function withPrioritizedReactions(Glyph | Tag | StandardButton | Shy ...$prio_reactions): self; //Further Areas /** * Reactions that are less prominent than Prioritized Reactions go here. */ - public function withReactions(Glyph | Tag ...$reactions): self; + public function withReactions(Glyph | Tag | Shy | StandardButton ...$reactions): self; /** * Properties that could potentially limit a users access to the object @@ -98,8 +97,13 @@ public function withDetails( ): self; /** - * Actions are the things you can actually _do_ with the entity, - * e.g. in context of repository items: view, copy, delete, etc. + * ManagingActions are the things an owner or admin can actually do _with_ the entity, + * e.g. in context of repository items: view, copy, delete, set online etc. + */ + public function withManagingActions(Shy ...$managing_actions): static; + + /** + * @deprecated for semantic reasons, "actions" is not precise enough. */ public function withActions(Shy ...$actions): self; @@ -111,4 +115,11 @@ public function withActions(Shy ...$actions): self; public function withPersonalStatus( PropertyListing | Content ...$personal_status ): self; + + /** + * This Workflow is used to create buttons on the entity. + * Only Workflow Steps which are AVAILABLE and either NOT_STARTED or IN_PROGRESS + * will be rendered as buttons. + */ + public function withWorkflow(Workflow\Linear $workflow): static; } diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/Factory.php b/components/ILIAS/UI/src/Component/Listing/Entity/Factory.php index 86f9db97115c..ce6f9930ebfa 100755 --- a/components/ILIAS/UI/src/Component/Listing/Entity/Factory.php +++ b/components/ILIAS/UI/src/Component/Listing/Entity/Factory.php @@ -29,11 +29,36 @@ interface Factory * --- * description: * purpose: > - * The Entity Listing yields uniform Entities according to a consumer - * defined concept and lists them one after the other. + * The Entity Listing yields uniform Entities according to a consumer + * defined concept and lists them one after the other. + * composition: > + * Entities are stacked one after the other. On very large screens the layout will have multiple columns to use + * the space optimally. The design of the entity is one that favors a more horizontal representation. * * --- + * @param \ILIAS\UI\Component\Listing\Entity\RecordToEntity $entity_mapping * @return \ILIAS\UI\Component\Listing\Entity\Standard */ public function standard(RecordToEntity $entity_mapping): Standard; + + /** + * --- + * description: + * purpose: > + * The Entity Listing yields uniform Entities according to a consumer + * defined concept and lists them in a grid. + * composition: + * Shows a grid of many entities in a card-style design. Images, Symbols and other secondary identifiers are + * stacked to favor a vertical representation. + * rules: + * usage: + * 1: > + * If you want all entity secondary identifier images to take on the same height, you must provide images + * with the same height. + * + * --- + * @param \ILIAS\UI\Component\Listing\Entity\RecordToEntity $entity_mapping + * @return \ILIAS\UI\Component\Listing\Entity\Grid + */ + public function grid(RecordToEntity $entity_mapping): Grid; } diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php b/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php new file mode 100644 index 000000000000..b9e060d93529 --- /dev/null +++ b/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php @@ -0,0 +1,25 @@ + + * Inline Lists are used to display a set of elements next to each other when the available + * space allows for it. The elements belong to a group of similar items and have about equal + * relevance. + * composition: > + * Inline Lists string up the items horizontally breaking into the next line if necessary. + * They are separated by a comma. + * rivals: + * Unordered List, Ordered Listing: > + * If there is enough space for a vertical list, Unordered and Ordered Listing should + * be preferred. Line by line items are better suited when the user is expected to be + * exploring or engaging with the list for longer than a casual glance. + * Property Listing: > + * To display key-value pairs in a row, use the Property Listing. + * + * context: + * - Inline Listings can be used as values in a Property Listing. + * + * rules: + * usage: + * - You MUST use the Inline Listing only when another component around it gives it a + * context or headline clarifying what is being listed. + * - You MUST only add items belonging to the same group or type. + * - The Inline Listing MAY be the value of a property listing item. + * - You MUST NOT use this component as a layout tool to force unrelated components next + * to each other. + * - You MAY change the comma delimiter in your component using CSS. + * ---- + * @param array $items + * @return \ILIAS\UI\Component\Listing\Inline + */ + public function inline(array $items): Inline; + /** * --- * description: @@ -160,16 +199,38 @@ public function entity(): Entity\Factory; * Entries are listed as label/value pair in one line. * Since the focus is strongly on the value, which might be * self-explaining, visibility of the label is optional. - * The value is a string, or one or several Symbols, Links or Legacy Components. + * The label is a string. A Symbol may be shown in its place. + * The value is a string, Links or Legacy Components. + * A Symbol may be shown as the value. + * Very long value strings will turn into a truncated paragraph + * with a clickable Show more/less toggle. * rivals: * Characteristic Value: > - * In Charakteristic Values, label/value pairs are displayed in a + * In Characteristic Values, label/value pairs are displayed in a * tabular way; labels cannot be omitted for display. * Descriptive: > * The Descriptive's (visual) emphasis is on the key, not the value. + * * context: - * - Property Listing is used in Entities + * - Property Listing is used in Entities * + * rules: + * usage: + * - You MUST NOT use html code as a value string as it may get truncated in + * unexpected ways. + * - With more than 6 properties, you SHOULD use multiple Property Listing's + * to segment properties into multiple visual groups/lines. Each new + * property component starts a new line. + * - You SHOULD use properties with short values (e.g. not full paragraphs). + * You SHOULD split off long properties into their own Property component so + * it will always start a new line. + * - When using a Symbol as a label and/or value, the chosen icon + * MUST be self-explanatory and easily understood by users. + * - When using a Symbol as a label, it SHOULD not have an action. + * accessibility: + * - When using a Symbol, you still MUST enter a label with a text that can + * be understood when read through a screen reader independently of any + * visuals. This label is passed onto the Symbol as the aria-label. * ---- * @return \ILIAS\UI\Component\Listing\Property */ diff --git a/components/ILIAS/UI/src/Component/Listing/Inline.php b/components/ILIAS/UI/src/Component/Listing/Inline.php new file mode 100644 index 000000000000..a9e05e21d5cc --- /dev/null +++ b/components/ILIAS/UI/src/Component/Listing/Inline.php @@ -0,0 +1,24 @@ + */ protected array $personal_status = []; + protected ?Workflow\Linear $workflow = null; + public function __construct( protected Symbol | Image | Shy | StandardLink | string $primary_identifier, protected Symbol | Image | Shy | StandardLink | string $secondary_identifier @@ -157,12 +161,12 @@ public function getMainDetails(): array /** * @inheritdoc */ - public function withPrioritizedReactions(Glyph | Tag ...$prio_reactions): self + public function withPrioritizedReactions(Glyph | Tag | StandardButton | Shy ...$prio_reactions): self { $this->checkArgListElements( "Entity Prioritized Reactions", $prio_reactions, - [Glyph::class, Tag::class] + [Glyph::class, Tag::class, StandardButton::class, Shy::class] ); $clone = clone $this; $clone->prio_reactions = $prio_reactions; @@ -179,12 +183,12 @@ public function getPrioritizedReactions(): array /** * @inheritdoc */ - public function withReactions(Glyph | Tag ...$reactions): self + public function withReactions(Glyph | Tag | Shy | StandardButton ...$reactions): self { $this->checkArgListElements( "Entity Reactions", $reactions, - [Glyph::class, Tag::class] + [Glyph::class, Tag::class, Shy::class, StandardButton::class] ); $clone = clone $this; @@ -235,21 +239,27 @@ public function getDetails(): array return $this->details; } + public function withManagingActions(Shy ...$managing_actions): static + { + $clone = clone $this; + $clone->managing_actions = $managing_actions; + return $clone; + } + /** * @inheritdoc */ public function withActions(Shy ...$actions): self { - $clone = clone $this; - $clone->actions = $actions; - return $clone; + return $this->withManagingActions(...$actions); } + /** * @return Shy[] */ - public function getActions(): array + public function getManagingActions(): array { - return $this->actions; + return $this->managing_actions; } /** @@ -269,4 +279,16 @@ public function getPersonalStatus(): array { return $this->personal_status; } + + public function withWorkflow(Workflow\Linear $workflow): static + { + $clone = clone $this; + $clone->workflow = $workflow; + return $clone; + } + + public function getWorkflow(): ?Workflow\Linear + { + return $this->workflow; + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Entity/Renderer.php index 878cd345346a..3bb4ff664005 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/Renderer.php @@ -20,12 +20,9 @@ namespace ILIAS\UI\Implementation\Component\Entity; -//use ILIAS\UI\Component\JavaScriptBindable; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; -use ILIAS\UI\Implementation\Render\ResourceRegistry; -use ILIAS\UI\Implementation\Render\Template; class Renderer extends AbstractComponentRenderer { @@ -34,7 +31,7 @@ class Renderer extends AbstractComponentRenderer */ public function render(Component\Component $component, RendererInterface $default_renderer): string { - if ($component instanceof Component\Entity\Entity) { + if ($component instanceof Entity) { return $this->renderEntity($component, $default_renderer); } $this->cannotHandleComponent($component); @@ -49,11 +46,11 @@ protected function renderEntity(Entity $component, RendererInterface $default_re $tpl->touchBlock('secondid_string'); } elseif ($secondary_identifier instanceof Component\Image\Image) { $tpl->touchBlock('secondid_image'); - } elseif ($secondary_identifier instanceof Component\Image\Symbol) { + } elseif ($secondary_identifier instanceof Component\Symbol\Symbol) { $tpl->touchBlock('secondid_symbol'); - } elseif ($secondary_identifier instanceof Component\Image\Link) { + } elseif ($secondary_identifier instanceof Component\Link\Link) { $tpl->touchBlock('secondid_link'); - } elseif ($secondary_identifier instanceof Component\Image\Shy) { + } elseif ($secondary_identifier instanceof Component\Button\Shy) { $tpl->touchBlock('secondid_shy'); } @@ -62,6 +59,7 @@ protected function renderEntity(Entity $component, RendererInterface $default_re $primary_identifier = $component->getPrimaryIdentifier(); $primary_identifier = is_string($primary_identifier) ? $primary_identifier : $this->maybeRender($default_renderer, $primary_identifier); $tpl->setVariable('PRIMARY_IDENTIFIER', $primary_identifier); + $tpl->setVariable('PRIMARY_IDENTIFIER_ID', $this->createId()); $tpl->setVariable('BLOCKING_CONDITIONS', $this->maybeRender($default_renderer, ...$component->getBlockingAvailabilityConditions())); $tpl->setVariable('FEATURES', $this->maybeRender($default_renderer, ...$component->getFeaturedProperties())); @@ -70,9 +68,13 @@ protected function renderEntity(Entity $component, RendererInterface $default_re $tpl->setVariable('AVAILABILITY', $this->maybeRender($default_renderer, ...$component->getAvailability())); $tpl->setVariable('DETAILS', $this->maybeRender($default_renderer, ...$component->getDetails())); - if ($actions = $component->getActions()) { + if (null !== $component->getWorkflow()) { + $button_components = $this->createUnfinishedWorkflowActions($component->getWorkflow()); + $tpl->setVariable('WORKFLOW_ACTIONS', $default_renderer->render($button_components)); + } + if ($actions = $component->getManagingActions()) { $actions_dropdown = $this->getUIFactory()->dropdown()->standard($actions); - $tpl->setVariable('ACTIONS', $default_renderer->render($actions_dropdown)); + $tpl->setVariable('MANAGING_ACTIONS', $default_renderer->render($actions_dropdown)); } if ($reactions = $component->getReactions()) { $tpl->setVariable('REACTIONS', $default_renderer->render($reactions)); @@ -93,4 +95,21 @@ protected function maybeRender(RendererInterface $default_renderer, Component\Co return $default_renderer->render($values); } + + /** @return Component\Button\Standard[] */ + protected function createUnfinishedWorkflowActions(Component\Listing\Workflow\Workflow $workflow): array + { + $actions = []; + foreach ($workflow->getSteps() as $step) { + if (null === $step->getAction() || + $step->getAvailability() !== Component\Listing\Workflow\Step::AVAILABLE || + ($step->getStatus() !== Component\Listing\Workflow\Step::NOT_STARTED && + $step->getStatus() !== Component\Listing\Workflow\Step::IN_PROGRESS) + ) { + continue; + } + $actions[] = $this->getUIFactory()->button()->standard($step->getLabel(), $step->getAction()); + } + return $actions; + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php index 49008e8fbd4f..e8fe586bdceb 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php @@ -28,4 +28,9 @@ public function standard(I\RecordToEntity $mapping): Standard { return new Standard($mapping); } + + public function grid(I\RecordToEntity $mapping): Grid + { + return new Grid($mapping); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Grid.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Grid.php new file mode 100644 index 000000000000..fcfb4b95da2c --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Grid.php @@ -0,0 +1,27 @@ +renderEntityListing($component, $default_renderer); + if ($component instanceof Standard) { + return $this->renderEntityListingStandard($component, $default_renderer); + } + if ($component instanceof Grid) { + return $this->renderEntityListingGrid($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function renderEntityListing(EntityListing $component, RendererInterface $default_renderer): string + protected function renderEntityListingStandard(EntityListing $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate('tpl.entitylisting.html', true, true); @@ -52,4 +55,17 @@ protected function renderEntityListing(EntityListing $component, RendererInterfa } return $tpl->get(); } + protected function renderEntityListingGrid(EntityListing $component, RendererInterface $default_renderer): string + { + $tpl = $this->getTemplate('tpl.entitylistinggrid.html', true, true); + + foreach ($component->getEntities( + $this->getUIFactory(), + ) as $entity) { + $tpl->setCurrentBlock('entry'); + $tpl->setVariable('ENTITY', $default_renderer->render($entity)); + $tpl->parseCurrentBlock(); + } + return $tpl->get(); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Factory.php index a4d6c219b5c1..d6260c1a05ea 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Factory.php @@ -46,6 +46,14 @@ public function descriptive(array $items): Descriptive return new Descriptive($items); } + public function inline(array $items): Inline + { + return new Inline($items); + } + + /** + * @inheritdoc + */ public function workflow(): Workflow\Factory { return $this->workflow_factory; diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php new file mode 100644 index 000000000000..bf6b3ebc84fc --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php @@ -0,0 +1,26 @@ +checkArgListElements("value", $value, self::ALLOWED_VALUE_TYPES); - } $clone = clone $this; $clone->items[] = [$label, $value, $show_label]; return $clone; diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index 1a2aced57f96..095a93b48c8b 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php @@ -21,8 +21,9 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; +use ILIAS\UI\Implementation\Render\Template; use ILIAS\UI\Renderer as RendererInterface; -use ILIAS\UI\Component; +use ILIAS\UI\Component\Component; /** * Class Renderer @@ -30,28 +31,35 @@ */ class Renderer extends AbstractComponentRenderer { + /** @var int amount of characters that fits into one line on desktop. */ + protected const MAX_CHARS_IN_LINE = 260; + /** * @inheritdocs */ - public function render(Component\Component $component, RendererInterface $default_renderer): string + public function render(Component $component, RendererInterface $default_renderer): string { - if ($component instanceof Component\Listing\Descriptive) { - return $this->render_descriptive($component, $default_renderer); + if ($component instanceof Descriptive) { + return $this->renderDescriptive($component, $default_renderer); } - - if ($component instanceof Component\Listing\Property) { + if ($component instanceof Property) { return $this->renderProperty($component, $default_renderer); } - - if ($component instanceof Component\Listing\Listing) { - return $this->render_simple($component, $default_renderer); + if ($component instanceof Ordered) { + return $this->renderOrdered($component, $default_renderer); + } + if ($component instanceof Unordered) { + return $this->renderUnordered($component, $default_renderer); + } + if ($component instanceof Inline) { + return $this->renderInline($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function render_descriptive( - Component\Listing\Descriptive $component, + protected function renderDescriptive( + Descriptive $component, RendererInterface $default_renderer ): string { $tpl = $this->getTemplate("tpl.descriptive.html", true, true); @@ -73,49 +81,75 @@ protected function render_descriptive( return $tpl->get(); } - protected function render_simple(Component\Listing\Listing $component, RendererInterface $default_renderer): string + protected function renderOrdered(Ordered $component, RendererInterface $default_renderer): string { - $tpl_name = ""; + $tpl = $this->getTemplate("tpl.ordered.html", true, true); - if ($component instanceof Component\Listing\Ordered) { - $tpl_name = "tpl.ordered.html"; - } - if ($component instanceof Component\Listing\Unordered) { - $tpl_name = "tpl.unordered.html"; - } + $tpl = $this->fillItems($tpl, $component, $default_renderer); - $tpl = $this->getTemplate($tpl_name, true, true); + return $tpl->get(); + } - if (count($component->getItems()) > 0) { - foreach ($component->getItems() as $item) { - $tpl->setCurrentBlock("item"); - if (is_string($item)) { - $tpl->setVariable("ITEM", $item); - } else { - $tpl->setVariable("ITEM", $default_renderer->render($item)); - } - $tpl->parseCurrentBlock(); + protected function renderUnordered(Unordered $component, RendererInterface $default_renderer): string + { + $tpl = $this->getTemplate("tpl.unordered.html", true, true); + + $tpl = $this->fillItems($tpl, $component, $default_renderer); + + return $tpl->get(); + } + + protected function renderInline(Inline $component, RendererInterface $default_renderer): string + { + $tpl = $this->getTemplate("tpl.inline.html", true, true); + + $tpl = $this->fillItems($tpl, $component, $default_renderer); + + return $tpl->get(); + } + + protected function fillItems(Template $tpl, Listing $component, RendererInterface $default_renderer): Template + { + $items = $component->getItems(); + + foreach ($items as $item) { + $tpl->setCurrentBlock("item"); + if ($item instanceof Component) { + $tpl->setVariable("ITEM", $default_renderer->render($item)); + } else { + $tpl->setVariable("ITEM", $item); } + $tpl->parseCurrentBlock(); } - return $tpl->get(); + + return $tpl; } protected function renderProperty( - Component\Listing\Property $component, + Property $component, RendererInterface $default_renderer ): string { $tpl = $this->getTemplate("tpl.propertylisting.html", true, true); - foreach ($component->getItems() as $property) { - list($label, $value, $show_label) = $property; - if (! is_string($value)) { - $value = $default_renderer->render($value); - } - + foreach ($component->getItems() as [$label, $value, $show_label]) { $tpl->setCurrentBlock("property"); - $tpl->setVariable("VALUE", $value); if ($show_label) { - $tpl->setVariable("LABEL", $label); + if ($label instanceof Component) { + $tpl->setVariable('LABEL', $default_renderer->render($label)); + } else { + $tpl->setVariable('LABEL', $this->convertSpecialCharacters($label)); + } + } + if (is_string($value) && self::MAX_CHARS_IN_LINE <= mb_strlen($value)) { + $tpl->setVariable("ID_SHOW_MORE_TOGGLE", $this->createId()); + $tpl->setVariable("MORE", $this->txt("show_more")); + $tpl->setVariable("LESS", $this->txt("show_less")); + $tpl->setVariable("LONG_VALUE", $this->convertSpecialCharacters($value)); + $tpl->parseCurrentBlock(); + } elseif (is_string($value)) { + $tpl->setVariable("SHORT_VALUE", $this->convertSpecialCharacters($value)); + } elseif ($value instanceof Component) { + $tpl->setVariable("SHORT_VALUE", $default_renderer->render($value)); } $tpl->parseCurrentBlock(); } diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php index c55ad835f9cd..151e0b0bbb94 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php @@ -24,303 +24,308 @@ class Factory implements G\Factory { - public function settings(): Glyph + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + + public function settings(): G\Glyph { - return new Glyph(G\Glyph::SETTINGS, "settings"); + return new Glyph(G\Glyph::SETTINGS, $this->language->txt("settings")); } public function collapse(): Glyph { - return new Glyph(G\Glyph::COLLAPSE, "collapse_content"); + return new Glyph(G\Glyph::COLLAPSE, $this->language->txt("collapse_content")); } public function expand(): Glyph { - return new Glyph(G\Glyph::EXPAND, "expand_content"); + return new Glyph(G\Glyph::EXPAND, $this->language->txt("expand_content")); } public function add(): Glyph { - return new Glyph(G\Glyph::ADD, "add"); + return new Glyph(G\Glyph::ADD, $this->language->txt("add")); } public function remove(): Glyph { - return new Glyph(G\Glyph::REMOVE, "remove"); + return new Glyph(G\Glyph::REMOVE, $this->language->txt("remove")); } public function up(): Glyph { - return new Glyph(G\Glyph::UP, "up"); + return new Glyph(G\Glyph::UP, $this->language->txt("up")); } public function down(): Glyph { - return new Glyph(G\Glyph::DOWN, "down"); + return new Glyph(G\Glyph::DOWN, $this->language->txt("down")); } public function back(): Glyph { - return new Glyph(G\Glyph::BACK, "back"); + return new Glyph(G\Glyph::BACK, $this->language->txt("back")); } public function next(): Glyph { - return new Glyph(G\Glyph::NEXT, "next"); + return new Glyph(G\Glyph::NEXT, $this->language->txt("next")); } public function sortAscending(): Glyph { - return new Glyph(G\Glyph::SORT_ASCENDING, "sort_ascending"); + return new Glyph(G\Glyph::SORT_ASCENDING, $this->language->txt("sort_ascending")); } public function briefcase(): Glyph { - return new Glyph(G\Glyph::BRIEFCASE, "briefcase"); + return new Glyph(G\Glyph::BRIEFCASE, $this->language->txt("briefcase")); } public function sortDescending(): Glyph { - return new Glyph(G\Glyph::SORT_DESCENDING, "sort_descending"); + return new Glyph(G\Glyph::SORT_DESCENDING, $this->language->txt("sort_descending")); } public function user(): Glyph { - return new Glyph(G\Glyph::USER, "show_who_is_online"); + return new Glyph(G\Glyph::USER, $this->language->txt("show_who_is_online")); } public function mail(): Glyph { - return new Glyph(G\Glyph::MAIL, "mail"); + return new Glyph(G\Glyph::MAIL, $this->language->txt("mail")); } public function notification(): Glyph { - return new Glyph(G\Glyph::NOTIFICATION, "notifications"); + return new Glyph(G\Glyph::NOTIFICATION, $this->language->txt("notifications")); } public function tag(): Glyph { - return new Glyph(G\Glyph::TAG, "tags"); + return new Glyph(G\Glyph::TAG, $this->language->txt("tags")); } public function note(): Glyph { - return new Glyph(G\Glyph::NOTE, "notes"); + return new Glyph(G\Glyph::NOTE, $this->language->txt("notes")); } public function comment(): Glyph { - return new Glyph(G\Glyph::COMMENT, "comments"); + return new Glyph(G\Glyph::COMMENT, $this->language->txt("comments")); } public function like(): Glyph { - return new Glyph(G\Glyph::LIKE, "like"); + return new Glyph(G\Glyph::LIKE, $this->language->txt("like")); } public function love(): Glyph { - return new Glyph(G\Glyph::LOVE, "love"); + return new Glyph(G\Glyph::LOVE, $this->language->txt("love")); } public function dislike(): Glyph { - return new Glyph(G\Glyph::DISLIKE, "dislike"); + return new Glyph(G\Glyph::DISLIKE, $this->language->txt("dislike")); } public function laugh(): Glyph { - return new Glyph(G\Glyph::LAUGH, "laugh"); + return new Glyph(G\Glyph::LAUGH, $this->language->txt("laugh")); } public function astounded(): Glyph { - return new Glyph(G\Glyph::ASTOUNDED, "astounded"); + return new Glyph(G\Glyph::ASTOUNDED, $this->language->txt("astounded")); } public function sad(): Glyph { - return new Glyph(G\Glyph::SAD, "sad"); + return new Glyph(G\Glyph::SAD, $this->language->txt("sad")); } public function angry(): Glyph { - return new Glyph(G\Glyph::ANGRY, "angry"); + return new Glyph(G\Glyph::ANGRY, $this->language->txt("angry")); } public function eyeopen(): Glyph { - return new Glyph(G\Glyph::EYEOPEN, "eyeopened"); + return new Glyph(G\Glyph::EYEOPEN, $this->language->txt("eyeopened")); } public function eyeclosed(): Glyph { - return new Glyph(G\Glyph::EYECLOSED, "eyeclosed"); + return new Glyph(G\Glyph::EYECLOSED, $this->language->txt("eyeclosed")); } public function attachment(): Glyph { - return new Glyph(G\Glyph::ATTACHMENT, "attachment"); + return new Glyph(G\Glyph::ATTACHMENT, $this->language->txt("attachment")); } public function reset(): Glyph { - return new Glyph(G\Glyph::RESET, "reset"); + return new Glyph(G\Glyph::RESET, $this->language->txt("reset")); } public function apply(): Glyph { - return new Glyph(G\Glyph::APPLY, "apply"); + return new Glyph(G\Glyph::APPLY, $this->language->txt("apply")); } public function search(): Glyph { - return new Glyph(G\Glyph::SEARCH, "search"); + return new Glyph(G\Glyph::SEARCH, $this->language->txt("search")); } public function help(): Glyph { - return new Glyph(G\Glyph::HELP, "help"); + return new Glyph(G\Glyph::HELP, $this->language->txt("help")); } public function calendar(): Glyph { - return new Glyph(G\Glyph::CALENDAR, "calendar"); + return new Glyph(G\Glyph::CALENDAR, $this->language->txt("calendar")); } public function time(): Glyph { - return new Glyph(G\Glyph::TIME, "time"); + return new Glyph(G\Glyph::TIME, $this->language->txt("time")); } public function close(): Glyph { - return new Glyph(G\Glyph::CLOSE, "close"); + return new Glyph(G\Glyph::CLOSE, $this->language->txt("close")); } public function more(): Glyph { - return new Glyph(G\Glyph::MORE, "show_more"); + return new Glyph(G\Glyph::MORE, $this->language->txt("show_more")); } public function disclosure(): Glyph { - return new Glyph(G\Glyph::DISCLOSURE, "disclose"); + return new Glyph(G\Glyph::DISCLOSURE, $this->language->txt("disclose")); } public function language(): Glyph { - return new Glyph(G\Glyph::LANGUAGE, "switch_language"); + return new Glyph(G\Glyph::LANGUAGE, $this->language->txt("switch_language")); } public function login(): Glyph { - return new Glyph(G\Glyph::LOGIN, "log_in"); + return new Glyph(G\Glyph::LOGIN, $this->language->txt("log_in")); } public function logout(): Glyph { - return new Glyph(G\Glyph::LOGOUT, "log_out"); + return new Glyph(G\Glyph::LOGOUT, $this->language->txt("log_out")); } public function bulletlist(): Glyph { - return new Glyph(G\Glyph::BULLETLIST, "bulletlist_action"); + return new Glyph(G\Glyph::BULLETLIST, $this->language->txt("bulletlist_action")); } public function numberedlist(): Glyph { - return new Glyph(G\Glyph::NUMBEREDLIST, "numberedlist_action"); + return new Glyph(G\Glyph::NUMBEREDLIST, $this->language->txt("numberedlist_action")); } public function listindent(): Glyph { - return new Glyph(G\Glyph::LISTINDENT, "listindent"); + return new Glyph(G\Glyph::LISTINDENT, $this->language->txt("listindent")); } public function listoutdent(): Glyph { - return new Glyph(G\Glyph::LISTOUTDENT, "listoutdent"); + return new Glyph(G\Glyph::LISTOUTDENT, $this->language->txt("listoutdent")); } public function filter(): Glyph { - return new Glyph(G\Glyph::FILTER, "filter"); + return new Glyph(G\Glyph::FILTER, $this->language->txt("filter")); } public function collapseHorizontal(): Glyph { - return new Glyph(G\Glyph::COLLAPSE_HORIZONTAL, "collapse/back"); + return new Glyph(G\Glyph::COLLAPSE_HORIZONTAL, $this->language->txt("collapse/back")); } public function header(): Glyph { - return new Glyph(G\Glyph::HEADER, "header_action"); + return new Glyph(G\Glyph::HEADER, $this->language->txt("header_action")); } public function italic(): Glyph { - return new Glyph(G\Glyph::ITALIC, "italic_action"); + return new Glyph(G\Glyph::ITALIC, $this->language->txt("italic_action")); } public function bold(): Glyph { - return new Glyph(G\Glyph::BOLD, "bold_action"); + return new Glyph(G\Glyph::BOLD, $this->language->txt("bold_action")); } public function link(): Glyph { - return new Glyph(G\Glyph::LINK, "link_action"); + return new Glyph(G\Glyph::LINK, $this->language->txt("link_action")); } public function launch(): Glyph { - return new Glyph(G\Glyph::LAUNCH, "launch"); + return new Glyph(G\Glyph::LAUNCH, $this->language->txt("launch")); } public function enlarge(): Glyph { - return new Glyph(G\Glyph::ENLARGE, "enlarge"); + return new Glyph(G\Glyph::ENLARGE, $this->language->txt("enlarge")); } public function listView(): Glyph { - return new Glyph(G\Glyph::LIST_VIEW, "list_view"); + return new Glyph(G\Glyph::LIST_VIEW, $this->language->txt("list_view")); } public function preview(): Glyph { - return new Glyph(G\Glyph::PREVIEW, "preview"); + return new Glyph(G\Glyph::PREVIEW, $this->language->txt("preview")); } public function sort(): Glyph { - return new Glyph(G\Glyph::SORT, "sort"); + return new Glyph(G\Glyph::SORT, $this->language->txt("sort")); } public function columnSelection(): Glyph { - return new Glyph(G\Glyph::COLUMN_SELECTION, "column_selection"); + return new Glyph(G\Glyph::COLUMN_SELECTION, $this->language->txt("column_selection")); } public function tileView(): Glyph { - return new Glyph(G\Glyph::TILE_VIEW, "tile_view"); + return new Glyph(G\Glyph::TILE_VIEW, $this->language->txt("tile_view")); } public function dragHandle(): G\Glyph { - return new Glyph(G\Glyph::DRAG_HANDLE, "drag_handle"); + return new Glyph(G\Glyph::DRAG_HANDLE, $this->language->txt("drag_handle")); } public function checked(): G\Glyph { - return new Glyph(G\Glyph::CHECKED, "checked"); + return new Glyph(G\Glyph::CHECKED, $this->language->txt("checked")); } public function unchecked(): G\Glyph { - return new Glyph(G\Glyph::UNCHECKED, "unchecked"); + return new Glyph(G\Glyph::UNCHECKED, $this->language->txt("unchecked")); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Renderer.php index 709d07a9c835..f8058992dfb7 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Renderer.php @@ -45,8 +45,7 @@ public function render(Component\Component $component, RendererInterface $defaul $label = $component->getLabel(); if ('' !== $label) { $tpl->touchBlock('with_aria_label'); - // @todo: move translation to factory, this breaks custom labels... - $tpl->setVariable("LABEL", $this->txt($label)); + $tpl->setVariable("LABEL", $label); $tpl->touchBlock('with_role'); } else { // glyph must be hidden if there is no label (semantic meaning) diff --git a/components/ILIAS/UI/src/examples/Entity/Standard/base.php b/components/ILIAS/UI/src/examples/Entity/Standard/base.php index 0c4298c3faa2..33b6dd521876 100755 --- a/components/ILIAS/UI/src/examples/Entity/Standard/base.php +++ b/components/ILIAS/UI/src/examples/Entity/Standard/base.php @@ -22,21 +22,23 @@ /** * --- + * description: > + * Entities being used to show a made up event object. * expected output: > * Entities arrange information about e.g. an object into semantic groups; - * this example focusses on the possible contents of those groups and shows + * this example focuses on the possible contents of those groups and shows * a possible representation of a made up event. * From top to bottom, left to right: + * - An icon indents the following. * - There is a precondition; it links to ilias.de. * - An action-dropdown is available with two entries linking to ilias/github. - * - An icon indents the following. - * - Prominently featured is the event's date proptery. * - Only after that, the title of the event is displayed in bold. + * - Prominently featured is the event's date property. * - A progress meter ("in progress") is followed by detailed properties: * - Room information * - Description * - in one line: Available seats and availability of the event - * - in the next line: duration and the information of available redording + * - in the next line: duration and the information of available recording * - The bottom "row" shows two tags on the left * - and two glyphs on the right, the first one with status counter, the second one with * both status- and novelty counter. @@ -69,7 +71,7 @@ function base() $f->button()->shy("ILIAS", "https://www.ilias.de"), $f->button()->shy("GitHub", "https://www.github.com") ]; - $entity = $entity->withActions(...$actions); + $entity = $entity->withManagingActions(...$actions); /* * Logic for Pulling Availabilty Properties to Blocking Conditions diff --git a/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php b/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php index 32e67f7aa118..36ff079ce184 100755 --- a/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php +++ b/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php @@ -22,13 +22,17 @@ /** * --- + * description: > + * The different semantic locations on an entity. * expected output: > * This example shows/identifies the semantic groups of entites; * from top to bottom, left to right, the order of groups is this: - * - blocking conditions (left) and actions in a dropdown (right) * - secondary indentifier (it indents all the latter) and featured properties + * - blocking conditions (left) and actions in a dropdown (right) * - primary identifier + * - featured properties * - personal status + * - a workflow step button * - main details * - availability * - details @@ -43,15 +47,33 @@ function semantic_groups() $entity = $f->entity()->standard('Primary Identifier', 'Secondary Identifier') ->withBlockingAvailabilityConditions($f->legacy()->content('Blocking Conditions')) - ->withFeaturedProperties($f->legacy()->content('Featured_properties')) + ->withFeaturedProperties($f->legacy()->content('Featured Properties')) ->withPersonalStatus($f->legacy()->content('Personal Status')) ->withMainDetails($f->legacy()->content('Main Details')) ->withAvailability($f->legacy()->content('Availability')) ->withDetails($f->legacy()->content('Details')) ->withReactions($f->button()->tag('reaction', '#')) - ->withPrioritizedReactions($f->symbol()->glyph()->like()) - ->withActions($f->button()->shy('action', '#')) + ->withPrioritizedReactions($f->button()->shy("Prioritized Reaction", "#")->withSymbol($f->symbol()->glyph()->like())) + ->withManagingActions($f->button()->shy('managing actions', '#')) ; + // to get buttons, they need to be created from a Workflow + $workflow_factory = $f->listing()->workflow(); + $dummy_step = $workflow_factory->step('', ''); + + // Creating Workflow Steps + $steps = [ + $workflow_factory->step("Workflow Step not longer available", "", "#") + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step("Start available Workflow Step", "", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step("Workflow Step not yet available", "", "#") + ->withAvailability($dummy_step::NOT_AVAILABLE)->withStatus($dummy_step::NOT_AVAILABLE), + ]; + + $video_workflow = $workflow_factory->linear("Workflow", $steps); + + $entity = $entity->withWorkflow($video_workflow); + return $renderer->render($entity); } diff --git a/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php b/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php new file mode 100644 index 000000000000..1c53f7dadc8d --- /dev/null +++ b/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php @@ -0,0 +1,111 @@ + + * Full example showing how the entity could be used to describe a video object with all of its features. + * + * expected output: > + * This example shows a representation of a made up video object. + * - A thumbnail is shown as a secondary identifier that indents all following elements + * - the title as primary identifier + * - a dropdown with managing options + * - upload date and publisher as Featured Property + * - Workflow buttons + * - duration adn a description as main details + * Below is an example workflow that was given to the entity to generate the workflow buttons on this entity. + * Two of the four steps are marked as not completed and available. These two options are rendered as buttons inside + * the entity. + * --- + */ +function video_object() +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + /* + * Basic Construction + */ + + $primary_id = "Mountains through the ages - the formation of giants"; + $secondary_id = $f->image()->responsive("assets/ui-examples/images/Image/mountains.jpg", "Some mountains in the dusk"); + + // creating the entity object now so it can be filled in the logic section + $entity = $f->entity()->standard( + $primary_id, + $secondary_id + ); + + /* + * Priority Areas + */ + + $glyph_calendar = $f->symbol()->glyph()->calendar()->withLabel("Published on"); + $glyph_user = $f->symbol()->glyph()->user()->withLabel("Created by"); + + $featured_properties = $f->listing()->property() + ->withProperty($glyph_calendar, '24.01.2025') + ->withProperty($glyph_user, 'BBC England, Co-Production: ARD/ZDF, Canal Plus') + ; + + $entity = $entity + ->withFeaturedProperties($featured_properties) + ; + + /* + * Dropdown Actions + */ + + $managing_actions = [ + $f->button()->shy("Copy", "https://www.ilias.de"), + $f->button()->shy("Delete", "https://www.github.com") + ]; + $entity = $entity->withManagingActions(...$managing_actions); + + /* + * Generating Action Buttons from Workflow + */ + + $workflow_factory = $f->listing()->workflow(); + $dummy_step = $workflow_factory->step('', ''); + + // Creating Workflow Steps + $steps = [ + $workflow_factory->step("Upload video file", "Upload an .mp4 file or start a recording.", "#") + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step("Cut video", "Trim or remove parts of the video.", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step("Add subtitles", "You must upload or generate subtitles for every video.", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step("Publish", "Set who can see this video.", "#") + ->withAvailability($dummy_step::NOT_AVAILABLE)->withStatus($dummy_step::NOT_AVAILABLE), + ]; + + $video_workflow = $workflow_factory->linear("Video Curation", $steps); + + $entity = $entity->withWorkflow($video_workflow); + + /* + * All Other Semantic Groups + */ + + $glyph_time = $f->symbol()->glyph()->time()->withLabel("Duration"); + + $main_details_01 = $f->listing()->property() + ->withProperty($glyph_time, '45:00') + ; + $main_details_02 = $f->listing()->property() + ->withProperty('Description', "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", false) + ; + + $entity = $entity + ->withMainDetails($main_details_01, $main_details_02) + ; + + return $renderer->render([$entity]); +} diff --git a/components/ILIAS/UI/src/examples/Link/Standard/with_open_in_new_viewport.php b/components/ILIAS/UI/src/examples/Link/Standard/with_open_in_new_viewport.php new file mode 100644 index 000000000000..6a3c1838d241 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Link/Standard/with_open_in_new_viewport.php @@ -0,0 +1,27 @@ + + * Example for rendering a standard link that opens to a new viewport. + * + * expected output: > + * ILIAS shows a link with the title "Goto ILIAS in new tab/window". + * Clicking the link opens the website ilias.ch in a new browser window or tab. + * --- + */ +function with_open_in_new_viewport(): string +{ + global $DIC; + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $link = $factory->link()->standard("Goto ILIAS", "http://ilias.ch"); + $link = $link->withOpenInNewViewport(true); + + return $renderer->render($link); +} diff --git a/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php b/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php new file mode 100644 index 000000000000..ee98483ace68 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php @@ -0,0 +1,128 @@ + + * A card-style grid presenting many entity objects side by side. + * + * expected output: > + * ILIAS shows a grid of entities looking like cards. Each card has a thumbnail image, title and some video related + * properties and actions. The grid reacts flexibly to the available space. If there is a lot of space, the grid + * will have more columns. With little available space, the cards will stack. + * --- + */ +function base(): string +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $record_to_entity = new class () implements RecordToEntity { + public function map(UIFactory $ui_factory, mixed $record): Entity + { + $glyph_user = $ui_factory->symbol()->glyph()->user() + ->withLabel("Created by"); + + $glyph_calendar = $ui_factory->symbol()->glyph()->calendar() + ->withLabel("Upload date"); + + $glyph_duration = $ui_factory->symbol()->glyph()->time() + ->withLabel("Duration"); + + list($title, $thumbnail_url, $creator, $availability, $description, $duration, $add_workflow, $date) = $record; + $managing_actions = [ + $ui_factory->button()->shy("Edit", "#"), + $ui_factory->button()->shy("Move", "#"), + ]; + $entity = $ui_factory->entity()->standard( + $ui_factory->link()->standard($title, ""), + $ui_factory->image()->responsive($thumbnail_url, $title)->withAction("#") + ) + ->withFeaturedProperties( + $ui_factory->listing()->property() + ->withProperty($glyph_user, $creator) + ) + ->withManagingActions(...$managing_actions) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty($glyph_duration, $description, false) + ->withProperty($glyph_duration, $duration) + ->withProperty($glyph_calendar, $date) + ) + ->withPrioritizedReactions($ui_factory->button()->shy("Like", "#")->withSymbol($ui_factory->symbol()->glyph()->like())) + ; + if ($availability) { + $entity = $entity->withBlockingAvailabilityConditions( + $ui_factory->listing()->property() + ->withProperty("Status", $ui_factory->legacy()->content($availability), false) + ); + } + if ($add_workflow) { + $workflow_factory = $ui_factory->listing()->workflow(); + $dummy_step = $workflow_factory->step('', ''); + + $steps = [ + $workflow_factory->step("Upload video file", "Upload an .mp4 file or start a recording.", "#") + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step("Cut video", "Trim or remove parts of the video.", "#") + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step("Add subtitles", "You must upload or generate subtitles for every video.", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step("Publish", "Set who can see this video.", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + ]; + + $video_workflow = $workflow_factory->linear("Video Curation", $steps); + + $entity = $entity->withWorkflow($video_workflow); + } + return $entity; + } + }; + + $glyph_eye_closed = $f->symbol()->glyph()->eyeclosed(); + $glyph_with_text = $renderer->render($glyph_eye_closed) . " offline"; + $card_data = [ + ['Snowboarding for beginners - How to avoid falling on your face', 'assets/ui-examples/images/Image/ski_widescreen-thumbnail.jpg', "Bobby's School of Snowboarding Austria", null, 'This is the perfect start for anyone wanting to get on a snowboard. We talk the best gear and the best locations for a beginner. And no worries - it is not expensive: Renting equipment will work just fine. Then we end with some first exercises to get the stability needed to tackle your first slope', '23 min', false, '01.01.2026'], + ['The History of Bridges', 'assets/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg', 'BBC England', $glyph_with_text,'One of the most monumental achievements of human kind is the invention of bridges. Crossing streets, rivers and sometimes oceans became a huge pillar for our our modern infrastructure. This documentary looks at the different types of bridges and how they have been developed and engineered throughout different cultures and centuries','90 min', true, "01.11.2026"], + ['Mountains through the ages - the formation of giants', 'assets/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg','ARD/ZDF, Canal Plus', null, "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", "45 min", false, "11.10.2026"], + ['Snowboarding for beginners - How to avoid falling on your face', 'assets/ui-examples/images/Image/ski_widescreen-thumbnail.jpg', "Bobby's School of Snowboarding Austria", null, 'This is the perfect start for anyone wanting to get on a snowboard. We talk the best gear and the best locations for a beginner. And no worries - it is not expensive: Renting equipment will work just fine. Then we end with some first exercises to get the stability needed to tackle your first slope', '23 min', false, "28.02.2026"], + ['The History of Bridges', 'assets/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg', 'BBC England', $glyph_with_text,'One of the most monumental achievements of human kind is the invention of bridges. Crossing streets, rivers and sometimes oceans became a huge pillar for our our modern infrastructure. This documentary looks at the different types of bridges and how they have been developed and engineered throughout different cultures and centuries','90 min', true, "15.12.2025"], + ['Mountains through the ages - the formation of giants', 'assets/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg','ARD/ZDF, Canal Plus', null, "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", "45 min", false, "09.06.2026"] + ]; + + $data = new class ($card_data) implements DataRetrieval { + protected array $data = []; + + public function __construct($card_data) + { + $this->data = $card_data; + } + + public function getEntities( + Mapping $mapping, + ?Range $range, + ?array $additional_parameters + ): \Generator { + foreach ($this->data as $vid) { + yield $mapping->map($vid); + } + } + }; + + $listing = $f->listing()->entity()->grid($record_to_entity) + ->withData($data); + + return $renderer->render($listing); +} diff --git a/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php b/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php index 2f2bdac4cdbd..3e445f019dcc 100755 --- a/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php +++ b/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php @@ -29,8 +29,11 @@ /** * --- + * description: > + * A component to list many entities. Has multiple columns on very large screens. * expected output: > - * ILIAS shows the rendered Component. + * ILIAS shows a list of entities. If there is a lot of space available, the list will switch to a layout with two + * columns. * --- */ function base() diff --git a/components/ILIAS/UI/src/examples/Listing/Inline/base.php b/components/ILIAS/UI/src/examples/Listing/Inline/base.php new file mode 100644 index 000000000000..fe9a7012b43c --- /dev/null +++ b/components/ILIAS/UI/src/examples/Listing/Inline/base.php @@ -0,0 +1,30 @@ + + * Example for rendering an inline list. + * + * expected output: > + * ILIAS shows the elements of a list horizontally in a row, separated by commas. + * --- + */ +function base(): string +{ + //Init Factory and Renderer + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + //Generate List + $inline = $f->listing()->inline( + ["Apple","Banana","Milk", "Toast", "Pumpkin Pie", "Bread"] + ); + + //Render + return $renderer->render($inline); +} diff --git a/components/ILIAS/UI/src/examples/Listing/Inline/property_listing.php b/components/ILIAS/UI/src/examples/Listing/Inline/property_listing.php new file mode 100644 index 000000000000..599f81fd0dc5 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Listing/Inline/property_listing.php @@ -0,0 +1,46 @@ + + * Example for rendering an inline list inside a property list. + * + * expected output: > + * ILIAS shows two properties in a single row (if space allows for it). + * One is a "Languages" property followed with flag icons. + * The other property lists video resolutions as text. + * The values are separated by commas. + * --- + */ +function property_listing(): string +{ + //Init Factory and Renderer + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + + $flag_de = $f->symbol()->icon()->custom( + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWRlIiB2aWV3Qm94PSIwIDAgNjQwIDQ4MCI+CiAgPHBhdGggZmlsbD0iI2ZjMCIgZD0iTTAgMzIwaDY0MHYxNjBIMHoiLz4KICA8cGF0aCBmaWxsPSIjMDAwMDAxIiBkPSJNMCAwaDY0MHYxNjBIMHoiLz4KICA8cGF0aCBmaWxsPSJyZWQiIGQ9Ik0wIDE2MGg2NDB2MTYwSDB6Ii8+Cjwvc3ZnPgo=", + "German" + ); + + $flag_gb = $f->symbol()->icon()->custom( + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJmbGFnLWljb25zLWdiIiB2aWV3Qm94PSIwIDAgNjQwIDQ4MCI+CiAgPHBhdGggZmlsbD0iIzAxMjE2OSIgZD0iTTAgMGg2NDB2NDgwSDB6Ii8+CiAgPHBhdGggZmlsbD0iI0ZGRiIgZD0ibTc1IDAgMjQ0IDE4MUw1NjIgMGg3OHY2Mkw0MDAgMjQxbDI0MCAxNzh2NjFoLTgwTDMyMCAzMDEgODEgNDgwSDB2LTYwbDIzOS0xNzhMMCA2NFYweiIvPgogIDxwYXRoIGZpbGw9IiNDODEwMkUiIGQ9Im00MjQgMjgxIDIxNiAxNTl2NDBMMzY5IDI4MXptLTE4NCAyMCA2IDM1TDU0IDQ4MEgwek02NDAgMHYzTDM5MSAxOTFsMi00NEw1OTAgMHpNMCAwbDIzOSAxNzZoLTYwTDAgNDJ6Ii8+CiAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTI0MSAwdjQ4MGgxNjBWMHpNMCAxNjB2MTYwaDY0MFYxNjB6Ii8+CiAgPHBhdGggZmlsbD0iI0M4MTAyRSIgZD0iTTAgMTkzdjk2aDY0MHYtOTZ6TTI3MyAwdjQ4MGg5NlYweiIvPgo8L3N2Zz4K", + "English" + ); + + $languages = $f->listing()->inline([$flag_de, $flag_gb]); + $resolutions = $f->listing()->inline(["480p", "720p", "1080p", "4k"]); + + $video_properties = $f->listing()->property() + ->withProperty("Languages", $languages) + ->withProperty("Resolutions", $resolutions); + + //Render + return $renderer->render($video_properties); +} diff --git a/components/ILIAS/UI/src/examples/Listing/Property/base.php b/components/ILIAS/UI/src/examples/Listing/Property/base.php index ccb0618cc478..f296ad64ba2d 100755 --- a/components/ILIAS/UI/src/examples/Listing/Property/base.php +++ b/components/ILIAS/UI/src/examples/Listing/Property/base.php @@ -22,8 +22,18 @@ /** * --- + * description: > + * Example of differently used properties. + * * expected output: > - * ILIAS shows the rendered Component. + * ILIAS shows the rendered Component. The following options are showcased at least once: + * - Key is a text string, value is a text string + * - Key is not shown, value is a Learning Progress status image followed by text + * - Key is a Glyph, value is a (date) text string + * - Key is a text string, value is a long text with a show more/less toggle + * - Key is a text string, value is a clickable link + * - Key is a text string, value is a Glyph + * * --- */ function base() @@ -32,26 +42,40 @@ function base() $f = $DIC->ui()->factory(); $renderer = $DIC->ui()->renderer(); + $some_legacy_code = $f->legacy()->content( + $renderer->render( + $f->symbol()->icon()->custom('./assets/images/learning_progress/in_progress.svg', 'incomplete'), + ) . ' in progress' + ); + + $glyph_calendar = $f->symbol()->glyph()->calendar()->withLabel("date of upload"); + $props = $f->listing()->property() ->withProperty('Title', 'Some Title') ->withProperty('number', '7') ->withProperty( 'status', - $renderer->render( - $f->symbol()->icon()->custom('./assets/images/learning_progress/in_progress.svg', 'incomplete'), - ) . ' in progress', + $some_legacy_code, false - ); + ) + ->withProperty($glyph_calendar, "21.03.2026", false); + + $props2 = $f->listing()->property() + ->withProperty("Description", "Heads up, this is a very long description. It is always a challenge: You have more to say, but there is so little space. And we still want this text to be shown with all the other properties. For this case we have the automatic text collapsing feature. This way we get the best of both worlds: The text doesn't expand beyond one line, but you can see the rest if you need to. A good use case is on the entity. As the entity might be used to show course entities with lengthy descriptions. Those will take up less space initially. Isn't that sweet? And the crazy thing is: It does not need JavaScript. It's pure HTML and CSS only. Isn't that nice?", false); + + $yes_checkmark = $f->symbol()->glyph()->apply()->withLabel("yes, approved"); - $props2 = $props->withItems([ + $props3 = $props->withItems([ ['a', "1"], ['y', "25", false], - ['link', $f->link()->standard('Goto ILIAS', 'http://www.ilias.de')] + ['link', $f->link()->standard('Goto ILIAS', 'http://www.ilias.de')], + ['approved', $yes_checkmark], ]); return $renderer->render([ $props, + $props2, $f->divider()->horizontal(), - $props2 + $props3 ]); } diff --git a/components/ILIAS/UI/src/templates/default/Entity/tpl.entity.html b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity.html index 42c1276d76d1..7dc8de0067b1 100755 --- a/components/ILIAS/UI/src/templates/default/Entity/tpl.entity.html +++ b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity.html @@ -1,46 +1,76 @@ -

diff --git a/components/ILIAS/UI/tests/Component/Card/RepositoryObjectTest.php b/components/ILIAS/UI/tests/Component/Card/RepositoryObjectTest.php index 6e1a50f6b90f..617cb15b2dfe 100755 --- a/components/ILIAS/UI/tests/Component/Card/RepositoryObjectTest.php +++ b/components/ILIAS/UI/tests/Component/Card/RepositoryObjectTest.php @@ -30,6 +30,8 @@ */ class RepositoryObjectTest extends ILIAS_UI_TestBase { + use LanguageStubs; + /** * @return NoUIFactory */ @@ -38,6 +40,7 @@ public function getFactory() $mocks = [ 'button' => $this->createMock(I\Component\Button\Factory::class), 'divider' => $this->createMock(I\Component\Divider\Factory::class), + 'language' => $this->createRelayArgumentLanguageStub(), ]; $factory = new class ($mocks) extends NoUIFactory { public function __construct( @@ -60,7 +63,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->mocks['language']), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Counter/CounterClientHtmlTest.php b/components/ILIAS/UI/tests/Component/Counter/CounterClientHtmlTest.php index 4d60efa503e0..c0cf079f25b2 100755 --- a/components/ILIAS/UI/tests/Component/Counter/CounterClientHtmlTest.php +++ b/components/ILIAS/UI/tests/Component/Counter/CounterClientHtmlTest.php @@ -27,9 +27,11 @@ */ class CounterClientHtmlTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getGlyphFactory(): \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory { - return new \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory(); + return new \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()); } public function getCounterFactory(): \ILIAS\UI\Implementation\Component\Counter\Factory diff --git a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php index 7d217a620c9b..a195d16a4102 100755 --- a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php +++ b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php @@ -23,11 +23,9 @@ use ILIAS\UI\Implementation\Component\Button; use ILIAS\UI\Implementation\Component\Link; use ILIAS\UI\Implementation\Component\Image; -use ILIAS\UI\Implementation\Component\Dropdown; use ILIAS\UI\Implementation\Component\Legacy\Content; use ILIAS\UI\Implementation\Component\SignalGenerator; -use ILIAS\UI\Component as I; -use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Implementation\Component as I; class EntityTest extends ILIAS_UI_TestBase { @@ -91,11 +89,11 @@ public function testEntityActionProperties(): void $entity = $this->getEntityFactory()->standard('primary', 'secondary') ->withPrioritizedReactions($glyph, $tag) ->withReactions($glyph, $glyph, $glyph) - ->withActions($shy); + ->withManagingActions($shy); $this->assertEquals([$glyph, $tag], $entity->getPrioritizedReactions()); $this->assertEquals([$glyph,$glyph,$glyph], $entity->getReactions()); - $this->assertEquals([$shy], $entity->getActions()); + $this->assertEquals([$shy], $entity->getManagingActions()); } public function testEntityComponentProperties(): void @@ -106,20 +104,70 @@ public function testEntityComponentProperties(): void $entity = $this->getEntityFactory()->standard('primary', 'secondary') ->withPrioritizedReactions($glyph, $tag) ->withReactions($glyph) - ->withActions($shy); + ->withManagingActions($shy); $this->assertEquals([$glyph, $tag], $entity->getPrioritizedReactions()); $this->assertEquals([$glyph], $entity->getReactions()); - $this->assertEquals([$shy], $entity->getActions()); + $this->assertEquals([$shy], $entity->getManagingActions()); } + public function testEntityWorkflowButtons(): void + { + $workflow_factory = $this->getUIFactory()->listing()->workflow(); + $dummy_step = $workflow_factory->step('', ''); + + // Creating Workflow Steps + $steps = [ + $workflow_factory->step("Upload video file", "Upload an .mp4 file or start a recording.", "#") + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step("Cut video", "Trim or remove parts of the video.", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step("Add subtitles", "You must upload or generate subtitles for every video.", "#") + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step("Publish", "Set who can see this video.", "#") + ->withAvailability($dummy_step::NOT_AVAILABLE)->withStatus($dummy_step::NOT_AVAILABLE), + ]; + + $video_workflow = $workflow_factory->linear("Video Curation", $steps); + + $entity = $this->getEntityFactory()->standard('primary', 'secondary') + ->withWorkflow($video_workflow); + + $rendered_entity = $this->getDefaultRenderer()->render($entity); + + $x = 1; + + $this->assertStringContainsString("Cut video", $rendered_entity); + $this->assertStringContainsString("Add subtitles", $rendered_entity); + $this->assertStringNotContainsString("Upload video file", $rendered_entity); + $this->assertStringNotContainsString("Publish", $rendered_entity); + } public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { - public function dropdown(): Dropdown\Factory + return new class ( + $this->createMock(I\Listing\CharacteristicValue\Factory::class), + ) extends NoUIFactory { + public function __construct( + protected I\Listing\CharacteristicValue\Factory $characteristic_value_factory, + ) { + } + + public function dropdown(): I\Dropdown\Factory + { + return new I\Dropdown\Factory(); + } + public function button(): I\Button\Factory + { + return new I\Button\Factory(); + } + public function listing(): I\Listing\Factory { - return new Dropdown\Factory(); + return new I\Listing\Factory( + new I\Listing\Workflow\Factory, + $this->characteristic_value_factory, + new I\Listing\Entity\Factory(), + ); } }; } @@ -131,7 +179,7 @@ public function testEntityRendering(): void $entity = $this->getEntityFactory()->standard('primary', 'secondary') ->withPrioritizedReactions($glyph, $tag) ->withReactions($glyph, $glyph) - ->withActions($shy, $shy) + ->withManagingActions($shy, $shy) ->withBlockingAvailabilityConditions($this->legacy('bc')) ->withFeaturedProperties($this->legacy('fp')) ->withMainDetails($this->legacy('md')) @@ -142,34 +190,48 @@ public function testEntityRendering(): void $r = $this->getDefaultRenderer(); $html = $this->brutallyTrimHTML($r->render($entity)); $expected = $this->brutallyTrimHTML(' -
-
bc
-
- -
-
secondary
-
primary
- -
ps
-
md
-
a
-
d
-
- - -
- -
- '); +
+ +
secondary
+ +
ps
+
md
+
a
+
d
+
+
+
+
+
+
+
+
+ +
+
+
+
+'); $this->assertEquals($expected, $html); } } diff --git a/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php b/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php index bf6d3104cd73..44e462c0b6fa 100644 --- a/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Container/Filter/FilterInputTest.php @@ -71,6 +71,8 @@ public function legacy(): I\Legacy\Factory class FilterInputTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected function buildFactory(): I\Input\Container\Filter\Factory { return new I\Input\Container\Filter\Factory( @@ -97,7 +99,7 @@ protected function buildSymbolFactory(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php b/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php index 6c9a8ec30382..14b74c4ed56d 100755 --- a/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php @@ -79,6 +79,8 @@ public function listing(): I\Listing\Factory class StandardFilterTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected function buildFactory(): I\Input\Container\Filter\Factory { return new I\Input\Container\Filter\Factory( @@ -110,7 +112,7 @@ protected function buildSymbolFactory(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Input/Field/DateTimeInputTest.php b/components/ILIAS/UI/tests/Component/Input/Field/DateTimeInputTest.php index 9fe4e6354672..fc11e8e0f66d 100755 --- a/components/ILIAS/UI/tests/Component/Input/Field/DateTimeInputTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Field/DateTimeInputTest.php @@ -32,6 +32,7 @@ class DateTimeInputTest extends ILIAS_UI_TestBase { use CommonFieldRendering; + use LanguageStubs; protected DefNamesource $name_source; protected Data\Factory $data_factory; @@ -46,12 +47,16 @@ public function setUp(): void public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } public function symbol(): I\Symbol\Factory { return new S\Factory( new S\Icon\Factory(), - new S\Glyph\Factory(), + new S\Glyph\Factory($this->language), new S\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Input/Field/DurationInputTest.php b/components/ILIAS/UI/tests/Component/Input/Field/DurationInputTest.php index a7f3314d2b30..045adccfa337 100755 --- a/components/ILIAS/UI/tests/Component/Input/Field/DurationInputTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Field/DurationInputTest.php @@ -33,6 +33,7 @@ class DurationInputTest extends ILIAS_UI_TestBase { use CommonFieldRendering; + use LanguageStubs; protected DefNamesource $name_source; protected Data\Factory $data_factory; @@ -74,12 +75,17 @@ protected function buildFactory(): I\Input\Field\Factory public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected ILIAS\Language\Language $language, + ) { + } + public function symbol(): I\Symbol\Factory { return new S\Factory( new S\Icon\Factory(), - new S\Glyph\Factory(), + new S\Glyph\Factory($this->language), new S\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Input/Field/FileInputTest.php b/components/ILIAS/UI/tests/Component/Input/Field/FileInputTest.php index 114fb092b3f3..86b908d65ac0 100755 --- a/components/ILIAS/UI/tests/Component/Input/Field/FileInputTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Field/FileInputTest.php @@ -57,6 +57,7 @@ public function symbol(): SymbolFactory class FileInputTest extends ILIAS_UI_TestBase { use CommonFieldRendering; + use LanguageStubs; protected DefNamesource $name_source; @@ -422,7 +423,7 @@ protected function buildSymbolFactory(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Input/Field/HasOptionFilterTestHelper.php b/components/ILIAS/UI/tests/Component/Input/Field/HasOptionFilterTestHelper.php index f479ee93a627..b155f944092d 100644 --- a/components/ILIAS/UI/tests/Component/Input/Field/HasOptionFilterTestHelper.php +++ b/components/ILIAS/UI/tests/Component/Input/Field/HasOptionFilterTestHelper.php @@ -23,14 +23,21 @@ trait HasOptionFilterTestHelper { + use LanguageStubs; + public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function symbol(): I\Symbol\Factory { return new S\Factory( new S\Icon\Factory(), - new S\Glyph\Factory(), + new S\Glyph\Factory($this->language), new S\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Input/ViewControl/ViewControlTestBase.php b/components/ILIAS/UI/tests/Component/Input/ViewControl/ViewControlTestBase.php index 6ca1688c7ce0..d7c1706416e1 100644 --- a/components/ILIAS/UI/tests/Component/Input/ViewControl/ViewControlTestBase.php +++ b/components/ILIAS/UI/tests/Component/Input/ViewControl/ViewControlTestBase.php @@ -29,6 +29,8 @@ abstract class ViewControlTestBase extends ILIAS_UI_TestBase { + use LanguageStubs; + protected function getNamesource() { return new class () implements NameSource { @@ -81,10 +83,11 @@ protected function buildVCFactory(): Control\Factory public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { protected SignalGenerator $sig_gen; - public function __construct() - { + public function __construct( + protected ILIAS\Language\Language $language, + ) { $this->sig_gen = new SignalGenerator(); } public function button(): I\Button\Factory @@ -95,7 +98,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Item/ItemNotificationClientHtmlTest.php b/components/ILIAS/UI/tests/Component/Item/ItemNotificationClientHtmlTest.php index 0ae2efbcecf3..3b94b6dd1adf 100755 --- a/components/ILIAS/UI/tests/Component/Item/ItemNotificationClientHtmlTest.php +++ b/components/ILIAS/UI/tests/Component/Item/ItemNotificationClientHtmlTest.php @@ -28,6 +28,8 @@ */ class ItemNotificationClientHtmlTest extends ILIAS_UI_TestBase { + use LanguageStubs; + /** * @var I\SignalGenerator */ @@ -42,9 +44,14 @@ public function setUp(): void public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\SignalGenerator $sig_gen; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function counter(): I\Counter\Factory { return new I\Counter\Factory(); @@ -57,7 +64,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Item/ItemNotificationTest.php b/components/ILIAS/UI/tests/Component/Item/ItemNotificationTest.php index 500218306080..15f5acb0024f 100755 --- a/components/ILIAS/UI/tests/Component/Item/ItemNotificationTest.php +++ b/components/ILIAS/UI/tests/Component/Item/ItemNotificationTest.php @@ -30,6 +30,8 @@ */ class ItemNotificationTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected I\Component\SignalGenerator $sig_gen; public function setUp(): void @@ -44,9 +46,14 @@ public function getIcon(): C\Symbol\Icon\Standard public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\Component\SignalGenerator $sig_gen; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function item(): I\Component\Item\Factory { return new I\Component\Item\Factory(); @@ -66,7 +73,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->language), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Launcher/LauncherInlineTest.php b/components/ILIAS/UI/tests/Component/Launcher/LauncherInlineTest.php index 12f160819a96..6d2e89359f56 100755 --- a/components/ILIAS/UI/tests/Component/Launcher/LauncherInlineTest.php +++ b/components/ILIAS/UI/tests/Component/Launcher/LauncherInlineTest.php @@ -28,6 +28,8 @@ class LauncherInlineTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected ILIAS\Data\Factory $df; protected ILIAS\Language\Language $language; @@ -65,10 +67,15 @@ protected function getIconFactory(): I\Symbol\Icon\Factory public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\SignalGenerator $sig_gen; public I\Input\Field\Factory $input_factory; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function button(): I\Button\Factory { return new I\Button\Factory(); @@ -77,7 +84,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php b/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php new file mode 100644 index 000000000000..35b5a85a26fb --- /dev/null +++ b/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php @@ -0,0 +1,177 @@ +entity()->standard('primary', 'secondary'); + } + }; + } + public function getUIFactory(): NoUIFactory + { + return new class ( + $this->createMock(I\Listing\Workflow\Factory::class), + $this->createMock(I\Listing\CharacteristicValue\Factory::class), + ) extends NoUIFactory { + public function __construct( + protected I\Listing\Workflow\Factory $workflow_factory, + protected I\Listing\CharacteristicValue\Factory $characteristic_value_factory, + ) { + } + public function listing(): I\Listing\Factory + { + return new I\Listing\Factory( + $this->workflow_factory, + $this->characteristic_value_factory, + new I\Listing\Entity\Factory(), + ); + } + public function entity(): I\Entity\Factory + { + return new Entity\Factory(); + } + }; + } + + public function testGridEntityListingFactory(): void + { + $this->assertInstanceOf( + C\Listing\Entity\EntityListing::class, + $this->getUIFactory()->listing()->entity()->grid($this->getEntityMapping()) + ); + } + + public function testGridEntityListingRendering(): void + { + $data = new class () implements C\Listing\Entity\DataRetrieval { + protected $data = [1,2,3]; + + public function getEntities( + C\Listing\Entity\Mapping $mapping, + ?Range $range, + ?array $additional_parameters + ): \Generator { + foreach ($this->data as $entry) { + yield $mapping->map($entry); + } + } + }; + + $listing = $this->getUIFactory()->listing()->entity() + ->grid($this->getEntityMapping()) + ->withData($data); + + $render = $this->getDefaultRenderer()->render($listing); + $expected = << +
  • +
    + +
    secondary
    +
    +
  • +
  • +
    + +
    secondary
    +
    +
  • +
  • +
    + +
    secondary
    +
    +
  • + + HTML; + + $this->assertEquals( + $this->brutallyTrimHTML($expected), + $this->brutallyTrimHTML($render) + ); + } + + public function testGridEntityListingYieldingEntities(): void + { + $data = new class () implements C\Listing\Entity\DataRetrieval { + protected $data = [1,2,3]; + + public function getEntities( + C\Listing\Entity\Mapping $mapping, + ?Range $range, + ?array $additional_parameters + ): \Generator { + foreach ($this->data as $entry) { + yield $mapping->map($entry); + } + } + }; + + $listing = $this->getUIFactory()->listing()->entity() + ->grid($this->getEntityMapping()) + ->withData($data); + + $entities = iterator_to_array($listing->getEntities($this->getUIFactory())); + + $this->assertCount(3, $entities); + + $this->assertInstanceOf(C\Entity\Entity::class, array_pop($entities)); + } +} diff --git a/components/ILIAS/UI/tests/Component/Listing/ListingTest.php b/components/ILIAS/UI/tests/Component/Listing/ListingTest.php index 09104c523037..568bfeaee976 100755 --- a/components/ILIAS/UI/tests/Component/Listing/ListingTest.php +++ b/components/ILIAS/UI/tests/Component/Listing/ListingTest.php @@ -51,6 +51,10 @@ public function testImplementsFactoryInterface(): void "ILIAS\\UI\\Component\\Listing\\Unordered", $f->unordered(array("1")) ); + $this->assertInstanceOf( + "ILIAS\\UI\\Component\\Listing\\Inline", + $f->inline(array("1")) + ); $this->assertInstanceOf( "ILIAS\\UI\\Component\\Listing\\Descriptive", $f->descriptive(array("k1" => "c1")) @@ -87,7 +91,14 @@ public function testUnorderedGetItems(): void $items = array("1","2"); $this->assertEquals($l->getItems(), $items); } + public function testInlineGetItems(): void + { + $f = $this->getListingFactory(); + $l = $f->inline(array("1","2")); + $items = array("1","2"); + $this->assertEquals($l->getItems(), $items); + } public function testDescriptiveGetItems(): void { $f = $this->getListingFactory(); @@ -114,6 +125,14 @@ public function testUnorderedWithItems(): void $items = array("1","2"); $this->assertEquals($l->getItems(), $items); } + public function testInlineWithItems(): void + { + $f = $this->getListingFactory(); + $l = $f->inline(array())->withItems(array("1","2")); + + $items = array("1","2"); + $this->assertEquals($l->getItems(), $items); + } public function testDescriptiveWithItems(): void { @@ -177,6 +196,21 @@ public function testRenderUnorderedListing(): void $this->assertEquals($expected, $html); } + public function testRenderInlineListing(): void + { + $f = $this->getListingFactory(); + $r = $this->getDefaultRenderer(); + $l = $f->inline(array("1","2")); + + $html = $this->brutallyTrimHTML($r->render($l)); + + $expected = <<< 'HTML' +
    • 1
    • 2
    + HTML; + $expected = $this->brutallyTrimHTML($expected); + + $this->assertEquals($expected, $html); + } public function testRenderDescriptiveListing(): void { diff --git a/components/ILIAS/UI/tests/Component/Listing/Property/PropertyListingTest.php b/components/ILIAS/UI/tests/Component/Listing/Property/PropertyListingTest.php index 92b100b52324..fe3946417233 100755 --- a/components/ILIAS/UI/tests/Component/Listing/Property/PropertyListingTest.php +++ b/components/ILIAS/UI/tests/Component/Listing/Property/PropertyListingTest.php @@ -19,10 +19,13 @@ declare(strict_types=1); use ILIAS\UI\Implementation\Component\Listing; +use ILIAS\UI\Implementation\Component\Symbol\Glyph; use ILIAS\UI\Component as I; class PropertyListingTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected function getListingFactory(): Listing\Factory { return new Listing\Factory( @@ -32,6 +35,11 @@ protected function getListingFactory(): Listing\Factory ); } + protected function getGlyphFactory(): Glyph\Factory + { + return new Glyph\Factory($this->createRelayArgumentLanguageStub()); + } + public function testPropertyListingConstruction(): void { $pl = $this->getListingFactory()->property(); @@ -49,7 +57,9 @@ public function testPropertyListingWithProperty(): void ->withProperty(...$props[0]) ->withProperty(...$props[1]); - $this->assertEquals($props, $pl->getItems()); + $created_items = $pl->getItems(); + + $this->assertEquals($props, $created_items); } public function testPropertyListingWithItems(): void @@ -65,6 +75,19 @@ public function testPropertyListingWithItems(): void $this->assertEquals($props, $pl->getItems()); } + public function testPropertyListingWithSymbols(): void + { + $symbol = $this->getGlyphFactory()->user(); + $props = [ + [$symbol, 'value1', true], + ['label2', $symbol, false], + ]; + $pl = $this->getListingFactory()->property(); + + $pl = $pl->withItems($props); + $this->assertEquals($props, $pl->getItems()); + } + public function testPropertyListingRendering(): void { $props = [ @@ -74,21 +97,81 @@ public function testPropertyListingRendering(): void $pl = $this->getListingFactory()->property() ->withItems($props); - $expected = $this->brutallyTrimHTML(' -
    -
    - label1 - value1 -
    -
    - value2 -
    -
    - '); + $expected = $this->brutallyTrimHTML(<< +
    +
    label1
    +
    value1
    +
    +
    +
    value2
    +
    +
    + HTML); + + $this->assertEquals( + $expected, + $this->brutallyTrimHTML($this->getDefaultRenderer()->render($pl)) + ); + } + + public function testPropertyListingLongValue(): void + { + $props = [ + ['label1', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.', true], + ['label2', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.', false] + ]; + $pl = $this->getListingFactory()->property() + ->withItems($props); + + $expected = $this->brutallyTrimHTML(<< +
    +
    label1
    + +
    Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
    +
    +
    + +
    Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
    +
    + + HTML); + + $this->assertEquals( + $this->brutallyTrimHTML($expected), + $this->brutallyTrimHTML($this->getDefaultRenderer()->render($pl)) + ); + } + + public function testPropertyListingSymbolsRendering(): void + { + $symbol = $this->getGlyphFactory()->user(); + $props = [ + [$symbol, 'value1'], + ['label2', $symbol], + ]; + + $pl = $this->getListingFactory()->property() + ->withItems($props); + + $expected = $this->brutallyTrimHTML(<< +
    +
    +
    value1
    +
    +
    +
    label2
    +
    +
    + + HTML); $this->assertEquals( $expected, $this->brutallyTrimHTML($this->getDefaultRenderer()->render($pl)) ); } + } diff --git a/components/ILIAS/UI/tests/Component/MainControls/MainBarTest.php b/components/ILIAS/UI/tests/Component/MainControls/MainBarTest.php index f5f5e98ce2a4..86b2c0d07930 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/MainBarTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/MainBarTest.php @@ -33,6 +33,8 @@ */ class MainBarTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected I\Button\Factory $button_factory; protected I\Link\Factory $link_factory; protected I\Symbol\Icon\Factory $icon_factory; @@ -51,7 +53,7 @@ public function setUp(): void $counter_factory, new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()), new I\Symbol\Avatar\Factory() ) ); @@ -181,9 +183,14 @@ public function testSignalsPresent(): void public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\Button\Factory $button_factory; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function button(): I\Button\Factory { return $this->button_factory; @@ -191,7 +198,7 @@ public function button(): I\Button\Factory public function symbol(): I\Symbol\Factory { $f_icon = new I\Symbol\Icon\Factory(); - $f_glyph = new I\Symbol\Glyph\Factory(); + $f_glyph = new I\Symbol\Glyph\Factory($this->language); $f_avatar = new I\Symbol\Avatar\Factory(); return new I\Symbol\Factory($f_icon, $f_glyph, $f_avatar); @@ -202,7 +209,7 @@ public function mainControls(): I\MainControls\Factory $counter_factory = new I\Counter\Factory(); $symbol_factory = new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); $slate_factory = new I\MainControls\Slate\Factory($sig_gen, $counter_factory, $symbol_factory); diff --git a/components/ILIAS/UI/tests/Component/MainControls/MetaBarTest.php b/components/ILIAS/UI/tests/Component/MainControls/MetaBarTest.php index d25f31a0bda0..2d94b49dec33 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/MetaBarTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/MetaBarTest.php @@ -32,6 +32,8 @@ */ class MetaBarTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected I\Component\Button\Factory $button_factory; protected I\Component\Symbol\Icon\Factory $icon_factory; protected I\Component\Counter\Factory $counter_factory; @@ -50,7 +52,7 @@ public function setUp(): void $this->counter_factory, new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()), new I\Component\Symbol\Avatar\Factory() ) ); @@ -110,11 +112,16 @@ public function testSignalsPresent(): void public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\Component\Button\Factory $button_factory; public I\Component\MainControls\Factory $mc_factory; public I\Component\Counter\Factory $counter_factory; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function button(): I\Component\Button\Factory { return $this->button_factory; @@ -127,7 +134,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->language), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/MainControls/ModeInfoTest.php b/components/ILIAS/UI/tests/Component/MainControls/ModeInfoTest.php index 331bc17f41bb..ba338fd76a0a 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/ModeInfoTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/ModeInfoTest.php @@ -33,6 +33,8 @@ */ class ModeInfoTest extends ILIAS_UI_TestBase { + use LanguageStubs; + private SignalGenerator $sig_gen; @@ -89,11 +91,12 @@ public function testData(): void public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public SignalGenerator $sig_gen; - public function __construct() - { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { $this->sig_gen = new SignalGenerator(); } @@ -101,7 +104,7 @@ public function symbol(): ILIAS\UI\Implementation\Component\Symbol\Factory { return new Factory( new \ILIAS\UI\Implementation\Component\Symbol\Icon\Factory(), - new \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory(), + new \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory($this->language), new \ILIAS\UI\Implementation\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/MainControls/Slate/DrilldownSlateTest.php b/components/ILIAS/UI/tests/Component/MainControls/Slate/DrilldownSlateTest.php index f6107644b1fc..4b50ae762796 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/Slate/DrilldownSlateTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/Slate/DrilldownSlateTest.php @@ -30,9 +30,16 @@ */ class DrilldownSlateTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + protected function getSigGen() { return new I\SignalGenerator(); @@ -49,7 +56,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/MainControls/Slate/NotificationSlateTest.php b/components/ILIAS/UI/tests/Component/MainControls/Slate/NotificationSlateTest.php index 572789878a74..c656bfcf48f0 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/Slate/NotificationSlateTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/Slate/NotificationSlateTest.php @@ -30,6 +30,8 @@ */ class NotificationSlateTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected I\SignalGenerator $sig_gen; public function setUp(): void @@ -44,9 +46,14 @@ public function getIcon(): C\Symbol\Icon\Standard public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\SignalGenerator $sig_gen; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function button(): I\Button\Factory { return new I\Button\Factory(); @@ -55,7 +62,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/MainControls/SystemInfoTest.php b/components/ILIAS/UI/tests/Component/MainControls/SystemInfoTest.php index ad71d15a0c33..66f30cffdada 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/SystemInfoTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/SystemInfoTest.php @@ -34,6 +34,8 @@ */ class SystemInfoTest extends ILIAS_UI_TestBase { + use LanguageStubs; + private SignalGenerator $sig_gen; public function setUp(): void @@ -233,11 +235,12 @@ public function getOnLoadCodeAsync(): string public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public SignalGenerator $sig_gen; - public function __construct() - { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { $this->sig_gen = new SignalGenerator(); } @@ -245,7 +248,7 @@ public function symbol(): ILIAS\UI\Implementation\Component\Symbol\Factory { return new Factory( new \ILIAS\UI\Implementation\Component\Symbol\Icon\Factory(), - new \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory(), + new \ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory($this->language), new \ILIAS\UI\Implementation\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Menu/Drilldown/DrilldownTest.php b/components/ILIAS/UI/tests/Component/Menu/Drilldown/DrilldownTest.php index df6486679c94..3d656d43dfb6 100755 --- a/components/ILIAS/UI/tests/Component/Menu/Drilldown/DrilldownTest.php +++ b/components/ILIAS/UI/tests/Component/Menu/Drilldown/DrilldownTest.php @@ -30,6 +30,8 @@ */ class DrilldownTest extends ILIAS_UI_TestBase { + use LanguageStubs; + protected C\Symbol\Icon\Standard $icon; protected C\Symbol\Glyph\Glyph $glyph; protected C\Button\Standard $button; @@ -38,7 +40,12 @@ class DrilldownTest extends ILIAS_UI_TestBase public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function menu(): I\Menu\Factory { return new Menu\Factory( @@ -57,7 +64,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } @@ -67,7 +74,7 @@ public function symbol(): I\Symbol\Factory public function setUp(): void { $icon_factory = new I\Symbol\Icon\Factory(); - $glyph_factory = new I\Symbol\Glyph\Factory(); + $glyph_factory = new I\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()); $button_factory = new I\Button\Factory(); $divider_factory = new I\Divider\Factory(); $this->icon = $icon_factory->standard('', ''); diff --git a/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryLegacyTest.php b/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryLegacyTest.php index 84c686aaaceb..ee52cfe9cbab 100755 --- a/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryLegacyTest.php +++ b/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryLegacyTest.php @@ -30,9 +30,16 @@ */ class PanelSecondaryLegacyTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function legacyPanel(string $title, C\Legacy\Content $content): I\Component\Panel\Secondary\Legacy { return new I\Component\Panel\Secondary\Legacy($title, $content); @@ -62,7 +69,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->language), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryListingTest.php b/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryListingTest.php index b154afe6d719..e7ca1081cc9f 100755 --- a/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryListingTest.php +++ b/components/ILIAS/UI/tests/Component/Panel/PanelSecondaryListingTest.php @@ -30,9 +30,16 @@ */ class PanelSecondaryListingTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function panelSecondary(): I\Component\Panel\Secondary\Factory { return new I\Component\Panel\Secondary\Factory(); @@ -57,7 +64,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->language), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Panel/PanelTest.php b/components/ILIAS/UI/tests/Component/Panel/PanelTest.php index 5db89e84d860..8b45f321a4a6 100755 --- a/components/ILIAS/UI/tests/Component/Panel/PanelTest.php +++ b/components/ILIAS/UI/tests/Component/Panel/PanelTest.php @@ -46,9 +46,16 @@ public function getCanonicalName(): string */ class PanelTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function panelSecondary(): I\Component\Panel\Secondary\Factory { return new I\Component\Panel\Secondary\Factory(); @@ -69,7 +76,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->language), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php b/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php index 499be2068fc2..e99081b51963 100755 --- a/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php +++ b/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php @@ -35,9 +35,11 @@ */ class GlyphTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getGlyphFactory(): G\Factory { - return new I\Symbol\Glyph\Factory(); + return new I\Symbol\Glyph\Factory($this->createRelayArgumentLanguageStub()); } public function getCounterFactory(): C\Factory diff --git a/components/ILIAS/UI/tests/Component/Table/PresentationTest.php b/components/ILIAS/UI/tests/Component/Table/PresentationTest.php index a565735cf06d..9b26f13c4983 100755 --- a/components/ILIAS/UI/tests/Component/Table/PresentationTest.php +++ b/components/ILIAS/UI/tests/Component/Table/PresentationTest.php @@ -30,6 +30,8 @@ */ class PresentationTest extends TableTestBase { + use LanguageStubs; + private function getFactory(): I\Component\Table\Factory { return new I\Component\Table\Factory( @@ -98,9 +100,14 @@ public function testRowConstruction(): void public function getUIFactory(): NoUIFactory { - $factory = new class () extends NoUIFactory { + $factory = new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { public I\Component\SignalGenerator $sig_gen; + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function button(): I\Component\Button\Factory { return new I\Component\Button\Factory(); @@ -109,7 +116,7 @@ public function symbol(): I\Component\Symbol\Factory { return new I\Component\Symbol\Factory( new I\Component\Symbol\Icon\Factory(), - new I\Component\Symbol\Glyph\Factory(), + new I\Component\Symbol\Glyph\Factory($this->language), new I\Component\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/Table/TableRendererTestBase.php b/components/ILIAS/UI/tests/Component/Table/TableRendererTestBase.php index 6b0a2d778651..728bcc1cc7bd 100644 --- a/components/ILIAS/UI/tests/Component/Table/TableRendererTestBase.php +++ b/components/ILIAS/UI/tests/Component/Table/TableRendererTestBase.php @@ -31,6 +31,8 @@ */ class TableRendererTestBase extends TableTestBase { + use LanguageStubs; + protected function getActionFactory() { return new I\Table\Action\Factory(); @@ -62,9 +64,13 @@ public function getDataFactory(): Data\Factory public function getUIFactory(): NoUIFactory { - $factory = new class ($this->getTableFactory()) extends NoUIFactory { + $factory = new class ( + $this->getTableFactory(), + $this->createRelayArgumentLanguageStub(), + ) extends NoUIFactory { public function __construct( - protected Component\Table\Factory $table_factory + protected Component\Table\Factory $table_factory, + protected \ILIAS\Language\Language $language, ) { } public function button(): I\Button\Factory @@ -79,7 +85,7 @@ public function symbol(): I\Symbol\Factory { return new I\Symbol\Factory( new I\Symbol\Icon\Factory(), - new I\Symbol\Glyph\Factory(), + new I\Symbol\Glyph\Factory($this->language), new I\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/Component/ViewControl/PaginationTest.php b/components/ILIAS/UI/tests/Component/ViewControl/PaginationTest.php index aa8df81bc1d2..5bb633d8daca 100755 --- a/components/ILIAS/UI/tests/Component/ViewControl/PaginationTest.php +++ b/components/ILIAS/UI/tests/Component/ViewControl/PaginationTest.php @@ -31,14 +31,21 @@ */ class PaginationTest extends ILIAS_UI_TestBase { + use LanguageStubs; + public function getUIFactory(): NoUIFactory { - return new class () extends NoUIFactory { + return new class ($this->createRelayArgumentLanguageStub()) extends NoUIFactory { + public function __construct( + protected \ILIAS\Language\Language $language, + ) { + } + public function symbol(): IC\Symbol\Factory { return new IC\Symbol\Factory( new IC\Symbol\Icon\Factory(), - new IC\Symbol\Glyph\Factory(), + new IC\Symbol\Glyph\Factory($this->language), new IC\Symbol\Avatar\Factory() ); } diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 95fae03ffecc..0ba2a1c745e5 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -230,7 +230,9 @@ public function getRefreshIntervalInMs(): int ); }; $c["ui.factory.symbol.glyph"] = function ($c) { - return new ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory(); + return new ILIAS\UI\Implementation\Component\Symbol\Glyph\Factory( + $c["lng"], + ); }; $c["ui.factory.symbol.icon"] = function ($c) { return new ILIAS\UI\Implementation\Component\Symbol\Icon\Factory(); diff --git a/components/ILIAS/UI/tests/LanguageStubs.php b/components/ILIAS/UI/tests/LanguageStubs.php new file mode 100644 index 000000000000..cf9fba608749 --- /dev/null +++ b/components/ILIAS/UI/tests/LanguageStubs.php @@ -0,0 +1,46 @@ + + */ +trait LanguageStubs +{ + protected function createFixedLanguageStub(string $translation): Language&MockObject + { + $stub = $this->createMock(Language::class); + $stub->method('txt')->willReturn($translation); + return $stub; + } + + protected function createRelayArgumentLanguageStub(): Language&MockObject + { + $stub = $this->createMock(Language::class); + $stub->method('txt')->willReturnArgument(0); + return $stub; + } +} diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 476a103a5fe0..fd86fa208a60 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -5705,6 +5705,7 @@ common#:#show_content#:#Inhalt anzeigen common#:#show_details#:#Details anzeigen common#:#show_filter#:#Filter anzeigen common#:#show_hidden_sections#:#Weitere Informationen anzeigen » +common#:#show_less#:#Weniger zeigen common#:#show_list#:#Auflistung anzeigen common#:#show_members#:#Mitglieder anzeigen common#:#show_more#:#Mehr zeigen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index dc21913a16ce..5eb2bbca8e96 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -5706,6 +5706,7 @@ common#:#show_content#:#Show Content common#:#show_details#:#Show Details common#:#show_filter#:#Show Filter common#:#show_hidden_sections#:#Show More Information » +common#:#show_less#:#Show less common#:#show_list#:#Show List common#:#show_members#:#Display Members common#:#show_more#:#Show More diff --git a/templates/default/030-tools/_index.scss b/templates/default/030-tools/_index.scss new file mode 100644 index 000000000000..b70d3f680d80 --- /dev/null +++ b/templates/default/030-tools/_index.scss @@ -0,0 +1,3 @@ +// Write the reason down, why these tools have to generate CSS utility classes +// Prefer using mixins and @extend instead whenever possible +@use "tool_text-more-less-toggle"; // many adjacent selectors; avoiding construct of 4+ mixins or 4+ parameters diff --git a/templates/default/030-tools/_tool_focus-outline.scss b/templates/default/030-tools/_tool_focus-outline.scss index 70fa7fea0e77..7a54799b6c56 100755 --- a/templates/default/030-tools/_tool_focus-outline.scss +++ b/templates/default/030-tools/_tool_focus-outline.scss @@ -6,14 +6,10 @@ $il-focus-outline-outer-width: 2px; // KEYBOARD FOCUS DEFAULT // This is the mixin you should be using if possible -@mixin il-focus($il-focus-outline-inner-width: $il-focus-outline-inner-width, $il-focus-outline-outer-width: $il-focus-outline-outer-width){ - $il-focus-outline-inner: $il-focus-outline-inner-width solid $il-focus-color; - $il-focus-outline-outer: $il-focus-outline-outer-width solid $il-focus-protection-color; - &:focus { - outline: none; - outline-offset: 0px; - } - &:focus-visible { +@mixin il-focus($il-focus-outline-inner-width: $il-focus-outline-inner-width, $il-focus-outline-outer-width: $il-focus-outline-outer-width, $apply-to-child: "") { + &:focus #{$apply-to-child}, &:focus-visible #{$apply-to-child} { + $il-focus-outline-inner: $il-focus-outline-inner-width solid $il-focus-color; + $il-focus-outline-outer: $il-focus-outline-outer-width solid $il-focus-protection-color; position: relative; // outermost protection color line outline: $il-focus-outline-outer; @@ -111,3 +107,5 @@ $il-focus-outline-outer-width: 2px; } } } + + diff --git a/templates/default/030-tools/_tool_text-more-less-toggle.scss b/templates/default/030-tools/_tool_text-more-less-toggle.scss new file mode 100644 index 000000000000..7ce3f6b470a7 --- /dev/null +++ b/templates/default/030-tools/_tool_text-more-less-toggle.scss @@ -0,0 +1,45 @@ +@use "../010-settings" as s; +@use "../030-tools/tool_multi-line-cap" as t-cap; +@use "../030-tools/tool_focus-outline" as t-focus; +@use "../030-tools/tool_screen-reader-only" as t-sr; +@use "../050-layout/basics" as l; +@use "../050-layout/layout_breakpoints" as l-brk; + +.t-text-more-less { + &__toggle { + &:checked + .t-text-more-less__label { + .t-text-more-less__label__more { + display: none; + } + } + &:not(:checked) + .t-text-more-less__label { + .t-text-more-less__label__less { + display: none; + } + } + } + &__label { + color: s.$il-link-color; + text-decoration: s.$il-link-decoration; + &:hover { + color: s.$il-link-hover-color; + text-decoration: s.$il-link-hover-decoration; + } + } + &:has(.t-text-more-less__toggle:not(:checked)) { + .t-text-more-less__text-body { + @include l-brk.on-screen-size(medium) { + @include t-cap.il-multi-line-cap-mixin(1); + } + @include l-brk.on-screen-size(small) { + @include t-cap.il-multi-line-cap-mixin(2); + } + } + } +} + +input[type="checkbox"].t-text-more-less__toggle { + @include t-sr.sr-only(); + @include t-focus.clear-focus-for-override(); + @include t-focus.il-focus($apply-to-child: "~ label"); +} diff --git a/templates/default/050-layout/_layout_element-bar.scss b/templates/default/050-layout/_layout_element-bar.scss index 0438deba55b1..d5286bb7797d 100755 --- a/templates/default/050-layout/_layout_element-bar.scss +++ b/templates/default/050-layout/_layout_element-bar.scss @@ -22,8 +22,15 @@ $l-bar__element__margin-bottom: $il-margin-xlarge-vertical; // this margin separ flex-direction: row; flex-wrap: wrap; align-items: center; + // avoid nested elements breaking off + .l-bar__group, + .l-bar__element { + flex-wrap: nowrap; + } } +// elements inside have a margin bottom to keep lines apart +// compensate this line gap so elementbar does not have a margin-bottom below .l-bar__space-keeper:not(:empty) { margin-bottom: calc(-1 * var(--l-bar__element__margin-bottom)); // counteract element margin-bottom, so outer edge doesn't have a visible margin-bottom &.l-bar__space-keeper--space-between { @@ -31,19 +38,22 @@ $l-bar__element__margin-bottom: $il-margin-xlarge-vertical; // this margin separ } } +// bottom margin to separate lines .l-bar__space-keeper > .l-bar__element, .l-bar__group > .l-bar__element { margin-bottom: var(--l-bar__element__margin-bottom); - &:last-of-type { - margin-right: 0; - } } +// gap between two elements .l-bar__group > .l-bar__element, .l-bar__space-keeper > .l-bar__element { margin-right: var(--l-bar__gap--elements); + &:last-child { + margin-right: 0; + } } +// gap between two groups .l-bar__space-keeper > .l-bar__group, .l-bar__group > .l-bar__group { margin-right: var(--l-bar__gap--groups); diff --git a/templates/default/050-layout/_layout_grid-auto-columns.scss b/templates/default/050-layout/_layout_grid-auto-columns.scss new file mode 100644 index 000000000000..bac5e66564b2 --- /dev/null +++ b/templates/default/050-layout/_layout_grid-auto-columns.scss @@ -0,0 +1,15 @@ +@use "sass:math"; +@use "sass:list"; +@use "../050-layout/basics" as l; + +$allowed-units: ("px", "ch", "rem", "em"); + +@mixin make-grid-with-auto-columns($column-min-width, $gap: l.$il-margin-xxxlarge-vertical) { + $unit: math.unit($column-min-width); + @if (list.index($allowed-units, $unit) == null) { + @error "Enter a width with one of these units: #{$allowed-units}"; + } + display: grid; + grid-template-columns: repeat(auto-fill, minmax($column-min-width, 1fr)); + gap: $gap; +} diff --git a/templates/default/060-elements/_elements_media.scss b/templates/default/060-elements/_elements_media.scss index cd46057a5285..60c61df57bb4 100755 --- a/templates/default/060-elements/_elements_media.scss +++ b/templates/default/060-elements/_elements_media.scss @@ -1,4 +1,5 @@ @use "../010-settings/" as *; +@use "../030-tools/tool_focus-outline" as focus; img { vertical-align: middle; @@ -6,4 +7,10 @@ img { /* height: auto; messes e.g. survey progress bar */ max-width: 100%; } -} \ No newline at end of file +} + +a:has(img) { + display: inline-block; + @include focus.clear-focus-for-override(); + @include focus.il-focus(); +} diff --git a/templates/default/070-components/UI-framework/Entity/_ui-component_entity.scss b/templates/default/070-components/UI-framework/Entity/_ui-component_entity.scss index ffe8ef22fe74..f089acabb44f 100755 --- a/templates/default/070-components/UI-framework/Entity/_ui-component_entity.scss +++ b/templates/default/070-components/UI-framework/Entity/_ui-component_entity.scss @@ -1,93 +1,125 @@ @use "sass:math"; @use "../../../010-settings/" as s; @use "../../../050-layout/basics/" as l; +@use "../../../050-layout/layout_breakpoints" as l-brk; + +$entity-padding-vertical: l.$il-padding-xlarge-vertical; +$entity-spacer-padding-vertical: l.$il-padding-xlarge-vertical * 2; +$entity-padding-horizontal: l.$il-padding-xlarge-horizontal; // counterstyling in use for padding-less images on mobile +$img-large-max-width: 360px; +$max-rows: 9; + +@mixin entity-limited-space-design() { + &__container { + display: flex; + flex-direction: column; + > * { + order: 2; + } + } + &__secondary-identifier { + order: 1; + &.--image { + img { + // break out of padding to reach from edge to edge + width: calc(100% + $entity-padding-horizontal * 2); + max-width: unset; + margin-left: -($entity-padding-horizontal); + margin-top: -($entity-padding-vertical); + } + } + } + &__reactionbar { + flex-grow: 1; + } +} .c-entity { - &.__container { + + &__container { display: grid; - grid-template-areas: - "f-blocking f-blocking f-blocking f-blocking actions" - "second-id f-prop f-prop f-prop actions" - "second-id prim-id prim-id prim-id actions" - "second-id status status status status" - "second-id f-details f-details f-details f-details" - "second-id availab availab availab availab" - "second-id details details details details" - "second-id reaction reaction f-reaction f-reaction"; - grid-template-columns: min-content auto auto min-content min-content; + grid-template-columns: max-content auto; + grid-template-rows: repeat($max-rows, minmax(min-content, auto)); + border: s.$il-main-border; background-color: s.$il-main-bg; - padding: math.div(l.$il-margin-xlarge-vertical, 2) math.div(l.$il-margin-xlarge-horizontal, 2); - > *:not(:empty) { - padding: math.div(l.$il-margin-xlarge-vertical, 2) math.div(l.$il-margin-xlarge-horizontal, 2); + padding: math.div($entity-padding-vertical, 2) math.div($entity-padding-horizontal, 2); + > * { + // half paddings edge to edge equal a full padding + padding: math.div($entity-padding-vertical, 2) math.div($entity-padding-horizontal, 2); + } + @include l-brk.on-screen-size(small) { + display: block; } } - &.__blocking-conditions { - grid-area: f-blocking; - font-size: s.$il-font-size-xlarge; - } - - &.__actions { - display: flex; - justify-content: end; - grid-area: actions; - .dropdown { - height: max-content; + &__featured-headerbar { + column-span: 2; + > .l-bar__space-keeper { + flex-wrap: nowrap; + .l-bar__group, + .l-bar__element { + align-self: flex-start; + } } } - &.__secondary-identifier { + &__secondary-identifier { + grid-column: 1; + grid-row: 1 / #{$max-rows + 1}; // number high enough so all others can fit on the right &.--string, &.--shy, &.--shylink { width: 10rem; } &.--symbol { - min-width: 3rem; + img { width: auto; } } &.--image { - width: 15rem; + img { max-width: $img-large-max-width; }; } - grid-area: second-id; } - &.__primary-identifier { - grid-area: prim-id; + &__primary-identifier { font-weight: s.$il-font-weight-bold; font-size: s.$il-font-size-xxlarge; } - &.__featured { - grid-area: f-prop; - font-size: s.$il-font-size-xlarge; + &__featured { + font-size: s.$il-font-size-large; } - &.__personal-status { - grid-area: status; + &__workflow-actions { + padding-top: $entity-spacer-padding-vertical; } - &.__main-details { - grid-area: f-details; + &__blocking-conditions { + padding-bottom: $entity-spacer-padding-vertical; + font-size: s.$il-font-size-large; } - - &.__availability { - grid-area: availab; + &__main-details { + padding-top: $entity-spacer-padding-vertical; + } + &__details { + font-size: s.$il-font-size-small; } - &.__details { - grid-area: details; + &__reactions { + padding-top: $entity-spacer-padding-vertical; } - &.__reactions { - // display: flex; - // flex-direction: row; - grid-area: reaction; + &__reactionbar { + align-self: stretch; + align-content: flex-end; + grid-column: 2; + grid-row: $max-rows; // always in last row } - &.__featured-reactions { - display: flex; - justify-content: end; - grid-area: f-reaction; + &__featured-reactions { + padding-top: $entity-spacer-padding-vertical; + text-align: end; min-width: max-content; } + @include l-brk.on-screen-size(small) { + @include entity-limited-space-design(); + } } diff --git a/templates/default/070-components/UI-framework/Listing/_ui-component_entitylisting.scss b/templates/default/070-components/UI-framework/Listing/_ui-component_entitylisting.scss index d04b2f0c9bf9..549919a3a6b7 100755 --- a/templates/default/070-components/UI-framework/Listing/_ui-component_entitylisting.scss +++ b/templates/default/070-components/UI-framework/Listing/_ui-component_entitylisting.scss @@ -1,4 +1,33 @@ -.c-listing-entity { +@use "../../../050-layout/basics" as l; +@use "../../../050-layout/layout_grid-auto-columns" as l-auto-grid; +@use "../../../030-tools/tool_multi-line-cap" as t-linecap; + +// so we can force entity small space design on large screens +@use "../../../070-components/UI-framework/Entity/ui-component_entity" as entity; + +$listing-column-min-width: 650px; +$grid-column-min-width: 400px; + +.c-listing-entity, +.c-listing-entity-grid { list-style: none; padding-left: 0; -} \ No newline at end of file +} + +.c-listing-entity { + @include l-auto-grid.make-grid-with-auto-columns($listing-column-min-width); +} + +.c-listing-entity-grid { + @include l-auto-grid.make-grid-with-auto-columns($grid-column-min-width); + .c-entity { + @include entity.entity-limited-space-design(); + &__container { + height: 100%; + .c-listing-property__propertyvalue:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + height: 2lh; + } + } + } +} + diff --git a/templates/default/070-components/UI-framework/Listing/_ui-component_inline.scss b/templates/default/070-components/UI-framework/Listing/_ui-component_inline.scss new file mode 100644 index 000000000000..9edc51348296 --- /dev/null +++ b/templates/default/070-components/UI-framework/Listing/_ui-component_inline.scss @@ -0,0 +1,19 @@ +@use "../../../010-settings" as s; +@use "../../../030-tools/tool_multi-line-cap" as t-cap; +@use "../../../030-tools/tool_focus-outline" as t-focus; +@use "../../../050-layout/basics/" as l; +@use "../../../050-layout/layout_breakpoints" as l-brk; + +.c-listing-inline { + padding: 0; + margin: 0; + list-style: none; + > .c-listing-inline__item { + &::after { + content: ", "; + } + &:last-child::after { + content: ""; + } + } +} diff --git a/templates/default/070-components/UI-framework/Listing/_ui-component_properties.scss b/templates/default/070-components/UI-framework/Listing/_ui-component_properties.scss index 6ab6876c386d..e499c128fbf8 100755 --- a/templates/default/070-components/UI-framework/Listing/_ui-component_properties.scss +++ b/templates/default/070-components/UI-framework/Listing/_ui-component_properties.scss @@ -1,9 +1,13 @@ +@use "../../../010-settings" as s; +@use "../../../030-tools/tool_multi-line-cap" as t-cap; +@use "../../../030-tools/tool_focus-outline" as t-focus; @use "../../../050-layout/basics/" as l; +@use "../../../050-layout/layout_breakpoints" as l-brk; -.c-listing-property__propertylabel:after { - content: ":" +.c-listing-property__propertylabel:not(:has(.glyphicon)):after { + content: ":"; } .c-listing-property + .c-listing-property { margin-top: l.$il-margin-large-vertical; -} \ No newline at end of file +} diff --git a/templates/default/070-components/_index.scss b/templates/default/070-components/_index.scss index fa625b1ab3aa..7e6c2de78437 100755 --- a/templates/default/070-components/_index.scss +++ b/templates/default/070-components/_index.scss @@ -21,6 +21,7 @@ @use "./UI-framework/Dropdown/_ui-component_dropdown.scss"; @use "./UI-framework/Dropzone/_ui-component_dropzone.scss"; @use "./UI-framework/Entity/ui-component_entity"; +@use "./UI-framework/Listing/ui-component_inline"; @use "./UI-framework/Item/_ui-component_item.scss"; @use "./UI-framework/Launcher/ui-component_launcher"; @use "./UI-framework/Layout/_ui-component_layout.scss"; diff --git a/templates/default/delos.css b/templates/default/delos.css index c23d5b29f046..b0ff73159ce8 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -715,6 +715,165 @@ table.mceToolbar tbody, table.mceToolbar tr, table.mceToolbar td { min-height: 23px; } +/* + These classes are used to limit the number of rows when displaying larger chunks of text. + The mixin receives $height-in-rows as an integer. The classes il-multi-line-cap-2,3,5,10 + can be used to limit the number of rows for text to 2,3,5 or 10 lines in any template, + e.g. the Standard Listing Panels limit the property values to 3 lines using il-multi-line-cap-3 + + Technical discussion can be found in https://mantis.ilias.de/view.php?id=21583 + The background/gradient fallback can be removed as soon as all browsers support line-clamp. + */ +.t-text-more-less__toggle:checked + .t-text-more-less__label .t-text-more-less__label__more { + display: none; +} +.t-text-more-less__toggle:not(:checked) + .t-text-more-less__label .t-text-more-less__label__less { + display: none; +} +.t-text-more-less__label { + color: #4c6586; + text-decoration: none; +} +.t-text-more-less__label:hover { + color: rgb(57.5428571429, 76.4714285714, 101.4571428571); + text-decoration: underline; +} +@media screen and (min-width: 769px) { + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + position: relative; + max-height: 1.5em; + overflow: hidden; + line-height: 1.5; + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body:after { + content: ""; + text-align: right; + position: absolute; + bottom: 0; + right: 0; + width: 30%; + height: 1.5em; + background: linear-gradient(to right, rgba(255, 255, 255, 0), rgb(255, 255, 255) 80%); + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + /* edge, chrome, safari go here... */ + } + @supports (-webkit-line-clamp: 2) { + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body:after { + display: none; + } + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + /* may come with next firefox 68, https://caniuse.com/#search=clamp */ + } + @supports (-moz-line-clamp: 2) { + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + overflow: hidden; + text-overflow: ellipsis; + display: -moz-box; + -moz-line-clamp: 1; + -moz-box-orient: vertical; + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body:after { + display: none; + } + } +} +@media screen and (max-width: 768px) { + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + position: relative; + max-height: 3em; + overflow: hidden; + line-height: 1.5; + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body:after { + content: ""; + text-align: right; + position: absolute; + bottom: 0; + right: 0; + width: 30%; + height: 1.5em; + background: linear-gradient(to right, rgba(255, 255, 255, 0), rgb(255, 255, 255) 80%); + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + /* edge, chrome, safari go here... */ + } + @supports (-webkit-line-clamp: 2) { + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body:after { + display: none; + } + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + /* may come with next firefox 68, https://caniuse.com/#search=clamp */ + } + @supports (-moz-line-clamp: 2) { + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + overflow: hidden; + text-overflow: ellipsis; + display: -moz-box; + -moz-line-clamp: 2; + -moz-box-orient: vertical; + } + .t-text-more-less:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body:after { + display: none; + } + } +} + +input[type=checkbox].t-text-more-less__toggle { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + white-space: nowrap; + clip: rect(0, 0, 0, 0); + border: 0; +} +input[type=checkbox].t-text-more-less__toggle:focus { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +input[type=checkbox].t-text-more-less__toggle:focus-visible { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +input[type=checkbox].t-text-more-less__toggle:focus ~ label, input[type=checkbox].t-text-more-less__toggle:focus-visible ~ label { + position: relative; + outline: 2px solid #FFFFFF; + outline-offset: 5px; +} +input[type=checkbox].t-text-more-less__toggle:focus ~ label::after, input[type=checkbox].t-text-more-less__toggle:focus-visible ~ label::after { + content: " "; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border: 2px solid #FFFFFF; + outline: 3px solid #0078D7; +} + /* * Normalize */ @@ -1767,6 +1926,12 @@ th { flex-wrap: wrap; align-items: center; } +.l-bar__space-keeper .l-bar__group, +.l-bar__space-keeper .l-bar__element, +.l-bar__group .l-bar__group, +.l-bar__group .l-bar__element { + flex-wrap: nowrap; +} .l-bar__space-keeper:not(:empty) { margin-bottom: calc(-1 * var(--l-bar__element__margin-bottom)); @@ -1779,15 +1944,15 @@ th { .l-bar__group > .l-bar__element { margin-bottom: var(--l-bar__element__margin-bottom); } -.l-bar__space-keeper > .l-bar__element:last-of-type, -.l-bar__group > .l-bar__element:last-of-type { - margin-right: 0; -} .l-bar__group > .l-bar__element, .l-bar__space-keeper > .l-bar__element { margin-right: var(--l-bar__gap--elements); } +.l-bar__group > .l-bar__element:last-child, +.l-bar__space-keeper > .l-bar__element:last-child { + margin-right: 0; +} .l-bar__space-keeper > .l-bar__group, .l-bar__group > .l-bar__group { @@ -2138,21 +2303,19 @@ fieldset[disabled] input[type=checkbox] { cursor: not-allowed; } -input[type=file]:focus, +input[type=file]:focus, input[type=file]:focus-visible, input[type=radio]:focus, -input[type=checkbox]:focus { - outline: none; - outline-offset: 0px; -} -input[type=file]:focus-visible, input[type=radio]:focus-visible, +input[type=checkbox]:focus, input[type=checkbox]:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -input[type=file]:focus-visible::after, +input[type=file]:focus::after, input[type=file]:focus-visible::after, +input[type=radio]:focus::after, input[type=radio]:focus-visible::after, +input[type=checkbox]:focus::after, input[type=checkbox]:focus-visible::after { content: " "; position: absolute; @@ -2212,6 +2375,37 @@ img { } } +a:has(img) { + display: inline-block; +} +a:has(img):focus { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +a:has(img):focus-visible { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +a:has(img):focus, a:has(img):focus-visible { + position: relative; + outline: 2px solid #FFFFFF; + outline-offset: 5px; +} +a:has(img):focus::after, a:has(img):focus-visible::after { + content: " "; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border: 2px solid #FFFFFF; + outline: 3px solid #0078D7; +} + script { display: none !important; } @@ -4900,70 +5094,112 @@ hr.il-divider-with-label { width: 100%; } -.c-entity.__container { +.c-entity__container { display: grid; - grid-template-areas: "f-blocking f-blocking f-blocking f-blocking actions" "second-id f-prop f-prop f-prop actions" "second-id prim-id prim-id prim-id actions" "second-id status status status status" "second-id f-details f-details f-details f-details" "second-id availab availab availab availab" "second-id details details details details" "second-id reaction reaction f-reaction f-reaction"; - grid-template-columns: min-content auto auto min-content min-content; + grid-template-columns: max-content auto; + grid-template-rows: repeat(9, minmax(min-content, auto)); + border: 1px solid #dddddd; background-color: white; padding: 4.5px 7.5px; } -.c-entity.__container > *:not(:empty) { +.c-entity__container > * { padding: 4.5px 7.5px; } -.c-entity.__blocking-conditions { - grid-area: f-blocking; - font-size: 1.115rem; +@media screen and (max-width: 768px) { + .c-entity__container { + display: block; + } } -.c-entity.__actions { - display: flex; - justify-content: end; - grid-area: actions; +.c-entity__featured-headerbar { + column-span: 2; } -.c-entity.__actions .dropdown { - height: max-content; +.c-entity__featured-headerbar > .l-bar__space-keeper { + flex-wrap: nowrap; } -.c-entity.__secondary-identifier.--string, .c-entity.__secondary-identifier.--shy, .c-entity.__secondary-identifier.--shylink { - width: 10rem; +.c-entity__featured-headerbar > .l-bar__space-keeper .l-bar__group, +.c-entity__featured-headerbar > .l-bar__space-keeper .l-bar__element { + align-self: flex-start; } -.c-entity.__secondary-identifier.--symbol { - min-width: 3rem; +.c-entity__secondary-identifier { + grid-column: 1; + grid-row: 1/10; } -.c-entity.__secondary-identifier.--image { - width: 15rem; +.c-entity__secondary-identifier.--string, .c-entity__secondary-identifier.--shy, .c-entity__secondary-identifier.--shylink { + width: 10rem; } -.c-entity.__secondary-identifier { - grid-area: second-id; +.c-entity__secondary-identifier.--symbol img { + width: auto; } -.c-entity.__primary-identifier { - grid-area: prim-id; +.c-entity__secondary-identifier.--image img { + max-width: 360px; +} +.c-entity__primary-identifier { font-weight: 600; font-size: 1.5rem; } -.c-entity.__featured { - grid-area: f-prop; - font-size: 1.115rem; +.c-entity__featured { + font-size: 1rem; } -.c-entity.__personal-status { - grid-area: status; +.c-entity__workflow-actions { + padding-top: 18px; } -.c-entity.__main-details { - grid-area: f-details; +.c-entity__blocking-conditions { + padding-bottom: 18px; + font-size: 1rem; } -.c-entity.__availability { - grid-area: availab; +.c-entity__main-details { + padding-top: 18px; } -.c-entity.__details { - grid-area: details; +.c-entity__details { + font-size: 0.75rem; } -.c-entity.__reactions { - grid-area: reaction; +.c-entity__reactions { + padding-top: 18px; } -.c-entity.__featured-reactions { - display: flex; - justify-content: end; - grid-area: f-reaction; +.c-entity__reactionbar { + align-self: stretch; + align-content: flex-end; + grid-column: 2; + grid-row: 9; +} +.c-entity__featured-reactions { + padding-top: 18px; + text-align: end; min-width: max-content; } +@media screen and (max-width: 768px) { + .c-entity__container { + display: flex; + flex-direction: column; + } + .c-entity__container > * { + order: 2; + } + .c-entity__secondary-identifier { + order: 1; + } + .c-entity__secondary-identifier.--image img { + width: calc(100% + 30px); + max-width: unset; + margin-left: -15px; + margin-top: -9px; + } + .c-entity__reactionbar { + flex-grow: 1; + } +} + +.c-listing-inline { + padding: 0; + margin: 0; + list-style: none; +} +.c-listing-inline > .c-listing-inline__item::after { + content: ", "; +} +.c-listing-inline > .c-listing-inline__item:last-child::after { + content: ""; +} .il-std-item-container:not(:last-child) { border-bottom: 1px solid #dddddd; @@ -5819,7 +6055,7 @@ a[aria-disabled].il-link.link-bulky:hover { background-color: unset; } -.c-listing-property__propertylabel:after { +.c-listing-property__propertylabel:not(:has(.glyphicon)):after { content: ":"; } @@ -6033,11 +6269,49 @@ a[aria-disabled].il-link.link-bulky:hover { padding: 15px 15px; } -.c-listing-entity { +.c-listing-entity, +.c-listing-entity-grid { list-style: none; padding-left: 0; } +.c-listing-entity { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(650px, 1fr)); + gap: 15px; +} + +.c-listing-entity-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 15px; +} +.c-listing-entity-grid .c-entity__container { + display: flex; + flex-direction: column; +} +.c-listing-entity-grid .c-entity__container > * { + order: 2; +} +.c-listing-entity-grid .c-entity__secondary-identifier { + order: 1; +} +.c-listing-entity-grid .c-entity__secondary-identifier.--image img { + width: calc(100% + 30px); + max-width: unset; + margin-left: -15px; + margin-top: -9px; +} +.c-listing-entity-grid .c-entity__reactionbar { + flex-grow: 1; +} +.c-listing-entity-grid .c-entity__container { + height: 100%; +} +.c-listing-entity-grid .c-entity__container .c-listing-property__propertyvalue:has(.t-text-more-less__toggle:not(:checked)) .t-text-more-less__text-body { + height: 2lh; +} + .il-maincontrols-slate.disengaged { display: none; } @@ -16902,10 +17176,10 @@ div.ilc_Page.readonly textarea[disabled] { .c-consecutive-scoring__answer__body h1:empty { display: none; } -.c-consecutive-scoring__answer__body .ilc_qanswer_Answer.solutionbox { +.c-consecutive-scoring__answer__body .ilc_qanswer_Answer .solutionbox { width: auto; } -.c-consecutive-scoring__answer__body .ilc_qanswer_Answer.ilc_answers.answers.ilAssClozeTest div input[type=text] { +.c-consecutive-scoring__answer__body .ilc_qanswer_Answer .ilc_answers.answers.ilAssClozeTest div input[type=text] { width: 100%; } .c-consecutive-scoring__answer__grade-card { @@ -17284,16 +17558,12 @@ div.ilc_va_icont_VAccordICont { border-bottom: 1px solid #dddddd; background-color: white; } -.ilAwarenessItem > div[role=button]:focus-visible:focus { - outline: none; - outline-offset: 0px; -} -.ilAwarenessItem > div[role=button]:focus-visible:focus-visible { +.ilAwarenessItem > div[role=button]:focus-visible:focus, .ilAwarenessItem > div[role=button]:focus-visible:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -.ilAwarenessItem > div[role=button]:focus-visible:focus-visible::after { +.ilAwarenessItem > div[role=button]:focus-visible:focus::after, .ilAwarenessItem > div[role=button]:focus-visible:focus-visible::after { content: " "; position: absolute; top: -2px; @@ -19130,16 +19400,12 @@ p#copg-auto-save { position: static; } -.ilPageVideo button:focus, .ilPageAudio button:focus { - outline: none; - outline-offset: 0px; -} -.ilPageVideo button:focus-visible, .ilPageAudio button:focus-visible { +.ilPageVideo button:focus, .ilPageVideo button:focus-visible, .ilPageAudio button:focus, .ilPageAudio button:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -.ilPageVideo button:focus-visible::after, .ilPageAudio button:focus-visible::after { +.ilPageVideo button:focus::after, .ilPageVideo button:focus-visible::after, .ilPageAudio button:focus::after, .ilPageAudio button:focus-visible::after { content: " "; position: absolute; top: -2px; @@ -20193,18 +20459,15 @@ a.mailunread, a.mailunread:visited { background-image: url("../images/media/bigplay.svg"); } -.mejs__overlay-button:focus, -.ilPlayerPreviewPlayButton:focus { - outline: none; - outline-offset: 0px; -} -.mejs__overlay-button:focus-visible, +.mejs__overlay-button:focus, .mejs__overlay-button:focus-visible, +.ilPlayerPreviewPlayButton:focus, .ilPlayerPreviewPlayButton:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -.mejs__overlay-button:focus-visible::after, +.mejs__overlay-button:focus::after, .mejs__overlay-button:focus-visible::after, +.ilPlayerPreviewPlayButton:focus::after, .ilPlayerPreviewPlayButton:focus-visible::after { content: " "; position: absolute; @@ -20216,16 +20479,12 @@ a.mailunread, a.mailunread:visited { outline: 3px solid #0078D7; } -.mejs__time-total:focus { - outline: none; - outline-offset: 0px; -} -.mejs__time-total:focus-visible { +.mejs__time-total:focus, .mejs__time-total:focus-visible { position: relative; outline: 1px solid #FFFFFF; outline-offset: 2px; } -.mejs__time-total:focus-visible::after { +.mejs__time-total:focus::after, .mejs__time-total:focus-visible::after { content: " "; position: absolute; top: -1px; @@ -22132,18 +22391,15 @@ a.ilMediaLightboxClose:hover { #ilTab > li > .c-tooltip__container > a:focus-visible::after { content: none; } -#ilTab > li > a:focus, -#ilTab > li > .c-tooltip__container > a:focus { - outline: none; - outline-offset: 0px; -} -#ilTab > li > a:focus-visible, +#ilTab > li > a:focus, #ilTab > li > a:focus-visible, +#ilTab > li > .c-tooltip__container > a:focus, #ilTab > li > .c-tooltip__container > a:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -#ilTab > li > a:focus-visible::after, +#ilTab > li > a:focus::after, #ilTab > li > a:focus-visible::after, +#ilTab > li > .c-tooltip__container > a:focus::after, #ilTab > li > .c-tooltip__container > a:focus-visible::after { content: " "; position: absolute; @@ -22214,18 +22470,15 @@ a.ilMediaLightboxClose:hover { #ilSubTab > li > .c-tooltip__container > a:focus-visible::after { content: none; } -#ilSubTab > li > a:focus, -#ilSubTab > li > .c-tooltip__container > a:focus { - outline: none; - outline-offset: 0px; -} -#ilSubTab > li > a:focus-visible, +#ilSubTab > li > a:focus, #ilSubTab > li > a:focus-visible, +#ilSubTab > li > .c-tooltip__container > a:focus, #ilSubTab > li > .c-tooltip__container > a:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -#ilSubTab > li > a:focus-visible::after, +#ilSubTab > li > a:focus::after, #ilSubTab > li > a:focus-visible::after, +#ilSubTab > li > .c-tooltip__container > a:focus::after, #ilSubTab > li > .c-tooltip__container > a:focus-visible::after { content: " "; position: absolute; @@ -22379,16 +22632,12 @@ img.ilUserXXSmall { .webdav-view-control { text-align: center; } -.webdav-view-control:focus { - outline: none; - outline-offset: 0px; -} -.webdav-view-control:focus-visible { +.webdav-view-control:focus, .webdav-view-control:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -.webdav-view-control:focus-visible::after { +.webdav-view-control:focus::after, .webdav-view-control:focus-visible::after { content: " "; position: absolute; top: -2px; @@ -22399,15 +22648,6 @@ img.ilUserXXSmall { outline: 3px solid #0078D7; } -/* - These classes are used to limit the number of rows when displaying larger chunks of text. - The mixin receives $height-in-rows as an integer. The classes il-multi-line-cap-2,3,5,10 - can be used to limit the number of rows for text to 2,3,5 or 10 lines in any template, - e.g. the Standard Listing Panels limit the property values to 3 lines using il-multi-line-cap-3 - - Technical discussion can be found in https://mantis.ilias.de/view.php?id=21583 - The background/gradient fallback can be removed as soon as all browsers support line-clamp. - */ /* * Hacks & Tweaks */ diff --git a/templates/default/delos.scss b/templates/default/delos.scss index 45a1b2b15d31..c1ce11074878 100755 --- a/templates/default/delos.scss +++ b/templates/default/delos.scss @@ -11,6 +11,7 @@ // ## Tools // include patterns and tools with @use in component when needed +@forward "./030-tools"; // avoid creating new tools like these that generate CSS utility classes // ## Normalize @use "./040-normalize/" as *; @@ -30,4 +31,4 @@ // # Relative paths differences // SCSS paths are relativ to the SCSS file -// CSS paths (linked fonts) are relative to the compiled template CSS \ No newline at end of file +// CSS paths (linked fonts) are relative to the compiled template CSS From a103329bdda5eebf3a022658a20f7af4c1f64236 Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 7 Jul 2026 09:08:05 +0200 Subject: [PATCH 048/333] [Fix] Init: Add installation signature to password assistance emails See: https://mantis.ilias.de/view.php?id=48027 --- .../classes/class.ilPasswordAssistanceGUI.php | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/components/ILIAS/Init/classes/class.ilPasswordAssistanceGUI.php b/components/ILIAS/Init/classes/class.ilPasswordAssistanceGUI.php index fef3663cbd05..93a10cca560e 100755 --- a/components/ILIAS/Init/classes/class.ilPasswordAssistanceGUI.php +++ b/components/ILIAS/Init/classes/class.ilPasswordAssistanceGUI.php @@ -45,6 +45,7 @@ class ilPasswordAssistanceGUI implements ilCtrlSecurityInterface private ilObjUser $actor; private ILIAS\Data\Clock\ClockInterface $clock; private ILIAS\Init\PasswordAssitance\PasswordAssistanceRepository $pwa_repository; + private readonly \ILIAS\Mail\Service\MailSignatureService $signature_service; public function __construct() { @@ -68,6 +69,7 @@ public function __construct() $this->clock ); $this->help->setScreenIdComponent('init'); + $this->signature_service = $DIC->mail()->signature(); } private function retrieveRequestedKey(): string @@ -419,9 +421,7 @@ private function sendPasswordAssistanceMail(ilObjUser $userObj): void $mm->From($sender); $mm->To($userObj->getEmail()); $mm->Body( - str_replace( - ["\\n", "\\t"], - ["\n", "\t"], + $this->buildSystemMailBody( sprintf( $this->lng->txt('pwassist_mail_body'), $pwassist_url, @@ -816,9 +816,7 @@ private function sendUsernameAssistanceMail(string $email, array $logins): void $mm->From($sender); $mm->To($email); $mm->Body( - str_replace( - ["\\n", "\\t"], - ["\n", "\t"], + $this->buildSystemMailBody( sprintf( $this->lng->txt('pwassist_username_mail_body'), implode(",\n", $logins), @@ -854,4 +852,21 @@ private function fillPermanentLink(string $context): void { $this->tpl->setPermanentLink('assistant', null, $context); } + + /** + * Appends the installation signature using line breaks that match the resolved mail body format. + * This should be not needed once "Mail" does not accept primitive strings anymore but supports + * real "Text" handling {@see docs/development/text-representation.md} + */ + private function buildSystemMailBody(string $content): string + { + $body = str_replace(["\\n", "\\t"], ["\n", "\t"], $content); + $signature = $this->signature_service->installation(); + + if (strip_tags($body) !== $body) { + $signature = nl2br($signature); + } + + return $body . $signature; + } } From 87db544f623c18bff9353a21d07bd1bc1d385c44 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 7 Jul 2026 09:37:18 +0200 Subject: [PATCH 049/333] ItemGroup: Fix Other Item Group Assignment Query See: https://mantis.ilias.de/view.php?id=48034 The "Already assigned to another Item Group" column in the Assigned Materials table showed invalid values because `getItemGroupsAssociatedWithItem` used a malformed `WHERE` clause without the `item_ref_id` column. Fixed the query to filter by `item_ref_id` correctly. --- components/ILIAS/ItemGroup/classes/class.ilItemGroupItems.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/ItemGroup/classes/class.ilItemGroupItems.php b/components/ILIAS/ItemGroup/classes/class.ilItemGroupItems.php index 2387257c60b1..704c693f7461 100755 --- a/components/ILIAS/ItemGroup/classes/class.ilItemGroupItems.php +++ b/components/ILIAS/ItemGroup/classes/class.ilItemGroupItems.php @@ -249,7 +249,7 @@ public static function getItemGroupsAssociatedWithItem(int $ref_id, int $filter_ 'SELECT ref_id, title FROM item_group_item ' . 'LEFT JOIN object_data ON item_group_item.item_group_id = object_data.obj_id ' . 'LEFT JOIN object_reference ON object_data.obj_id = object_reference.obj_id ' - . 'WHERE %s ' + . 'WHERE item_ref_id = %s ' . 'AND item_group_id != %s ' . 'AND object_data.type = \'itgr\'', [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], From 3544658789102a1ef6d3f8f33495e2adbade5fd9 Mon Sep 17 00:00:00 2001 From: Abraham Date: Tue, 7 Jul 2026 13:24:32 +0200 Subject: [PATCH 050/333] [Survey] Implement and adapt castings and purifier (#11727) * fix: Use correct HTMLPurifier to process inputs * fix: Add strict types declaration * fix: Add type castings * fix: Editing types correction --- .../Editing/class.ilSurveyEditorGUI.php | 51 ++++++++++--------- .../Survey/Settings/class.SettingsFormGUI.php | 18 ++++--- .../ILIAS/Survey/Settings/class.UIFactory.php | 3 +- .../Questions/class.SurveyQuestionGUI.php | 18 ++++--- 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/components/ILIAS/Survey/Editing/class.ilSurveyEditorGUI.php b/components/ILIAS/Survey/Editing/class.ilSurveyEditorGUI.php index 60b941a8f1c9..be28acdba425 100755 --- a/components/ILIAS/Survey/Editing/class.ilSurveyEditorGUI.php +++ b/components/ILIAS/Survey/Editing/class.ilSurveyEditorGUI.php @@ -16,6 +16,8 @@ * *********************************************************************/ +declare(strict_types=1); + use ILIAS\Survey\Editing\EditManager; use ILIAS\Survey\Editing\EditingGUIRequest; @@ -48,6 +50,7 @@ class ilSurveyEditorGUI protected ilObjSurveyGUI $parent_gui; protected ilObjSurvey $object; protected array $print_options; + protected \ilHtmlPurifierInterface $purifier; public function __construct(ilObjSurveyGUI $a_parent_gui) { @@ -72,6 +75,7 @@ public function __construct(ilObjSurveyGUI $a_parent_gui) $this->tpl = $tpl; $this->ctrl->saveParameter($this, array("pgov", "pgov_pos")); + $this->purifier = new ilSvyStandardPurifier(); $this->print_options = array( //0 => $this->lng->txt('none'), @@ -224,7 +228,7 @@ public function questionsObject(): void $ilToolbar->setFormAction($this->ctrl->getFormAction($this)); $types = new ilSelectInputGUI($this->lng->txt("create_new"), "sel_question_types"); $types->setOptions($qtypes); - $ilToolbar->addStickyItem($types, ""); + $ilToolbar->addStickyItem($types, false); $this->gui->button( $this->lng->txt("svy_create_question"), @@ -289,7 +293,7 @@ protected function gatherSelectedTableItems( ): array { $block_map = array(); foreach ($this->object->getSurveyQuestions() as $item) { - $block_map[$item["question_id"]] = $item["questionblock_id"]; + $block_map[(int) $item["question_id"]] = (int) $item["questionblock_id"]; } $questions = $blocks = $headings = array(); @@ -300,16 +304,16 @@ protected function gatherSelectedTableItems( if ($allow_questions && preg_match("/cb_(\d+)/", $key, $matches)) { if (($allow_questions_in_blocks || !$block_map[$matches[1]]) && !in_array($block_map[$matches[1]], $blocks)) { - $questions[] = $matches[1]; + $questions[] = (int) $matches[1]; } } // blocks if ($allow_blocks && preg_match("/cb_qb_(\d+)/", $key, $matches)) { - $blocks[] = $matches[1]; + $blocks[] = (int) $matches[1]; } // headings if ($allow_headings && preg_match("/cb_tb_(\d+)/", $key, $matches)) { - $headings[] = $matches[1]; + $headings[] = (int) $matches[1]; } } } @@ -370,7 +374,7 @@ public function moveQuestionsObject(): void $move_questions = $items["questions"]; foreach ($items["blocks"] as $block_id) { - foreach ($this->object->getQuestionblockQuestionIds($block_id) as $qid) { + foreach ($this->object->getQuestionblockQuestionIds((int) ($block_id)) as $qid) { $move_questions[] = $qid; } } @@ -418,7 +422,7 @@ protected function insertQuestions( } } if (!$insert_id && preg_match("/^cb_qb_(\d+)$/", $target, $matches)) { - $ids = $this->object->getQuestionblockQuestionIds($matches[1]); + $ids = $this->object->getQuestionblockQuestionIds((int) $matches[1]); if (count($ids)) { if ($insert_mode === 0) { $insert_id = $ids[0]; @@ -475,7 +479,7 @@ public function removeQuestionsForm( $cgui->addItem( "q_id[]", - $data["question_id"], + (string) $data["question_id"], $type . ": " . $data["title"] ); } elseif ((in_array($data["questionblock_id"], $checked_questionblocks))) { @@ -483,13 +487,13 @@ public function removeQuestionsForm( $cgui->addItem( "cb[" . $data["questionblock_id"] . "]", - $data["questionblock_id"], + (string) $data["questionblock_id"], $data["questionblock_title"] . " - " . $type . ": " . $data["title"] ); } elseif (in_array($data["question_id"], $checked_headings)) { $cgui->addItem( "heading[" . $data["question_id"] . "]", - $data["question_id"], + (string) $data["question_id"], $data["heading"] ); } @@ -524,7 +528,7 @@ public function copyQuestionsToPoolObject(): void // gather questions from blocks $copy_questions = $items["questions"]; foreach ($items["blocks"] as $block_id) { - foreach ($this->object->getQuestionblockQuestionIds($block_id) as $qid) { + foreach ($this->object->getQuestionblockQuestionIds((int) $block_id) as $qid) { $copy_questions[] = $qid; } } @@ -533,7 +537,7 @@ public function copyQuestionsToPoolObject(): void // only if not already in pool if (count($copy_questions)) { foreach ($copy_questions as $idx => $question_id) { - $question = ilObjSurvey::_instanciateQuestion($question_id); + $question = ilObjSurvey::_instanciateQuestion((int) $question_id); if ($question->getOriginalId()) { unset($copy_questions[$idx]); } @@ -691,7 +695,7 @@ public function changeDatatypeObject(): void { $ilUser = $this->user; - $ilUser->writePref('svy_insert_type', $this->request->getDataType()); + $ilUser->writePref('svy_insert_type', (string) $this->request->getDataType()); switch ($this->request->getDataType()) { case 2: @@ -893,7 +897,7 @@ protected function initQuestionblockForm( if ($a_question_ids) { foreach ($a_question_ids as $q_id) { $hidden = new ilHiddenInputGUI("qids[]"); - $hidden->setValue($q_id); + $hidden->setValue((string) $q_id); $form->addItem($hidden); } } @@ -915,9 +919,9 @@ public function saveDefineQuestionblockObject(): void $form = $this->initQuestionblockForm($block_id); if ($form->checkInput()) { $title = $form->getInput("title"); - $show_questiontext = $form->getInput("show_questiontext"); - $show_blocktitle = $form->getInput("show_blocktitle") ; - $compress_view = $form->getInput("compress_view") ; + $show_questiontext = (bool) $form->getInput("show_questiontext"); + $show_blocktitle = (bool) $form->getInput("show_blocktitle"); + $compress_view = (bool) $form->getInput("compress_view"); if ($block_id) { $this->object->modifyQuestionblock( $block_id, @@ -962,6 +966,8 @@ protected function initHeadingForm( $heading->setRows(10); $heading->setCols(80); $heading->setRequired(true); + $heading->usePurifier(true); + $heading->setPurifier($this->purifier); $form->addItem($heading); $insertbefore = new ilSelectInputGUI($this->lng->txt("insert"), "insertbefore"); @@ -1027,12 +1033,11 @@ public function saveHeadingObject(): void $form = $this->initHeadingForm($q_id); if ($form->checkInput()) { - $purifier = new ilSvyStandardPurifier(); - $heading = $form->getInput("heading"); - - $heading = $purifier->purify($heading); - - $this->object->saveHeading($heading, $form->getInput("insertbefore")); + $heading = $this->purifier->purify($form->getInput("heading")); + $this->object->saveHeading( + $heading, + (int) $form->getInput("insertbefore") + ); $this->ctrl->redirect($this, "questions"); } diff --git a/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php b/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php index 5bc33bcd02bf..3568823bf0ef 100755 --- a/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php +++ b/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php @@ -39,13 +39,15 @@ class SettingsFormGUI protected \ILIAS\Survey\Mode\FeatureConfig $feature_config; protected \ilRbacSystem $rbacsystem; private \ilGlobalTemplateInterface $main_tpl; + protected \ilHtmlPurifierInterface $purifier; public function __construct( InternalGUIService $ui_service, InternalDomainService $domain_service, \ilObjectService $object_service, \ilObjSurvey $survey, - UIModifier $modifier + UIModifier $modifier, + \ilHtmlPurifierInterface $purifier ) { global $DIC; $this->main_tpl = $DIC->ui()->mainTemplate(); @@ -58,6 +60,7 @@ public function __construct( $this->domain_service = $domain_service; $this->modifier = $modifier; $this->feature_config = $this->domain_service->modeFeatureConfig($survey->getMode()); + $this->purifier = $purifier; } public function checkForm(\ilPropertyFormGUI $form): bool @@ -333,6 +336,9 @@ public function withBeforeStart( $intro->setUseRte(true); $intro->setRteTagSet("mini"); } + $intro->usePurifier(true); + $intro->setPurifier(new \ilSvyStandardPurifier()); + $form->addItem($intro); return $form; @@ -451,6 +457,8 @@ public function withAfterEnd( $finalstatement->setUseRte(true); $finalstatement->setRteTagSet("mini"); } + $finalstatement->usePurifier(true); + $finalstatement->setPurifier(new \ilSvyStandardPurifier()); $form->addItem($finalstatement); // mail notification @@ -884,14 +892,10 @@ public function saveForm( } else { $survey->setEndDate(""); } + $introduction = $this->purifier->purify($form->getInput('introduction')); - $purifier = new ilSvyStandardPurifier(); - - $introduction = $form->getInput("introduction"); - $introduction = $purifier->purify($introduction); $survey->setIntroduction($introduction); - $outro = $form->getInput("outro"); - $outro = $purifier->purify($outro); + $outro = $this->purifier->purify($form->getInput('outro')); $survey->setOutro($outro); $survey->setShowQuestionTitles((bool) $form->getInput("show_question_titles")); diff --git a/components/ILIAS/Survey/Settings/class.UIFactory.php b/components/ILIAS/Survey/Settings/class.UIFactory.php index c75f313f2035..e3394524deab 100755 --- a/components/ILIAS/Survey/Settings/class.UIFactory.php +++ b/components/ILIAS/Survey/Settings/class.UIFactory.php @@ -48,7 +48,8 @@ public function __construct( $this->domain_service, $object_service, $survey, - $mode_ui_modifier + $mode_ui_modifier, + new \ilSvyStandardPurifier() ); } diff --git a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyQuestionGUI.php b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyQuestionGUI.php index 072870353935..62122aa57bbb 100755 --- a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyQuestionGUI.php +++ b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyQuestionGUI.php @@ -16,6 +16,8 @@ * *********************************************************************/ +declare(strict_types=1); + use ILIAS\SurveyQuestionPool\Editing\EditingGUIRequest; use ILIAS\SurveyQuestionPool\Editing\EditManager; @@ -44,6 +46,7 @@ abstract class SurveyQuestionGUI protected string $parent_url = ""; protected ilLogger $log; public ?SurveyQuestion $object = null; + protected \ilHtmlPurifierInterface $purifier; public function __construct($a_id = -1) { @@ -90,6 +93,7 @@ public function __construct($a_id = -1) ->editing(); $this->gui = $DIC->survey()->internal()->gui(); $this->domain = $DIC->survey()->internal()->domain(); + $this->purifier = new ilSvyStandardPurifier(); } abstract protected function initObject(): void; @@ -264,11 +268,13 @@ protected function initEditForm(): ilPropertyFormGUI $question->setUseRte(true); $question->setRteTagSet("mini"); } + $question->usePurifier(true); + $question->setPurifier($this->purifier); $form->addItem($question); // obligatory $shuffle = new ilCheckboxInputGUI($this->lng->txt("obligatory"), "obligatory"); - $shuffle->setValue(1); + $shuffle->setValue("1"); $shuffle->setRequired(false); $form->addItem($shuffle); @@ -329,13 +335,11 @@ protected function saveForm(): bool $this->object->setAuthor($form->getInput("author")); $this->object->setDescription($form->getInput("description")); - $purifier = new ilSvyStandardPurifier(); - $question = $form->getInput("question"); - - $question = $purifier->purify($question); + $this->object->setQuestiontext( + $this->purifier->purify($form->getInput("question")) + ); - $this->object->setQuestiontext($question); - $this->object->setObligatory($form->getInput("obligatory")); + $this->object->setObligatory((bool) $form->getInput("obligatory")); $this->importEditFormValues($form); From c1acdac5d63f9f7a8fd60def5afb185e8a95db78 Mon Sep 17 00:00:00 2001 From: Abraham Date: Wed, 8 Jul 2026 10:42:17 +0200 Subject: [PATCH 051/333] [Survey] fix: Export pdf format (#11733) --- .../templates/default/tpl.il_svy_svy_results_details.html | 2 +- .../templates/default/tpl.svy_results_details_panel.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Survey/Evaluation/templates/default/tpl.il_svy_svy_results_details.html b/components/ILIAS/Survey/Evaluation/templates/default/tpl.il_svy_svy_results_details.html index 902e2272d0f3..4247bcd3412e 100755 --- a/components/ILIAS/Survey/Evaluation/templates/default/tpl.il_svy_svy_results_details.html +++ b/components/ILIAS/Survey/Evaluation/templates/default/tpl.il_svy_svy_results_details.html @@ -1,2 +1,2 @@ -
    {PANEL_TOC}
    +
    {PANEL_TOC}
    {PANEL_REPORT} diff --git a/components/ILIAS/Survey/Evaluation/templates/default/tpl.svy_results_details_panel.html b/components/ILIAS/Survey/Evaluation/templates/default/tpl.svy_results_details_panel.html index fb77d8bd7b0f..0d8dc0365099 100755 --- a/components/ILIAS/Survey/Evaluation/templates/default/tpl.svy_results_details_panel.html +++ b/components/ILIAS/Survey/Evaluation/templates/default/tpl.svy_results_details_panel.html @@ -1,5 +1,5 @@ -
    +
    {TABLE} {CHART} {TEXT} From 03d40f35a53e172130e4c13f460868bf266aee7f Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 23 Jun 2026 12:02:12 +0200 Subject: [PATCH 052/333] [Bugfix] Badge: Mark Badges without Images as migrated --- .../classes/Setup/class.ilBadgeTemplatesFilesMigration.php | 7 +++++++ .../Badge/classes/Setup/class.ilBadgesFilesMigration.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/components/ILIAS/Badge/classes/Setup/class.ilBadgeTemplatesFilesMigration.php b/components/ILIAS/Badge/classes/Setup/class.ilBadgeTemplatesFilesMigration.php index 25a93f60a0e9..cf1e5599d142 100644 --- a/components/ILIAS/Badge/classes/Setup/class.ilBadgeTemplatesFilesMigration.php +++ b/components/ILIAS/Badge/classes/Setup/class.ilBadgeTemplatesFilesMigration.php @@ -115,6 +115,13 @@ public function step(Environment $environment): void . ' (table: ' . self::TABLE_NAME . ') because no image is set.', true ); + $this->helper->getDatabase()->update( + self::TABLE_NAME, + [ + 'image_rid' => [ilDBConstants::T_TEXT, '-'], + ], + ['id' => [ilDBConstants::T_INTEGER, $id]] + ); } } diff --git a/components/ILIAS/Badge/classes/Setup/class.ilBadgesFilesMigration.php b/components/ILIAS/Badge/classes/Setup/class.ilBadgesFilesMigration.php index d331f24be9c8..3507e40c1350 100644 --- a/components/ILIAS/Badge/classes/Setup/class.ilBadgesFilesMigration.php +++ b/components/ILIAS/Badge/classes/Setup/class.ilBadgesFilesMigration.php @@ -112,6 +112,13 @@ public function step(Environment $environment): void . ' (table: ' . self::TABLE_NAME . ') because no image is set.', true ); + $this->helper->getDatabase()->update( + self::TABLE_NAME, + [ + 'image_rid' => [ilDBConstants::T_TEXT, '-'], + ], + ['id' => [ilDBConstants::T_INTEGER, $id]] + ); } } From 1b23fca6ae83538f79352099d9468eb34605dd3d Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Wed, 8 Jul 2026 16:47:20 +0200 Subject: [PATCH 053/333] 47633: TOC shows chapters with no active pages --- .../LearningModule/Presentation/class.ilLMTOCExplorerGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/LearningModule/Presentation/class.ilLMTOCExplorerGUI.php b/components/ILIAS/LearningModule/Presentation/class.ilLMTOCExplorerGUI.php index be7d81494781..bb532a804db2 100755 --- a/components/ILIAS/LearningModule/Presentation/class.ilLMTOCExplorerGUI.php +++ b/components/ILIAS/LearningModule/Presentation/class.ilLMTOCExplorerGUI.php @@ -75,7 +75,7 @@ public function __construct( $this->export_all_languages = $export_all_languages; $this->activation_repo = new ilPageActivationDBRepository(); - + $this->setPreloadChilds(false); $this->initTreeData(); } From 6b977511f1eab7de7fa026bfe1bb75620ce869f2 Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Wed, 8 Jul 2026 18:00:14 +0200 Subject: [PATCH 054/333] 0047549: Member View disappears when you click on a folder in a course and the slide displays the repository. --- .../Container/MemberView/class.ilMemberViewSettings.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/components/ILIAS/Container/MemberView/class.ilMemberViewSettings.php b/components/ILIAS/Container/MemberView/class.ilMemberViewSettings.php index 56c95c9b71ae..d978ca4ebc74 100755 --- a/components/ILIAS/Container/MemberView/class.ilMemberViewSettings.php +++ b/components/ILIAS/Container/MemberView/class.ilMemberViewSettings.php @@ -180,15 +180,6 @@ protected function read(): void $this->active = true; $this->container = (int) ilSession::get(self::SESSION_MEMBER_VIEW_CONTAINER); $this->container_items = $this->tree->getSubTreeIds($this->getContainer()); - - // deactivate if out of scope - if ( - $this->getCurrentRefId() && - !in_array($this->getCurrentRefId(), $this->container_items) && - $this->getCurrentRefId() !== $this->getContainer() - ) { - $this->deactivate(); - } } } From c86440aebee15fbf51b0da3c79cbfee535dad259 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Wed, 8 Jul 2026 18:51:14 +0200 Subject: [PATCH 055/333] [FIX] COPage: 47862: Insert Code: Missing buttons Insert/Cancel --- components/ILIAS/COPage/Editor/Server/class.UIWrapper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/COPage/Editor/Server/class.UIWrapper.php b/components/ILIAS/COPage/Editor/Server/class.UIWrapper.php index 2378e21ae1ec..3b9a9268f63d 100755 --- a/components/ILIAS/COPage/Editor/Server/class.UIWrapper.php +++ b/components/ILIAS/COPage/Editor/Server/class.UIWrapper.php @@ -248,8 +248,9 @@ public function getRenderedAdapterForm( } $html = $form->render(); $tag = "button"; + /* not necessary, since top buttons have been removed in general for forms $del_count = $in_modal ? 2 : 1; - $html = preg_replace("#\\<" . $tag . "([^>]*)btn-default(.*)/" . $tag . ">#iUs", "", $html, $del_count); + $html = preg_replace("#\\<" . $tag . "([^>]*)btn-default(.*)/" . $tag . ">#iUs", "", $html, $del_count);*/ $footer_pos = stripos($html, "il-standard-form-footer"); if (!is_int($footer_pos)) { $footer_pos = stripos($html, "c-form__footer"); From 497cb68e818461d99c49363d397e68ddc649ad6a Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Wed, 8 Jul 2026 23:56:13 +0200 Subject: [PATCH 056/333] 47946: Link areas do not display tooltip --- .../ILIAS/MediaObjects/js/src/presentation/presentation.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/ILIAS/MediaObjects/js/src/presentation/presentation.js b/components/ILIAS/MediaObjects/js/src/presentation/presentation.js index b115f5900246..2a3065faa847 100755 --- a/components/ILIAS/MediaObjects/js/src/presentation/presentation.js +++ b/components/ILIAS/MediaObjects/js/src/presentation/presentation.js @@ -35,6 +35,7 @@ const presentation = (function () { const coordsAtt = areaEl.getAttribute('coords'); const hlMode = areaEl.getAttribute('data-hl-mode'); const hlClass = areaEl.getAttribute('data-hl-class'); + const title = areaEl.getAttribute('title'); const area = areaFactory.area( shapeAtt, coordsAtt, @@ -47,6 +48,11 @@ const presentation = (function () { shapeEl.classList.add(`copg-iim-hl-class-${hlClass}`); shapeEl.style.pointerEvents = 'auto'; shapeEl.style.cursor = 'pointer'; + if (title) { + const titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title'); + shapeEl.prepend(titleEl); + titleEl.textContent = title; + } shapeEl.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); From c8404c93f6f84bc633d252b7e0ee4122c54020af Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Thu, 9 Jul 2026 09:47:42 +0200 Subject: [PATCH 057/333] blog: fixed parameter --- components/ILIAS/Blog/Editing/EditingGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Blog/Editing/EditingGUI.php b/components/ILIAS/Blog/Editing/EditingGUI.php index a65691edc7f4..5470bf43c4ff 100644 --- a/components/ILIAS/Blog/Editing/EditingGUI.php +++ b/components/ILIAS/Blog/Editing/EditingGUI.php @@ -47,7 +47,7 @@ public function __construct( protected int $node_id, protected int $id_type, protected PermissionManager $perm, - protected ?string $current_month = null, + protected ?string $current_month, protected \ILIAS\Style\Content\Object\ObjectFacade $content_style_domain, protected ilObjBlogGUI $parent_gui ) { From 74d0bf1067b198d65ae832a0b62c693160bd57fb Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Fri, 10 Jul 2026 12:12:11 +0200 Subject: [PATCH 058/333] Staff, EmployeeTalk: add PRIVACY.md --- components/ILIAS/EmployeeTalk/PRIVACY.md | 186 ++++++++++++++++++++++ components/ILIAS/MyStaff/PRIVACY.md | 191 +++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 components/ILIAS/EmployeeTalk/PRIVACY.md create mode 100644 components/ILIAS/MyStaff/PRIVACY.md diff --git a/components/ILIAS/EmployeeTalk/PRIVACY.md b/components/ILIAS/EmployeeTalk/PRIVACY.md new file mode 100644 index 000000000000..08f28738a96e --- /dev/null +++ b/components/ILIAS/EmployeeTalk/PRIVACY.md @@ -0,0 +1,186 @@ +# EmployeeTalk Privacy + +**Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing +or incorrect information via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories).** + +## Integrated Components + +The **EmployeeTalk** component integrates various ILIAS components. Please consult the respective privacy documentation: + +- [OrgUnit](../OrgUnit/PRIVACY.md) provides information about accounts' assignment to **organisational units**, + what accounts they have authority over, and which permissions this authority grants them via + the **position access** mechanism. +- [AdvancedMetadata](../AdvancedMetaData/PRIVACY.md) provides **custom metadata sets** that can be + attached to talk templates, and filled out for talks. +- **Calendar** is used to add talks as appointments to personal calendars. +- [Mail](../Mail/PRIVACY.md) is used to send notifications about talks. +- **User** handles **account identification**, provides names, logins, and email addresses from the **personal profile data** of accounts, + as well as their **personal preferences**, and is used to register a 'User action'. +- **ILIASObject**, [Container](../Container/PRIVACY.md), and **Tree** are used + to handle the hierarchy of template administration, templates, talk series, + and talks, as well as their owners, titles, and descriptions. +- [AccessControl](../AccessControl/PRIVACY.md) is used to check role-based permissions. +- [Staff](../MyStaff/PRIVACY.md) is used to check access via **OrgUnit** positions to the talk views within + the 'Organisation' main menu entry. +- [InfoScreen](../InfoScreen/PRIVACY.md) to offer an 'Info'-tab in templates, + the template administration, and in talks. + +## General Information + +Talks carry a 'location' field (handled by **EmployeeTalk** itself), +a title and description (handled by **ILIASObject**), +as well as further fields from attached **Custom Metadata** +sets (if configured). The latter are intended to be used as minutes for +the talks. All of these fields are filled out manually, but are likely +to contain personal data of at least the employee involved in the talk. + +Note that talks share their title with the series they are a part of. +The title is stored redundantly, again by **ILIASObject**. + +### Access to Talks + +Access in **EmployeeTalk** depends mostly on the position access configuration +of **OrgUnit**, and its concept of authority. The corresponding permissions in the +**OrgUnit** position administration are 'Read access talk appointments', +'Create talk appointments / edit talk appointments that you have created yourself', +and 'Edit Talk appointments', and relate to access to talks as follows: + +- **Create:** The current account has the 'Create talk appointments / edit talk appointments that you have created yourself' + permission over the employee. Access to the creation dialogue is granted if + the account has any position with that permission. +- **Read:** Does not affect whether a talk is shown in the [talk list](#talk-list). + The current account is either superior or employee of the talk, or they have the + 'Read access talk appointments' permission over the employee. +- **Edit:** The current account is either superior of the talk, or they have the + 'Edit Talk appointments' permission over the employee. If the setting + 'Lock the editing of all appointments in this series' is enabled in + the series, only the superior can edit. +- **Delete:** The current account is superior of the talk and has read-access to the talk template + administration via RBAC (AccessControl), or they have the global 'Administrator' role. + +Note that the root account can always access every talk, and can always create +a talk. If position access for 'Employee Talks' is deactivated, only the root +account can access and create talks. + +## Data being stored + +**EmployeeTalk** itself stores the following data for each talk: + +- **Superior** and **Employee** of the talk, by their user ID. +- **Start Date** and **End Date** of the talk, including whether it is an all day event. +- **Location** of the talk. +- Whether the talk has been **completed** already. + +## Data being presented + +### Talk List + +A list of talks is offered under the same main menu entry as the various +views of the **Staff** component, and is available +under similar conditions: 'Enable Main Menu Entry' must be +enabled in the **OrgUnit** settings, and the current account must have +at least one of the 'Employee Talk' position access permissions over at least one account under their authority. + +The list contains all talks where the current account, or an account they +have the 'Read access talk appointments' permission over, is superior +or employee. The following data is shown for every talk: + +- **Title** of the talk. +- **Superior** and **Employee** of the talk, by their login. If the corresponding + user ID doesn't exist, for example because the account was deleted, 'Unknown User' + is shown instead +- **Start Date** and **End Date** of the talk, with time if applicable. +- Completion **Status** of the talk. + +Aside from the main talk list, an account-specific talk list is +offered. It is identical to the main list (in content and conditions of +access), except that it only contains talks where the selected +account is employee. It is only available under the additional condition +that the current user has authority over the selected user. + +From the talk lists, talks can be created if the current account +has [create access](#access-to-talks). There, an autocomplete +is offered when typing in the employee. Suggestions are only shown +after at least three letters are already entered, and only +accounts which the current account can [create talks for](#access-to-talks) +are shown. + +### Talk + +In the talks themselves, in addition what is included in the +[talk list](#talk-list), the following data is shown: + +- **Title** and **Description** of the talk. +- **Location** of the talk. +- The attached **Custom Metadata** fields. +- The **Info**-tab of the talk. There the superior of the talk + is listed as its **owner**, but only to accounts with the global + 'Administrator' role (and the superior themselves). They are identified via their **login**. + If their personal profile is published via the User component, + their **first name**, **last name**, and a **link to their profile** are + also shown. + +This data is available to accounts with [read or edit access](#access-to-talks) +to the talk. Accounts with the latter can also edit these fields +(except for **Superior** and **Employee**, which can't be changed at all). + +Additionally, editing the talk allows enabling the setting +'Lock the editing of all appointments in this series', with which +one can prevent editing of talks in the same series by anyone +but the superior themselves. + +### Calendar Appointments + +The employee and superior of a talk have the talk as an appointment +in their personal calendar, only accessible by them, with the following data: + +- **Superior** and **Employee** of the talk, by first and last name. + If the account's profile is published via the **User** component, + their personal profile is linked. +- **Date and Time of the last change** of the talk's date and time. + +Additionally, the [talk](#talk) itself is linked. + +### Notifications + +When a talk (series) is created, deleted, or its settings or dates changed, +a system notification is sent to the employee, with the superior in CC. +This notification includes the following data: + +- **Title** and **Description** of the talks. +- **Location** of the talks. +- **Superior** of the talks by full name, including title if available, + as given by the **User** component. +- **Start Date** and **End Date** of the talks, with time if applicable. + +The notification also includes a link to the first [talk](#talk) in the series. Attached +to the notification is an ics-file, which includes in addition to the above: + +- **Employee** of the talks by full name, including title if available, + as given by the **User** component. + +With the ics-file, the talk series can be imported as events into external +calendar applications. + +## Data being deleted + +Talks can be deleted via the [talk list](#talk-list) (both main +and account-specific), by accounts with [delete access](#access-to-talks). + +Talks circumvent the trash, they are always removed permanently +from the system. When a talk is deleted, the [data stored](#data-being-stored) +by the component itself is deleted, as well as the corresponding +data handled by **ILIASObject**, and the appointments in personal +calendars. + +When the last talk in a series is deleted, the series is deleted +along with it. + +Note that deletion of an account does not trigger the deletion of +talks with that account as superior or employee. Their user ID +is stored with those talks until the talks are deleted. + +## Data being exported + +[Notifications](#notifications) about talks include ics-files as +attachments, see above for details. diff --git a/components/ILIAS/MyStaff/PRIVACY.md b/components/ILIAS/MyStaff/PRIVACY.md new file mode 100644 index 000000000000..053d5c43e5ba --- /dev/null +++ b/components/ILIAS/MyStaff/PRIVACY.md @@ -0,0 +1,191 @@ +# Staff Privacy + +**Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing +or incorrect information via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories).** + +## Integrated Components + +The **Staff** component aggregates data from various ILIAS components. Please consult the respective privacy documentation: + +- [OrgUnit](../OrgUnit/PRIVACY.md) provides information about accounts' assignment to **organisational units**, + what accounts they have authority over, and which data this authority grants access to via + the **position access** mechanism. +- [Skill](../Skill/PRIVACY.md) provides information about **achieved skill levels**, and a pre-built UI + showing that information for a given account. +- **User** handles **account identification**, provides **personal profile data** and whether a profile + is **published**, and provides a list of 'User Actions'. +- **Tree** is used together with **ILIASObject** to check the hierarchy of **organisational units**. +- **ILIASObject** is also used to retrieve **titles** of courses. +- [AccessControl](../AccessControl/PRIVACY.md) is used to check permissions to decide whether objects can be linked to. +- **Tracking** provides information about the **learning progress status** of accounts in courses. +- [Course](../Course/PRIVACY.md) together with **Membership** provides information about **enrolment status + in courses** of accounts. +- [Certificate](../Certificate/PRIVACY.md) provides information about **awarded certificates**, + and a pre-built UI showing that information for a given account. + +## General Information + +What data is shown in **Staff** depends heavily on the position access configuration +of **OrgUnit**. + +The component provides a number of reporting views to give accounts +an overview over the status of everyone they have authority over +(as defined via organisational units). This can include personal profile +data, enrolment and learning progress status in courses, +achieved skill levels, and awarded certificates. + +Note that a view by [EmployeeTalk](../EmployeeTalk/PRIVACY.md) is offered in the same main +menu entry, see that component for the related privacy information. + +## Data being stored + +The component does not store any data itself, it only aggregates +data stored elsewhere. + +## Data being presented + +The component offers different views that show different data +of all accounts that the current account has authority over via the +organisational units. + +Additionally, views that report individually on single accounts +under the authority of the current account are also available, +see [below](#account-specific-views). + +All views are only available if 'Enable Main Menu Entry' is +enabled in the **OrgUnit** settings, and if the current account +has authority over at least one account. + +### Staff List + +The Staff List presents the following **personal profile** data of +all relevant accounts: + +- **Profile Picture** +- **Login** +- All other **personal profile fields** of type 'Default' + and set to 'Searchable' in the **User** profile administration. + +Further, a selection of 'User Actions' is available, +which exposes the accounts' **user ID**. + +### Course Memberships + +'Course Memberships' is only available if the current account has the +'Manage Members' permission in courses over at least one account under +their authority, as granted by position access in the **OrgUnit** +administration. This permission can be set as a default per position, +but can also be overwritten locally in individual courses. + +The view shows a table of those **course enrolments** of accounts where +the current account has the 'Manage Members' permission over that +account in that course. Included is the following data: + +- **Title** of the course. +- **Login** of the account. +- **First Name**, **Last Name**, **E-Mail**, and **Organisational Units** + of the account, only if the corresponding **personal profile field** is + set to 'Searchable' via the **User** component. +- **Member Status** of the account in the course: 'Registered', 'Waiting List', + 'Requested' +- **Learning Progress** status of the account in the course. Only shown if + learning progress is active on the installation, and the + current account has the 'View learning progress of other users' + position permission over the account in the course. + +### Certificates + +'Certificates' is only available if certificates are activated +on the installation. Also, the current account must have the +'View certificates of other users' permission in courses, exercises, +or tests over at least one account under their authority, as granted +by position access in the **OrgUnit** administration. This permission +can be set as a default per position, but can also be overwritten locally in +individual courses, exercises, and tests. + +The view presents data related to certificates achieved by accounts in +courses, exercises, or tests. For a certificate to appear, +the current account must have the 'View certificates of other users' +permission over that account in that object. + +A table of all such **certificates** of those accounts is shown, +with the following data: + +- **Title** of the object in which the certificate was awarded. +- **Issued On:** Date on which the certificate was awarded. +- **Login** of the account to which the certificate was awarded. +- **First Name**, **Last Name**, **E-Mail**, and **Organisational Units** + of the account, only if the corresponding **personal profile field** is + set to 'Searchable' via the **User** component. + +### Competences + +'Competences' is only available if competence management is +activated on the installation. Also, the current account must have the +'View competences of other users' permission in courses, groups, surveys, +or tests over at least one account under their authority, as granted +by position access in the **OrgUnit** administration. This permission +can be set as a default per position, but can also be overwritten locally in +individual courses, groups, surveys, and tests. + +It presents data related to competences achieved by accounts +in courses, groups, surveys, or tests. For a competence to appear, +the current account must have the 'View competences of other users' +permission over that account in that object. + +A table of all such **certificates** of those accounts is shown, +with the following data: + +- **Competence:** Title of the competence. +- **Competence Level:** Title of the achieved level in the competence. +- **Login** of the account. +- **First Name**, **Last Name**, **E-Mail**, and **Organisational Units** + of the account, only if the corresponding **personal profile field** is + set to 'Searchable' via the **User** component. + +### Account-Specific Views + +All account-specific views always show the **profile picture** and the **login** +of the selected account. They also show their **first name** and +**last name**, if the account's profile is published. + +The following account-specific views are offered: + +- **Course Memberships:** Same as the [Course Memberships](#course-memberships) overview, + but without the fields **Login**, **First Name**, **Last Name**, + **E-Mail**, and **Organisational Units**. Available under the same conditions. +- **Certificates:** All certificates of the account, regardless of position + access, as presented to the user by the **Certificate** component + via 'Achievements > Certificates'. Available under the same conditions as the + [Certificates](#certificates) overview. +- **Competences:** All competences of the account, regardless of position + access, as presented to the user by the **Skill** component + via 'Achievements > Competences > Selected Competences'. + Available under the same conditions as the [Competences](#competences) overview. +- **Profile:** The profile of the account supplied by the **User** component. Only + available if the account's profile is published. + +Note that a view by [EmployeeTalk](../EmployeeTalk/PRIVACY.md) is also +offered here, see that component for the related privacy information. + +## Data being deleted + +The component does not store any data itself. Its behaviour when +deleting e.g. accounts or courses depends on the behaviour of the +related integrated component. + +## Data being exported + +Many of the tables offered in the component can be exported +in Excel or CSV format. The data being exported matches the +data shown in the UI, see [above](#data-being-presented). + +The following views contain exportable tables: + +- Staff List +- Course Membership (both the overview, and the account-specific view) +- Certificates (overview only) +- Competences (overview only) + +Additionally, in the 'Certificates' views (account-specific and +overview) the certificates awarded to accounts can be downloaded. From 0fc1d13c41d645c5ff0567fe74d01fac1009e3a9 Mon Sep 17 00:00:00 2001 From: Fabian Helfer <82493694+fhelfer@users.noreply.github.com> Date: Mon, 13 Jul 2026 11:04:19 +0200 Subject: [PATCH 059/333] [Bugfix] Mail 047999: Fix CC/BCC Mails Generic Placeholder not replaced with corresponding value (#11713) --- .../Mail/classes/Service/MailService.php | 9 -- .../ILIAS/Mail/classes/class.ilMail.php | 14 +-- .../classes/class.ilMailTemplateContext.php | 83 ++++++++++++++- ...MailTemplatePlaceholderToEmptyResolver.php | 32 ------ .../MailTemplateContextAdapter.php | 8 ++ ...TemplatePlaceholderToEmptyResolverTest.php | 100 ++++++++++++++++++ components/ILIAS/Mail/tests/ilMailTest.php | 1 - 7 files changed, 190 insertions(+), 57 deletions(-) delete mode 100755 components/ILIAS/Mail/classes/class.ilMailTemplatePlaceholderToEmptyResolver.php create mode 100644 components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php diff --git a/components/ILIAS/Mail/classes/Service/MailService.php b/components/ILIAS/Mail/classes/Service/MailService.php index 44e029ffa7d7..0fb87a845c05 100755 --- a/components/ILIAS/Mail/classes/Service/MailService.php +++ b/components/ILIAS/Mail/classes/Service/MailService.php @@ -28,7 +28,6 @@ use ilMailTemplateServiceInterface; use ILIAS\Data\Factory as DataFactory; use ilMailTemplatePlaceholderResolver; -use ilMailTemplatePlaceholderToEmptyResolver; use ILIAS\Mail\Autoresponder\AutoresponderService; use ILIAS\Mail\Autoresponder\AutoresponderServiceImpl; use ILIAS\Mail\Autoresponder\AutoresponderDatabaseRepository; @@ -104,10 +103,6 @@ public static function init(Container $container): void ); }; - $container[ilMailTemplatePlaceholderToEmptyResolver::class] = static function (Container $c): ilMailTemplatePlaceholderToEmptyResolver { - return new ilMailTemplatePlaceholderToEmptyResolver(); - }; - $container['mail.template_engine.factory'] = static function (Container $c): MustacheTemplateEngineFactory { return new MustacheTemplateEngineFactory(); }; @@ -142,10 +137,6 @@ public function placeholderResolver(): ilMailTemplatePlaceholderResolver return $this->dic[ilMailTemplatePlaceholderResolver::class]; } - public function placeholderToEmptyResolver(): ilMailTemplatePlaceholderToEmptyResolver - { - return $this->dic[ilMailTemplatePlaceholderToEmptyResolver::class]; - } public function templateEngineFactory(): TemplateEngineFactoryInterface { diff --git a/components/ILIAS/Mail/classes/class.ilMail.php b/components/ILIAS/Mail/classes/class.ilMail.php index a3c04849cf89..c0ac9028415f 100755 --- a/components/ILIAS/Mail/classes/class.ilMail.php +++ b/components/ILIAS/Mail/classes/class.ilMail.php @@ -74,7 +74,6 @@ public function __construct( private ?int $mail_obj_ref_id = null, private ?ilObjUser $actor = null, private ?ilMailTemplatePlaceholderResolver $placeholder_resolver = null, - private ?ilMailTemplatePlaceholderToEmptyResolver $placeholder_to_empty_resolver = null, ?Conductor $legal_documents = null, ?MailSignatureService $signature_service = null, ) { @@ -102,7 +101,6 @@ public function __construct( $this->table_mail_saved = 'mail_saved'; $this->setSaveInSentbox(false); $this->placeholder_resolver = $placeholder_resolver ?? $DIC->mail()->placeholderResolver(); - $this->placeholder_to_empty_resolver = $placeholder_to_empty_resolver ?? $DIC->mail()->placeholderToEmptyResolver(); $this->legal_documents = $legal_documents ?? $DIC['legalDocuments']; $this->signature_service = $signature_service ?? $DIC->mail()->signature(); $this->refinery = $DIC->refinery(); @@ -616,7 +614,7 @@ private function sendInternalMail( private function replacePlaceholders( string $message, - int $usr_id = 0 + ?int $usr_id = null ): string { try { if ($this->context_id) { @@ -625,7 +623,7 @@ private function replacePlaceholders( $context = new ilMailTemplateGenericContext(); } - $user = $usr_id > 0 ? $this->getUserInstanceById($usr_id) : null; + $user = ($usr_id !== null && $usr_id > 0) ? $this->getUserInstanceById($usr_id) : null; $message = $this->placeholder_resolver->resolve( $context, $message, @@ -644,10 +642,6 @@ private function replacePlaceholders( return $message; } - private function replacePlaceholdersEmpty(string $message): string - { - return $this->placeholder_to_empty_resolver->resolve($message); - } private function distributeMail(MailDeliveryData $mail_data): bool { @@ -709,7 +703,7 @@ private function sendMailWithReplacedEmptyPlaceholder( $this->sendChanneledMails( $mail_data, $recipients, - $this->replacePlaceholdersEmpty($mail_data->getMessage()), + $this->replacePlaceholders($mail_data->getMessage()), ); } @@ -1193,7 +1187,7 @@ public function sendMail( $external_eail_recipients_bcc, $mail_data->getSubject(), $mail_data->isUsePlaceholder() ? - $this->replacePlaceholders($mail_data->getMessage(), 0) : + $this->replacePlaceholders($mail_data->getMessage()) : $mail_data->getMessage(), $mail_data->getAttachments() ); diff --git a/components/ILIAS/Mail/classes/class.ilMailTemplateContext.php b/components/ILIAS/Mail/classes/class.ilMailTemplateContext.php index 12b002411c44..1001ab0af517 100755 --- a/components/ILIAS/Mail/classes/class.ilMailTemplateContext.php +++ b/components/ILIAS/Mail/classes/class.ilMailTemplateContext.php @@ -18,8 +18,8 @@ declare(strict_types=1); -use OrgUnit\PublicApi\OrgUnitUserService; use OrgUnit\User\ilOrgUnitUser; +use OrgUnit\PublicApi\OrgUnitUserService; abstract class ilMailTemplateContext { @@ -53,7 +53,16 @@ abstract public function getTitle(): string; abstract public function getDescription(): string; /** - * @return array{mail_salutation: array{placeholder: string, label: string, supportsNestedPlaceholders?: true}, first_name: array{placeholder: string, label: string}, last_name: array{placeholder: string, label: string}, login: array{placeholder: string, label: string}, title: array{placeholder: string, label: string, supportsCondition: true}, firstname_lastname_superior: array{placeholder: string, label: string}, ilias_url: array{placeholder: string, label: string}, installation_name: array{placeholder: string, label: string}} + * @return array{ + * mail_salutation: array{placeholder: string, label: string, requiresRecipient: true, supportsNestedPlaceholders: true}, + * first_name: array{placeholder: string, label: string, requiresRecipient: true}, + * last_name: array{placeholder: string, label: string, requiresRecipient: true}, + * login: array{placeholder: string, label: string, requiresRecipient: true}, + * title: array{placeholder: string, label: string, requiresRecipient: true}, + * firstname_lastname_superior: array{placeholder: string, label: string, requiresRecipient: true}, + * ilias_url: array{placeholder: string, label: string}, + * installation_name: array{placeholder: string, label: string} + * } */ private function getGenericPlaceholders(): array { @@ -61,36 +70,43 @@ private function getGenericPlaceholders(): array 'mail_salutation' => [ 'placeholder' => 'MAIL_SALUTATION', 'label' => $this->getLanguage()->txt('mail_nacc_salutation'), + 'requiresRecipient' => true, 'supportsNestedPlaceholders' => true, ], 'first_name' => [ 'placeholder' => 'FIRST_NAME', 'label' => $this->getLanguage()->txt('firstname'), + 'requiresRecipient' => true, ], 'last_name' => [ 'placeholder' => 'LAST_NAME', 'label' => $this->getLanguage()->txt('lastname'), + 'requiresRecipient' => true, ], 'login' => [ 'placeholder' => 'LOGIN', 'label' => $this->getLanguage()->txt('mail_nacc_login'), + 'requiresRecipient' => true, ], 'title' => [ 'placeholder' => 'TITLE', 'label' => $this->getLanguage()->txt('mail_nacc_title'), - 'supportsCondition' => true, + 'requiresRecipient' => true, ], 'firstname_lastname_superior' => [ 'placeholder' => 'FIRSTNAME_LASTNAME_SUPERIOR', 'label' => $this->getLanguage()->txt('mail_firstname_last_name_superior'), + 'requiresRecipient' => true, ], 'ilias_url' => [ 'placeholder' => 'ILIAS_URL', 'label' => $this->getLanguage()->txt('mail_nacc_ilias_url'), + 'requiresRecipient' => false, ], 'installation_name' => [ 'placeholder' => 'INSTALLATION_NAME', 'label' => $this->getLanguage()->txt('mail_nacc_installation_name'), + 'requiresRecipient' => false, ], ]; } @@ -111,7 +127,12 @@ public function getNestedPlaceholders(): array } /** - * @return array + * @return array */ final public function getPlaceholders(): array { @@ -122,10 +143,62 @@ final public function getPlaceholders(): array } /** - * @return array + * @return array */ abstract public function getSpecificPlaceholders(): array; + public function requiresRecipientByPlaceholderName(string $placeholder_name): bool + { + $found = $this->findPlaceholderByMustacheName($placeholder_name); + if ($found === null) { + return false; + } + + return $this->placeholderDefinitionRequiresRecipient($found['definition']); + } + + /** + * @return array{key: string, definition: array{ + * placeholder: string, + * label: string, + * requiresRecipient?: bool, + * supportsNestedPlaceholders?: bool + * }}|null + */ + private function findPlaceholderByMustacheName(string $placeholder_name): ?array + { + foreach ($this->getPlaceholders() as $key => $placeholder_definition) { + if (strtoupper($placeholder_definition['placeholder']) !== strtoupper($placeholder_name)) { + continue; + } + + return [ + 'key' => (string) $key, + 'definition' => $placeholder_definition, + ]; + } + + return null; + } + + /** + * @param array{ + * placeholder: string, + * label: string, + * requiresRecipient?: bool, + * supportsNestedPlaceholders?: bool + * } $placeholder_definition + */ + private function placeholderDefinitionRequiresRecipient(array $placeholder_definition): bool + { + return $placeholder_definition['requiresRecipient'] ?? false; + } + /** * @param array $context_parameters */ diff --git a/components/ILIAS/Mail/classes/class.ilMailTemplatePlaceholderToEmptyResolver.php b/components/ILIAS/Mail/classes/class.ilMailTemplatePlaceholderToEmptyResolver.php deleted file mode 100755 index fccd7e723113..000000000000 --- a/components/ILIAS/Mail/classes/class.ilMailTemplatePlaceholderToEmptyResolver.php +++ /dev/null @@ -1,32 +0,0 @@ -recipient === null) { + foreach ($this->contexts as $context) { + if ($context->requiresRecipientByPlaceholderName($name)) { + return $name; + } + } + } + return ''; } } diff --git a/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php b/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php new file mode 100644 index 000000000000..a763e348304f --- /dev/null +++ b/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php @@ -0,0 +1,100 @@ +getMockBuilder(ilLanguage::class) + ->disableOriginalConstructor() + ->onlyMethods(['txt', 'loadLanguageModule']) + ->getMock(); + $lng->method('txt')->willReturnArgument(0); + $this->setGlobalVariable('lng', $lng); + ilDatePresentation::setLanguage($lng); + + $env_helper = $this->createMock(ilMailEnvironmentHelper::class); + $env_helper->method('getClientId')->willReturn('phpunit_client'); + $env_helper->method('getHttpPath')->willReturn('https://ilias.example/'); + + $lng_helper = $this->createMock(ilMailLanguageHelper::class); + $lng_helper->method('getCurrentLanguage')->willReturn($lng); + + $context = new class ( + new OrgUnitUserService(), + $env_helper, + new ilMailUserHelper(), + $lng_helper + ) extends ilMailTemplateContext { + public function getId(): string + { + return 'phpunit_context'; + } + + public function getTitle(): string + { + return 'phpunit'; + } + + public function getDescription(): string + { + return 'phpunit'; + } + + public function getSpecificPlaceholders(): array + { + return [ + 'course_title' => [ + 'placeholder' => 'COURSE_TITLE', + 'label' => 'Course Title', + ], + ]; + } + + public function resolveSpecificPlaceholder( + string $placeholder_id, + array $context_parameters, + ilObjUser $recipient = null + ): string { + if ('course_title' === $placeholder_id) { + return 'My Course'; + } + + return ''; + } + }; + + $resolver = new ilMailTemplatePlaceholderResolver(new MustacheTemplateEngine(new \Mustache\Engine())); + $message = 'Hello {{FIRST_NAME}} {{LAST_NAME}}, welcome to {{COURSE_TITLE}} at {{ILIAS_URL}}{{INSTALLATION_NAME}}'; + + $resolved = $resolver->resolve($context, $message, null, ['ref_id' => 123]); + + $this->assertStringContainsString('FIRST_NAME', $resolved); + $this->assertStringContainsString('LAST_NAME', $resolved); + $this->assertStringNotContainsString('{{FIRST_NAME}}', $resolved); + $this->assertStringNotContainsString('{{LAST_NAME}}', $resolved); + $this->assertStringContainsString('My Course', $resolved); + $this->assertStringContainsString('https://ilias.example/', $resolved); + $this->assertStringContainsString('phpunit_client', $resolved); + } +} diff --git a/components/ILIAS/Mail/tests/ilMailTest.php b/components/ILIAS/Mail/tests/ilMailTest.php index af1b17589623..80507a8b0ce1 100755 --- a/components/ILIAS/Mail/tests/ilMailTest.php +++ b/components/ILIAS/Mail/tests/ilMailTest.php @@ -691,7 +691,6 @@ private function create(int $ref_id = 234, int $usr_id = 123): ilMail $this->getMockBuilder(ilObjUser::class)->disableOriginalConstructor()->getMock(), $this->getMockBuilder(ilMailTemplatePlaceholderResolver::class)->disableOriginalConstructor()->getMock(), null, - null, $this->getMockBuilder(MailSignatureService::class)->disableOriginalConstructor()->getMock(), ); From 6dc4b901f7fb6c540d68c356936713afa47671b6 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 13 Jul 2026 11:43:39 +0200 Subject: [PATCH 060/333] =?UTF-8?q?48062:=20Unterschiedliches=20Verhalten?= =?UTF-8?q?=20bei=20Klick=20in=20Aktionen-Men=C3=BC=20von=20Plugins=20und?= =?UTF-8?q?=20Kernfunktionen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ILIAS/Repository/PluginSlot/class.ilObjectPluginListGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Repository/PluginSlot/class.ilObjectPluginListGUI.php b/components/ILIAS/Repository/PluginSlot/class.ilObjectPluginListGUI.php index 3ec73f1a4477..6960341f1cca 100755 --- a/components/ILIAS/Repository/PluginSlot/class.ilObjectPluginListGUI.php +++ b/components/ILIAS/Repository/PluginSlot/class.ilObjectPluginListGUI.php @@ -82,7 +82,7 @@ public function txt(string $a_str): string public function getCommandFrame(string $cmd): string { - return ilFrameTargetInfo::_getFrame("MainContent"); + return ""; } public function getProperties(): array From 9d3189690040f9b314680bd20f1e8b808d7ae3e4 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 13 Jul 2026 12:09:22 +0200 Subject: [PATCH 061/333] Search: show view controls, info on occasionally empty last result page (47885) --- .../Search/classes/GUI/Direct/SearcherImpl.php | 2 +- .../Search/classes/GUI/Lucene/SearcherImpl.php | 2 +- .../Presentation/Result/ResultPresenter.php | 2 +- .../Result/ResultPresenterImpl.php | 18 ++++++++++++++++-- .../Result/UI/ComponentFactory.php | 2 ++ .../Result/UI/ComponentFactoryImpl.php | 5 +++++ lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + 8 files changed, 28 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/Search/classes/GUI/Direct/SearcherImpl.php b/components/ILIAS/Search/classes/GUI/Direct/SearcherImpl.php index 7780781956db..d140e4040cc9 100755 --- a/components/ILIAS/Search/classes/GUI/Direct/SearcherImpl.php +++ b/components/ILIAS/Search/classes/GUI/Direct/SearcherImpl.php @@ -129,7 +129,7 @@ protected function renderResults( string $term, ViewControlInfos $view_control_infos ): void { - if ($results->getResults()) { + if ($results->getResults() || $view_control_infos->currentPage() > 1) { $result_panel_and_modals = $this->presenter->getDirectSearchResultAsPanel( $results, $view_control_infos diff --git a/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php b/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php index 509139c554fc..b3ced2fde54d 100755 --- a/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php +++ b/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php @@ -164,7 +164,7 @@ protected function renderResults( string $term, ViewControlInfos $view_control_infos ): void { - if ($filter->getResults() && $highlighter !== null) { + if ($filter->getResults() || $view_control_infos->currentPage() > 1) { $result_panel_and_modals = $this->presenter->getLuceneSearchResultAsPanel( $filter, $highlighter, diff --git a/components/ILIAS/Search/classes/Presentation/Result/ResultPresenter.php b/components/ILIAS/Search/classes/Presentation/Result/ResultPresenter.php index b19193488890..9522a56aaf2c 100755 --- a/components/ILIAS/Search/classes/Presentation/Result/ResultPresenter.php +++ b/components/ILIAS/Search/classes/Presentation/Result/ResultPresenter.php @@ -43,7 +43,7 @@ public function getDirectSearchResultAsPanel( */ public function getLuceneSearchResultAsPanel( ilLuceneSearchResultFilter $result, - ilLuceneHighlighterResultParser $highlighter, + ?ilLuceneHighlighterResultParser $highlighter, ViewControlInfos $view_control_infos ): array; diff --git a/components/ILIAS/Search/classes/Presentation/Result/ResultPresenterImpl.php b/components/ILIAS/Search/classes/Presentation/Result/ResultPresenterImpl.php index 336fafa2e146..a532f1b3b514 100755 --- a/components/ILIAS/Search/classes/Presentation/Result/ResultPresenterImpl.php +++ b/components/ILIAS/Search/classes/Presentation/Result/ResultPresenterImpl.php @@ -110,6 +110,11 @@ public function getDirectSearchResultAsPanel( $items = $this->sortObjectItems($view_control_infos->sortation(), ...$items_with_sort_data); + if ($items->current() === null) { + // currently only relevant when the total hits are a multiple of max page size (47885) + $items = [$this->component_factory->getNoResultItem()]; + } + return [ $this->component_factory->getPanel( $view_control_infos, @@ -154,14 +159,18 @@ protected function getItemsForSubitemsFromDirectSearch( */ public function getLuceneSearchResultAsPanel( ilLuceneSearchResultFilter $result, - ilLuceneHighlighterResultParser $highlighter, + ?ilLuceneHighlighterResultParser $highlighter, ViewControlInfos $view_control_infos ): array { $items = []; $subitem_modals = []; $items_with_sort_data = []; - foreach ($result->getResults() as $ref_id => $obj_id) { + $results = $result->getResults(); + if ($highlighter === null) { + $results = []; + } + foreach ($results as $ref_id => $obj_id) { $creation_date = $this->obj_properties->lookupCreationDate($obj_id); $type = $this->obj_properties->lookupType($obj_id); $title_no_highlights = $this->obj_properties->lookupTitle($obj_id); @@ -205,6 +214,11 @@ public function getLuceneSearchResultAsPanel( $items = $this->sortObjectItems($view_control_infos->sortation(), ...$items_with_sort_data); + if ($items->current() === null) { + // currently only relevant when the total hits are a multiple of max page size (47885) + $items = [$this->component_factory->getNoResultItem()]; + } + return [ $this->component_factory->getPanel( $view_control_infos, diff --git a/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactory.php b/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactory.php index 2b52080c81f4..216f0a225508 100755 --- a/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactory.php +++ b/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactory.php @@ -63,4 +63,6 @@ public function getModalForSubitems( bool $show_too_many_items_warning, Item ...$items ): ?Modal; + + public function getNoResultItem(): Item; } diff --git a/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactoryImpl.php b/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactoryImpl.php index 5908b3492613..f487850e11b7 100755 --- a/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactoryImpl.php +++ b/components/ILIAS/Search/classes/Presentation/Result/UI/ComponentFactoryImpl.php @@ -213,4 +213,9 @@ public function getItemForSubitem( ->withDescription($this->sanitizer->sanitizeAndSetUpPlaceholders($content)) ->withProperties($properties); } + + public function getNoResultItem(): Item + { + return $this->ui_factory->item()->shy($this->lng->txt('search_no_further_match')); + } } diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index fd86fa208a60..9d5fe860cc80 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -15681,6 +15681,7 @@ search#:#search_minimum_info#:#Die Suchbegriffe müssen mindestens %s Zeichen la search#:#search_minimum_three#:#Die Suchbegriffe müssen mindestens 3 Zeichen lang sein. search#:#search_newer_than#:#Objekte erstellt vor dem... search#:#search_no_connection_lucene#:#Es kann keine Verbindung zum Lucene-Server hergestellt werden. +search#:#search_no_further_match#:#Ihre Suche ergab keine weiteren Treffer. search#:#search_no_match#:#Ihre Suche ergab keine Treffer. search#:#search_no_match_hint#:#Es wurden keine Treffer für Ihre Suchanfrage nach %s gefunden.
    Vorschläge:

    • Vergewissern Sie sich, dass alle Wörter richtig geschrieben sind.
    • Probieren Sie andere Suchbegriffe.
    • Probieren Sie allgemeinere Suchbegriffe.
    • Probieren Sie weniger Suchbegriffe. search#:#search_no_selection#:#Sie haben keine Auswahl getroffen. diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 5eb2bbca8e96..4b3e0b577f70 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -15652,6 +15652,7 @@ search#:#search_minimum_info#:#Your search must be at least %s characters long search#:#search_minimum_three#:#Your search must be at least three characters long. search#:#search_newer_than#:#Objects newer than search#:#search_no_connection_lucene#:#Cannot connect to Lucene server. +search#:#search_no_further_match#:#No further matches found. search#:#search_no_match#:#Your search did not match any results. search#:#search_no_match_hint#:#Your search for %s did not match any documents.

    Suggestions:
    • Make sure all words are spelled correctly.
    • Try different keywords.
    • Try more general keywords.
    • Try fewer keywords. search#:#search_no_selection#:#You made no selection. From a1f7807f3a5e3d0d775d7295c0f4f1b42f32b7c9 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 13 Jul 2026 12:14:31 +0200 Subject: [PATCH 062/333] Search: fix empty lucene search (47885) --- components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php b/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php index b3ced2fde54d..9b77e0ab69c8 100755 --- a/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php +++ b/components/ILIAS/Search/classes/GUI/Lucene/SearcherImpl.php @@ -164,7 +164,10 @@ protected function renderResults( string $term, ViewControlInfos $view_control_infos ): void { - if ($filter->getResults() || $view_control_infos->currentPage() > 1) { + if ( + ($filter->getResults() && $highlighter !== null) || + $view_control_infos->currentPage() > 1 + ) { $result_panel_and_modals = $this->presenter->getLuceneSearchResultAsPanel( $filter, $highlighter, From a442660d5968a4e6c50c570e65b0ff1665882a5c Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Mon, 13 Jul 2026 12:49:45 +0200 Subject: [PATCH 063/333] [Bugfix] Mail 047999: Fix php8.4 deprecation in ilMailTemplatePlaceholderToEmptyResolverTest --- .../Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php b/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php index a763e348304f..53391dca9863 100644 --- a/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php +++ b/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php @@ -74,7 +74,7 @@ public function getSpecificPlaceholders(): array public function resolveSpecificPlaceholder( string $placeholder_id, array $context_parameters, - ilObjUser $recipient = null + ?ilObjUser $recipient = null ): string { if ('course_title' === $placeholder_id) { return 'My Course'; From cd6940eb6b4909c62847844041b548b2fdae2fd0 Mon Sep 17 00:00:00 2001 From: Christian Bogen Date: Fri, 3 Jul 2026 12:03:37 +0200 Subject: [PATCH 064/333] =?UTF-8?q?Add=20Thomas=20Jou=C3=9Fen=20as=20addit?= =?UTF-8?q?ional=20Authority=20to=20Sign=20off=20on=20Code=20Changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development/maintenance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/maintenance.md b/docs/development/maintenance.md index 9ce2fdbf9071..233228e767ff 100755 --- a/docs/development/maintenance.md +++ b/docs/development/maintenance.md @@ -556,7 +556,7 @@ of ILIAS. The file contains the following fields: * **ECS Interface** * Authority to Sign off on Conceptual Changes: [bogen](https://docu.ilias.de/go/usr/13815), [mglaubitz](https://docu.ilias.de/go/usr/28309) - * Authority to Sign off on Code Changes: [sdyhr](https://docu.ilias.de/go/usr/102107) + * Authority to Sign off on Code Changes: [sdyhr](https://docu.ilias.de/go/usr/102107), [tjoussen](https://docu.ilias.de/go/usr/103745) * Authority to Curate Test Cases: [jheim](https://docu.ilias.de/go/usr/40167), [SIG CampusConnect und ECS(A)](https://docu.ilias.de/go/grp/7893) * Authority to (De-)Assign Authorities: [bogen](https://docu.ilias.de/go/usr/13815), [mglaubitz](https://docu.ilias.de/go/usr/28309) * Assignee for Issues: [sdyhr](https://docu.ilias.de/go/usr/102107) From fbc5debdba6a54a23889b05c515a349f222cddf5 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Mon, 13 Jul 2026 13:29:53 +0200 Subject: [PATCH 065/333] [Bugfix] Mail 047999: Fix DatePresentation lng tearDown in ilMailTemplatePlaceholderToEmptyResolverTest --- ...ailTemplatePlaceholderToEmptyResolverTest.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php b/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php index 53391dca9863..a8151ef787ca 100644 --- a/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php +++ b/components/ILIAS/Mail/tests/ilMailTemplatePlaceholderToEmptyResolverTest.php @@ -23,16 +23,18 @@ class ilMailTemplatePlaceholderToEmptyResolverTest extends ilMailBaseTestCase { + protected function tearDown(): void + { + $lang_property = new ReflectionProperty(ilDatePresentation::class, 'lang'); + $lang_property->setValue(null, null); + + parent::tearDown(); + } + public function testNullRecipientResolvesContextPlaceholdersAndKeepsRecipientDependentNames(): void { - $lng = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->onlyMethods(['txt', 'loadLanguageModule']) - ->getMock(); - $lng->method('txt')->willReturnArgument(0); + $lng = $this->createMock(ilLanguage::class); $this->setGlobalVariable('lng', $lng); - ilDatePresentation::setLanguage($lng); - $env_helper = $this->createMock(ilMailEnvironmentHelper::class); $env_helper->method('getClientId')->willReturn('phpunit_client'); $env_helper->method('getHttpPath')->willReturn('https://ilias.example/'); From 16e3700807269d16dc52930f973484182183c5ef Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 13 Jul 2026 13:36:41 +0200 Subject: [PATCH 066/333] Search: new lang vars (47958) --- components/ILIAS/Search/classes/GUI/class.ilSearchGUI.php | 8 ++++---- .../ILIAS/Search/classes/class.ilMainMenuSearchGUI.php | 5 ++--- lang/ilias_de.lang | 2 ++ lang/ilias_en.lang | 2 ++ 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/components/ILIAS/Search/classes/GUI/class.ilSearchGUI.php b/components/ILIAS/Search/classes/GUI/class.ilSearchGUI.php index 06978fc17a30..1c3dc17c22df 100755 --- a/components/ILIAS/Search/classes/GUI/class.ilSearchGUI.php +++ b/components/ILIAS/Search/classes/GUI/class.ilSearchGUI.php @@ -237,10 +237,10 @@ protected function renderSearchInput(string $term): void $this->tpl->setVariable("FORM_ACTION", $this->actions->search()); $this->tpl->setVariable("TERM", ilLegacyFormElementsUtil::prepareFormOutput($term)); - $this->tpl->setVariable("SEARCH_LABEL", $this->lng->txt("search")); + $this->tpl->setVariable("SEARCH_LABEL", $this->lng->txt("search_field")); $btn = ilSubmitButton::getInstance(); $btn->setCommand("performSearch"); - $btn->setCaption("search"); + $btn->setCaption("btn_search"); $this->tpl->setVariable("SUBMIT_BTN", $btn->render()); } @@ -261,12 +261,12 @@ protected function fillHeaderAndTabs(): void // tabs $this->tabs->addTab( 'search', - $this->lng->txt('search'), + $this->lng->txt('search_tab_content'), (string) $this->actions->showSavedResults() ); if ($this->settings->enabledLucene() && $this->settings->isLuceneUserSearchEnabled()) { $this->tabs->addTarget( - 'search_user', + 'search_tab_user', $this->ctrl->getLinkTargetByClass(ilLuceneUserSearchGUI::class) ); } diff --git a/components/ILIAS/Search/classes/class.ilMainMenuSearchGUI.php b/components/ILIAS/Search/classes/class.ilMainMenuSearchGUI.php index 9713961fe160..9a149c745f29 100755 --- a/components/ILIAS/Search/classes/class.ilMainMenuSearchGUI.php +++ b/components/ILIAS/Search/classes/class.ilMainMenuSearchGUI.php @@ -108,9 +108,9 @@ public function getHTML(): string } $this->tpl->setVariable( 'FORMACTION', - $this->buildSearchLink('remoteSearch', false) + $this->buildSearchLink('remoteSearch') ); - $this->tpl->setVariable('BTN_SEARCH', $this->lng->txt('search')); + $this->tpl->setVariable('BTN_SEARCH', $this->lng->txt('btn_search')); $this->tpl->setVariable('SEARCH_INPUT_LABEL', $this->lng->txt('search_field')); $this->tpl->setVariable('IMG_MM_SEARCH', ilUtil::img( @@ -125,7 +125,6 @@ public function getHTML(): string ); $this->tpl->setVariable('TXT_SEARCH_LINK', $this->lng->txt("last_search_result")); } - $this->tpl->setVariable('TXT_SEARCH', $this->lng->txt("search")); return $this->tpl->get(); } diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 9d5fe860cc80..f02be254f6dc 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -15704,6 +15704,8 @@ search#:#search_sort_generic_desc#:#%s, abst. search#:#search_sort_relevance#:#Nach Relevanz search#:#search_sort_title_asc#:#Alphabetisch: A-Z search#:#search_sort_title_desc#:#Alphabetisch: Z-A +search#:#search_tab_content#:#Inhalt +search#:#search_tab_user#:#Personen search#:#search_term_combination#:#Kombination search#:#search_title_description#:#Titel / Beschreibung search#:#search_tst_svy#:#Tests/Umfragen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 4b3e0b577f70..9557220c8e06 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -15675,6 +15675,8 @@ search#:#search_sort_generic_desc#:#%s, desc. search#:#search_sort_relevance#:#By Relevance search#:#search_sort_title_asc#:#Alphabetically: A-Z search#:#search_sort_title_desc#:#Alphabetically: Z-A +search#:#search_tab_content#:#Content +search#:#search_tab_user#:#Users search#:#search_term_combination#:#Combination search#:#search_title_description#:#Title / Description search#:#search_tst_svy#:#Tests/Surveys From dd1b9b2817fc0791666ed5c7e979c22640dadb4b Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 13 Jul 2026 14:04:04 +0200 Subject: [PATCH 067/333] Search: rename scope in filter (47966) --- components/ILIAS/Search/classes/class.ilSearchFilterGUI.php | 2 +- lang/ilias_de.lang | 2 +- lang/ilias_en.lang | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/Search/classes/class.ilSearchFilterGUI.php b/components/ILIAS/Search/classes/class.ilSearchFilterGUI.php index d7c6a48797a5..32f4a02b449b 100644 --- a/components/ILIAS/Search/classes/class.ilSearchFilterGUI.php +++ b/components/ILIAS/Search/classes/class.ilSearchFilterGUI.php @@ -55,7 +55,7 @@ public function __construct(URI $action, bool $for_lucene) } $scope_options[(string) $item["ref_id"]] = strip_tags($item["title"]); } - $inputs["search_scope"] = $field_factory->select($txt("scope"), $scope_options) + $inputs["search_scope"] = $field_factory->select($txt("search_area_filter"), $scope_options) ->withRequired(true) ->withValue(ROOT_FOLDER_ID); $inputs_activated[] = true; diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index f02be254f6dc..97afdc5281be 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -15636,7 +15636,7 @@ search#:#lucene_tbl_create_ini#:#Konfigurationsdatei für Java-Server erstellen search#:#search_add_members_from_container_crs#:#Benutzer aus aktuellem Kurs hinzufügen search#:#search_add_members_from_container_grp#:#Benutzer aus aktueller Gruppe hinzufügen search#:#search_any#:#-- Beliebig -- -search#:#search_area#:#Suchbereich +search#:#search_area_filter#:#Suchbereich search#:#search_area_info#:#Bitte wählen Sie einen Bereich aus, innerhalb dem die Suche starten soll. search#:#search_auto_complete_length#:#Anzahl der Einträge in Auto-Complete-Liste search#:#search_auto_complete_length_info#:#Wählen Sie „0“, um keine Auto-Complete-Liste anzubieten. diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 9557220c8e06..46f3ada25129 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -15607,7 +15607,7 @@ search#:#lucene_tbl_create_ini#:#Create Java-Server Ini-File search#:#search_add_members_from_container_crs#:#Add Users From Current Course search#:#search_add_members_from_container_grp#:#Add Users From Current Group search#:#search_any#:#-- Any -- -search#:#search_area#:#Search Area +search#:#search_area_filter#:#Scope search#:#search_area_info#:#Please select an area where the search should start. search#:#search_auto_complete_length#:#Number of Auto-Complete List Entries search#:#search_auto_complete_length_info#:#Choose ‘0‘ to switch off the auto-complete list. From 6f4e2725ba42891b3204585d34b7db3795598b8d Mon Sep 17 00:00:00 2001 From: Ahmed Hamouda Date: Mon, 13 Jul 2026 15:07:41 +0200 Subject: [PATCH 068/333] Mantis #30823: add Course AboStatus to XML export & import (TRUNK) (#11074) * add Course AboStatus to XML export & import * revert change to ilias_crs_9_0.xsd * 0030823: make AboStatus optional to keep existing ILIAS 10 exports compatible * 0030823: make AboStatus optional to keep existing ILIAS 11 exports compatible * 0030823: add optional AboStatus to course administration SOAP schema --- components/ILIAS/Course/classes/class.ilCourseXMLParser.php | 4 ++++ components/ILIAS/Course/classes/class.ilCourseXMLWriter.php | 2 ++ .../ILIAS/Export/xml/SchemaValidation/ilias_crs_10_0.xsd | 1 + .../ILIAS/Export/xml/SchemaValidation/ilias_crs_11_0.xsd | 1 + .../ILIAS/Export/xml/SchemaValidation/ilias_ws_crs_11_0.xsd | 1 + 5 files changed, 9 insertions(+) diff --git a/components/ILIAS/Course/classes/class.ilCourseXMLParser.php b/components/ILIAS/Course/classes/class.ilCourseXMLParser.php index 2bd8bcb96c4d..e4801811407b 100755 --- a/components/ILIAS/Course/classes/class.ilCourseXMLParser.php +++ b/components/ILIAS/Course/classes/class.ilCourseXMLParser.php @@ -635,6 +635,10 @@ public function handlerEndTag($a_xml_parser, string $a_name): void $this->course_obj->setTimingMode((int) $this->cdata); break; + case 'AboStatus': + $this->course_obj->setAboStatus((int) $this->cdata); + break; + case 'StatusDetermination': $this->course_obj->setStatusDetermination((int) $this->cdata); break; diff --git a/components/ILIAS/Course/classes/class.ilCourseXMLWriter.php b/components/ILIAS/Course/classes/class.ilCourseXMLWriter.php index d8a715295b82..39465de484c2 100755 --- a/components/ILIAS/Course/classes/class.ilCourseXMLWriter.php +++ b/components/ILIAS/Course/classes/class.ilCourseXMLWriter.php @@ -334,6 +334,8 @@ public function __buildSetting(): void $this->xmlElement('TimingMode', null, $this->course_obj->getTimingMode()); } + $this->xmlElement('AboStatus', null, $this->course_obj->getAboStatus() ? 1 : 0); + $this->xmlElement('TutorialSupportBlock', [ 'active' => $this->course_obj->getTutorialSupportBlockSettingValue() ]); diff --git a/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_10_0.xsd b/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_10_0.xsd index ab429d0a8838..bf730646101b 100644 --- a/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_10_0.xsd +++ b/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_10_0.xsd @@ -65,6 +65,7 @@ + diff --git a/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_11_0.xsd b/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_11_0.xsd index 8e6a17884708..96da01a10ceb 100644 --- a/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_11_0.xsd +++ b/components/ILIAS/Export/xml/SchemaValidation/ilias_crs_11_0.xsd @@ -30,6 +30,7 @@ + diff --git a/components/ILIAS/Export/xml/SchemaValidation/ilias_ws_crs_11_0.xsd b/components/ILIAS/Export/xml/SchemaValidation/ilias_ws_crs_11_0.xsd index fb932d8455ac..f008415cfc04 100755 --- a/components/ILIAS/Export/xml/SchemaValidation/ilias_ws_crs_11_0.xsd +++ b/components/ILIAS/Export/xml/SchemaValidation/ilias_ws_crs_11_0.xsd @@ -70,6 +70,7 @@ + From ef7706a16e977c9616a1662936ecd3076d202918 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 13 Jul 2026 15:41:00 +0200 Subject: [PATCH 069/333] =?UTF-8?q?[FIX]=20repository:=2046457:=20Eintrag?= =?UTF-8?q?=20-obj=5Fgdtr-=20in=20Objekttypen=20im=20Magazin=20zerst=C3=B6?= =?UTF-8?q?rt=20Create-Dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ILIAS/Repository/Administration/class.ilModulesTableGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Repository/Administration/class.ilModulesTableGUI.php b/components/ILIAS/Repository/Administration/class.ilModulesTableGUI.php index ff9855007cc9..f3706b884100 100755 --- a/components/ILIAS/Repository/Administration/class.ilModulesTableGUI.php +++ b/components/ILIAS/Repository/Administration/class.ilModulesTableGUI.php @@ -131,7 +131,7 @@ public function getComponents(): void if ($this->obj_definition->isSystemObject($id)) { continue; } - if (in_array($id, ["lng", "rolt", "sty", "tax", "usr"])) { + if (in_array($id, ["lng", "rolt", "sty", "tax", "usr", "gdtr"])) { continue; } $obj_types[$id] = [ From be13114e1a99c15edb500737170c7d1ec99c6e61 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 13 Jul 2026 15:59:02 +0200 Subject: [PATCH 070/333] [FIX] portfolio 46975: prtt_edit does not resolve in Portfolio Template Edit mode --- .../ILIAS/Portfolio/classes/class.ilObjPortfolioBaseGUI.php | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBaseGUI.php b/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBaseGUI.php index 05fe7fbcf62e..92a626532d2b 100755 --- a/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBaseGUI.php +++ b/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBaseGUI.php @@ -71,6 +71,7 @@ public function __construct( $this->lng->loadLanguageModule("prtf"); $this->lng->loadLanguageModule("user"); $this->lng->loadLanguageModule("obj"); + $this->lng->loadLanguageModule("prtt"); $this->requested_ppage = $this->port_request->getPortfolioPageId(); $this->requested_user_page = $this->port_request->getUserPage(); From 52f693e8efa57c0a08c3b5b61bc608a0e2f356cb Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 13 Jul 2026 16:21:17 +0200 Subject: [PATCH 071/333] [FIX] COPage: 42945: Print view doesn't show svg --- components/ILIAS/COPage/xsl/page.xsl | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/components/ILIAS/COPage/xsl/page.xsl b/components/ILIAS/COPage/xsl/page.xsl index 5d368b5851b4..9a9942d93117 100755 --- a/components/ILIAS/COPage/xsl/page.xsl +++ b/components/ILIAS/COPage/xsl/page.xsl @@ -2543,6 +2543,27 @@ Comment to have separate iframe ending tag + + + + + + + + + + + + + + + attributes + + + Comment to have separate embed ending tag + + +
    @@ -2737,27 +2758,6 @@
    - - - - - - - - - - - - - - attributes - - - Comment to have separate embed ending tag - - - - {{{{{No Media Type}}}}} From 6e91f88453279ef7307ddcdf65e8bcaf8c02e519 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 13 Jul 2026 16:35:36 +0200 Subject: [PATCH 072/333] =?UTF-8?q?[FIX]=20Container:=2031459:=20Karten=20?= =?UTF-8?q?einf=C3=BCgen=20weiterhin=20als=20Element=20f=C3=BCr=20Seitenge?= =?UTF-8?q?staltung=20ausw=C3=A4hlbar,=20obwohl=20in=20Administration=20de?= =?UTF-8?q?aktiviert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ILIAS/Container/Page/class.ilContainerPageConfig.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Container/Page/class.ilContainerPageConfig.php b/components/ILIAS/Container/Page/class.ilContainerPageConfig.php index a66c96abc490..9992d2f49097 100755 --- a/components/ILIAS/Container/Page/class.ilContainerPageConfig.php +++ b/components/ILIAS/Container/Page/class.ilContainerPageConfig.php @@ -36,7 +36,9 @@ public function init(): void $this->setEnableInternalLinks(true); $this->setIntLinkHelpDefaultType("RepositoryItem"); $this->setEnablePCType("FileList", false); - $this->setEnablePCType("Map", true); + if (ilMapUtil::isActivated()) { + $this->setEnablePCType("Map", true); + } $this->setEnablePCType("Resources", true); $this->setMultiLangSupport(true); $this->setSinglePageMode(true); From 7c52faf91e4424a16c0b35a97c3a2d6549b4a5f1 Mon Sep 17 00:00:00 2001 From: iszmais <45942348+iszmais@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:41:04 +0200 Subject: [PATCH 073/333] fix data persitation on confirmation (#11747) --- .../classes/Fields/Base/class.ilDclBaseRecordFieldModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/DataCollection/classes/Fields/Base/class.ilDclBaseRecordFieldModel.php b/components/ILIAS/DataCollection/classes/Fields/Base/class.ilDclBaseRecordFieldModel.php index 7b0098a58d19..925196938e72 100755 --- a/components/ILIAS/DataCollection/classes/Fields/Base/class.ilDclBaseRecordFieldModel.php +++ b/components/ILIAS/DataCollection/classes/Fields/Base/class.ilDclBaseRecordFieldModel.php @@ -295,10 +295,10 @@ public function getSortingValue(bool $link = true) public function addHiddenItemsToConfirmation(ilConfirmationGUI $confirmation) { if (!is_array($this->getValue())) { - $confirmation->addHiddenItem('field_' . $this->field->getId(), (string) $this->getValue()); + $confirmation->addHiddenItem('field_' . $this->field->getId(), htmlspecialchars((string) $this->getValue())); } else { foreach ($this->getValue() as $key => $value) { - $confirmation->addHiddenItem('field_' . $this->field->getId() . "[$key]", (string) $value); + $confirmation->addHiddenItem('field_' . $this->field->getId() . "[$key]", htmlspecialchars((string) $value)); } } } From 2b22adafce456ea4d9fbb629d57744f2ee73b3f4 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Tue, 14 Jul 2026 09:31:59 +0200 Subject: [PATCH 074/333] clean up unit tests in a few components --- .../ilAdvancedMDRecordObjectOrderingsTest.php | 2 +- .../ilCalendarRecurrenceCalculationTest.php | 15 ++- ...teCourseLearningProgressEvaluationTest.php | 61 ++++------- ...ficateSettingsCourseFormRepositoryTest.php | 100 +++++------------- .../ilCoursePlaceholderDescriptionTest.php | 38 ++----- .../ilCoursePlaceholderValuesTest.php | 81 +++++--------- .../tests/Timings/ilTimingAcceptedTest.php | 2 +- .../ilCourseMailTemplateTutorContextTest.php | 4 +- .../tests/ilDidacticTemplatePatternTest.php | 11 +- .../tests/DataSet/DataSetImportParserTest.php | 4 +- .../PublicAccess/Link/HandlerTest.php | 4 +- .../Repository/Element/HandlerTest.php | 10 +- .../PublicAccess/Repository/HandlerTest.php | 48 ++++----- .../Repository/Key/Collection.php | 12 +-- .../Repository/Key/HandlerTest.php | 6 +- .../Repository/Element/CollectionTest.php | 16 +-- .../Repository/Element/HandlerTest.php | 20 ++-- .../ExportHandler/Repository/Handler.php | 40 +++---- .../Repository/Key/CollectionTest.php | 12 +-- .../Repository/Key/HandlerTest.php | 6 +- .../Table/RowId/CollectionTest.php | 6 +- .../tests/ImportHandler/File/HandlerTest.php | 14 +-- .../File/Namespace/CollectionTest.php | 26 ++--- .../NodeInfo/Attribute/ilCollectionTest.php | 32 +++--- .../Parser/NodeInfo/ilCollectionTest.php | 6 +- .../tests/ImportHandler/Path/HandlerTest.php | 18 ++-- .../Path/Node/ilAttributeTest.php | 4 +- .../ImportHandler/Path/Node/ilIndexTest.php | 4 +- .../Validation/Set/HandlerTest.php | 6 +- .../Validation/Set/ilCollectionTest.php | 6 +- .../Export/tests/ilExportOptionsTest.php | 2 +- .../Group/tests/ilGroupEventHandlerTest.php | 9 +- .../Logging/tests/ilLogComponentLevelTest.php | 2 +- .../Membership/tests/ilWaitingListTest.php | 4 +- .../tests/Copyright/CopyrightDataTest.php | 2 +- .../Copyright/DatabaseRepositoryTest.php | 2 +- .../MetaData/tests/Copyright/RendererTest.php | 17 +-- .../tests/OERExposer/OAIPMH/HandlerTest.php | 2 +- .../OERExposer/OAIPMH/Requests/ParserTest.php | 2 +- .../OAIPMH/Requests/RequestTest.php | 2 +- .../Responses/RequestProcessorTestCase.php | 2 +- .../OAIPMH/Responses/WriterTest.php | 2 +- .../CronJob/AutomaticPublisherTest.php | 2 +- .../OERHarvester/Publisher/PublisherTest.php | 2 +- .../MetaData/tests/Presentation/DataTest.php | 2 +- .../tests/Presentation/UtilitiesTest.php | 10 +- .../CopyrightHelper/CopyrightTest.php | 8 +- .../XML/Copyright/CopyrightHandlerTest.php | 2 +- components/ILIAS/Poll/tests/PollBlockTest.php | 4 +- .../tests/ilPrivacySettingsTest.php | 10 +- .../ILIAS/Session/tests/EventItemsTest.php | 4 +- .../tests/ilSystemCheckTaskTest.php | 15 +-- .../Tracking/tests/ilLPStatusIconsTest.php | 18 ++-- .../tests/ilTrackingCollectionTest.php | 11 +- .../ilWebResourceDatabaseRepositoryTest.php | 42 +++----- .../tests/ilWebResourceItemExternalTest.php | 16 +-- .../tests/ilWebResourceItemInternalTest.php | 17 ++- .../tests/ilWebResourceItemTest.php | 15 +-- .../tests/ilWebResourceItemsContainerTest.php | 11 +- .../tests/ilWebResourceParameterTest.php | 31 ++---- 60 files changed, 327 insertions(+), 555 deletions(-) diff --git a/components/ILIAS/AdvancedMetaData/tests/record/ilAdvancedMDRecordObjectOrderingsTest.php b/components/ILIAS/AdvancedMetaData/tests/record/ilAdvancedMDRecordObjectOrderingsTest.php index 0e8dabc23d35..c71bb762def5 100755 --- a/components/ILIAS/AdvancedMetaData/tests/record/ilAdvancedMDRecordObjectOrderingsTest.php +++ b/components/ILIAS/AdvancedMetaData/tests/record/ilAdvancedMDRecordObjectOrderingsTest.php @@ -78,6 +78,6 @@ protected function initRecordSortingDependencies(): void { $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); } } diff --git a/components/ILIAS/Calendar/tests/ilCalendarRecurrenceCalculationTest.php b/components/ILIAS/Calendar/tests/ilCalendarRecurrenceCalculationTest.php index 4843f9a6e9cf..5c2d9d1934c0 100755 --- a/components/ILIAS/Calendar/tests/ilCalendarRecurrenceCalculationTest.php +++ b/components/ILIAS/Calendar/tests/ilCalendarRecurrenceCalculationTest.php @@ -298,18 +298,15 @@ protected function initDependencies(): void $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); - $this->setGlobalVariable('lng', $this->createMock(ilLanguage::class)); - $this->setGlobalVariable('ilErr', $this->createMock(ilErrorHandling::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); + $this->setGlobalVariable('lng', $this->createStub(ilLanguage::class)); + $this->setGlobalVariable('ilErr', $this->createStub(ilErrorHandling::class)); - $logger = $this->getMockBuilder(ilLogger::class) + $logger = $this->getStubBuilder(ilLogger::class) ->disableOriginalConstructor() - ->getMock(); + ->getStub(); - $logger_factory = $this->getMockBuilder(ilLoggerFactory::class) - ->disableOriginalConstructor() - ->onlyMethods(['getComponentLogger']) - ->getMock(); + $logger_factory = $this->createStub(ilLoggerFactory::class); $logger_factory->method('getComponentLogger')->willReturn($logger); $this->setGlobalVariable('ilLoggerFactory', $logger_factory); } diff --git a/components/ILIAS/Course/tests/Certificate/ilCertificateCourseLearningProgressEvaluationTest.php b/components/ILIAS/Course/tests/Certificate/ilCertificateCourseLearningProgressEvaluationTest.php index 81a0145f0368..30027795dc33 100755 --- a/components/ILIAS/Course/tests/Certificate/ilCertificateCourseLearningProgressEvaluationTest.php +++ b/components/ILIAS/Course/tests/Certificate/ilCertificateCourseLearningProgressEvaluationTest.php @@ -43,7 +43,7 @@ class ilCertificateCourseLearningProgressEvaluationTest extends TestCase { public function testOnlyOneCourseIsCompletedOnLPChange(): void { - $templateRepository = $this->getMockBuilder(ilCertificateTemplateRepository::class)->getMock(); + $templateRepository = $this->createStub(ilCertificateTemplateRepository::class); $templateRepository->method('fetchActiveCertificateTemplatesForCoursesWithDisabledLearningProgress') ->willReturn( @@ -79,9 +79,7 @@ public function testOnlyOneCourseIsCompletedOnLPChange(): void ] ); - $setting = $this->getMockBuilder(ilSetting::class) - ->disableOriginalConstructor() - ->getMock(); + $setting = $this->createStub(ilSetting::class); $consecutive_get = [ ['cert_subitems_5', '[10,20]'], @@ -97,8 +95,7 @@ function (string $k) use (&$consecutive_get): string { } ); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); $consecutive_lookup = [10, 20, 10, 50]; $objectHelper @@ -112,8 +109,7 @@ function (int $id) use (&$consecutive_lookup): int { } ); - $statusHelper = $this->getMockBuilder(ilCertificateLPStatusHelper::class) - ->getMock(); + $statusHelper = $this->createStub(ilCertificateLPStatusHelper::class); $consecutive_status = [ [100, ilLPStatus::LP_STATUS_COMPLETED_NUM], @@ -132,8 +128,7 @@ function (int $id) use (&$consecutive_status): int { } ); - $trackingHelper = $this->getMockBuilder(ilCertificateObjUserTrackingHelper::class) - ->getMock(); + $trackingHelper = $this->createStub(ilCertificateObjUserTrackingHelper::class); $trackingHelper->method('enabledLearningProgress')->willReturn(true); $evaluation = new CertificateCourseLearningProgressEvaluation( @@ -151,7 +146,7 @@ function (int $id) use (&$consecutive_status): int { public function testAllCoursesAreCompletedOnLPChange(): void { - $templateRepository = $this->getMockBuilder(ilCertificateTemplateRepository::class)->getMock(); + $templateRepository = $this->createStub(ilCertificateTemplateRepository::class); $templateRepository->method('fetchActiveCertificateTemplatesForCoursesWithDisabledLearningProgress') ->willReturn( @@ -187,13 +182,9 @@ public function testAllCoursesAreCompletedOnLPChange(): void ] ); - $setting = $this->getMockBuilder(ilSetting::class) - ->disableOriginalConstructor() - ->getMock(); + $setting = $this->createStub(ilSetting::class); - $setting = $this->getMockBuilder(ilSetting::class) - ->disableOriginalConstructor() - ->getMock(); + $setting = $this->createStub(ilSetting::class); $consecutive_get = [ ['cert_subitems_5', '[10,20]'], @@ -209,8 +200,7 @@ function (string $k) use (&$consecutive_get): string { } ); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); $consecutive_lookup = [ [10, 100], @@ -229,8 +219,7 @@ function (int $id) use (&$consecutive_lookup): int { } ); - $statusHelper = $this->getMockBuilder(ilCertificateLPStatusHelper::class) - ->getMock(); + $statusHelper = $this->createStub(ilCertificateLPStatusHelper::class); $consecutive_status = [ [100, ilLPStatus::LP_STATUS_COMPLETED_NUM], @@ -250,8 +239,7 @@ function (int $id) use (&$consecutive_status): int { } ); - $trackingHelper = $this->getMockBuilder(ilCertificateObjUserTrackingHelper::class) - ->getMock(); + $trackingHelper = $this->createStub(ilCertificateObjUserTrackingHelper::class); $trackingHelper->method('enabledLearningProgress')->willReturn(false); $evaluation = new CertificateCourseLearningProgressEvaluation( @@ -270,7 +258,7 @@ function (int $id) use (&$consecutive_status): int { public function testNoSubitemDefinedForEvaluation(): void { - $templateRepository = $this->getMockBuilder(ilCertificateTemplateRepository::class)->getMock(); + $templateRepository = $this->createStub(ilCertificateTemplateRepository::class); $templateRepository->method('fetchActiveCertificateTemplatesForCoursesWithDisabledLearningProgress') ->willReturn( @@ -306,9 +294,7 @@ public function testNoSubitemDefinedForEvaluation(): void ] ); - $setting = $this->getMockBuilder(ilSetting::class) - ->disableOriginalConstructor() - ->getMock(); + $setting = $this->createStub(ilSetting::class); $consecutive_get = [ 'cert_subitems_5', @@ -325,14 +311,11 @@ function (string $k) use (&$consecutive_get) { } ); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); - $statusHelper = $this->getMockBuilder(ilCertificateLPStatusHelper::class) - ->getMock(); + $statusHelper = $this->createStub(ilCertificateLPStatusHelper::class); - $trackingHelper = $this->getMockBuilder(ilCertificateObjUserTrackingHelper::class) - ->getMock(); + $trackingHelper = $this->createStub(ilCertificateObjUserTrackingHelper::class); $trackingHelper->method('enabledLearningProgress')->willReturn(false); $evaluation = new CertificateCourseLearningProgressEvaluation( @@ -397,7 +380,7 @@ public function testRetrievingCertificateTemplatesForCoursesWorksAsExpectedWhenU bool $isGlobalLpEnabled, array $template_recods ): void { - $statement = $database = $this->getMockBuilder(ilDBStatement::class)->getMock(); + $statement = $database = $this->createStub(ilDBStatement::class); $i = 0; $database->method('fetch')->willReturnCallback(static function () use (&$i, $template_recods): ?array { $result = $template_recods[$i] ?? null; @@ -424,13 +407,9 @@ public function testRetrievingCertificateTemplatesForCoursesWorksAsExpectedWhenU return $statement->fetch(PDO::FETCH_ASSOC); }); - $logger = $this->getMockBuilder(ilLogger::class) - ->disableOriginalConstructor() - ->getMock(); + $logger = $this->createStub(ilLogger::class); - $objectDataCache = $this->getMockBuilder(ilObjectDataCache::class) - ->disableOriginalConstructor() - ->getMock(); + $objectDataCache = $this->createStub(ilObjectDataCache::class); $repository = new ilCertificateTemplateDatabaseRepository( $database, @@ -446,7 +425,7 @@ public function testRetrievingCertificateTemplatesForCoursesWorksAsExpectedWhenU public function testRetrievingCertificateTemplatesForCoursesWillBeCachedWhenCachingRepositoryIsUsed(): void { - $wrappedTemplateRepository = $this->getMockBuilder(ilCertificateTemplateRepository::class)->getMock(); + $wrappedTemplateRepository = $this->createMock(ilCertificateTemplateRepository::class); $wrappedTemplateRepository ->expects($this->exactly(2)) ->method('fetchActiveCertificateTemplatesForCoursesWithDisabledLearningProgress') diff --git a/components/ILIAS/Course/tests/Certificate/ilCertificateSettingsCourseFormRepositoryTest.php b/components/ILIAS/Course/tests/Certificate/ilCertificateSettingsCourseFormRepositoryTest.php index ad1bb9001ef2..bfafafbd1112 100644 --- a/components/ILIAS/Course/tests/Certificate/ilCertificateSettingsCourseFormRepositoryTest.php +++ b/components/ILIAS/Course/tests/Certificate/ilCertificateSettingsCourseFormRepositoryTest.php @@ -42,67 +42,41 @@ class ilCertificateSettingsCourseFormRepositoryTest extends TestCase { public function testSaveSettings(): void { - $object = $this->getMockBuilder(ilObjCourse::class) - ->disableOriginalConstructor() - ->getMock(); + $object = $this->createMock(ilObjCourse::class); $object ->expects($this->atLeastOnce()) ->method('getId') ->willReturn(100); - $language = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->getMock(); + $language = $this->createStub(ilLanguage::class); - $controller = $this->getMockBuilder(ilCtrlInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $controller = $this->createStub(ilCtrlInterface::class); - $access = $this->getMockBuilder(ilAccess::class) - ->disableOriginalConstructor() - ->getMock(); + $access = $this->createStub(ilAccess::class); - $toolbar = $this->getMockBuilder(ilToolbarGUI::class) - ->disableOriginalConstructor() - ->getMock(); + $toolbar = $this->createStub(ilToolbarGUI::class); - $placeholderDescriptionObject = $this->getMockBuilder(ilCertificatePlaceholderDescription::class) - ->disableOriginalConstructor() - ->getMock(); + $placeholderDescriptionObject = $this->createStub(ilCertificatePlaceholderDescription::class); - $settingsFormFactory = $this->getMockBuilder(ilCertificateSettingsFormRepository::class) - ->disableOriginalConstructor() - ->getMock(); + $settingsFormFactory = $this->createStub(ilCertificateSettingsFormRepository::class); - $trackingHelper = $this->getMockBuilder(ilCertificateObjUserTrackingHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $trackingHelper = $this->createStub(ilCertificateObjUserTrackingHelper::class); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); - $lpHelper = $this->getMockBuilder(ilCertificateObjectLPHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $lpHelper = $this->createStub(ilCertificateObjectLPHelper::class); - $lpMock = $this->getMockBuilder(ilObjectLP::class) - ->disableOriginalConstructor() - ->getMock(); + $lpMock = $this->createStub(ilObjectLP::class); $lpMock->method('getCurrentMode') ->willReturn(100); $lpHelper->method('getInstance')->willReturn($lpMock); - $tree = $this->getMockBuilder(ilTree::class) - ->disableOriginalConstructor() - ->getMock(); + $tree = $this->createStub(ilTree::class); - $setting = $this->getMockBuilder(ilSetting::class) - ->disableOriginalConstructor() - ->getMock(); + $setting = $this->createMock(ilSetting::class); $setting ->expects($this->atLeastOnce()) @@ -130,38 +104,24 @@ public function testSaveSettings(): void public function testFetchFormFieldData(): void { - $object = $this->getMockBuilder(ilObjCourse::class) - ->disableOriginalConstructor() - ->getMock(); + $object = $this->createMock(ilObjCourse::class); $object ->expects($this->atLeastOnce()) ->method('getId') ->willReturn(100); - $language = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->getMock(); + $language = $this->createStub(ilLanguage::class); - $controller = $this->getMockBuilder(ilCtrlInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $controller = $this->createStub(ilCtrlInterface::class); - $access = $this->getMockBuilder(ilAccess::class) - ->disableOriginalConstructor() - ->getMock(); + $access = $this->createStub(ilAccess::class); - $toolbar = $this->getMockBuilder(ilToolbarGUI::class) - ->disableOriginalConstructor() - ->getMock(); + $toolbar = $this->createStub(ilToolbarGUI::class); - $placeholderDescriptionObject = $this->getMockBuilder(ilCertificatePlaceholderDescription::class) - ->disableOriginalConstructor() - ->getMock(); + $placeholderDescriptionObject = $this->createStub(ilCertificatePlaceholderDescription::class); - $settingsFormFactory = $this->getMockBuilder(ilCertificateSettingsFormRepository::class) - ->disableOriginalConstructor() - ->getMock(); + $settingsFormFactory = $this->createMock(ilCertificateSettingsFormRepository::class); $settingsFormFactory ->expects($this->atLeastOnce()) @@ -173,25 +133,15 @@ public function testFetchFormFieldData(): void ] ); - $trackingHelper = $this->getMockBuilder(ilCertificateObjUserTrackingHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $trackingHelper = $this->createStub(ilCertificateObjUserTrackingHelper::class); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); - $lpHelper = $this->getMockBuilder(ilCertificateObjectLPHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $lpHelper = $this->createStub(ilCertificateObjectLPHelper::class); - $tree = $this->getMockBuilder(ilTree::class) - ->disableOriginalConstructor() - ->getMock(); + $tree = $this->createStub(ilTree::class); - $setting = $this->getMockBuilder(ilSetting::class) - ->disableOriginalConstructor() - ->getMock(); + $setting = $this->createMock(ilSetting::class); $setting ->expects($this->atLeastOnce()) diff --git a/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderDescriptionTest.php b/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderDescriptionTest.php index cc9d9cf6f1ca..6d425368b41a 100644 --- a/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderDescriptionTest.php +++ b/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderDescriptionTest.php @@ -35,21 +35,14 @@ class ilCoursePlaceholderDescriptionTest extends TestCase { public function testPlaceholderGetHtmlDescription(): void { - $languageMock = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->onlyMethods(['txt', 'loadLanguageModule']) - ->getMock(); + $languageMock = $this->createStub(ilLanguage::class); - $templateMock = $this->getMockBuilder(ilTemplate::class) - ->disableOriginalConstructor() - ->getMock(); + $templateMock = $this->createStub(ilTemplate::class); $templateMock->method('get') ->willReturn(''); - $userDefinePlaceholderMock = $this->getMockBuilder(ilUserDefinedFieldsPlaceholderDescription::class) - ->disableOriginalConstructor() - ->getMock(); + $userDefinePlaceholderMock = $this->createStub(ilUserDefinedFieldsPlaceholderDescription::class); $userDefinePlaceholderMock->method('createPlaceholderHtmlDescription') ->willReturn(''); @@ -57,9 +50,7 @@ public function testPlaceholderGetHtmlDescription(): void $userDefinePlaceholderMock->method('getPlaceholderDescriptions') ->willReturn([]); - $customUserPlaceholderObject = $this->getMockBuilder(ilObjectCustomUserFieldsPlaceholderDescription::class) - ->disableOriginalConstructor() - ->getMock(); + $customUserPlaceholderObject = $this->createStub(ilObjectCustomUserFieldsPlaceholderDescription::class); $customUserPlaceholderObject->method('getPlaceholderDescriptions') ->willReturn([ @@ -70,9 +61,7 @@ public function testPlaceholderGetHtmlDescription(): void $customUserPlaceholderObject->method('createPlaceholderHtmlDescription') ->willReturn(''); - $profile = $this->getMockBuilder(Profile::class) - ->disableOriginalConstructor() - ->getMock(); + $profile = $this->createStub(Profile::class); $placeholderDescriptionObject = new CoursePlaceholderDescription(200, null, $languageMock, $userDefinePlaceholderMock, $customUserPlaceholderObject, $profile); @@ -83,18 +72,13 @@ public function testPlaceholderGetHtmlDescription(): void public function testPlaceholderDescriptions(): void { - $languageMock = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->onlyMethods(['txt']) - ->getMock(); + $languageMock = $this->createMock(ilLanguage::class); $languageMock->expects($this->exactly(3)) ->method('txt') ->willReturn('Something translated'); - $defaultPlaceholder = $this->getMockBuilder(ilDefaultPlaceholderDescription::class) - ->disableOriginalConstructor() - ->getMock(); + $defaultPlaceholder = $this->createStub(ilDefaultPlaceholderDescription::class); $defaultPlaceholder->method('getPlaceholderDescriptions') ->willReturn( @@ -104,9 +88,7 @@ public function testPlaceholderDescriptions(): void ] ); - $customUserPlaceholderObject = $this->getMockBuilder(ilObjectCustomUserFieldsPlaceholderDescription::class) - ->disableOriginalConstructor() - ->getMock(); + $customUserPlaceholderObject = $this->createStub(ilObjectCustomUserFieldsPlaceholderDescription::class); $customUserPlaceholderObject->method('getPlaceholderDescriptions') ->willReturn( @@ -116,9 +98,7 @@ public function testPlaceholderDescriptions(): void ] ); - $profile = $this->getMockBuilder(Profile::class) - ->disableOriginalConstructor() - ->getMock(); + $profile = $this->createStub(Profile::class); $placeholderDescriptionObject = new CoursePlaceholderDescription(200, $defaultPlaceholder, $languageMock, null, $customUserPlaceholderObject, $profile); diff --git a/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderValuesTest.php b/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderValuesTest.php index 1b35ce01374a..ddad122a9ff0 100644 --- a/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderValuesTest.php +++ b/components/ILIAS/Course/tests/Certificate/ilCoursePlaceholderValuesTest.php @@ -67,43 +67,33 @@ protected function setGlobalVariable(string $name, $value): void public function testGetPlaceholderValues(): void { - $customUserFieldsPlaceholderValues = $this->getMockBuilder(ilObjectCustomUserFieldsPlaceholderValues::class) - ->disableOriginalConstructor() - ->getMock(); + $customUserFieldsPlaceholderValues = $this->createStub(ilObjectCustomUserFieldsPlaceholderValues::class); $customUserFieldsPlaceholderValues->method('getPlaceholderValues') ->willReturn([]); - $defaultPlaceholderValues = $this->getMockBuilder(ilDefaultPlaceholderValues::class) - ->disableOriginalConstructor() - ->getMock(); + $defaultPlaceholderValues = $this->createStub(ilDefaultPlaceholderValues::class); $defaultPlaceholderValues->method('getPlaceholderValues') ->willReturn([]); - $language = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->getMock(); + $language = $this->createStub(ilLanguage::class); $language->method('txt') ->willReturn('Something'); - $objectMock = $this->getMockBuilder(ilObjCourse::class) - ->disableOriginalConstructor() - ->getMock(); + $objectMock = $this->createStub(ilObjCourse::class); $objectMock->method('getTitle') ->willReturn('Some Title'); - $obj_translation = $this->getMockBuilder(ilObjectTranslations::class) - ->disableOriginalConstructor() - ->getMock(); + $obj_translation = $this->createStub(ilObjectTranslations::class); - $german = $this->createMock(ilObjectTranslationLanguage::class); + $german = $this->createStub(ilObjectTranslationLanguage::class); $german->method('getLanguageCode') ->willReturn('de'); - $english = $this->createMock(ilObjectTranslationLanguage::class); + $english = $this->createStub(ilObjectTranslationLanguage::class); $english->method('getLanguageCode') ->willReturn('en'); @@ -116,12 +106,9 @@ public function testGetPlaceholderValues(): void $objectMock->method('getObjectTranslation') ->willReturn($obj_translation); - $user_object = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->getMock(); + $user_object = $this->createStub(ilObjUser::class); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); $objectHelper->method('getInstanceByObjId') ->willReturnMap( [ @@ -130,21 +117,17 @@ public function testGetPlaceholderValues(): void ] ); - $participantsHelper = $this->getMockBuilder(CertificateParticipantsHelper::class) - ->getMock(); + $participantsHelper = $this->createStub(CertificateParticipantsHelper::class); $participantsHelper->method('getDateTimeOfPassed') ->willReturn('2018-09-10'); - $ilUtilHelper = $this->getMockBuilder(ilCertificateUtilHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $ilUtilHelper = $this->createStub(ilCertificateUtilHelper::class); $ilUtilHelper->method('prepareFormOutput') ->willReturn('Some Title'); - $ilDateHelper = $this->getMockBuilder(ilCertificateDateHelper::class) - ->getMock(); + $ilDateHelper = $this->createStub(ilCertificateDateHelper::class); $ilDateHelper->method('formatDate') ->willReturn('2018-09-10'); @@ -152,8 +135,7 @@ public function testGetPlaceholderValues(): void $ilDateHelper->method('formatDateTime') ->willReturn('2018-09-10 10:32:00'); - $database = $this->getMockBuilder(ilDBInterface::class) - ->getMock(); + $database = $this->createStub(ilDBInterface::class); $this->setGlobalVariable('ilDB', $database); $this->setGlobalVariable('lng', $language); @@ -181,9 +163,7 @@ public function testGetPlaceholderValues(): void public function testGetPreviewPlaceholderValues(): void { - $customUserFieldsPlaceholderValues = $this->getMockBuilder(ilObjectCustomUserFieldsPlaceholderValues::class) - ->disableOriginalConstructor() - ->getMock(); + $customUserFieldsPlaceholderValues = $this->createStub(ilObjectCustomUserFieldsPlaceholderValues::class); $customUserFieldsPlaceholderValues->method('getPlaceholderValuesForPreview') ->willReturn( @@ -193,9 +173,7 @@ public function testGetPreviewPlaceholderValues(): void ] ); - $defaultPlaceholderValues = $this->getMockBuilder(ilDefaultPlaceholderValues::class) - ->disableOriginalConstructor() - ->getMock(); + $defaultPlaceholderValues = $this->createStub(ilDefaultPlaceholderValues::class); $defaultPlaceholderValues->method('getPlaceholderValuesForPreview') ->willReturn( @@ -205,29 +183,23 @@ public function testGetPreviewPlaceholderValues(): void ] ); - $language = $this->getMockBuilder(ilLanguage::class) - ->disableOriginalConstructor() - ->getMock(); + $language = $this->createStub(ilLanguage::class); $language->method('txt') ->willReturn('Something'); - $objectMock = $this->getMockBuilder(ilObjCourse::class) - ->disableOriginalConstructor() - ->getMock(); + $objectMock = $this->createStub(ilObjCourse::class); $objectMock->method('getTitle') ->willReturn('SomeTitle'); - $obj_translation = $this->getMockBuilder(ilObjectTranslations::class) - ->disableOriginalConstructor() - ->getMock(); + $obj_translation = $this->createStub(ilObjectTranslations::class); - $german = $this->createMock(ilObjectTranslationLanguage::class); + $german = $this->createStub(ilObjectTranslationLanguage::class); $german->method('getLanguageCode') ->willReturn('de'); - $english = $this->createMock(ilObjectTranslationLanguage::class); + $english = $this->createStub(ilObjectTranslationLanguage::class); $english->method('getLanguageCode') ->willReturn('en'); @@ -240,26 +212,21 @@ public function testGetPreviewPlaceholderValues(): void $objectMock->method('getObjectTranslation') ->willReturn($obj_translation); - $objectHelper = $this->getMockBuilder(ilCertificateObjectHelper::class) - ->getMock(); + $objectHelper = $this->createStub(ilCertificateObjectHelper::class); $objectHelper->method('getInstanceByObjId') ->willReturn($objectMock); - $participantsHelper = $this->getMockBuilder(CertificateParticipantsHelper::class) - ->getMock(); + $participantsHelper = $this->createStub(CertificateParticipantsHelper::class); - $utilHelper = $this->getMockBuilder(ilCertificateUtilHelper::class) - ->disableOriginalConstructor() - ->getMock(); + $utilHelper = $this->createStub(ilCertificateUtilHelper::class); $utilHelper->method('prepareFormOutput') ->willReturnCallback(function ($input) { return $input; }); - $database = $this->getMockBuilder(ilDBInterface::class) - ->getMock(); + $database = $this->createStub(ilDBInterface::class); $this->setGlobalVariable('ilDB', $database); $this->setGlobalVariable('lng', $language); diff --git a/components/ILIAS/Course/tests/Timings/ilTimingAcceptedTest.php b/components/ILIAS/Course/tests/Timings/ilTimingAcceptedTest.php index 3664ec6b90f3..5e9be896bd63 100755 --- a/components/ILIAS/Course/tests/Timings/ilTimingAcceptedTest.php +++ b/components/ILIAS/Course/tests/Timings/ilTimingAcceptedTest.php @@ -71,6 +71,6 @@ protected function initDependencies(): void { $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); } } diff --git a/components/ILIAS/Course/tests/ilCourseMailTemplateTutorContextTest.php b/components/ILIAS/Course/tests/ilCourseMailTemplateTutorContextTest.php index 9a4de1467e9b..550b2ea5ef67 100755 --- a/components/ILIAS/Course/tests/ilCourseMailTemplateTutorContextTest.php +++ b/components/ILIAS/Course/tests/ilCourseMailTemplateTutorContextTest.php @@ -32,8 +32,8 @@ protected function setUp(): void $this->dic_backup = is_object($DIC) ? clone $DIC : $DIC; $DIC = new Container(); - $DIC['ilObjDataCache'] = $this->createMock(ilObjectDataCache::class); - $DIC['ilDB'] = $this->createMock(ilDBInterface::class); + $DIC['ilObjDataCache'] = $this->createStub(ilObjectDataCache::class); + $DIC['ilDB'] = $this->createStub(ilDBInterface::class); } protected function tearDown(): void diff --git a/components/ILIAS/DidacticTemplate/tests/ilDidacticTemplatePatternTest.php b/components/ILIAS/DidacticTemplate/tests/ilDidacticTemplatePatternTest.php index d5ff54cb3a20..ef23377e9d88 100755 --- a/components/ILIAS/DidacticTemplate/tests/ilDidacticTemplatePatternTest.php +++ b/components/ILIAS/DidacticTemplate/tests/ilDidacticTemplatePatternTest.php @@ -74,16 +74,11 @@ protected function initPatternDependencies(): void $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); - $logger = $this->getMockBuilder(ilLogger::class) - ->disableOriginalConstructor() - ->getMock(); + $logger = $this->createStub(ilLogger::class); - $logger_factory = $this->getMockBuilder(ilLoggerFactory::class) - ->disableOriginalConstructor() - ->onlyMethods(['getComponentLogger']) - ->getMock(); + $logger_factory = $this->createStub(ilLoggerFactory::class); $logger_factory->method('getComponentLogger')->willReturn($logger); $this->setGlobalVariable('ilLoggerFactory', $logger_factory); } diff --git a/components/ILIAS/Export/tests/DataSet/DataSetImportParserTest.php b/components/ILIAS/Export/tests/DataSet/DataSetImportParserTest.php index 6c30008ddb48..be2aa03e54a7 100755 --- a/components/ILIAS/Export/tests/DataSet/DataSetImportParserTest.php +++ b/components/ILIAS/Export/tests/DataSet/DataSetImportParserTest.php @@ -39,8 +39,8 @@ protected function tearDown(): void public function testInstanceAndParseValidXML(): void { - $map_mock = $this->createMock(ilImportMapping::class); - $ds_mock = $this->createMock(ilDataSet::class); + $map_mock = $this->createStub(ilImportMapping::class); + $ds_mock = $this->createStub(ilDataSet::class); $parser = new ilDataSetImportParser( "ent", "1.0.0", diff --git a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Link/HandlerTest.php b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Link/HandlerTest.php index 955084a0be16..07d3259baaca 100644 --- a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Link/HandlerTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Link/HandlerTest.php @@ -35,10 +35,10 @@ public function testExportHandlerPublicAccessLink(): void $reference_id = 1; $uri_mock = $this->createMock(Uri::class); $uri_mock->expects($this->once())->method("__toString")->willReturn($download_url); - $reference_id_mock = $this->createMock(ReferenceId::class); + $reference_id_mock = $this->createStub(ReferenceId::class); $reference_id_mock->method("toInt")->willReturn($reference_id); $reference_id_mock->method("toObjectId")->willThrowException(new Exception("unexpected conversion to object id")); - $static_url_wrapper_mock = $this->createMock(ilExportHandlerPublicAccessLinkStaticURLWrapperInterface::class); + $static_url_wrapper_mock = $this->createStub(ilExportHandlerPublicAccessLinkStaticURLWrapperInterface::class); $static_url_wrapper_mock->method("withStaticURL")->willThrowException(new Exception("unexpected overwrite of static URL service object")); $static_url_wrapper_mock->method("buildDownloadURI")->willReturn($uri_mock); try { diff --git a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Element/HandlerTest.php b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Element/HandlerTest.php index 49cdf36e4932..1c91863bd821 100644 --- a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Element/HandlerTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Element/HandlerTest.php @@ -31,17 +31,17 @@ class HandlerTest extends TestCase { public function testExportHandlerPublicAccessRepositoryElement(): void { - $object_id_mock_01 = $this->createMock(ObjectId::class); + $object_id_mock_01 = $this->createStub(ObjectId::class); $object_id_mock_01->method("toInt")->willReturn(20); $object_id_mock_01->method("toReferenceIds")->willThrowException(new Exception("unexpected reference id access")); - $values_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryValuesInteface::class); + $values_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryValuesInteface::class); $values_mock->method("isValid")->willReturn(true); $values_mock->method("getIdentification")->willReturn("id"); $values_mock->method("withIdentification")->willThrowException(new Exception("unexpected id overwrite")); $values_mock->method("getExportOptionId")->willReturn("exp_id"); $values_mock->method("withExportOptionId")->willThrowException(new Exception("unexpected exp id overwrite")); $values_mock->method("getLastModified")->willThrowException(new Exception("unexpected last modified access")); - $values_not_storable_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryValuesInteface::class); + $values_not_storable_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryValuesInteface::class); $values_not_storable_mock->method("isValid")->willReturn(false); $values_not_storable_mock->method("getIdentification")->willThrowException(new Exception("unexpected id access")); $values_not_storable_mock->method("withIdentification")->willThrowException(new Exception("unexpected id overwrite")); @@ -54,11 +54,11 @@ public function testExportHandlerPublicAccessRepositoryElement(): void $values_not_storable_mock->method("equals")->willReturnMap([ [$values_mock, false], [$values_not_storable_mock, true] ]); - $key_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock->method("isValid")->willReturn(true); $key_mock->method("getObjectId")->willReturn($object_id_mock_01); $key_mock->method("withObjectId")->willThrowException(new Exception("unexpected object id overwrite")); - $key_not_storable_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_not_storable_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_not_storable_mock->method("isValid")->willReturn(false); $key_not_storable_mock->method("getObjectId")->willThrowException(new Exception("unexpected object id access")); $key_not_storable_mock->method("withObjectId")->willThrowException(new Exception("unexpected object id overwrite")); diff --git a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/HandlerTest.php b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/HandlerTest.php index 06d1a5ba19f3..b1d662559831 100644 --- a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/HandlerTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/HandlerTest.php @@ -44,31 +44,31 @@ public function testExportHandlerPublicAccessRepository(): void { $this->repository_elements = []; - $object_id_mock_01 = $this->createMock(ObjectId::class); + $object_id_mock_01 = $this->createStub(ObjectId::class); $object_id_mock_01->method("toInt")->willReturn(1); $object_id_mock_01->method("toReferenceIds")->willThrowException(new UnexpectedValueException("unexpected method call")); - $object_id_mock_02 = $this->createMock(ObjectId::class); + $object_id_mock_02 = $this->createStub(ObjectId::class); $object_id_mock_02->method("toInt")->willReturn(2); $object_id_mock_02->method("toReferenceIds")->willThrowException(new UnexpectedValueException("unexpected method call")); - $object_id_mock_03 = $this->createMock(ObjectId::class); + $object_id_mock_03 = $this->createStub(ObjectId::class); $object_id_mock_03->method("toInt")->willReturn(3); $object_id_mock_03->method("toReferenceIds")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_mock_01 = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock_01 = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock_01->method("getObjectId")->willReturn($object_id_mock_01); $key_mock_01->method("withObjectId")->willThrowException(new UnexpectedValueException("unexpected method call")); $key_mock_01->method("equals")->willThrowException(new UnexpectedValueException("unexpected method call")); $key_mock_01->method("isValid")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_mock_02 = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock_02 = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock_02->method("getObjectId")->willReturn($object_id_mock_02); $key_mock_02->method("withObjectId")->willThrowException(new UnexpectedValueException("unexpected method call")); $key_mock_02->method("equals")->willThrowException(new UnexpectedValueException("unexpected method call")); $key_mock_02->method("isValid")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_mock_03 = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock_03 = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock_03->method("getObjectId")->willReturn($object_id_mock_03); $key_mock_03->method("withObjectId")->willThrowException(new UnexpectedValueException("unexpected method call")); $key_mock_03->method("equals")->willThrowException(new UnexpectedValueException("unexpected method call")); @@ -80,7 +80,7 @@ public function testExportHandlerPublicAccessRepository(): void $element_mock_01->method("withValues")->willThrowException(new UnexpectedValueException("unexpected method call")); $element_mock_01->method("withKey")->willThrowException(new UnexpectedValueException("unexpected method call")); $element_mock_01->method("isStorable")->willReturn(true); - $element_mock_01->method("equals")->with($element_mock_01)->willReturn(true); + $element_mock_01->expects($this->atLeastOnce())->method("equals")->with($element_mock_01)->willReturn(true); $element_mock_02 = $this->createMock(ilExportHandlerPublicAccessRepositoryElementInterface::class); $element_mock_02->method("getValues")->willThrowException(new UnexpectedValueException("unexpected method call")); @@ -88,17 +88,9 @@ public function testExportHandlerPublicAccessRepository(): void $element_mock_02->method("withValues")->willThrowException(new UnexpectedValueException("unexpected method call")); $element_mock_02->method("withKey")->willThrowException(new UnexpectedValueException("unexpected method call")); $element_mock_02->method("isStorable")->willReturn(true); - $element_mock_02->method("equals")->with($element_mock_02)->willReturn(true); + $element_mock_02->expects($this->atLeastOnce())->method("equals")->with($element_mock_02)->willReturn(true); - $element_not_storable_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryElementInterface::class); - $element_not_storable_mock->method("getValues")->willThrowException(new UnexpectedValueException("unexpected method call")); - $element_not_storable_mock->method("getKey")->willReturn($key_mock_03); - $element_not_storable_mock->method("withValues")->willThrowException(new UnexpectedValueException("unexpected method call")); - $element_not_storable_mock->method("withKey")->willThrowException(new UnexpectedValueException("unexpected method call")); - $element_not_storable_mock->method("isStorable")->willReturn(false); - $element_not_storable_mock->method("equals")->with($element_not_storable_mock)->willReturn(true); - - $key_collection_01_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); + $key_collection_01_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); # next, rewind are void $key_collection_01_mock->method("key")->willReturn(0, 1); $key_collection_01_mock->method("valid")->willReturn(true, false); @@ -106,7 +98,7 @@ public function testExportHandlerPublicAccessRepository(): void $key_collection_01_mock->method("current")->willReturn($key_mock_01); $key_collection_01_mock->method("withElement")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_collection_02_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); + $key_collection_02_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); # next, rewind are void $key_collection_02_mock->method("key")->willReturn(0, 1); $key_collection_02_mock->method("valid")->willReturn(true, false); @@ -114,7 +106,7 @@ public function testExportHandlerPublicAccessRepository(): void $key_collection_02_mock->method("current")->willReturn($key_mock_02); $key_collection_02_mock->method("withElement")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_collection_03_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); + $key_collection_03_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); # next, rewind are void $key_collection_03_mock->method("key")->willReturn(0, 1); $key_collection_03_mock->method("valid")->willReturn(true, false); @@ -122,7 +114,7 @@ public function testExportHandlerPublicAccessRepository(): void $key_collection_03_mock->method("current")->willReturn($key_mock_03); $key_collection_03_mock->method("withElement")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_collection_all_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); + $key_collection_all_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); # next, rewind are void $key_collection_all_mock->method("key")->willReturn(0, 1, 2); $key_collection_all_mock->method("valid")->willReturn(true, true, false); @@ -130,7 +122,7 @@ public function testExportHandlerPublicAccessRepository(): void $key_collection_all_mock->method("current")->willReturn($key_mock_01, $key_mock_02); $key_collection_all_mock->method("withElement")->willThrowException(new UnexpectedValueException("unexpected method call")); - $key_collection_empty_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); + $key_collection_empty_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyCollectionInterface::class); # next, rewind are void $key_collection_empty_mock->method("key")->willReturn(0); $key_collection_empty_mock->method("valid")->willReturn(false); @@ -142,11 +134,11 @@ public function testExportHandlerPublicAccessRepository(): void [$key_mock_03, $key_collection_03_mock], ]); - $key_factory_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyFactoryInterface::class); + $key_factory_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyFactoryInterface::class); $key_factory_mock->method("handler")->willThrowException(new UnexpectedValueException("unexpected method call")); $key_factory_mock->method("collection")->willReturn($key_collection_empty_mock); - $db_wrapper_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryDBWrapperInterface::class); + $db_wrapper_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryDBWrapperInterface::class); $db_wrapper_mock->method("storeElement")->willReturnCallback(function ($x) { $this->mockDBWrapperStore($x); }); @@ -219,13 +211,13 @@ public function testExportHandlerPublicAccessRepository(): void } protected function mockDBWrapperStore( - ilExportHandlerPublicAccessRepositoryElementInterface&MockObject $element_mock + ilExportHandlerPublicAccessRepositoryElementInterface $element_mock ): void { $this->repository_elements[] = $element_mock; } protected function mockDBWrapperRemoveByKeyCollection( - ilExportHandlerPublicAccessRepositoryKeyCollectionInterface&MockObject $key_collection_mock + ilExportHandlerPublicAccessRepositoryKeyCollectionInterface $key_collection_mock ): void { $ids = []; for ($i = 0; $i < count($key_collection_mock); $i++) { @@ -241,8 +233,8 @@ protected function mockDBWrapperRemoveByKeyCollection( } protected function mockDBWrapperGetElementsByKeyCollection( - ilExportHandlerPublicAccessRepositoryKeyCollectionInterface&MockObject $key_collection_mock - ): ilExportHandlerPublicAccessRepositoryElementCollectionInterface&MockObject { + ilExportHandlerPublicAccessRepositoryKeyCollectionInterface $key_collection_mock + ): ilExportHandlerPublicAccessRepositoryElementCollectionInterface { $elements = []; for ($i = 0; $i < $key_collection_mock->count(); $i++) { $current = $key_collection_mock->current(); @@ -252,7 +244,7 @@ protected function mockDBWrapperGetElementsByKeyCollection( } } } - $element_collection_mock = $this->createMock(ilExportHandlerPublicAccessRepositoryElementCollectionInterface::class); + $element_collection_mock = $this->createStub(ilExportHandlerPublicAccessRepositoryElementCollectionInterface::class); $element_collection_mock->method("withElement")->willThrowException(new UnexpectedValueException("unexpected method call")); $element_collection_mock->method("key")->willThrowException(new UnexpectedValueException("unexpected method call")); $element_collection_mock->method("next")->willThrowException(new UnexpectedValueException("unexpected method call")); diff --git a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/Collection.php b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/Collection.php index c7408b11b632..8522332a6889 100644 --- a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/Collection.php +++ b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/Collection.php @@ -30,24 +30,24 @@ class Collection extends TestCase { public function testExportHandlerPublicAccessRepositoryKeyCollection(): void { - $object_id_mock_01 = $this->createMock(ObjectId::class); + $object_id_mock_01 = $this->createStub(ObjectId::class); $object_id_mock_01->method("toInt")->willReturn(1); $object_id_mock_01->method("toReferenceIds")->willThrowException(new Exception("unexpected access of reference ids")); - $object_id_mock_02 = $this->createMock(ObjectId::class); + $object_id_mock_02 = $this->createStub(ObjectId::class); $object_id_mock_02->method("toInt")->willReturn(2); $object_id_mock_02->method("toReferenceIds")->willThrowException(new Exception("unexpected access of reference ids")); - $object_id_mock_03 = $this->createMock(ObjectId::class); + $object_id_mock_03 = $this->createStub(ObjectId::class); $object_id_mock_03->method("toInt")->willReturn(3); $object_id_mock_03->method("toReferenceIds")->willThrowException(new Exception("unexpected access of reference ids")); - $key_mock_01 = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock_01 = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock_01->method("isValid")->willReturn(true); $key_mock_01->method("getObjectId")->willReturn($object_id_mock_01); $key_mock_01->method("withObjectId")->willThrowException(new Exception("unexpected overwrite of object id")); - $key_mock_02 = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock_02 = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock_02->method("isValid")->willReturn(true); $key_mock_02->method("getObjectId")->willReturn($object_id_mock_02); $key_mock_02->method("withObjectId")->willThrowException(new Exception("unexpected overwrite of object id")); - $key_mock_03 = $this->createMock(ilExportHandlerPublicAccessRepositoryKeyInterface::class); + $key_mock_03 = $this->createStub(ilExportHandlerPublicAccessRepositoryKeyInterface::class); $key_mock_03->method("isValid")->willReturn(true); $key_mock_03->method("getObjectId")->willReturn($object_id_mock_03); $key_mock_03->method("withObjectId")->willThrowException(new Exception("unexpected overwrite of object id")); diff --git a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/HandlerTest.php b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/HandlerTest.php index 587742a9f1e5..f0099fbee61c 100644 --- a/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/HandlerTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/PublicAccess/Repository/Key/HandlerTest.php @@ -32,13 +32,13 @@ class HandlerTest extends TestCase public function testExportHandlerPublicAccessRepositoryKey(): void { $object_id = 2; - $object_id_mock = $this->createMock(ObjectId::class); + $object_id_mock = $this->createStub(ObjectId::class); $object_id_mock->method("toInt")->willReturn($object_id); $object_id_mock->method("toReferenceIds")->willThrowException(new Exception("unexpected access of reference ids")); - $object_id_invalid_mock = $this->createMock(ObjectId::class); + $object_id_invalid_mock = $this->createStub(ObjectId::class); $object_id_invalid_mock->method('toInt')->willReturn(ilExportHandlerPublicAccessRepositoryKeyInterface::EMPTY_OBJECT_ID); $object_id_invalid_mock->method("toReferenceIds")->willThrowException(new Exception("toReferenceIds should not be called")); - $df_factory_wrapper_mock = $this->createMock(ilExportHandlerDataFactoryWrapperInterface::class); + $df_factory_wrapper_mock = $this->createStub(ilExportHandlerDataFactoryWrapperInterface::class); $df_factory_wrapper_mock->method('objId')->willReturn($object_id_invalid_mock); try { $key = new ilExportHandlerPublicAccessRepositoryKey($df_factory_wrapper_mock); diff --git a/components/ILIAS/Export/tests/ExportHandler/Repository/Element/CollectionTest.php b/components/ILIAS/Export/tests/ExportHandler/Repository/Element/CollectionTest.php index ff41f6e268f8..e7a228cb4916 100644 --- a/components/ILIAS/Export/tests/ExportHandler/Repository/Element/CollectionTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/Repository/Element/CollectionTest.php @@ -34,21 +34,21 @@ public function testExportHandlerRepositoryElementCollection(): void $date_1 = new DateTimeImmutable('2020-01-01'); $date_2 = new DateTimeImmutable('2020-01-02'); $date_3 = new DateTimeImmutable('2020-01-03'); - $values_mock_01 = $this->createMock(ilExportHandlerRepositoryValuesInterface::class); + $values_mock_01 = $this->createStub(ilExportHandlerRepositoryValuesInterface::class); $values_mock_01->method("getCreationDate")->willReturn($date_1); - $values_mock_02 = $this->createMock(ilExportHandlerRepositoryValuesInterface::class); + $values_mock_02 = $this->createStub(ilExportHandlerRepositoryValuesInterface::class); $values_mock_02->method("getCreationDate")->willReturn($date_2); - $values_mock_03 = $this->createMock(ilExportHandlerRepositoryValuesInterface::class); + $values_mock_03 = $this->createStub(ilExportHandlerRepositoryValuesInterface::class); $values_mock_03->method("getCreationDate")->willReturn($date_2); - $values_mock_04 = $this->createMock(ilExportHandlerRepositoryValuesInterface::class); + $values_mock_04 = $this->createStub(ilExportHandlerRepositoryValuesInterface::class); $values_mock_04->method("getCreationDate")->willReturn($date_3); - $element_mock_01 = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_mock_01 = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_mock_01->method('getValues')->willReturn($values_mock_01); - $element_mock_02 = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_mock_02 = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_mock_02->method('getValues')->willReturn($values_mock_02); - $element_mock_03 = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_mock_03 = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_mock_03->method('getValues')->willReturn($values_mock_03); - $element_mock_04 = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_mock_04 = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_mock_04->method('getValues')->willReturn($values_mock_04); $element_mock_01->method("equals")->willReturnMap([ [$element_mock_01, true], [$element_mock_02, false], diff --git a/components/ILIAS/Export/tests/ExportHandler/Repository/Element/HandlerTest.php b/components/ILIAS/Export/tests/ExportHandler/Repository/Element/HandlerTest.php index d42b88463b10..b90e9b636c0c 100644 --- a/components/ILIAS/Export/tests/ExportHandler/Repository/Element/HandlerTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/Repository/Element/HandlerTest.php @@ -37,31 +37,31 @@ class HandlerTest extends TestCase public function testExportHandlerRepositoryElement(): void { $resouce_id_serialized = "keykeykey"; - $object_id_mock = $this->createMock(ObjectId::class); - $key_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $object_id_mock = $this->createStub(ObjectId::class); + $key_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $key_mock->method("isCompleteKey")->willReturn(true); $key_mock->method("isObjectIdKey")->willReturn(false); $key_mock->method("isResourceIdKey")->willReturn(false); $key_mock->method("getResourceIdSerialized")->willReturn($resouce_id_serialized); $key_mock->method("getObjectId")->willReturn($object_id_mock); - $date_time_mock = $this->createMock(DateTimeImmutable::class); - $value_mock = $this->createMock(ilExportHandlerRepositoryValuesInterface::class); + $date_time_mock = $this->createStub(DateTimeImmutable::class); + $value_mock = $this->createStub(ilExportHandlerRepositoryValuesInterface::class); $value_mock->method("isValid")->willReturn(true); $value_mock->method("getOwnerId")->willReturn(1); $value_mock->method("getCreationDate")->willReturn($date_time_mock); $irss_wrapper_mock = $this->createMock(ilExportHandlerRepositoryElementIRSSWrapperInterface::class); - $irss_wrapper_mock->method("withResourceIdSerialized")->with($resouce_id_serialized)->willReturn($irss_wrapper_mock); - $irss_wrapper_factory_mock = $this->createMock(ilExportHandlerRepositoryElementIRSSWrapperFactoryInterface::class); + $irss_wrapper_mock->expects($this->atLeastOnce())->method("withResourceIdSerialized")->with($resouce_id_serialized)->willReturn($irss_wrapper_mock); + $irss_wrapper_factory_mock = $this->createStub(ilExportHandlerRepositoryElementIRSSWrapperFactoryInterface::class); $irss_wrapper_factory_mock->method("handler")->willReturn($irss_wrapper_mock); $irss_info_wrapper_mock = $this->createMock(ilExportHandlerRepositoryElementIRSSInfoWrapperInterface::class); - $irss_info_wrapper_mock->method("withResourceIdSerialized")->with($resouce_id_serialized)->willReturn($irss_info_wrapper_mock); - $irss_info_wrapper_factory_mock = $this->createMock(ilExportHandlerRepositoryElementIRSSInfoWrapperFactoryInterface::class); + $irss_info_wrapper_mock->expects($this->atLeastOnce())->method("withResourceIdSerialized")->with($resouce_id_serialized)->willReturn($irss_info_wrapper_mock); + $irss_info_wrapper_factory_mock = $this->createStub(ilExportHandlerRepositoryElementIRSSInfoWrapperFactoryInterface::class); $irss_info_wrapper_factory_mock->method("handler")->willReturn($irss_info_wrapper_mock); try { - $element = (new ilExportHandlerRepositoryElement( + $element = new ilExportHandlerRepositoryElement( $irss_wrapper_factory_mock, $irss_info_wrapper_factory_mock - )) + ) ->withKey($key_mock) ->withValues($value_mock); $element_not_storable_0 = new ilExportHandlerRepositoryElement( diff --git a/components/ILIAS/Export/tests/ExportHandler/Repository/Handler.php b/components/ILIAS/Export/tests/ExportHandler/Repository/Handler.php index ce2f4ce0452e..60e44c0afc20 100644 --- a/components/ILIAS/Export/tests/ExportHandler/Repository/Handler.php +++ b/components/ILIAS/Export/tests/ExportHandler/Repository/Handler.php @@ -47,35 +47,35 @@ public function testExportHandlerRepository(): void $resource_id_serialized = "rid"; $owner_id = 6; $creation_date = new DateTimeImmutable(); - $element_collection_mock01 = $this->createMock(ilExportHandlerRepositoryElementCollectionInterface::class); - $stakeholder_mock = $this->createMock(ilExportHandlerRepositoryStakeholderInterface::class); + $element_collection_mock01 = $this->createStub(ilExportHandlerRepositoryElementCollectionInterface::class); + $stakeholder_mock = $this->createStub(ilExportHandlerRepositoryStakeholderInterface::class); $stakeholder_mock->method("getOwnerId")->willReturn($owner_id); $stakeholder_mock->method("withOwnerId")->willThrowException(new Exception("owner id changed")); $this->repository_elements = []; - $object_id_mock01 = $this->createMock(ObjectId::class); + $object_id_mock01 = $this->createStub(ObjectId::class); $object_id_mock01->method("toInt")->willReturn(1); $object_id_mock01->method("toReferenceIds")->willThrowException(new Exception("unexpected method call")); - $key_complete_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $key_complete_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $key_complete_mock->method("withObjectId")->with($object_id_mock01)->willReturn($key_complete_mock); $key_complete_mock->method("withResourceIdSerialized")->with($resource_id_serialized)->willReturn($key_complete_mock); $key_complete_mock->method("getObjectId")->willReturn($object_id_mock01); $key_complete_mock->method("getResourceIdSerialized")->willReturn($resource_id_serialized); - $key_obj_id_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $key_obj_id_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $key_obj_id_mock->method("withObjectId")->with($object_id_mock01)->willReturn($key_obj_id_mock); $key_obj_id_mock->method("withResourceIdSerialized")->with($resource_id_serialized)->willReturn($key_complete_mock); $key_obj_id_mock->method("getObjectId")->willReturn($object_id_mock01); $key_obj_id_mock->method("getResourceIdSerialized")->willThrowException(new Exception("resource id not set")); - $key_res_id_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $key_res_id_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $key_res_id_mock->method("withObjectId")->with($object_id_mock01)->willReturn($key_complete_mock); $key_res_id_mock->method("withResourceIdSerialized")->with($resource_id_serialized)->willReturn($key_res_id_mock); $key_res_id_mock->method("getObjectId")->willThrowException(new Exception("obj id not set")); $key_res_id_mock->method("getResourceIdSerialized")->willReturn($resource_id_serialized); - $key_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $key_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $key_mock->method("withObjectId")->with($object_id_mock01)->willReturn($key_obj_id_mock); $key_mock->method("withResourceIdSerialized")->with($resource_id_serialized)->willReturn($key_res_id_mock); $key_mock->method("getObjectId")->willThrowException(new Exception("obj id not set")); $key_mock->method("getResourceIdSerialized")->willThrowException(new Exception("resource id not set")); - $key_collection_with_element_mock = $this->createMock(ilExportHandlerRepositoryKeyCollectionInterface::class); + $key_collection_with_element_mock = $this->createStub(ilExportHandlerRepositoryKeyCollectionInterface::class); $key_collection_with_element_mock->method("withElement")->willThrowException(new Exception("to many keys added to collection")); $key_collection_with_element_mock->method("current")->willReturn($key_complete_mock); $key_collection_with_element_mock->method("key")->willReturn(0, 1); @@ -83,7 +83,7 @@ public function testExportHandlerRepository(): void # rewind() does not return anything $key_collection_with_element_mock->method("valid")->willReturn(true, false); $key_collection_with_element_mock->method("count")->willReturn(1); - $key_collection_mock01 = $this->createMock(ilExportHandlerRepositoryKeyCollectionInterface::class); + $key_collection_mock01 = $this->createStub(ilExportHandlerRepositoryKeyCollectionInterface::class); $key_collection_mock01->method("withElement")->with($key_complete_mock)->willReturn($key_collection_with_element_mock); $key_collection_mock01->method("withElement")->with($key_mock)->willThrowException(new Exception("key incomplete")); $key_collection_mock01->method("withElement")->with($key_obj_id_mock)->willThrowException(new Exception("key incomplete")); @@ -94,39 +94,39 @@ public function testExportHandlerRepository(): void # rewind() does not return anything $key_collection_mock01->method("valid")->willReturn(false); $key_collection_mock01->method("count")->willReturn(0); - $key_factory_mock = $this->createMock(ilExportHandlerRepositoryKeyFactoryInterface::class); + $key_factory_mock = $this->createStub(ilExportHandlerRepositoryKeyFactoryInterface::class); $key_factory_mock->method("handler")->willReturn($key_mock); $key_factory_mock->method("collection")->willReturn($key_collection_mock01); - $export_info_mock = $this->createMock(ilExportHandlerExportInfoInterface::class); - $irss_wrapper_mock = $this->createMock(ilExportHandlerRepositoryIRSSWrapperInterface::class); + $export_info_mock = $this->createStub(ilExportHandlerExportInfoInterface::class); + $irss_wrapper_mock = $this->createStub(ilExportHandlerRepositoryIRSSWrapperInterface::class); $irss_wrapper_mock->method('createEmptyContainer')->with($export_info_mock, $stakeholder_mock)->willReturn($resource_id_serialized); $irss_wrapper_mock->method("getCreationDate")->with($resource_id_serialized)->willReturn($creation_date); - $value_mock = $this->createMock(ilExportHandlerRepositoryValuesInterface::class); + $value_mock = $this->createStub(ilExportHandlerRepositoryValuesInterface::class); $value_mock->method("withOwnerId")->with($owner_id)->willReturn($value_mock); $value_mock->method("withCreationDate")->with($creation_date)->willReturn($value_mock); $value_mock->method("getOwnerId")->willReturn($owner_id); $value_mock->method("getCreationDate")->willReturn($creation_date); - $values_factory_mock = $this->createMock(ilExportHandlerRepositoryValuesFactoryInterface::class); + $values_factory_mock = $this->createStub(ilExportHandlerRepositoryValuesFactoryInterface::class); $values_factory_mock->method("handler")->willReturn($value_mock); - $element_complete_mock = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_complete_mock = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_complete_mock->method("isStorable")->willReturn(true); $element_complete_mock->method("withKey")->with($key_mock)->willReturn($element_complete_mock); $element_complete_mock->method("withValues")->with($value_mock)->willReturn($element_complete_mock); - $element_w_key_mock = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_w_key_mock = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_w_key_mock->method("isStorable")->willReturn(false); $element_w_key_mock->method("withKey")->with($key_mock)->willReturn($element_w_key_mock); $element_w_key_mock->method("withValues")->with($value_mock)->willReturn($element_complete_mock); - $element_w_values_mock = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_w_values_mock = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_w_values_mock->method("isStorable")->willReturn(false); $element_w_values_mock->method("withKey")->with($key_mock)->willReturn($element_complete_mock); $element_w_values_mock->method("withValues")->with($value_mock)->willReturn($element_w_values_mock); - $element_emtpy_mock = $this->createMock(ilExportHandlerRepositoryElementInterface::class); + $element_emtpy_mock = $this->createStub(ilExportHandlerRepositoryElementInterface::class); $element_emtpy_mock->method("isStorable")->willReturn(false); $element_emtpy_mock->method("withKey")->with($key_mock)->willReturn($element_w_key_mock); $element_emtpy_mock->method("withValues")->with($value_mock)->willReturn($element_w_values_mock); - $element_factory_mock = $this->createMock(ilExportHandlerRepositoryElementFactoryInterface::class); + $element_factory_mock = $this->createStub(ilExportHandlerRepositoryElementFactoryInterface::class); $element_factory_mock->method("handler")->willReturn($element_emtpy_mock); - $db_wrapper_mock = $this->createMock(ilExportHandlerRepositoryDBWrapperInterface::class); + $db_wrapper_mock = $this->createStub(ilExportHandlerRepositoryDBWrapperInterface::class); $db_wrapper_mock->method("getElements")->with($key_collection_mock01)->willReturnCallback(function ($x) { return $this->mockDBWrapperGetElements($x); }); diff --git a/components/ILIAS/Export/tests/ExportHandler/Repository/Key/CollectionTest.php b/components/ILIAS/Export/tests/ExportHandler/Repository/Key/CollectionTest.php index 72c7fa9534d3..747fb5b0690c 100644 --- a/components/ILIAS/Export/tests/ExportHandler/Repository/Key/CollectionTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/Repository/Key/CollectionTest.php @@ -30,19 +30,19 @@ class CollectionTest extends TestCase { public function testExportHandlerRepositoryKeyCollection(): void { - $object_id_mock_01 = $this->createMock(ObjectId::class); + $object_id_mock_01 = $this->createStub(ObjectId::class); $object_id_mock_01->method('toInt')->willReturn(1); - $object_id_mock_02 = $this->createMock(ObjectId::class); + $object_id_mock_02 = $this->createStub(ObjectId::class); $object_id_mock_02->method('toInt')->willReturn(2); - $object_id_mock_03 = $this->createMock(ObjectId::class); + $object_id_mock_03 = $this->createStub(ObjectId::class); $object_id_mock_03->method('toInt')->willReturn(3); - $element_1_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $element_1_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $element_1_mock->method('getResourceIdSerialized')->willReturn('r1'); $element_1_mock->method('getObjectId')->willReturn($object_id_mock_01); - $element_2_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $element_2_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $element_2_mock->method('getResourceIdSerialized')->willReturn('r2'); $element_2_mock->method('getObjectId')->willReturn($object_id_mock_02); - $element_3_mock = $this->createMock(ilExportHandlerRepositoryKeyInterface::class); + $element_3_mock = $this->createStub(ilExportHandlerRepositoryKeyInterface::class); $element_3_mock->method('getResourceIdSerialized')->willReturn('r3'); $element_3_mock->method('getObjectId')->willReturn($object_id_mock_03); $empty_collection = new ilExportHandlerRepositoryKeyCollection(); diff --git a/components/ILIAS/Export/tests/ExportHandler/Repository/Key/HandlerTest.php b/components/ILIAS/Export/tests/ExportHandler/Repository/Key/HandlerTest.php index 8a9c48a34129..4267caa22237 100644 --- a/components/ILIAS/Export/tests/ExportHandler/Repository/Key/HandlerTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/Repository/Key/HandlerTest.php @@ -32,13 +32,13 @@ class HandlerTest extends TestCase public function testExportHandlerRepositoryKey(): void { $resource_identification = "abc"; - $object_id_mock = $this->createMock(ObjectId::class); + $object_id_mock = $this->createStub(ObjectId::class); $object_id_mock->method('toInt')->willReturn(123); $object_id_mock->method("toReferenceIds")->willThrowException(new Exception("toReferenceIds should not be called")); - $object_id_invalid_mock = $this->createMock(ObjectId::class); + $object_id_invalid_mock = $this->createStub(ObjectId::class); $object_id_invalid_mock->method('toInt')->willReturn(ilExportHandlerRepositoryKeyInterface::EMPTY_OBJECT_ID); $object_id_invalid_mock->method("toReferenceIds")->willThrowException(new Exception("toReferenceIds should not be called")); - $df_factory_wrapper_mock = $this->createMock(ilExportHandlerDataFactoryWrapperInterface::class); + $df_factory_wrapper_mock = $this->createStub(ilExportHandlerDataFactoryWrapperInterface::class); $df_factory_wrapper_mock->method('objId')->willReturn($object_id_invalid_mock); try { $repository_key_empty = new ilExportHandlerRepositoryKey( diff --git a/components/ILIAS/Export/tests/ExportHandler/Table/RowId/CollectionTest.php b/components/ILIAS/Export/tests/ExportHandler/Table/RowId/CollectionTest.php index 76fc2d8c8f3a..76bd0b275d0a 100644 --- a/components/ILIAS/Export/tests/ExportHandler/Table/RowId/CollectionTest.php +++ b/components/ILIAS/Export/tests/ExportHandler/Table/RowId/CollectionTest.php @@ -29,15 +29,15 @@ class CollectionTest extends TestCase { public function testExportHandlerTableRowIdCollection(): void { - $table_row_id_mock_1 = $this->createMock(ilExportHandlerTableRowIdInterface::class); + $table_row_id_mock_1 = $this->createStub(ilExportHandlerTableRowIdInterface::class); $table_row_id_mock_1->method('getFileIdentifier')->willReturn("1"); $table_row_id_mock_1->method('getExportOptionId')->willReturn("e"); $table_row_id_mock_1->method('getCompositId')->willReturn("e:1"); - $table_row_id_mock_2 = $this->createMock(ilExportHandlerTableRowIdInterface::class); + $table_row_id_mock_2 = $this->createStub(ilExportHandlerTableRowIdInterface::class); $table_row_id_mock_2->method('getFileIdentifier')->willReturn("2"); $table_row_id_mock_2->method('getExportOptionId')->willReturn("e"); $table_row_id_mock_2->method('getCompositId')->willReturn("e:2"); - $table_row_id_mock_3 = $this->createMock(ilExportHandlerTableRowIdInterface::class); + $table_row_id_mock_3 = $this->createStub(ilExportHandlerTableRowIdInterface::class); $table_row_id_mock_3->method('getFileIdentifier')->willReturn("3"); $table_row_id_mock_3->method('getExportOptionId')->willReturn("e"); $table_row_id_mock_3->method('getCompositId')->willReturn("e:3"); diff --git a/components/ILIAS/Export/tests/ImportHandler/File/HandlerTest.php b/components/ILIAS/Export/tests/ImportHandler/File/HandlerTest.php index 1e9928b297d3..ea3086e3497f 100755 --- a/components/ILIAS/Export/tests/ImportHandler/File/HandlerTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/File/HandlerTest.php @@ -35,15 +35,15 @@ public function testFileHandler(): void . 'B' . DIRECTORY_SEPARATOR . 'C'; $file_path = $file_dir . DIRECTORY_SEPARATOR . $file_name; - $namespaces = $this->createMock(ilFileNamespaceCollection::class); + $namespaces = $this->createStub(ilFileNamespaceCollection::class); - $namespace = $this->createMock(ilFileNamespaceFactory::class); - $namespace->expects($this->any())->method('collection')->willReturn($namespaces); + $namespace = $this->createStub(ilFileNamespaceFactory::class); + $namespace->method('collection')->willReturn($namespaces); - $file_info = $this->createMock(SplFileInfo::class); - $file_info->expects($this->any())->method('getFilename')->willReturn($file_name); - $file_info->expects($this->any())->method('getRealPath')->willReturn(false); - $file_info->expects($this->any())->method('getPath')->willReturn($file_dir); + $file_info = $this->createStub(SplFileInfo::class); + $file_info->method('getFilename')->willReturn($file_name); + $file_info->method('getRealPath')->willReturn(false); + $file_info->method('getPath')->willReturn($file_dir); $file_handler = new ilFileHandler($namespace); $file_handler = $file_handler->withFileInfo($file_info); diff --git a/components/ILIAS/Export/tests/ImportHandler/File/Namespace/CollectionTest.php b/components/ILIAS/Export/tests/ImportHandler/File/Namespace/CollectionTest.php index 0f0b0d2e69ff..7e57a77ca727 100755 --- a/components/ILIAS/Export/tests/ImportHandler/File/Namespace/CollectionTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/File/Namespace/CollectionTest.php @@ -29,7 +29,7 @@ class CollectionTest extends TestCase { protected function setUp(): void { - $namespace_1 = $this->createMock(ilFileNamespaceHandler::class); + $namespace_1 = $this->createStub(ilFileNamespaceHandler::class); } /** @@ -55,24 +55,24 @@ protected function checkCollection( public function testCollection(): void { - $namespace_1 = $this->createMock(ilFileNamespaceHandler::class); - $namespace_1->expects($this->any())->method('getNamespace')->willReturn('namespace_1'); - $namespace_1->expects($this->any())->method('getPrefix')->willReturn('prefix_1'); + $namespace_1 = $this->createStub(ilFileNamespaceHandler::class); + $namespace_1->method('getNamespace')->willReturn('namespace_1'); + $namespace_1->method('getPrefix')->willReturn('prefix_1'); - $namespace_2 = $this->createMock(ilFileNamespaceHandler::class); - $namespace_2->expects($this->any())->method('getNamespace')->willReturn('namespace_2'); - $namespace_2->expects($this->any())->method('getPrefix')->willReturn('prefix_2'); + $namespace_2 = $this->createStub(ilFileNamespaceHandler::class); + $namespace_2->method('getNamespace')->willReturn('namespace_2'); + $namespace_2->method('getPrefix')->willReturn('prefix_2'); - $namespace_3 = $this->createMock(ilFileNamespaceHandler::class); - $namespace_3->expects($this->any())->method('getNamespace')->willReturn('namespace_3'); - $namespace_3->expects($this->any())->method('getPrefix')->willReturn('prefix_3'); + $namespace_3 = $this->createStub(ilFileNamespaceHandler::class); + $namespace_3->method('getNamespace')->willReturn('namespace_3'); + $namespace_3->method('getPrefix')->willReturn('prefix_3'); - $collection_one_element = (new ilFileNamespaceCollection()) + $collection_one_element = new ilFileNamespaceCollection() ->withElement($namespace_1); - $collection_two_elements = (new ilFileNamespaceCollection()) + $collection_two_elements = new ilFileNamespaceCollection() ->withElement($namespace_1) ->withElement($namespace_2); - $collection_three_elements = (new ilFileNamespaceCollection()) + $collection_three_elements = new ilFileNamespaceCollection() ->withElement($namespace_1) ->withElement($namespace_2) ->withElement($namespace_3); diff --git a/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/Attribute/ilCollectionTest.php b/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/Attribute/ilCollectionTest.php index 89884d765755..601eb8f7c158 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/Attribute/ilCollectionTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/Attribute/ilCollectionTest.php @@ -30,31 +30,31 @@ class ilCollectionTest extends TestCase { public function testNodeInfoAttributeCollection(): void { - $logger = $this->createMock(ilLogger::class); - $node_info = $this->createMock(ilXMLFileNodeInfoDOMNodeHandler::class); - $node_info->expects($this->any())->method('getValueOfAttribute')->willReturnMap([ + $logger = $this->createStub(ilLogger::class); + $node_info = $this->createStub(ilXMLFileNodeInfoDOMNodeHandler::class); + $node_info->method('getValueOfAttribute')->willReturnMap([ ['key1', 'val1'], ['key2', 'val2'], ['key3', 'val3'], ]); - $node_info->expects($this->any())->method('hasAttribute')->willReturnMap([ + $node_info->method('hasAttribute')->willReturnMap([ ['key1', true], ['key2', true], ['key3', true], ['key4', false] ]); - $pair1 = $this->createMock(ilXMLFileNodeInfoAttributePair::class); - $pair1->expects($this->any())->method('getKey')->willReturn('key1'); - $pair1->expects($this->any())->method('getValue')->willReturn('val1'); - $pair2 = $this->createMock(ilXMLFileNodeInfoAttributePair::class); - $pair2->expects($this->any())->method('getKey')->willReturn('key2'); - $pair2->expects($this->any())->method('getValue')->willReturn('val2'); - $pair3 = $this->createMock(ilXMLFileNodeInfoAttributePair::class); - $pair3->expects($this->any())->method('getKey')->willReturn('key3'); - $pair3->expects($this->any())->method('getValue')->willReturn('val3'); - $pair4 = $this->createMock(ilXMLFileNodeInfoAttributePair::class); - $pair4->expects($this->any())->method('getKey')->willReturn('key4'); - $pair4->expects($this->any())->method('getValue')->willReturn('val4'); + $pair1 = $this->createStub(ilXMLFileNodeInfoAttributePair::class); + $pair1->method('getKey')->willReturn('key1'); + $pair1->method('getValue')->willReturn('val1'); + $pair2 = $this->createStub(ilXMLFileNodeInfoAttributePair::class); + $pair2->method('getKey')->willReturn('key2'); + $pair2->method('getValue')->willReturn('val2'); + $pair3 = $this->createStub(ilXMLFileNodeInfoAttributePair::class); + $pair3->method('getKey')->willReturn('key3'); + $pair3->method('getValue')->willReturn('val3'); + $pair4 = $this->createStub(ilXMLFileNodeInfoAttributePair::class); + $pair4->method('getKey')->willReturn('key4'); + $pair4->method('getValue')->willReturn('val4'); $collection = (new ilXMLFileNodeInfoAttributeCollection($logger)) ->withElement($pair1) diff --git a/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/ilCollectionTest.php b/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/ilCollectionTest.php index a5418672e3e2..4975007eb24e 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/ilCollectionTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Parser/NodeInfo/ilCollectionTest.php @@ -28,9 +28,9 @@ class ilCollectionTest extends TestCase { public function testNodeInfoCollection(): void { - $node1 = $this->createMock(Handler::class); - $node2 = $this->createMock(Handler::class); - $node3 = $this->createMock(Handler::class); + $node1 = $this->createStub(Handler::class); + $node2 = $this->createStub(Handler::class); + $node3 = $this->createStub(Handler::class); $collection = new Collection(); $collection = $collection->withElement($node1); diff --git a/components/ILIAS/Export/tests/ImportHandler/Path/HandlerTest.php b/components/ILIAS/Export/tests/ImportHandler/Path/HandlerTest.php index 01a96908b221..4576efc267e7 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Path/HandlerTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Path/HandlerTest.php @@ -33,15 +33,15 @@ protected function setUp(): void public function testPath(): void { - $node1 = $this->createMock(ilSimpleFilePathNode::class); - $node1->expects($this->any())->method('toString')->willReturn('Node1'); - $node1->expects($this->any())->method('requiresPathSeparator')->willReturn(true); - $node2 = $this->createMock(ilSimpleFilePathNode::class); - $node2->expects($this->any())->method('toString')->willReturn('Node2'); - $node2->expects($this->any())->method('requiresPathSeparator')->willReturn(true); - $node3 = $this->createMock(ilSimpleFilePathNode::class); - $node3->expects($this->any())->method('toString')->willReturn('Node3'); - $node3->expects($this->any())->method('requiresPathSeparator')->willReturn(true); + $node1 = $this->createStub(ilSimpleFilePathNode::class); + $node1->method('toString')->willReturn('Node1'); + $node1->method('requiresPathSeparator')->willReturn(true); + $node2 = $this->createStub(ilSimpleFilePathNode::class); + $node2->method('toString')->willReturn('Node2'); + $node2->method('requiresPathSeparator')->willReturn(true); + $node3 = $this->createStub(ilSimpleFilePathNode::class); + $node3->method('toString')->willReturn('Node3'); + $node3->method('requiresPathSeparator')->willReturn(true); $nodes = [$node1, $node2, $node3]; $path = new ilFilePathHandler(); diff --git a/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilAttributeTest.php b/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilAttributeTest.php index ca10dc89ac3f..07b1fec260ed 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilAttributeTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilAttributeTest.php @@ -33,8 +33,8 @@ protected function setUp(): void public function testAttributeTest(): void { - $comp = $this->createMock(ilFilePathComparisonHandler::class); - $comp->expects($this->any())->method('toString')->willReturn('<3'); + $comp = $this->createStub(ilFilePathComparisonHandler::class); + $comp->method('toString')->willReturn('<3'); $node = new ilAttributeFilePathNode(); $node2 = $node diff --git a/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilIndexTest.php b/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilIndexTest.php index 30928d8bd86c..58c848d2c10f 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilIndexTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Path/Node/ilIndexTest.php @@ -28,8 +28,8 @@ class ilIndexTest extends TestCase { public function testIndexNode(): void { - $comp = $this->createMock(ilFilePathComparisonHandler::class); - $comp->expects($this->any())->method('toString')->willReturn('<3'); + $comp = $this->createStub(ilFilePathComparisonHandler::class); + $comp->method('toString')->willReturn('<3'); $node = new ilIndexFilePathNode(); $node2 = $node->withIndex(20); diff --git a/components/ILIAS/Export/tests/ImportHandler/Validation/Set/HandlerTest.php b/components/ILIAS/Export/tests/ImportHandler/Validation/Set/HandlerTest.php index 3c006a39774a..af82994e243e 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Validation/Set/HandlerTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Validation/Set/HandlerTest.php @@ -30,9 +30,9 @@ class HandlerTest extends TestCase { public function testFileValidationSetHandler(): void { - $xsd_file = $this->createMock(ilXSDFileHandler::class); - $xml_file = $this->createMock(ilXMLFileHandler::class); - $file_path = $this->createMock(ilFilePathHandler::class); + $xsd_file = $this->createStub(ilXSDFileHandler::class); + $xml_file = $this->createStub(ilXMLFileHandler::class); + $file_path = $this->createStub(ilFilePathHandler::class); $set = (new ilFileValidationSetHandler()) ->withFilePathHandler($file_path) diff --git a/components/ILIAS/Export/tests/ImportHandler/Validation/Set/ilCollectionTest.php b/components/ILIAS/Export/tests/ImportHandler/Validation/Set/ilCollectionTest.php index b2e6e03c4163..bfe9fbd51dfe 100755 --- a/components/ILIAS/Export/tests/ImportHandler/Validation/Set/ilCollectionTest.php +++ b/components/ILIAS/Export/tests/ImportHandler/Validation/Set/ilCollectionTest.php @@ -28,9 +28,9 @@ class ilCollectionTest extends TestCase { public function testSetCollection(): void { - $set1 = $this->createMock(ilFileValidationSetHandler::class); - $set2 = $this->createMock(ilFileValidationSetHandler::class); - $set3 = $this->createMock(ilFileValidationSetHandler::class); + $set1 = $this->createStub(ilFileValidationSetHandler::class); + $set2 = $this->createStub(ilFileValidationSetHandler::class); + $set3 = $this->createStub(ilFileValidationSetHandler::class); $sets = [$set1, $set2, $set3]; $collection = (new ilFileValidationSetCollection()) diff --git a/components/ILIAS/Export/tests/ilExportOptionsTest.php b/components/ILIAS/Export/tests/ilExportOptionsTest.php index 8f6ef139f280..57a27e5a557a 100755 --- a/components/ILIAS/Export/tests/ilExportOptionsTest.php +++ b/components/ILIAS/Export/tests/ilExportOptionsTest.php @@ -66,6 +66,6 @@ protected function initDependencies(): void $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); } } diff --git a/components/ILIAS/Group/tests/ilGroupEventHandlerTest.php b/components/ILIAS/Group/tests/ilGroupEventHandlerTest.php index 70da423715ec..83b73b035def 100755 --- a/components/ILIAS/Group/tests/ilGroupEventHandlerTest.php +++ b/components/ILIAS/Group/tests/ilGroupEventHandlerTest.php @@ -60,14 +60,9 @@ protected function initDependencies(): void $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $logger = $this->getMockBuilder(ilLogger::class) - ->disableOriginalConstructor() - ->getMock(); + $logger = $this->createStub(ilLogger::class); - $logger_factory = $this->getMockBuilder(ilLoggerFactory::class) - ->disableOriginalConstructor() - ->onlyMethods(['getComponentLogger']) - ->getMock(); + $logger_factory = $this->createStub(ilLoggerFactory::class); $logger_factory->method('getComponentLogger')->willReturn($logger); $this->setGlobalVariable('ilLoggerFactory', $logger_factory); } diff --git a/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php b/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php index 5d61818f82fa..6044770181fd 100755 --- a/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php +++ b/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php @@ -62,6 +62,6 @@ protected function initDependencies(): void { $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); } } diff --git a/components/ILIAS/Membership/tests/ilWaitingListTest.php b/components/ILIAS/Membership/tests/ilWaitingListTest.php index c3d686dffd68..22f90feb4e13 100755 --- a/components/ILIAS/Membership/tests/ilWaitingListTest.php +++ b/components/ILIAS/Membership/tests/ilWaitingListTest.php @@ -52,8 +52,8 @@ protected function initDependencies(): void $this->dic = new Container(); $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); - $this->setGlobalVariable('ilAppEventHandler', $this->createMock(ilAppEventHandler::class)); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); + $this->setGlobalVariable('ilAppEventHandler', $this->createStub(ilAppEventHandler::class)); } protected function setGlobalVariable(string $name, $value): void diff --git a/components/ILIAS/MetaData/tests/Copyright/CopyrightDataTest.php b/components/ILIAS/MetaData/tests/Copyright/CopyrightDataTest.php index 5d5c9e409598..1c71e54a313d 100755 --- a/components/ILIAS/MetaData/tests/Copyright/CopyrightDataTest.php +++ b/components/ILIAS/MetaData/tests/Copyright/CopyrightDataTest.php @@ -28,7 +28,7 @@ class CopyrightDataTest extends TestCase { protected function getMockURI(): URI { - return $this->createMock(URI::class); + return $this->createStub(URI::class); } protected function getData(?URI $image_link, string $image_file): CopyrightData diff --git a/components/ILIAS/MetaData/tests/Copyright/DatabaseRepositoryTest.php b/components/ILIAS/MetaData/tests/Copyright/DatabaseRepositoryTest.php index 3291ec775c4d..41fafb0c3f79 100755 --- a/components/ILIAS/MetaData/tests/Copyright/DatabaseRepositoryTest.php +++ b/components/ILIAS/MetaData/tests/Copyright/DatabaseRepositoryTest.php @@ -127,7 +127,7 @@ public function insert(string $table, array $values): void protected function getMockURI(): URI|MockObject { - return $this->createMock(URI::class); + return $this->createStub(URI::class); } protected function getRepo(WrapperInterface $wrapper): DatabaseRepository diff --git a/components/ILIAS/MetaData/tests/Copyright/RendererTest.php b/components/ILIAS/MetaData/tests/Copyright/RendererTest.php index 485d33d0871b..058cf4adefe3 100755 --- a/components/ILIAS/MetaData/tests/Copyright/RendererTest.php +++ b/components/ILIAS/MetaData/tests/Copyright/RendererTest.php @@ -124,31 +124,22 @@ public function exposeLegacyData(): array protected function getMockIcon(): MockObject|Icon { - return $this->getMockBuilder(IIcon::class) - ->disableOriginalConstructor() - ->getMock(); + return $this->createStub(IIcon::class); } protected function getMockLink(): MockObject|Link { - return $this->getMockBuilder(ILink::class) - ->disableOriginalConstructor() - ->onlyMethods(['withAdditionalRelationshipToReferencedResource']) - ->getMock(); + return $this->createStub(ILink::class); } protected function getMockLegacy(): MockObject|Content { - return $this->getMockBuilder(ILegacy::class) - ->disableOriginalConstructor() - ->getMock(); + return $this->createStub(ILegacy::class); } protected function getMockURI(string $link): URI { - $uri = $this->getMockBuilder(URI::class) - ->disableOriginalConstructor() - ->getMock(); + $uri = $this->createStub(URI::class); $uri->method('__toString')->willReturn($link); return $uri; } diff --git a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/HandlerTest.php b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/HandlerTest.php index 7f86881e57ca..24d5eb3933e1 100644 --- a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/HandlerTest.php +++ b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/HandlerTest.php @@ -37,7 +37,7 @@ class HandlerTest extends TestCase { protected function getURI(string $string): URI { - $url = $this->createMock(URI::class); + $url = $this->createStub(URI::class); $url->method('__toString')->willReturn($string); return $url; } diff --git a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/ParserTest.php b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/ParserTest.php index dcf0faf65b3d..010943242417 100644 --- a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/ParserTest.php +++ b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/ParserTest.php @@ -29,7 +29,7 @@ class ParserTest extends TestCase { protected function getURI(string $string): URI { - $url = $this->createMock(URI::class); + $url = $this->createStub(URI::class); $url->method('__toString')->willReturn($string); return $url; } diff --git a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/RequestTest.php b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/RequestTest.php index dd582c41d112..6442a7c61138 100644 --- a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/RequestTest.php +++ b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Requests/RequestTest.php @@ -29,7 +29,7 @@ class RequestTest extends TestCase { protected function getURI(): URI { - return $this->createMock(URI::class); + return $this->createStub(URI::class); } protected function getEmptyRequest(): Request diff --git a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/RequestProcessorTestCase.php b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/RequestProcessorTestCase.php index c269ebb49e3d..61110f3bc6e4 100644 --- a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/RequestProcessorTestCase.php +++ b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/RequestProcessorTestCase.php @@ -46,7 +46,7 @@ protected function getDate(string $string): \DateTimeImmutable protected function getURI(string $string): URI { - $url = $this->createMock(URI::class); + $url = $this->createStub(URI::class); $url->method('__toString')->willReturn($string); return $url; } diff --git a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/WriterTest.php b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/WriterTest.php index 48c637d306ce..a829375a2947 100644 --- a/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/WriterTest.php +++ b/components/ILIAS/MetaData/tests/OERExposer/OAIPMH/Responses/WriterTest.php @@ -32,7 +32,7 @@ class WriterTest extends TestCase { protected function getURI(string $string): URI { - $url = $this->createMock(URI::class); + $url = $this->createStub(URI::class); $url->method('__toString')->willReturn($string); return $url; } diff --git a/components/ILIAS/MetaData/tests/OERHarvester/CronJob/AutomaticPublisherTest.php b/components/ILIAS/MetaData/tests/OERHarvester/CronJob/AutomaticPublisherTest.php index 01cbd4813035..8eae5661a969 100644 --- a/components/ILIAS/MetaData/tests/OERHarvester/CronJob/AutomaticPublisherTest.php +++ b/components/ILIAS/MetaData/tests/OERHarvester/CronJob/AutomaticPublisherTest.php @@ -400,7 +400,7 @@ public function writeSimpleDCMetaData(int $obj_id, int $ref_id, string $type): \ protected function getNullLogger(): \ilLogger { - return $this->createMock(\ilLogger::class); + return $this->createStub(\ilLogger::class); } protected function getCronResultWrapper(): WrapperInterface diff --git a/components/ILIAS/MetaData/tests/OERHarvester/Publisher/PublisherTest.php b/components/ILIAS/MetaData/tests/OERHarvester/Publisher/PublisherTest.php index 67073dfbeb58..ffce7ba46f6a 100644 --- a/components/ILIAS/MetaData/tests/OERHarvester/Publisher/PublisherTest.php +++ b/components/ILIAS/MetaData/tests/OERHarvester/Publisher/PublisherTest.php @@ -255,7 +255,7 @@ public function writeSimpleDCMetaData(int $obj_id, int $ref_id, string $type): \ protected function getNullAccess(): \ilAccess { - return $this->createMock(\ilAccess::class); + return $this->createStub(\ilAccess::class); } public function testBlock(): void diff --git a/components/ILIAS/MetaData/tests/Presentation/DataTest.php b/components/ILIAS/MetaData/tests/Presentation/DataTest.php index 2519c9208146..7ebf6bb52fc3 100755 --- a/components/ILIAS/MetaData/tests/Presentation/DataTest.php +++ b/components/ILIAS/MetaData/tests/Presentation/DataTest.php @@ -69,7 +69,7 @@ public function vocabularySlot(): SlotIdentifier protected function getData(): Data { - $format = $this->createMock(DateFormat::class); + $format = $this->createStub(DateFormat::class); $format->method('applyTo')->willReturnCallback(function (\DateTimeImmutable $arg) { return $arg->format('d:m:Y'); }); diff --git a/components/ILIAS/MetaData/tests/Presentation/UtilitiesTest.php b/components/ILIAS/MetaData/tests/Presentation/UtilitiesTest.php index 692f40f83491..8c2a9c9d50c6 100755 --- a/components/ILIAS/MetaData/tests/Presentation/UtilitiesTest.php +++ b/components/ILIAS/MetaData/tests/Presentation/UtilitiesTest.php @@ -43,13 +43,13 @@ protected function setUp(): void return key_exists($arg, $map); }); - $this->format = $this->createMock(DateFormat::class); - $user = $this->createMock(\ilObjUser::class); + $this->format = $this->createStub(DateFormat::class); + $user = $this->createStub(\ilObjUser::class); $user->method('getDateFormat')->willReturn($this->format); - $refinery = $this->createMock(Refinery::class); - $encoding_group = $this->createMock(EncodeGroup::class); - $transformation = $this->createMock(Transformation::class); + $refinery = $this->createStub(Refinery::class); + $encoding_group = $this->createStub(EncodeGroup::class); + $transformation = $this->createStub(Transformation::class); $transformation->method('transform')->willReturnCallback(function ($arg) { return '~encoded:' . $arg . '~'; }); diff --git a/components/ILIAS/MetaData/tests/Services/CopyrightHelper/CopyrightTest.php b/components/ILIAS/MetaData/tests/Services/CopyrightHelper/CopyrightTest.php index d912de2dc928..8693a2d41dfb 100644 --- a/components/ILIAS/MetaData/tests/Services/CopyrightHelper/CopyrightTest.php +++ b/components/ILIAS/MetaData/tests/Services/CopyrightHelper/CopyrightTest.php @@ -42,16 +42,12 @@ class CopyrightTest extends TestCase { protected function getIcon(): Icon { - return $this->getMockBuilder(IIcon::class) - ->disableOriginalConstructor() - ->getMock(); + return $this->createStub(IIcon::class); } protected function getLink(): Link { - return $this->getMockBuilder(ILink::class) - ->disableOriginalConstructor() - ->getMock(); + return $this->createStub(ILink::class); } protected function getRenderer( diff --git a/components/ILIAS/MetaData/tests/XML/Copyright/CopyrightHandlerTest.php b/components/ILIAS/MetaData/tests/XML/Copyright/CopyrightHandlerTest.php index 1e92e59a645c..a5a60d42f90e 100644 --- a/components/ILIAS/MetaData/tests/XML/Copyright/CopyrightHandlerTest.php +++ b/components/ILIAS/MetaData/tests/XML/Copyright/CopyrightHandlerTest.php @@ -39,7 +39,7 @@ class CopyrightHandlerTest extends TestCase { protected function getURI(string $link): URI { - $url = $this->createMock(URI::class); + $url = $this->createStub(URI::class); $url->method('__toString')->willReturn($link); return $url; } diff --git a/components/ILIAS/Poll/tests/PollBlockTest.php b/components/ILIAS/Poll/tests/PollBlockTest.php index fb84e11c3f60..63bdafdbf229 100755 --- a/components/ILIAS/Poll/tests/PollBlockTest.php +++ b/components/ILIAS/Poll/tests/PollBlockTest.php @@ -48,8 +48,8 @@ protected function setUp(): void $dic = new ILIAS\DI\Container(); $GLOBALS['DIC'] = $dic; - $db = $this->createMock(ilDBInterface::class); - $lng = $this->createMock(ilLanguage::class); + $db = $this->createStub(ilDBInterface::class); + $lng = $this->createStub(ilLanguage::class); $this->setGlobalVariable( "ilDB", diff --git a/components/ILIAS/PrivacySecurity/tests/ilPrivacySettingsTest.php b/components/ILIAS/PrivacySecurity/tests/ilPrivacySettingsTest.php index ede535384ef8..cfdefd6ceff4 100755 --- a/components/ILIAS/PrivacySecurity/tests/ilPrivacySettingsTest.php +++ b/components/ILIAS/PrivacySecurity/tests/ilPrivacySettingsTest.php @@ -74,10 +74,10 @@ protected function initDependencies(): void define('SYSTEM_FOLDER_ID', 9); } - $this->setGlobalVariable('ilDB', $this->createMock(ilDBInterface::class)); - $this->setGlobalVariable('ilSetting', $this->getMockBuilder(ilSetting::class)->disableOriginalConstructor()->getMock()); - $this->setGlobalVariable('ilUser', $this->getMockBuilder(ilObjUser::class)->disableOriginalConstructor()->getMock()); - $this->setGlobalVariable('ilAccess', $this->getMockBuilder(ilAccess::class)->disableOriginalConstructor()->getMock()); - $this->setGlobalVariable('rbacsystem', $this->getMockBuilder(ilRbacSystem::class)->disableOriginalConstructor()->getMock()); + $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); + $this->setGlobalVariable('ilSetting', $this->createStub(ilSetting::class)); + $this->setGlobalVariable('ilUser', $this->createStub(ilObjUser::class)); + $this->setGlobalVariable('ilAccess', $this->createStub(ilAccess::class)); + $this->setGlobalVariable('rbacsystem', $this->createStub(ilRbacSystem::class)); } } diff --git a/components/ILIAS/Session/tests/EventItemsTest.php b/components/ILIAS/Session/tests/EventItemsTest.php index 3fcbc88e1da0..c2e97b045a33 100755 --- a/components/ILIAS/Session/tests/EventItemsTest.php +++ b/components/ILIAS/Session/tests/EventItemsTest.php @@ -48,8 +48,8 @@ protected function setUp(): void $dic = new ILIAS\DI\Container(); $GLOBALS['DIC'] = $dic; - $db = $this->createMock(ilDBInterface::class); - $tree = $this->createMock(ilTree::class); + $db = $this->createStub(ilDBInterface::class); + $tree = $this->createStub(ilTree::class); $this->setGlobalVariable( "ilDB", diff --git a/components/ILIAS/SystemCheck/tests/ilSystemCheckTaskTest.php b/components/ILIAS/SystemCheck/tests/ilSystemCheckTaskTest.php index 9efd05f50bd9..f3c9893975e4 100755 --- a/components/ILIAS/SystemCheck/tests/ilSystemCheckTaskTest.php +++ b/components/ILIAS/SystemCheck/tests/ilSystemCheckTaskTest.php @@ -44,9 +44,7 @@ public function testConstruct(): void public function testLastUpdate(): void { - $this->getMockBuilder(ilDateTime::class) - ->disableOriginalConstructor() - ->getMock(); + $this->createStub(ilDateTime::class); $task = new ilSCTask(); $last_update = $task->getLastUpdate(); @@ -70,16 +68,11 @@ protected function initDependencies(): void $GLOBALS['DIC'] = $this->dic; $this->setGlobalVariable( 'ilDB', - $this->createMock(ilDBInterface::class) + $this->createStub(ilDBInterface::class) ); - $logger = $this->getMockBuilder(ilLogger::class) - ->disableOriginalConstructor() - ->getMock(); + $logger = $this->createStub(ilLogger::class); - $logger_factory = $this->getMockBuilder(ilLoggerFactory::class) - ->disableOriginalConstructor() - ->onlyMethods(['getComponentLogger']) - ->getMock(); + $logger_factory = $this->createStub(ilLoggerFactory::class); $logger_factory->method('getComponentLogger')->willReturn($logger); $this->setGlobalVariable('ilLoggerFactory', $logger_factory); } diff --git a/components/ILIAS/Tracking/tests/ilLPStatusIconsTest.php b/components/ILIAS/Tracking/tests/ilLPStatusIconsTest.php index 1181ee365682..8fed1e19830a 100755 --- a/components/ILIAS/Tracking/tests/ilLPStatusIconsTest.php +++ b/components/ILIAS/Tracking/tests/ilLPStatusIconsTest.php @@ -38,7 +38,7 @@ class ilLPStatusIconsTest extends TestCase protected function getUIFactory(): UIFactory { - $custom_icon = $this->createMock(Custom::class); + $custom_icon = $this->createStub(Custom::class); $custom_icon->method('getIconPath') ->willReturn($this->path); $custom_icon->method('getSize') @@ -46,15 +46,15 @@ protected function getUIFactory(): UIFactory $custom_icon->method('getLabel') ->willReturn($this->alt); - $icon_factory = $this->createMock(IconFactory::class); + $icon_factory = $this->createStub(IconFactory::class); $icon_factory->method('custom') ->willReturn($custom_icon); - $symbol_factory = $this->createMock(SymbolFactory::class); + $symbol_factory = $this->createStub(SymbolFactory::class); $symbol_factory->method('icon') ->willReturn($icon_factory); - $factory = $this->createMock(UIFactory::class); + $factory = $this->createStub(UIFactory::class); $factory->method('symbol') ->willReturn($symbol_factory); @@ -63,7 +63,7 @@ protected function getUIFactory(): UIFactory protected function getUIRenderer(): UIRenderer { - $renderer = $this->createMock(UIRenderer::class); + $renderer = $this->createStub(UIRenderer::class); $renderer->method('render') ->willReturnCallback(function ($arg) { return 'rendered: path(' . $arg->getIconPath() . @@ -104,13 +104,9 @@ public function testTripleton(): array public function testGetInstanceForInvalidVariant(): void { - $renderer = $this->getMockBuilder(UIRenderer::class) - ->disableOriginalConstructor() - ->getMock(); + $renderer = $this->createStub(UIRenderer::class); - $factory = $this->getMockBuilder(UIFactory::class) - ->disableOriginalConstructor() - ->getMock(); + $factory = $this->createStub(UIFactory::class); $this->expectException(ilLPException::class); ilLPStatusIcons::getInstance(793, $renderer, $factory); diff --git a/components/ILIAS/Tracking/tests/ilTrackingCollectionTest.php b/components/ILIAS/Tracking/tests/ilTrackingCollectionTest.php index 542e95367506..8d00b7b9e156 100755 --- a/components/ILIAS/Tracking/tests/ilTrackingCollectionTest.php +++ b/components/ILIAS/Tracking/tests/ilTrackingCollectionTest.php @@ -62,16 +62,11 @@ protected function initDependencies(): void $GLOBALS['DIC'] = $this->dic; $this->setGlobalVariable( 'ilDB', - $this->createMock(ilDBInterface::class) + $this->createStub(ilDBInterface::class) ); - $logger = $this->getMockBuilder(ilLogger::class) - ->disableOriginalConstructor() - ->getMock(); + $logger = $this->createStub(ilLogger::class); - $logger_factory = $this->getMockBuilder(ilLoggerFactory::class) - ->disableOriginalConstructor() - ->onlyMethods(['getComponentLogger']) - ->getMock(); + $logger_factory = $this->createStub(ilLoggerFactory::class); $logger_factory->method('getComponentLogger')->willReturn($logger); $this->setGlobalVariable('ilLoggerFactory', $logger_factory); } diff --git a/components/ILIAS/WebResource/tests/ilWebResourceDatabaseRepositoryTest.php b/components/ILIAS/WebResource/tests/ilWebResourceDatabaseRepositoryTest.php index edd1660cb667..624fc6274e74 100644 --- a/components/ILIAS/WebResource/tests/ilWebResourceDatabaseRepositoryTest.php +++ b/components/ILIAS/WebResource/tests/ilWebResourceDatabaseRepositoryTest.php @@ -20,6 +20,7 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use ILIAS\DI\Container; /** @@ -44,9 +45,7 @@ protected function initDependencies(): void $this->dic = is_object($DIC) ? clone $DIC : $DIC; $GLOBALS['DIC'] = new Container(); - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->getMock(); + $user = $this->createMock(ilObjUser::class); $user->expects($this->never()) ->method($this->anything()); @@ -55,10 +54,10 @@ protected function initDependencies(): void } /** - * @param ilDBInterface&MockObject $mock_db + * @param ilDBInterface&Stub $mock_db * @param int $webr_id * @param int $current_time - * @param DateTimeImmutable&MockObject[] $datetimes + * @param DateTimeImmutable[] $datetimes * @return void */ protected function setGlobalDBAndRepo( @@ -93,7 +92,7 @@ protected function setGlobalDBAndRepo( ->willReturnOnConsecutiveCalls(...$datetimes); } - protected function setGlobal(string $name, MockObject $obj): void + protected function setGlobal(string $name, MockObject|Stub $obj): void { global $DIC; @@ -112,14 +111,11 @@ protected function tearDown(): void } /** - * @return DateTimeImmutable&MockObject + * @return DateTimeImmutable */ - protected function getNewDateTimeMock(int $timestamp): MockObject + protected function getNewDateTimeMock(int $timestamp): Stub { - $datetime = $this->getMockBuilder(DateTimeImmutable::class) - ->disableOriginalConstructor() - ->onlyMethods(['getTimestamp']) - ->getMock(); + $datetime = $this->createStub(DateTimeImmutable::class); $datetime->method('getTimestamp') ->willReturn($timestamp); @@ -134,9 +130,7 @@ protected function getNewDateTimeMock(int $timestamp): MockObject #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] public function testCreateExternalItem(): void { - $mock_db = $this->getMockBuilder(ilDBInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $mock_db = $this->createMock(ilDBInterface::class); $mock_db->expects($this->exactly(3)) ->method('nextId') @@ -260,9 +254,7 @@ public function testCreateExternalItem(): void #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] public function testCreateInternalItemWithBrokenParameter(): void { - $mock_db = $this->getMockBuilder(ilDBInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $mock_db = $this->createMock(ilDBInterface::class); $mock_db->expects($this->exactly(3)) ->method('nextId') ->willReturn(7, 71, 72); @@ -364,9 +356,7 @@ public function testCreateInternalItemWithBrokenParameter(): void #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] public function testCreateItemBrokenInternalLinkException(): void { - $mock_db = $this->getMockBuilder(ilDBInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $mock_db = $this->createMock(ilDBInterface::class); $mock_db->expects($this->once()) ->method('nextId') @@ -414,11 +404,10 @@ public function testCreateItemBrokenInternalLinkException(): void #[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)] #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] public function testCreateList(): void { - $mock_db = $this->getMockBuilder(ilDBInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $mock_db = $this->createMock(ilDBInterface::class); $mock_db->expects($this->never()) ->method('nextId'); @@ -465,11 +454,10 @@ public function testCreateList(): void #[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)] #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] public function testCreateAllItemsInDraftContainer(): void { - $mock_db = $this->getMockBuilder(ilDBInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $mock_db = $this->createStub(ilDBInterface::class); $datetime1 = $this->getNewDateTimeMock(12345678); $datetime2 = $this->getNewDateTimeMock(12345678); diff --git a/components/ILIAS/WebResource/tests/ilWebResourceItemExternalTest.php b/components/ILIAS/WebResource/tests/ilWebResourceItemExternalTest.php index 9833325bff25..7da8a932c94f 100755 --- a/components/ILIAS/WebResource/tests/ilWebResourceItemExternalTest.php +++ b/components/ILIAS/WebResource/tests/ilWebResourceItemExternalTest.php @@ -28,29 +28,21 @@ class ilWebResourceItemExternalTest extends TestCase { public function testGetResolvedLink(): void { - $param1 = $this->getMockBuilder(ilWebLinkParameter::class) - ->disableOriginalConstructor() - ->onlyMethods(['appendToLink', 'getValue']) - ->getMock(); + $param1 = $this->createMock(ilWebLinkParameter::class); $param1->expects($this->once()) ->method('appendToLink') ->with('target') ->willReturn('target?param1'); - $param1->expects($this->any()) - ->method('getValue') + $param1->method('getValue') ->willReturn(1); - $param2 = $this->getMockBuilder(ilWebLinkParameter::class) - ->disableOriginalConstructor() - ->onlyMethods(['appendToLink', 'getValue']) - ->getMock(); + $param2 = $this->createMock(ilWebLinkParameter::class); $param2->expects($this->once()) ->method('appendToLink') ->with('target?param1') ->willReturn('target?param1¶m2'); - $param2->expects($this->any()) - ->method('getValue') + $param2->method('getValue') ->willReturn(1); $item = new ilWebLinkItemExternal( diff --git a/components/ILIAS/WebResource/tests/ilWebResourceItemInternalTest.php b/components/ILIAS/WebResource/tests/ilWebResourceItemInternalTest.php index acbca2912cbb..2288ff26d1b0 100644 --- a/components/ILIAS/WebResource/tests/ilWebResourceItemInternalTest.php +++ b/components/ILIAS/WebResource/tests/ilWebResourceItemInternalTest.php @@ -19,6 +19,7 @@ declare(strict_types=1); use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\Stub; /** * Unit tests for ilWebLinkItemInternal @@ -26,9 +27,9 @@ */ class ilWebResourceItemInternalTest extends TestCase { - protected function getItem(string $target, ilWebLinkParameter ...$parameters): ilWebLinkItemInternal + protected function getItem(string $target, ilWebLinkParameter ...$parameters): ilWebLinkItemInternal|Stub { - $item = $this->getMockBuilder(ilWebLinkItemInternal::class) + $item = $this->getStubBuilder(ilWebLinkItemInternal::class) ->setConstructorArgs([ 0, 1, @@ -41,7 +42,7 @@ protected function getItem(string $target, ilWebLinkParameter ...$parameters): i $parameters ]) ->onlyMethods(['appendParameter', 'getStaticLink']) - ->getMock(); + ->getStub(); $item->method('appendParameter')->willReturnCallback( fn(string $link, string $key, string $value) => $link . '.' . $key . '.' . $value ); @@ -53,18 +54,12 @@ protected function getItem(string $target, ilWebLinkParameter ...$parameters): i public function testGetResolvedLink(): void { - $param1 = $this->getMockBuilder(ilWebLinkParameter::class) - ->disableOriginalConstructor() - ->onlyMethods(['appendToLink']) - ->getMock(); + $param1 = $this->createMock(ilWebLinkParameter::class); $param1->expects($this->once()) ->method('appendToLink') ->with('tar:13') ->willReturn('tar:13?param1'); - $param2 = $this->getMockBuilder(ilWebLinkParameter::class) - ->disableOriginalConstructor() - ->onlyMethods(['appendToLink']) - ->getMock(); + $param2 = $this->createMock(ilWebLinkParameter::class); $param2->expects($this->once()) ->method('appendToLink') ->with('tar:13?param1') diff --git a/components/ILIAS/WebResource/tests/ilWebResourceItemTest.php b/components/ILIAS/WebResource/tests/ilWebResourceItemTest.php index 994a545eb0c8..5dc513e56d03 100755 --- a/components/ILIAS/WebResource/tests/ilWebResourceItemTest.php +++ b/components/ILIAS/WebResource/tests/ilWebResourceItemTest.php @@ -28,10 +28,7 @@ class ilWebResourceItemTest extends TestCase { public function testToXML(): void { - $writer = $this->getMockBuilder(ilXmlWriter::class) - ->disableOriginalConstructor() - ->onlyMethods(['xmlStartTag', 'xmlElement', 'xmlEndTag']) - ->getMock(); + $writer = $this->createMock(ilXmlWriter::class); $writer->expects($this->once()) ->method('xmlStartTag') ->with('WebLink', [ @@ -58,17 +55,11 @@ public function testToXML(): void ->method('xmlEndTag') ->with('WebLink'); - $param1 = $this->getMockBuilder(ilWebLinkParameter::class) - ->disableOriginalConstructor() - ->onlyMethods(['toXML']) - ->getMock(); + $param1 = $this->createMock(ilWebLinkParameter::class); $param1->expects($this->once()) ->method('toXML') ->with($writer); - $param2 = $this->getMockBuilder(ilWebLinkParameter::class) - ->disableOriginalConstructor() - ->onlyMethods(['toXML']) - ->getMock(); + $param2 = $this->createMock(ilWebLinkParameter::class); $param2->expects($this->once()) ->method('toXML') ->with($writer); diff --git a/components/ILIAS/WebResource/tests/ilWebResourceItemsContainerTest.php b/components/ILIAS/WebResource/tests/ilWebResourceItemsContainerTest.php index 227eb742bd7f..061d3d229517 100644 --- a/components/ILIAS/WebResource/tests/ilWebResourceItemsContainerTest.php +++ b/components/ILIAS/WebResource/tests/ilWebResourceItemsContainerTest.php @@ -19,7 +19,7 @@ declare(strict_types=1); use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; /** * Unit tests for ilWebLinkItemsContainer @@ -28,23 +28,20 @@ class ilWebResourceItemsContainerTest extends TestCase { /** - * @return ilWebLinkItem&MockObject + * @return ilWebLinkItem&Stub */ protected function createItemMock( bool $internal, string $title, int $link_id - ): MockObject { + ): Stub { if ($internal) { $class = ilWebLinkItemInternal::class; } else { $class = ilWebLinkItemExternal::class; } - $item = $this->getMockBuilder($class) - ->disableOriginalConstructor() - ->onlyMethods(['getTitle','getLinkId']) - ->getMock(); + $item = $this->createStub($class); $item->method('getTitle')->willReturn($title); $item->method('getLinkId')->willReturn($link_id); diff --git a/components/ILIAS/WebResource/tests/ilWebResourceParameterTest.php b/components/ILIAS/WebResource/tests/ilWebResourceParameterTest.php index 76c72351e9dc..9709582ba2d7 100755 --- a/components/ILIAS/WebResource/tests/ilWebResourceParameterTest.php +++ b/components/ILIAS/WebResource/tests/ilWebResourceParameterTest.php @@ -29,10 +29,7 @@ class ilWebResourceParameterTest extends TestCase { protected function initDependencies(): void { - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->onlyMethods(['getLogin', 'getId', 'getMatriculation']) - ->getMock(); + $user = $this->createStub(ilObjUser::class); $user->method('getLogin')->willReturn('login'); $user->method('getId')->willReturn(37); $user->method('getMatriculation')->willReturn('matriculation'); @@ -40,10 +37,7 @@ protected function initDependencies(): void public function testAppendToLink(): void { - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->onlyMethods(['getLogin', 'getId', 'getMatriculation']) - ->getMock(); + $user = $this->createStub(ilObjUser::class); $user->method('getLogin')->willReturn('login'); $user->method('getId')->willReturn(37); $user->method('getMatriculation')->willReturn('matriculation'); @@ -108,9 +102,7 @@ public function testAppendToLink(): void public function testAppendToLinkException(): void { - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->getMock(); + $user = $this->createMock(ilObjUser::class); $user->expects($this->never()) ->method($this->anything()); @@ -122,16 +114,11 @@ public function testAppendToLinkException(): void public function testToXML(): void { - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->getMock(); + $user = $this->createMock(ilObjUser::class); $user->expects($this->never()) ->method($this->anything()); - $writer = $this->getMockBuilder(ilXmlWriter::class) - ->disableOriginalConstructor() - ->onlyMethods(['xmlElement']) - ->getMock(); + $writer = $this->createMock(ilXmlWriter::class); /* * willReturnCallback is a workaround to replace withConsecutive. * The return value is irrelevant here, but if an unexpected parameter @@ -192,9 +179,7 @@ public function testToXML(): void public function testGetInfo(): void { - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->getMock(); + $user = $this->createMock(ilObjUser::class); $user->expects($this->never()) ->method($this->anything()); @@ -229,9 +214,7 @@ public function testGetInfo(): void public function testGetInfoException(): void { - $user = $this->getMockBuilder(ilObjUser::class) - ->disableOriginalConstructor() - ->getMock(); + $user = $this->createMock(ilObjUser::class); $user->expects($this->never()) ->method($this->anything()); From 96659b42bdb031a44ddc6434a5f2859a5d13746f Mon Sep 17 00:00:00 2001 From: selyesa Date: Tue, 14 Jul 2026 11:45:53 +0200 Subject: [PATCH 075/333] Create PRIVACY.md for Dashboard (#11750) * Create PRIVACY.md for Dashboard --------- Co-authored-by: iszmais <45942348+iszmais@users.noreply.github.com> --- components/ILIAS/Dashboard/PRIVACY.md | 124 ++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 components/ILIAS/Dashboard/PRIVACY.md diff --git a/components/ILIAS/Dashboard/PRIVACY.md b/components/ILIAS/Dashboard/PRIVACY.md new file mode 100644 index 000000000000..9358db08c33e --- /dev/null +++ b/components/ILIAS/Dashboard/PRIVACY.md @@ -0,0 +1,124 @@ +# Dashboard Privacy + +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** + + +## General Information + +The Dashboard is the central landing page after login. It aggregates personal data from various +ILIAS components and presents it in configurable blocks: Favourites (selected items), My Memberships, +Recommended Content, Learning Sequences, and Study Programmes. The Dashboard itself stores a small +amount of personal data (favourites and view preferences). All other personal data shown on the +Dashboard is managed by the respective integrated components. + +The Favourites feature can be globally enabled or disabled via the Repository settings +(`rep_favourites`). When disabled, no favourites data presented. Similarly, +the "My Memberships" view can be globally toggled (`mmbr_my_crs_grp`). The Achievements area +(learning history, competences, learning progress, badges, certificates) is accessible from the +Dashboard but each sub-feature has its own activation setting. + +Whether a user can switch between list and tile presentation depends on the "change_presentation" +permission on the Dashboard Settings administration object. + +## Integrated Components + +- The Dashboard component employs the following components, please consult the respective + PRIVACY.md files: + - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) - manages permissions for Dashboard + administration and the "change_presentation" permission. + - [Notes](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Notes/PRIVACY.md) - the Dashboard can display personal notes and comments + if enabled in the settings. + - [News](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/News/PRIVACY.md) - the Dashboard side panel can display news items for + the user. + - [Mail](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Mail/PRIVACY.md) - the Dashboard side panel can display a mail block + showing recent messages. + - [Contact](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Contact/PRIVACY.md) - the Dashboard forwards to the contact/buddy list + interface. + - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md) - the Dashboard supports customizable page content + per language via the COPage service. + - Badge - badge information is accessible via the Achievements + area on the Dashboard. + - [Skill](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Skill/PRIVACY.md) - personal skills are accessible via the Achievements + area on the Dashboard. + - [Certificate](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Certificate/PRIVACY.md) - user certificates are accessible via the + Achievements area on the Dashboard. + - [StudyProgramme](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/StudyProgramme/PRIVACY.md) - study programme progress is shown + in a dedicated Dashboard view. + - [LearningSequence](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/LearningSequence/PRIVACY.md) - learning sequences are shown + in a dedicated Dashboard view. + - [Repository](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Repository/PRIVACY.md) - the Favourites feature references repository + objects and checks access permissions through the Repository. + - [Group](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Group/PRIVACY.md) - memberships in groups are listed in the "My Memberships" + view. Users can unsubscribe from groups via the Dashboard. + - Calendar - the Dashboard side panel can display a calendar block. Calendar has no + PRIVACY.md yet. + - User - the Dashboard accesses user preference data for sorting and presentation + settings. + - Tracking - the Dashboard preloads learning progress status for listed items. + Tracking has no PRIVACY.md yet. + - [LearningHistory](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/LearningHistory/PRIVACY.md) - the Achievements area provides access to the learning history. + - Course - memberships in courses are listed in the "My Memberships" view. Users can + unsubscribe from courses via the Dashboard. Course has no PRIVACY.md yet. + +## Data being stored + +- **User ID of the favourites owner**: When a user adds a repository object to their favourites, + the **user ID** is stored together with the **reference ID** of the object and its **type** in + the `desktop_item` table. This links each user to the objects they have marked as favourites. +- **Reference ID of the favourited object**: The **item_id** (reference ID) of the repository + object is stored in the `desktop_item` table to identify which object the user has favourited. +- **Object type of the favourited object**: The **type** of the favourited repository object is + stored in the `desktop_item` table to enable type-based filtering when retrieving favourites. +- **Presentation preference**: Each user's chosen presentation mode (list or tile) per Dashboard + view is stored as a **user preference** with the key `pd_view_pres_{view}`. +- **Sorting preference**: Each user's chosen sorting mode per Dashboard view is stored as a + **user preference** with the key `pd_order_items_{view}`. +- **Manual sort order data**: When a user selects manual sorting, the custom order of items is + stored as a **user preference** with the key `pd_order_data_{view}_{mode}` in JSON format. + +## Data being presented + +- **Each user** can view their own Dashboard, including: + - their own favourited repository objects (title, type icon, description, parent location). + - their own course and group memberships (title, type, description, period dates). + - their recommended content, learning sequences, and study programme items. +- **Each user** can manage their own favourites by adding or removing items. They can also + unsubscribe from courses and groups directly from the "My Memberships" view, provided they + have the "leave" permission on the respective object. +- **Persons with the "change_presentation" permission** on the Dashboard Settings administration + object can switch between all enabled modes. Users without this permission see + only the default presentation mode configured by a person with the "Edit Settings" permission. +- **Persons with the "read" permission** on the Dashboard Settings administration object can + view the Dashboard configuration, including which views are enabled and their sorting and + presentation defaults. +- **Persons with the "write" permission** on the Dashboard Settings administration object can + modify Dashboard configuration, including enabling or disabling views (Favourites, Memberships, + Study Programmes, Learning Sequences), enabling and setting default sorting and presentation modes, and + configuring side panel modules (Calendar, News, Mail, Tasks). + +## Data being deleted + +- **When a user removes a single favourite**: The corresponding entry (user ID, item reference ID, + type) is deleted from the `desktop_item` table. +- **When a user removes multiple favourites at once**: Each selected entry is deleted from the + `desktop_item` table. +- **When a repository object is deleted**: All favourite entries referencing that object are removed + from the `desktop_item` table via `removeFavouritesOfRefId()`, removing the association for all + users who had favourited that object. +- **When a user account is deleted**: The Dashboard listens for the `deleteUser` event from the + User service. Upon receiving it, all entries in the `desktop_item` table belonging to that user + are deleted. Additionally, a database update step ensures orphaned entries (where the + user no longer exists in `usr_data`) are cleaned up. +- **When a user unsubscribes from a course or group via the Dashboard**: The user's membership is + removed from the respective course or group. This is handled by the Course or Group component, + not by the Dashboard itself. The Dashboard only triggers the action if the user has the "leave" + permission. +- **User preferences**: When a user account is deleted, user preferences (presentation mode, + sorting mode, manual sort data) are deleted together with the user account by the User component. + +## Data being exported + +- The Dashboard component does not provide any dedicated export functionality for personal data. +- Favourites data is not included in any XML or file-based export. +- Data presented on the Dashboard (e.g., course memberships, learning progress, badges) is managed + and potentially exported by the respective integrated components. From 407e7107872acd585a729bf10e4d9f7da5055d0f Mon Sep 17 00:00:00 2001 From: selyesa Date: Tue, 14 Jul 2026 11:06:26 +0200 Subject: [PATCH 076/333] Create PRIVACY.md for ContentPage --- components/ILIAS/ContentPage/PRIVACY.md | 107 ++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 components/ILIAS/ContentPage/PRIVACY.md diff --git a/components/ILIAS/ContentPage/PRIVACY.md b/components/ILIAS/ContentPage/PRIVACY.md new file mode 100644 index 000000000000..876d5ceccfdd --- /dev/null +++ b/components/ILIAS/ContentPage/PRIVACY.md @@ -0,0 +1,107 @@ +# Content Page Privacy + +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** + + +## General Information + +The Content Page component provides a standalone content page object for use in the +repository. It allows the creation of rich-text content pages using the ILIAS page editor (COPage). +Content Pages can be placed in categories, courses, groups, and folders. + +The Content Page component itself does not store personal data. Its two database tables +(`content_page_data` and `content_page_metrics`) contain only object-level configuration +(stylesheet reference) and computed page metrics (estimated reading time per language), neither of +which includes user IDs or other personal data. All personal data handling – including learning +progress tracking, page edit history, notes, and metadata authorship – is delegated to the +respective integrated components listed below. + +A global administration setting controls whether the estimated **reading time** is displayed to +users in repository listings. This setting does not affect personal data, as reading time is a +property of the page content, not of individual users. + +## Integrated Components + +- The Content Page component employs the following components, please consult the respective + PRIVACY.md files: + - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md) — provides the page editor and page rendering engine. The + COPage component manages the page content, its edit history, and internal media objects. + - [MetaData](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/MetaData/PRIVACY.md) — stores metadata (e.g., author information) associated + with the Content Page object. + - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) — manages permissions for reading, editing, and + administering Content Page objects. + - [Export](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Export/PRIVACY.md) — provides the XML export functionality for Content Page + objects, including page content, metadata, and styles. + - [InfoScreen](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/InfoScreen/PRIVACY.md) — renders the Info tab, which may display metadata + and allows private notes. The Info tab can be enabled or disabled per Content Page. + - [Notes](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Notes/PRIVACY.md) — enables private notes for users on the Content Page Info + screen. + - [KioskMode](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/KioskMode/PRIVACY.md) — provides an embedded presentation mode used when a + Content Page is viewed inside a course or learning sequence context. + - Style — manages content styles applied to the Content Page. No personal data is handled by + the Style component in this context. + - ILIASObject — the Object service stores the account which created the Content Page object + and its timestamps. + - Tracking — tracks user progress on Content Page objects. Supports three modes: + deactivated, manual completion, and content visited. In manual mode, users can toggle their + completion status. In content-visited mode, progress is recorded automatically when the page + is viewed. + - [Container](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Container/PRIVACY.md) — used to store per-object settings such as Info tab visibility via container + settings. + +## Data being stored + +The Content Page component itself does not store personal data. Its database tables contain only: + +- `content_page_data`: the **Content Page ID** and a **stylesheet** reference (integer). No user + IDs or personal information. +- `content_page_metrics`: the **Content Page ID**, **page ID**, **language**, and computed + **reading time** in minutes. No user IDs or personal information. + +All personal data storage (such as learning progress records, page edit history, notes, and object +ownership) is handled by the integrated components listed above. + +## Data being presented + +- **Each user** with the "Read" permission can view the page content of the Content Page. +- **Each user** with the "Read" permission has their learning progress tracked automatically + when the Content Page uses the "content visited" learning progress mode. +- **Persons with the "Write" permission** can access the page editor to edit content, change + settings (title, description, online status, Info tab visibility), manage content styles, + manage translations, edit metadata, and access the export tab. +- **Persons with access to the Learning Progress** (as determined by + `ilLearningProgressAccess::checkAccess`) can view the Learning Progress tab, which may display + user names and completion statuses. The presentation of this data is handled by the + LearningProgress component. +- **Persons with the "Edit Permission" permission** can manage the permissions tab. +- If the **Info tab** is enabled (configurable per Content Page), persons with the "Visible" or + "Read" permission can access it. The Info tab may display metadata and allows private notes + (handled by the InfoScreen and Notes components). + +The Content Page component does not directly display user names, login names, or other personal +identifiers in its own user interface. Any such presentation occurs through delegated components +(e.g., LearningProgress, COPage, Notes). + +## Data being deleted + +- **When a Content Page object is deleted from trash**: all data in `content_page_data` and + `content_page_metrics` for that object is deleted. The associated page object is deleted via + the COPage component. Metadata, learning progress records, and other associated data managed + by integrated components are deleted according to their own lifecycle rules. +- **When a Content Page object is moved to trash**: the object becomes inaccessible but its data + remains in the database until the trash is emptied manually or by a cron job. +- **When a user account is deleted**: the Content Page component does not store user IDs, so no + data within its own tables is affected. Learning progress records, notes, and other + user-specific data associated with the Content Page are handled by the respective integrated + components. + +The Content Page component does not implement its own user-specific deletion methods (such as +`deleteByUserId`), as it does not store personal data. + +## Data being exported + +- **Persons with the "Write" permission** can export a Content Page object via the Export tab. + The XML export includes the Content Page's title, description, and Info tab visibility setting. + It also includes dependent data from integrated components: page content (COPage), metadata + (MetaData), content styles (Style), and common object data (ILIASObject). The export does not + include learning progress records or user-specific data. From 38f29e26d55209de6eee586e57da241135665c01 Mon Sep 17 00:00:00 2001 From: selyesa Date: Wed, 8 Jul 2026 14:23:27 +0200 Subject: [PATCH 077/333] Create PRIVACY.md for Chatroom --- components/ILIAS/Chatroom/PRIVACY.md | 153 +++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 components/ILIAS/Chatroom/PRIVACY.md diff --git a/components/ILIAS/Chatroom/PRIVACY.md b/components/ILIAS/Chatroom/PRIVACY.md new file mode 100644 index 000000000000..2b6f713d29ea --- /dev/null +++ b/components/ILIAS/Chatroom/PRIVACY.md @@ -0,0 +1,153 @@ +# Chatroom Privacy + +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any +> missing or incorrect information via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories).** + +## General Information + +The Chatroom component provides real-time text-based communication within ILIAS. It consists of +two interconnected parts: a PHP backend that manages room configuration, user connections, bans, +and message history via the ILIAS database, and a Node.js chat server that handles real-time +message delivery via WebSocket connections. The Node.js server also manages On-Screen Chat (OSC) +conversations, which enable direct messaging between users outside of chatroom objects. + +Several features affect the scope of personal data processing: + +- **Chat history**: When enabled per room (via the "Enable History" setting), session records are + created that log user connection and disconnection times. When disabled, no session records are + persisted. +- **Custom usernames**: When enabled per room, users may choose a display name different from their + real name. When disabled, the user's public name is used automatically. +- **On-Screen Chat (OSC)**: When enabled globally, direct conversations between users are stored in + separate database tables (`osc_conversation`, `osc_messages`, `osc_activity`). Users control + their participation via the `chat_osc_accept_msg` preference. +- **Message history display**: A per-room setting controls how many past messages are shown when a + user enters a room. +- **Periodic cleanup**: The Node.js server includes a timed process that deletes old chat messages, + OSC messages, OSC conversations, and OSC activity records based on a configured age threshold. + +## Integrated Components + +- The Chatroom component employs the following components, please consult the respective + PRIVACY.md files: + - AccessControl – manages permissions for chatroom access, + moderation, and settings. + - User – resolves user names and profile information for display in the chat. The Chatroom + component listens to the User component's `deleteUser` event to clean up ban records. + - Notifications – delivers on-screen notifications for chat + invitations. + - ILIASObject – the Object service stores the account which created a chatroom object and + its timestamps. + - InfoScreen – provides the info screen tab for chatroom objects. + - OnScreenChat – manages direct messaging conversations between users. The Chatroom component + provides user settings for On-Screen Chat participation. + - Export – provides the export framework. Chatroom objects can be + exported as XML including message history. + - Mail – used for sending chat invitation notifications via email. + +## Data being stored + +- **User ID of connected users**: When a user enters a chatroom, their **user ID** is stored in + the `chatroom_users` table together with the **room ID** and a **connection timestamp**. This + tracks which users are currently connected to which rooms. +- **User data of connected users**: A JSON object containing the user's **login name**, **user ID**, + and **profile picture visibility** preference is stored in the `userdata` column of the + `chatroom_users` table. This data is used to display connected users in the chat interface. +- **Session records**: When chat history is enabled for a room and a user disconnects, a session + record is written to the `chatroom_sessions` table containing the **user ID**, **user data** + (login name, user ID), **connection timestamp**, and **disconnection timestamp**. This enables + the history feature to reconstruct who was present during a conversation. +- **Message history**: Chat messages are stored in the `chatroom_history` table as JSON objects + containing the **sender's user ID**, the **sender's username**, the **message content**, and + a **timestamp**. Private messages additionally contain the **recipient's user ID** and + **username**. This persists the conversation for history viewing and export. +- **Ban records**: When a user is banned from a chatroom, the `chatroom_bans` table stores the + banned **user ID**, the **actor ID** (user who performed the ban), a **timestamp**, and an + optional **remark** text. This enforces and documents the ban. +- **On-Screen Chat conversations**: The `osc_conversation` table stores a **conversation ID**, + participant data (including **user IDs** and names as JSON), and a group flag. This enables + direct messaging between users. +- **On-Screen Chat messages**: The `osc_messages` table stores a **message ID**, **conversation + ID**, **sender user ID**, **message content**, and a **timestamp**. This persists direct messages. +- **On-Screen Chat activity**: The `osc_activity` table stores a **conversation ID**, **user ID**, + **timestamp**, and a **closed flag**. This tracks when users last interacted with a conversation. +- **User preference -- accept On-Screen Chat messages** (`chat_osc_accept_msg`): Stores whether + the user accepts direct messages from other users. Displayed on the user's Privacy Settings page. +- **User preference -- broadcast typing** (`chat_broadcast_typing`): Stores whether the user's + typing activity is broadcast to other participants in real time. Displayed on the user's + Privacy Settings page. +- **User preference -- browser notifications** (`chat_osc_browser_notifications`): Stores whether + the user receives browser notifications for On-Screen Chat messages. +- **User preference -- hide automatic messages** (`chat_hide_automsg_{room_id}`): Stores per room + whether the user has chosen to hide system-generated messages (join/leave notices). +- **User preference -- invitation notification mute timestamp** (`chatinv_nc_muted_until`): Stores + a timestamp indicating when the user last dismissed chat invitation notifications. + +## Data being presented + +- **Each user** can view: + - the list of currently connected users in a chatroom (username and profile picture). + - their own and other users' public chat messages, including sender username, message content, + and timestamp. + - private messages they have sent or received. + - their own On-Screen Chat conversations and messages (when OSC is enabled). +- **Each user** can view the chat history (when the "Enable History" setting is active for the + room), filtered by date range. The history shows message content and sender usernames. +- **Persons with the "moderate" permission** can additionally: + - view the list of banned users, including **login name**, **first name**, **last name**, + **ban timestamp**, and the **name of the person who performed the ban**. + - kick users from the chatroom. + - ban users from the chatroom. + - clear the entire message history of a room. +- **Persons with the "Write" permission** can: + - edit chatroom settings, including enabling or disabling chat history. + - access the export tab. +- Whether a user's profile picture is shown or an anonymous avatar depends on the + **profile picture visibility** setting chosen by the user upon entering the room (or + automatically set when custom usernames are disabled). +- Whether other users are visible for direct messaging depends on each user's + **`chat_osc_accept_msg`** preference. + +## Data being deleted + +- **When a user disconnects from a chatroom**: Their entry in `chatroom_users` is deleted. If chat + history is enabled, a session record is created in `chatroom_sessions` before deletion. +- **When a chatroom object is deleted from trash** by a person with appropriate permissions: All + related data is deleted, including: + - all user connection records from `chatroom_users` + - all message history from `chatroom_history` + - all ban records from `chatroom_bans` + - all session records from `chatroom_sessions` + - the room settings from `chatroom_settings` +- **When the message history is cleared** by a person with the "moderate" permission: All entries + in `chatroom_history` for that room are deleted. Session records in `chatroom_sessions` with a + disconnection time in the past are also deleted. +- **When a ban is lifted** by a person with the "moderate" permission: The ban record is deleted + from `chatroom_bans`. +- **When a user account is deleted**: The Chatroom component listens to the User component's + `deleteUser` event and deletes all ban records for that user from `chatroom_bans`. **Residual + data**: Other personal data referencing the deleted user may persist: + - Messages sent by the deleted user remain in `chatroom_history` with the original user ID + and username embedded in the JSON message body. + - Session records in `chatroom_sessions` may retain the deleted user's ID and username. + - If the deleted user was the actor who banned another user, the `actor_id` field in + `chatroom_bans` retains their user ID. + - On-Screen Chat data in `osc_conversation`, `osc_messages`, and `osc_activity` may retain + references to the deleted user's ID. +- **Periodic cleanup by the Node.js server**: Old chat messages in `chatroom_history`, OSC messages + in `osc_messages`, OSC conversations in `osc_conversation`, and OSC activity records in + `osc_activity` are deleted based on a configured age threshold. +- **User preferences** (`chat_osc_accept_msg`, `chat_broadcast_typing`, `chat_osc_browser_notifications`, + `chat_hide_automsg_*`, `chatinv_nc_muted_until`) are deleted when the user account is deleted, + as they are stored in the user preferences system. + +## Data being exported + +- Chatroom objects can be exported as XML via the ILIAS export framework. The export includes room + settings and the full message history. Message history entries contain the **message content** + (including sender username and user ID embedded in JSON) and **timestamps**. The export is + available to persons with the "Write" permission. +- The chat history can be exported as an HTML file for a selected date range by any user with the + "read" permission (when the "Enable History" setting is active). The HTML export contains + **sender usernames** and **message content**. +- There is no dedicated personal data export for individual users' chat participation records. From f33e487b3cdb8dd6beef4f16b4f464ab888d60dc Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Tue, 14 Jul 2026 16:50:33 +0200 Subject: [PATCH 078/333] [FIX] Exercise, try to address 48020: Silent Failure: Failed file upload creates 0-byte file without user notification --- .../class.ilExSubmissionFileGUI.php | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/components/ILIAS/Exercise/Submission/class.ilExSubmissionFileGUI.php b/components/ILIAS/Exercise/Submission/class.ilExSubmissionFileGUI.php index 5031b34c3194..b4c1e66967eb 100755 --- a/components/ILIAS/Exercise/Submission/class.ilExSubmissionFileGUI.php +++ b/components/ILIAS/Exercise/Submission/class.ilExSubmissionFileGUI.php @@ -270,10 +270,10 @@ protected function getUploadForm(): \ILIAS\Repository\Form\FormAdapterGUI "deliver", $this->lng->txt("files"), $this->handleUploadResult(...), - "mep_id", + "filename", "", $max_file - ); + )->required(true); return $form_adapter; } @@ -282,30 +282,40 @@ protected function handleUploadResult( \ILIAS\FileUpload\DTO\UploadResult $result ): \ILIAS\FileUpload\Handler\BasicHandlerResult { $title = $result->getName(); - - //$this->submission->addFileUpload($result); - $subm = $this->domain->submission($this->assignment->getId()); - $subm->addUpload( - $this->user->getid(), - $result, - $title - ); - + if ($result->isOK()) { + $subm = $this->domain->submission($this->assignment->getId()); + $subm->addUpload( + $this->user->getId(), + $result, + $title + ); + return new \ILIAS\FileUpload\Handler\BasicHandlerResult( + 'filename', + \ILIAS\FileUpload\Handler\HandlerResult::STATUS_OK, + $title, + '' + ); + } return new \ILIAS\FileUpload\Handler\BasicHandlerResult( '', - \ILIAS\FileUpload\Handler\HandlerResult::STATUS_OK, - $title, - '' + \ILIAS\FileUpload\Handler\HandlerResult::STATUS_FAILED, + '', + $result->getStatus()->getMessage() ); } public function addUploadObject(): void { $ilCtrl = $this->ctrl; - $this->tpl->setOnScreenMessage('success', $this->lng->txt("file_added"), true); - $this->handleNewUpload(); - - $ilCtrl->redirect($this, "submissionScreen"); + $mt = $this->gui->ui()->mainTemplate(); + $form = $this->getUploadForm(); + if ($form->isValid()) { + $this->tpl->setOnScreenMessage('success', $this->lng->txt("file_added"), true); + $this->handleNewUpload(); + $ilCtrl->redirect($this, "submissionScreen"); + } else { + $mt->setContent($form->render()); + } } From 8f261123d1bdb10d9ff4b981cae5aded82bb55a8 Mon Sep 17 00:00:00 2001 From: kevenClausenKPG Date: Wed, 15 Jul 2026 19:38:31 +0200 Subject: [PATCH 079/333] Merge Kioskmode_ to Kioskmode (#11760) --- .../classes/class.ilKioskModeService.php | 0 .../{KioskMode_ => KioskMode}/classes/class.ilKioskModeView.php | 0 .../tests/class.ilDummyKioskModeView.php | 0 .../{KioskMode_ => KioskMode}/tests/ilKioskModeServiceTest.php | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename components/ILIAS/{KioskMode_ => KioskMode}/classes/class.ilKioskModeService.php (100%) rename components/ILIAS/{KioskMode_ => KioskMode}/classes/class.ilKioskModeView.php (100%) rename components/ILIAS/{KioskMode_ => KioskMode}/tests/class.ilDummyKioskModeView.php (100%) rename components/ILIAS/{KioskMode_ => KioskMode}/tests/ilKioskModeServiceTest.php (100%) diff --git a/components/ILIAS/KioskMode_/classes/class.ilKioskModeService.php b/components/ILIAS/KioskMode/classes/class.ilKioskModeService.php similarity index 100% rename from components/ILIAS/KioskMode_/classes/class.ilKioskModeService.php rename to components/ILIAS/KioskMode/classes/class.ilKioskModeService.php diff --git a/components/ILIAS/KioskMode_/classes/class.ilKioskModeView.php b/components/ILIAS/KioskMode/classes/class.ilKioskModeView.php similarity index 100% rename from components/ILIAS/KioskMode_/classes/class.ilKioskModeView.php rename to components/ILIAS/KioskMode/classes/class.ilKioskModeView.php diff --git a/components/ILIAS/KioskMode_/tests/class.ilDummyKioskModeView.php b/components/ILIAS/KioskMode/tests/class.ilDummyKioskModeView.php similarity index 100% rename from components/ILIAS/KioskMode_/tests/class.ilDummyKioskModeView.php rename to components/ILIAS/KioskMode/tests/class.ilDummyKioskModeView.php diff --git a/components/ILIAS/KioskMode_/tests/ilKioskModeServiceTest.php b/components/ILIAS/KioskMode/tests/ilKioskModeServiceTest.php similarity index 100% rename from components/ILIAS/KioskMode_/tests/ilKioskModeServiceTest.php rename to components/ILIAS/KioskMode/tests/ilKioskModeServiceTest.php From e16fd328cd06e194463f5b1db7043dc16b4ad3ae Mon Sep 17 00:00:00 2001 From: dkippKPG Date: Wed, 15 Jul 2026 19:41:40 +0200 Subject: [PATCH 080/333] 10 map 43204 display markers (#11690) * [FIX]: replace MarkerImage with icon object for Google markers (fix mantis #43204) * [FIX]: display multiple Google maps on one page --- .../Maps/templates/default/tpl.google_map.js | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/components/ILIAS/Maps/templates/default/tpl.google_map.js b/components/ILIAS/Maps/templates/default/tpl.google_map.js index 29fff34fa9c6..c3d40ee9870a 100755 --- a/components/ILIAS/Maps/templates/default/tpl.google_map.js +++ b/components/ILIAS/Maps/templates/default/tpl.google_map.js @@ -13,27 +13,35 @@ * https://github.com/ILIAS-eLearning */ -ilMapData = Array(); -ilMap = Array(); -ilMapOptions = []; -ilCM = Array(); -ilMapUserMarker = Array(); +window.ilMapData = window.ilMapData || []; +window.ilMap = window.ilMap || []; +window.ilMapOptions = window.ilMapOptions || []; +window.ilCM = window.ilCM || []; +window.ilMapUserMarker = window.ilMapUserMarker || []; ilMapData["{MAP_ID}"] = new Array({LAT},{LONG},{ZOOM},{TYPE_CONTROL},{NAV_CONTROL},{UPDATE_LISTENER},{LARGE_CONTROL},{CENTRAL_MARKER}); ilMapUserMarker["{MAP_ID}"] = Array(); ilMapUserMarker["{UMAP_ID}"][{CNT}] = new Array({ULAT},{ULONG}, "
    {USER_INFO}<\/span><\/div>"); -if (google.maps) +var ilMarkerImage = null; +if (typeof google !== "undefined" && google.maps) { - var ilMarkerImage = new google.maps.MarkerImage( - "./assets/images/standard/icon_mapm.svg", - new google.maps.Size(12, 20), - new google.maps.Point(0,0), - new google.maps.Point(6, 20)); + // Google Maps still supports legacy `google.maps.Marker`, but `google.maps.MarkerImage` + // is an older wrapper API for marker icons. Using the icon as a plain object with + // `url`, `scaledSize`, `origin`, and `anchor` passes the same marker configuration in + // the format the current marker API expects. This keeps the existing legacy marker + // behavior and custom SVG icon, while avoiding the deprecated `MarkerImage` wrapper + // that no longer rendered reliably here. + ilMarkerImage = { + url: "./assets/images/standard/icon_mapm.svg", + scaledSize: new google.maps.Size(12, 20), + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(6, 20) + }; } -if (google.maps) +if (typeof google !== "undefined" && google.maps) { ilInitMaps(); } @@ -52,7 +60,7 @@ function ilInitMaps() for (var i=0;i Date: Wed, 15 Jul 2026 20:22:23 +0200 Subject: [PATCH 081/333] Category: #47834, missing merge --- .../Permission/CategoryCmdPermission.php | 169 ++++++++++++++++++ .../Service/class.InternalGUIService.php | 12 ++ .../classes/class.StandardGUIRequest.php | 5 + .../classes/class.ilObjCategoryGUI.php | 49 ++--- .../Service/Permission/CmdEntity.php | 40 +++++ .../Service/Permission/CmdPermission.php | 139 ++++++++++++++ .../Permission/CmdPermissionInterface.php | 72 ++++++++ .../Permission/ilNoCmdPermissionException.php | 27 +++ 8 files changed, 493 insertions(+), 20 deletions(-) create mode 100644 components/ILIAS/Category/Permission/CategoryCmdPermission.php create mode 100644 components/ILIAS/Repository/Service/Permission/CmdEntity.php create mode 100644 components/ILIAS/Repository/Service/Permission/CmdPermission.php create mode 100644 components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php create mode 100644 components/ILIAS/Repository/Service/Permission/ilNoCmdPermissionException.php diff --git a/components/ILIAS/Category/Permission/CategoryCmdPermission.php b/components/ILIAS/Category/Permission/CategoryCmdPermission.php new file mode 100644 index 000000000000..fd08a2a9e766 --- /dev/null +++ b/components/ILIAS/Category/Permission/CategoryCmdPermission.php @@ -0,0 +1,169 @@ +hasCatCmdPerm($cmd, $node_id); + } + return false; + } + + protected function hasCatCmdPerm(string $cmd, int $node_id): bool + { + // administrate users + if (in_array($cmd, [ + "resetFilter", "applyFilter", "listUsers", "addUserAutoComplete", "performDeleteUsers", + "deleteUsers", "assignRoles", "assignSave" + ])) { + return $this->access->checkAccess("cat_administrate_users", "", $node_id); + } + + // write permission + if (in_array($cmd, [ + "editInfo", "updateInfo", "update" + ])) { + return $this->access->checkAccess("write", "", $node_id); + } + + // read permission + if (in_array($cmd, [ + "render", "view", "showTaxAsSideBlock", "hideTaxAsSideBlock" + ])) { + return $this->access->checkAccess("read", "", $node_id); + } + + // visible permission + if (in_array($cmd, [ + "infoScreen" + ])) { + return $this->access->checkAccess("visible", "", $node_id); + } + + + return false; + } + + public function getDefaultCommand(): string + { + if ($this->isClass(\ilObjCategoryGUI::class)) { + return "render"; + } + return ""; + } + + public function getRequestEntity(): ?CmdEntity + { + if ($this->isClass(\ilObjCategoryGUI::class)) { + return $this->cmdEntity( + self::CAT + ); + } + return null; + } + + public function getRequestNodeId(): int + { + return $this->request->getRefId(); + } + + public function isForwardPermitted( + string $from_class, + string $to_class + ): bool { + $node_id = $this->getRequestNodeId(); + if ($from_class === \ilObjCategoryGUI::class) { + + // write permission + if (in_array($to_class, [ + \ilRepositoryTrashGUI::class, + \ilContainerFilterAdminGUI::class, + \ilObjectContentStyleSettingsGUI::class, + \ilDidacticTemplateGUI::class, + \ilExportGUI::class, + \ilObjectTranslationGUI::class, + \ilTaxonomySettingsGUI::class, + \ilObjectMetaDataGUI::class, + \ilContainerNewsSettingsGUI::class, + ])) { + return $this->access->checkAccess("write", "", $node_id); + } + + // administration users + if (in_array($to_class, [ + \ilObjUserGUI::class, + \ilObjUserFolderGUI::class, + \ilUserTableGUI::class, + ])) { + return $this->access->checkAccess("cat_administrate_users", "", $node_id); + } + + // edit permission + if ($to_class === \ilPermissionGUI::class) { + return $this->access->checkAccess("edit_permission", "", $node_id); + } + + // read permission + // note on ilObjectCopyGUI: This class performs the copy permission checks and + // can act on multiple source ids + if ($to_class === \ilObjectCopyGUI::class) { + return $this->access->checkAccess("read", "", $node_id); + } + + // visible or read + if ($to_class === \ilCommonActionDispatcherGUI::class) { + return $this->access->checkAccess("visible", "", $node_id) || + $this->access->checkAccess("read", "", $node_id); + } + + // visible + if ($to_class === \ilInfoScreenGUI::class) { + return $this->access->checkAccess("visible", "", $node_id); + } + } + return false; + } + +} diff --git a/components/ILIAS/Category/Service/class.InternalGUIService.php b/components/ILIAS/Category/Service/class.InternalGUIService.php index 9f441dfe5e99..e550bdb848a1 100755 --- a/components/ILIAS/Category/Service/class.InternalGUIService.php +++ b/components/ILIAS/Category/Service/class.InternalGUIService.php @@ -23,6 +23,7 @@ use ILIAS\DI; use ILIAS\Repository; use ILIAS\Catgory\AssignRoleTableBuilder; +use ILIAS\Category\Permission\CategoryCmdPermission; /** * @author Alexander Killing @@ -69,4 +70,15 @@ public function assignedRoleTableBuilder( $parent_cmd ); } + + public function cmdPerm(): CategoryCmdPermission + { + return new CategoryCmdPermission( + $this->domain_service->lng(), + $this->domain_service->access(), + $this->ui()->mainTemplate(), + $this->ctrl(), + $this->standardRequest() + ); + } } diff --git a/components/ILIAS/Category/classes/class.StandardGUIRequest.php b/components/ILIAS/Category/classes/class.StandardGUIRequest.php index ca4fbccb264d..6ac38410bd7a 100755 --- a/components/ILIAS/Category/classes/class.StandardGUIRequest.php +++ b/components/ILIAS/Category/classes/class.StandardGUIRequest.php @@ -93,4 +93,9 @@ public function getTaxId(): int return $this->int("cat_tax_id"); } + public function getItemRefId(): int + { + return $this->int("item_ref_id"); + } + } diff --git a/components/ILIAS/Category/classes/class.ilObjCategoryGUI.php b/components/ILIAS/Category/classes/class.ilObjCategoryGUI.php index e8cbcab694dd..f6d19d89c6e2 100755 --- a/components/ILIAS/Category/classes/class.ilObjCategoryGUI.php +++ b/components/ILIAS/Category/classes/class.ilObjCategoryGUI.php @@ -40,6 +40,7 @@ class ilObjCategoryGUI extends ilContainerGUI implements \ILIAS\Taxonomy\Setting public const CONTAINER_SETTING_TAXBLOCK = "tax_sblock_"; protected \ILIAS\Category\InternalDomainService $cat_domain; protected \ILIAS\Category\InternalGUIService $cat_gui; + protected \ILIAS\Category\Permission\CategoryCmdPermission $cmd_perm; protected \ILIAS\Taxonomy\Service $taxonomy; protected ilNavigationHistory $nav_history; @@ -96,6 +97,7 @@ public function __construct( $this->taxonomy = $DIC->taxonomy(); $this->cat_gui = $DIC->category()->internal()->gui(); $this->cat_domain = $DIC->category()->internal()->domain(); + $this->cmd_perm = $DIC->category()->internal()->gui()->cmdPerm(); } public function executeCommand(): void @@ -113,7 +115,7 @@ public function executeCommand(): void case strtolower(ilRepositoryTrashGUI::class): $ru = new ilRepositoryTrashGUI($this); $this->ctrl->setReturn($this, 'trash'); - $this->ctrl->forwardCommand($ru); + $this->cmd_perm->forwardPermitted($this, $ru); break; case "ilobjusergui": @@ -134,7 +136,7 @@ public function executeCommand(): void ); } $this->gui_obj->setCreationMode($this->creation_mode); - $this->ctrl->forwardCommand($this->gui_obj); + $this->cmd_perm->forwardPermitted($this, $this->gui_obj); $ilTabs->clearTargets(); $ilTabs->setBackTarget($this->lng->txt('backto_lua'), $this->ctrl->getLinkTarget($this, 'listUsers')); @@ -152,7 +154,7 @@ public function executeCommand(): void ); $this->gui_obj->setUserOwnerId($this->cat_request->getRefId()); $this->gui_obj->setCreationMode($this->creation_mode); - $this->ctrl->forwardCommand($this->gui_obj); + $this->cmd_perm->forwardPermitted($this, $this->gui_obj); $ilTabs->clearTargets(); $ilTabs->setBackTarget($this->lng->txt('backto_lua'), $this->ctrl->getLinkTarget($this, 'listUsers')); @@ -176,10 +178,11 @@ public function executeCommand(): void $this->prepareOutput(); $this->tabs_gui->setTabActive('perm_settings'); $perm_gui = new ilPermissionGUI($this); - $this->ctrl->forwardCommand($perm_gui); + $this->cmd_perm->forwardPermitted($this, $perm_gui); break; case 'ilinfoscreengui': + $this->checkPermission("visible"); if ($this->info_screen_enabled) { $this->prepareOutput(); $this->infoScreen(); @@ -201,11 +204,10 @@ public function executeCommand(): void $cp = new ilObjectCopyGUI($this); $cp->setType('cat'); - $this->ctrl->forwardCommand($cp); + $this->cmd_perm->forwardPermitted($this, $cp); break; case "ilobjectcontentstylesettingsgui": - $this->checkPermission("write"); $this->setTitleAndDescription(); $this->showContainerPageTabs(); $settings_gui = $this->content_style_gui @@ -213,26 +215,26 @@ public function executeCommand(): void null, $this->object->getRefId() ); - $this->ctrl->forwardCommand($settings_gui); + $this->cmd_perm->forwardPermitted($this, $settings_gui); break; case 'ilusertablegui': $u_table = new ilUserTableGUI($this, "listUsers"); $u_table->initFilter(); $this->ctrl->setReturn($this, 'listUsers'); - $this->ctrl->forwardCommand($u_table); + $this->cmd_perm->forwardPermitted($this, $u_table); break; case "ilcommonactiondispatchergui": $this->prepareOutput(); $gui = ilCommonActionDispatcherGUI::getInstanceFromAjaxCall(); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case 'ildidactictemplategui': $this->ctrl->setReturn($this, 'edit'); $did = new ilDidacticTemplateGUI($this, $this->getDidacticTemplateIdFromQuery()); - $this->ctrl->forwardCommand($did); + $this->cmd_perm->forwardPermitted($this, $did); break; case 'ilexportgui': @@ -240,7 +242,7 @@ public function executeCommand(): void $this->tabs_gui->setTabActive('export'); $exp = new ilExportGUI($this); $exp->addFormat('xml'); - $this->ctrl->forwardCommand($exp); + $this->cmd_perm->forwardPermitted($this, $exp); break; case strtolower(TranslationGUI::class): @@ -261,11 +263,11 @@ public function executeCommand(): void $this->refinery, $this->toolbar ); - $this->ctrl->forwardCommand($transgui); + $this->cmd_perm->forwardPermitted($this, $transgui); break; case strtolower(ilTaxonomySettingsGUI::class): - $this->checkPermissionBool("write"); + $this->checkPermission("write"); $this->prepareOutput(); $this->setEditTabs("taxonomy"); $tax_gui = $this->taxonomy->gui()->getSettingsGUI( @@ -274,14 +276,13 @@ public function executeCommand(): void true, $this ); - $this->ctrl->forwardCommand($tax_gui); + $this->cmd_perm->forwardPermitted($this, $tax_gui); break; case 'ilobjectmetadatagui': - $this->checkPermissionBool("write"); $this->prepareOutput(); $this->tabs_gui->activateTab('meta_data'); - $this->ctrl->forwardCommand($this->getObjectMetadataGUI()); + $this->cmd_perm->forwardPermitted($this, $this->getObjectMetadataGUI()); break; case "ilcontainernewssettingsgui": @@ -291,15 +292,14 @@ public function executeCommand(): void $this->tabs_gui->activateSubTab('obj_news_settings'); $news_set_gui = new ilContainerNewsSettingsGUI($this); $news_set_gui->setHideByDate(true); - $this->ctrl->forwardCommand($news_set_gui); + $this->cmd_perm->forwardPermitted($this, $news_set_gui); break; case 'ilcontainerfilteradmingui': - $this->checkPermissionBool("write"); $this->prepareOutput(); $this->setEditTabs($active_tab = "settings_filter"); $this->tabs_gui->activateTab('settings'); - $this->ctrl->forwardCommand(new ilContainerFilterAdminGUI($this)); + $this->cmd_perm->forwardPermitted($this, new ilContainerFilterAdminGUI($this)); break; default: @@ -332,6 +332,15 @@ public function executeCommand(): void } $cmd .= "Object"; $this->tabs_gui->activateTab("view_content"); // see #19868 + + if ($this->cmd_perm->classImplementsMethodDirectly(get_class($this), $cmd)) { + $cmd = $this->cmd_perm->getPermittedCommand(); + if ($cmd === "") { + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); + self::_gotoRepositoryRoot(); + } + $cmd .= "Object"; + } $this->$cmd(); break; @@ -683,7 +692,7 @@ public function infoScreen(): string // forward the command if ($ilCtrl->getNextClass() === "ilinfoscreengui") { - $ilCtrl->forwardCommand($info); + $this->cmd_perm->forwardPermitted($this, $info); } else { return $ilCtrl->getHTML($info); } diff --git a/components/ILIAS/Repository/Service/Permission/CmdEntity.php b/components/ILIAS/Repository/Service/Permission/CmdEntity.php new file mode 100644 index 000000000000..c1209be11d6f --- /dev/null +++ b/components/ILIAS/Repository/Service/Permission/CmdEntity.php @@ -0,0 +1,40 @@ +type; + } + + public function getId(): string + { + return $this->id; + } +} diff --git a/components/ILIAS/Repository/Service/Permission/CmdPermission.php b/components/ILIAS/Repository/Service/Permission/CmdPermission.php new file mode 100644 index 000000000000..b500c7de54a0 --- /dev/null +++ b/components/ILIAS/Repository/Service/Permission/CmdPermission.php @@ -0,0 +1,139 @@ +ctrl)) { + return false; + } + return $this->isCommandPermitted( + $this->getRequestCommand(), + $this->getRequestNodeId(), + (string) $this?->getRequestEntity()->getType(), + (string) $this?->getRequestEntity()->getId() + ); + } + + public function isTableCommandPermitted(string $cmd, string $entity_id): bool + { + if (is_null($this->ctrl)) { + return false; + } + return $this->isCommandPermitted( + $cmd, + $this->getRequestNodeId(), + (string) $this?->getRequestEntity()->getType(), + $entity_id + ); + } + + public function getPermittedCommand(): string + { + if ($this->isRequestCommandPermitted()) { + return $this->getRequestCommand(); + } + return ""; + } + + public function getRequestCommand(): string + { + return $this->ctrl->getCmd($this->getDefaultCommand()); + } + + protected function isClass(string $class): bool + { + if (is_null($this->ctrl)) { + return false; + } + $cmd_class = $this->ctrl->getCmdClass(); + return $cmd_class === strtolower($class); + } + + public function getDefaultCommand(): string + { + return ""; + } + + + /** + * @throws \ILIAS\Repository\Permission\ilNoCmdPermissionException + */ + public function checkCommand(string $cmd, int $node_id, string $entity, string $entity_id = ""): void + { + if (!$this->isCommandPermitted($cmd, $node_id, $entity, $entity_id)) { + throw new \ILIAS\Repository\Permission\ilNoCmdPermissionException("No permission to execute command $cmd on $entity $entity_id"); + } + } + + protected function cmdEntity( + string $type, + string $id = "" + ): CmdEntity { + return new CmdEntity( + $type, + $id + ); + } + + public function forwardPermitted( + object $from_gui, + object $to_gui + ): void { + if (is_null($this->ctrl)) { + return; + } + if ($this->isForwardPermitted(get_class($from_gui), get_class($to_gui))) { + $this->ctrl->forwardCommand($to_gui); + return; + } + if ($this->access->checkAccess("read", "", ROOT_FOLDER_ID)) { + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); + $this->ctrl->setParameterByClass("ilRepositoryGUI", "ref_id", ROOT_FOLDER_ID); + $this->ctrl->redirectByClass("ilRepositoryGUI"); + } + throw new \ilPermissionException($this->lng->txt("permission_denied")); + } + + /** + * For intermediate solutions that don't implement the concept for all subclasses yet + */ + public function classImplementsMethodDirectly(string $className, string $method): bool + { + try { + $class = new \ReflectionClass($className); + } catch (\Exception $e) { + return false; + } + + return $class->hasMethod($method) + && $class->getMethod($method)->getDeclaringClass()->getName() === $class->getName(); + } +} diff --git a/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php b/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php new file mode 100644 index 000000000000..41bd629f70c4 --- /dev/null +++ b/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php @@ -0,0 +1,72 @@ + Date: Thu, 16 Jul 2026 09:32:57 +0200 Subject: [PATCH 082/333] blog: centralise permission checks --- .../Blog/Permission/BlogCmdPermission.php | 214 ++++++++++++++++++ .../Blog/Permission/PermissionManager.php | 34 ++- .../Blog/Posting/class.ilBlogPostingGUI.php | 24 +- .../Service/class.InternalDomainService.php | 1 + .../Blog/Service/class.InternalGUIService.php | 13 ++ .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 83 +++---- .../Service/Permission/CmdPermission.php | 7 +- .../Permission/CmdPermissionInterface.php | 2 +- 8 files changed, 321 insertions(+), 57 deletions(-) create mode 100644 components/ILIAS/Blog/Permission/BlogCmdPermission.php diff --git a/components/ILIAS/Blog/Permission/BlogCmdPermission.php b/components/ILIAS/Blog/Permission/BlogCmdPermission.php new file mode 100644 index 000000000000..48265179e1c9 --- /dev/null +++ b/components/ILIAS/Blog/Permission/BlogCmdPermission.php @@ -0,0 +1,214 @@ +access = $perm_manager->getAccessHandler(); + } + + public function isCommandPermitted( + string $cmd, // derived or from table + int $node_id, + string $entity, + string $entity_id = "" + ): bool { + switch ($entity) { + case self::BLOG: + return $this->hasBlogCmdPerm($cmd, $node_id); + case self::POSTING: + return $this->hasPostingCmdPerm($cmd, $node_id, (int) $entity_id); + } + return false; + } + + protected function hasBlogCmdPerm(string $cmd, int $node_id): bool + { + // write permission + if (in_array($cmd, [ + "edit", + "createExportFileWithComments", + "createExportFile", + "export", + "contributors", + "addUserFromAutoComplete", + "addContributor", + "confirmRemoveContributor", + "removeContributor", + "setContentStyleSheet", + "exportWithComments" + ])) { + return $this->access->checkAccess("write", "", $node_id); + } + + if (in_array($cmd, [ + "createPosting", + ])) { + return $this->perm_manager->mayContribute(); + } + + // read permission + if (in_array($cmd, [ + "render", "preview", "addToDesk", "removeFromDesk", "renderFullscreen", + "setNotification", + "printViewSelection", "printPostings" + ])) { + return $this->access->checkAccess("read", "", $node_id); + } + + // visible permission + if (in_array($cmd, [ + "infoScreen" + ])) { + return $this->access->checkAccess("visible", "", $node_id); + } + + + return false; + } + + protected function hasPostingCmdPerm(string $cmd, int $node_id, int $posting_id): bool + { + // redact or write permission + if (in_array($cmd, [ + "approve", + "deactivateAdmin" + ])) { + return $this->perm_manager->canApprove($posting_id); + } + + return false; + } + + public function getDefaultCommand(): string + { + if ($this->isClass(\ilObjBlogGUI::class)) { + return "render"; + } + return ""; + } + + public function getRequestEntity(): ?CmdEntity + { + if ($this->isClass(\ilObjBlogGUI::class)) { + + if (in_array($this->ctrl->getCmd(), ["approve", "deactivateAdmin"])) { + $posting_id = $this->request->getApId(); + return $this->cmdEntity( + self::POSTING, + (string) $posting_id + ); + } + return $this->cmdEntity( + self::BLOG + ); + } + return null; + } + + public function getRequestNodeId(): int + { + if ($this->access instanceof \ilWorkspaceAccessHandler) { + return $this->request->getWspId(); + } + return $this->request->getRefId(); + } + + public function isForwardPermitted( + string $from_class, + string $to_class + ): bool { + $node_id = $this->getRequestNodeId(); + $posting_id = $this->request->getBlogPage(); + if ($from_class === \ilObjBlogGUI::class) { + + // visible or read + if ($to_class === \ilCommonActionDispatcherGUI::class) { + return $this->access->checkAccess("visible", "", $node_id) || + $this->access->checkAccess("read", "", $node_id); + } + + // edit permission + if ($to_class === \ilPermissionGUI::class) { + return $this->access->checkAccess("edit_permission", "", $node_id); + } + + // read posting permission + if ($to_class === \ilBlogPostingGUI::class) { + return $this->perm_manager->canReadPosting($posting_id); + } + + // write permission + if (in_array($to_class, [ + \ilExportGUI::class, + \ilRepositorySearchGUI::class, + \ilObjectContentStyleSettingsGUI::class, + \ilObjNotificationSettingsGUI::class, + \ilObjectMetaDataGUI::class, + SettingsGUI::class, + \ILIAS\Blog\Settings\BlockSettingsGUI::class, + \ILIAS\Blog\Editing\EditingGUI::class, + ContributorGUI::class, + ])) { + return $this->access->checkAccess("write", "", $node_id); + } + + // read permission + // note on ilObjectCopyGUI: This class performs the copy permission checks and + // can act on multiple source ids + if (in_array($to_class, [ + \ilObjectCopyGUI::class, + \ilBlogExerciseGUI::class, + \ILIAS\Blog\Presentation\PresentationGUI::class + ])) { + return $this->access->checkAccess("read", "", $node_id); + } + + // visible + if ($to_class === \ilInfoScreenGUI::class) { + return $this->access->checkAccess("visible", "", $node_id); + } + } + return false; + } + +} diff --git a/components/ILIAS/Blog/Permission/PermissionManager.php b/components/ILIAS/Blog/Permission/PermissionManager.php index c1def58786f3..d06b34c409c4 100755 --- a/components/ILIAS/Blog/Permission/PermissionManager.php +++ b/components/ILIAS/Blog/Permission/PermissionManager.php @@ -20,8 +20,12 @@ namespace ILIAS\Blog\Permission; +use ILIAS\Blog\InternalDomainService; + class PermissionManager { + protected \ILIAS\Blog\Posting\PostingManager $posting_manager; + protected \ilWorkspaceTree $ws_tree; protected int $owner; protected int $id_type; protected \ilWorkspaceAccessHandler|\ilAccessHandler $access; @@ -29,6 +33,7 @@ class PermissionManager protected int $user_id; public function __construct( + InternalDomainService $domain, \ilWorkspaceAccessHandler|\ilAccessHandler $access_handler, ?int $node_id, int $id_type, @@ -40,6 +45,8 @@ public function __construct( $this->id_type = $id_type; $this->user_id = $user_id; $this->owner = $owner; + $this->ws_tree = new \ilWorkspaceTree($this->owner); + $this->posting_manager = $domain->posting(); } public function canWrite(): bool @@ -108,8 +115,21 @@ public function canManage(): bool public function canReadPosting(int $posting_id): bool { - return ($this->mayContribute() || - \ilBlogPosting::_lookupActive($posting_id, "blp")); + return ( + $this->postingIdMatches($posting_id) && + $this->checkPermissionBool("read") && + ($this->mayContribute() || + \ilBlogPosting::_lookupActive($posting_id, "blp"))); + } + + public function canApprove(int $posting_id): bool + { + if (!$this->postingIdMatches($posting_id)) { + return false; + } + return ($this->checkPermissionBool("redact") || + $this->checkPermissionBool("write") + ); } public function isActive(int $posting_id): bool @@ -117,6 +137,16 @@ public function isActive(int $posting_id): bool return (\ilBlogPosting::_lookupActive($posting_id, "blp")); } + protected function postingIdMatches(int $posting_id): bool + { + if ($this->id_type === \ilObject2GUI::WORKSPACE_NODE_ID) { + $obj_id = $this->ws_tree->lookupObjectId($this->node_id); + } else { + $obj_id = \ilObject::_lookupObjId($this->node_id); + } + return $this->posting_manager->lookupBlogId($posting_id) === $obj_id; + } + public function getAccessHandler(): \ilAccessHandler|\ilWorkspaceAccessHandler { return $this->access; diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index af3fc1ae46a3..aac0b5e505e2 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -598,7 +598,13 @@ public function deactivatePage(bool $a_to_list = false): void $this->ctrl->redirect($this, "edit"); } else { $this->ctrl->setParameterByClass("ilobjbloggui", "blpg", ""); - $this->ctrl->redirectByClass("ilobjbloggui", ""); + $this->ctrl->redirectByClass( + [ + ilObjBlogGUI::class, + \ILIAS\Blog\Editing\EditingGUI::class, + ], + "" + ); } } @@ -620,7 +626,13 @@ public function activatePage(bool $a_to_list = false): void $this->ctrl->redirect($this, "edit"); } else { $this->ctrl->setParameterByClass("ilobjbloggui", "blpg", ""); - $this->ctrl->redirectByClass("ilobjbloggui", ""); + $this->ctrl->redirectByClass( + [ + ilObjBlogGUI::class, + \ILIAS\Blog\Editing\EditingGUI::class, + ], + "" + ); } } @@ -868,6 +880,12 @@ protected function showEditToolbar(): void public function finishEditing(): void { $this->ctrl->setParameterByClass("ilobjbloggui", "bmn", ""); - $this->ctrl->redirectByClass("ilobjbloggui", "render"); + $this->ctrl->redirectByClass( + [ + ilObjBlogGUI::class, + \ILIAS\Blog\Editing\EditingGUI::class + ], + "" + ); } } diff --git a/components/ILIAS/Blog/Service/class.InternalDomainService.php b/components/ILIAS/Blog/Service/class.InternalDomainService.php index 4480081ecc63..cebf650819bd 100755 --- a/components/ILIAS/Blog/Service/class.InternalDomainService.php +++ b/components/ILIAS/Blog/Service/class.InternalDomainService.php @@ -77,6 +77,7 @@ public function perm( int $owner ): PermissionManager { return new PermissionManager( + $this, $access_handler, $node_id, $id_type, diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index bc9d853621f7..b368756a485f 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -26,6 +26,8 @@ use ILIAS\Blog\ReadingTime\GUIService; use ILIAS\Blog\RSS\RSSGUI; use ILIAS\Blog\Posting\Service\GUIService as PostingGUIService; +use ILIAS\Blog\Permission\BlogCmdPermission; +use ILIAS\Blog\Permission\PermissionManager; class InternalGUIService { @@ -144,4 +146,15 @@ public function rss(): RSSGUI $this ); } + + public function cmdPerm(PermissionManager $blog_access): BlogCmdPermission + { + return new BlogCmdPermission( + $this->domain_service->lng(), + $blog_access, + $this->ui()->mainTemplate(), + $this->ctrl(), + $this->standardRequest() + ); + } } diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 7d44a01e4e8f..7688978f71c8 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -48,6 +48,7 @@ class ilObjBlogGUI extends ilObject2GUI implements ilDesktopItemHandling { protected PostingManager $posting_manager; + protected \ILIAS\Blog\Permission\BlogCmdPermission $cmd_perm; protected ?Settings $blog_settings = null; protected ProfileGUI $profile_gui; protected ProfileAdapter $profile; @@ -182,6 +183,7 @@ public function __construct( ); $this->profile = $domain->profile(); $this->profile_gui = $gui->profile(); + $this->cmd_perm = $gui->cmdPerm($this->perm); } public function getType(): string @@ -350,8 +352,6 @@ public function executeCommand(): void $this->triggerAssignmentTool(); } - $cmd = $ilCtrl->getCmd(); - // add entry to navigation history if (($this->id_type === self::REPOSITORY_NODE_ID) && !$this->getCreationMode() && $this->getAccessHandler()->checkAccess("read", "", $this->node_id)) { @@ -377,35 +377,35 @@ public function executeCommand(): void $gui = ilCommonActionDispatcherGUI::getInstanceFromAjaxCall(); $gui->enableCommentsSettings(false); $this->prepareOutput(); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case "ilpermissiongui": $this->prepareOutput(); $ilTabs->activateTab("id_permissions"); $perm_gui = new ilPermissionGUI($this); - $this->ctrl->forwardCommand($perm_gui); + $this->cmd_perm->forwardPermitted($this, $perm_gui); break; case "ilobjectcopygui": $this->prepareOutput(); $cp = new ilObjectCopyGUI($this); $cp->setType("blog"); - $this->ctrl->forwardCommand($cp); + $this->cmd_perm->forwardPermitted($this, $cp); break; case strtolower(\ilRepositorySearchGUI::class): $this->checkPermission("write"); $this->prepareOutput(); $ilTabs->activateTab("contributors"); - $this->ctrl->forwardCommand($this->gui->contributor()->contributorGUI($this->node_id, $this->object)); + $this->cmd_perm->forwardPermitted($this, $this->gui->contributor()->contributorGUI($this->node_id, $this->object)); break; case 'ilexportgui': $this->prepareOutput(); $this->tabs->activateTab("export"); $exp_gui = new ilExportGUI($this); - $this->ctrl->forwardCommand($exp_gui); + $this->cmd_perm->forwardPermitted($this, $exp_gui); break; case "ilobjectcontentstylesettingsgui": @@ -429,14 +429,14 @@ public function executeCommand(): void $this->object->getId() ); } - $this->ctrl->forwardCommand($settings_gui); + $this->cmd_perm->forwardPermitted($this, $settings_gui); break; case "ilblogexercisegui": $this->ctrl->setReturn($this, "render"); $gui = $this->gui->exercise()->ilBlogExerciseGUI($this->node_id); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case 'ilobjnotificationsettingsgui': @@ -444,7 +444,7 @@ public function executeCommand(): void $ilTabs->activateTab("settings"); $this->setSettingsSubTabs("notifications"); $gui = new ilObjNotificationSettingsGUI($this->object->getRefId()); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case strtolower(ilObjectMetaDataGUI::class): @@ -452,7 +452,7 @@ public function executeCommand(): void $this->prepareOutput(); $ilTabs->activateTab("meta_data"); $gui = new ilObjectMetaDataGUI($this->object, null, null, $this->call_by_reference); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case strtolower(SettingsGUI::class): @@ -464,7 +464,7 @@ public function executeCommand(): void $this->obj_id, $this->id_type === self::REPOSITORY_NODE_ID ); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case strtolower(\ILIAS\Blog\Editing\EditingGUI::class): @@ -478,7 +478,7 @@ public function executeCommand(): void $this->content_style_domain, $this ); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case strtolower(\ILIAS\Blog\Presentation\PresentationGUI::class): @@ -492,7 +492,7 @@ public function executeCommand(): void $this->node_id, $this->id_type, ); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case strtolower(ContributorGUI::class): @@ -503,7 +503,7 @@ public function executeCommand(): void $this->node_id, $this->object ); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; case strtolower(\ILIAS\Blog\Settings\BlockSettingsGUI::class): @@ -515,30 +515,23 @@ public function executeCommand(): void $this->obj_id, $this->id_type === self::REPOSITORY_NODE_ID ); - $this->ctrl->forwardCommand($gui); + $this->cmd_perm->forwardPermitted($this, $gui); break; - default: - if ($cmd === "preview") { - $this->ctrl->setCmdClass(\ILIAS\Blog\Presentation\PresentationGUI::class); - $this->executeCommand(); - return; - } - if ($cmd === "" || $cmd === "render") { - $this->ctrl->setCmdClass(\ILIAS\Blog\Editing\EditingGUI::class); - $this->ctrl->setCmd("render"); - $this->executeCommand(); - return; - } + case "ilworkspaceaccessgui": + $this->checkPermission("write"); + parent::executeCommand(); + break; - if ($cmd !== "gethtml") { - // desktop item handling, must be toggled before header action - if ($cmd === "addToDesk" || $cmd === "removeFromDesk") { - $this->{$cmd . "Object"}(); - } - $this->addHeaderAction(); + default: + // desktop item handling, must be toggled before header action + $cmd = $ilCtrl->getCmd(); + //$this->addHeaderActionForCommand($cmd); + $this->prepareOutput(); + if ($this->cmd_perm->classImplementsMethodDirectly(get_class($this), $cmd)) { + $cmd = $this->cmd_perm->getPermittedCommand(); } - parent::executeCommand(); + $this->$cmd(); } } @@ -617,7 +610,7 @@ public function infoScreenForward(): void // standard meta data $info->addMetaDataSections($this->object->getId(), 0, $this->object->getType()); - $this->ctrl->forwardCommand($info); + $this->cmd_perm->forwardPermitted($this, $info); } @@ -641,16 +634,6 @@ protected function getListItemsInternal( ); } - /** - * Render fullscreen presentation - */ - public function preview(): void - { - $this->ctrl->setCmdClass(\ILIAS\Blog\Presentation\PresentationGUI::class); - $this->ctrl->setCmd("preview"); - $this->executeCommand(); - } - /** * Build and deliver export file */ @@ -1063,7 +1046,13 @@ public function approve(): void $this->tpl->setOnScreenMessage('success', $this->lng->txt("settings_saved"), true); } - $this->ctrl->redirect($this, "render"); + $this->ctrl->redirectByClass( + [ + ilObjBlogGUI::class, + EditingGUI::class, + ], + "" + ); } diff --git a/components/ILIAS/Repository/Service/Permission/CmdPermission.php b/components/ILIAS/Repository/Service/Permission/CmdPermission.php index b500c7de54a0..67d5640be4cf 100644 --- a/components/ILIAS/Repository/Service/Permission/CmdPermission.php +++ b/components/ILIAS/Repository/Service/Permission/CmdPermission.php @@ -106,13 +106,12 @@ protected function cmdEntity( public function forwardPermitted( object $from_gui, object $to_gui - ): void { + ): mixed { if (is_null($this->ctrl)) { - return; + return null; } if ($this->isForwardPermitted(get_class($from_gui), get_class($to_gui))) { - $this->ctrl->forwardCommand($to_gui); - return; + return $this->ctrl->forwardCommand($to_gui); } if ($this->access->checkAccess("read", "", ROOT_FOLDER_ID)) { $this->tpl->setOnScreenMessage('failure', $this->lng->txt('permission_denied'), true); diff --git a/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php b/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php index 41bd629f70c4..1d34015a994f 100644 --- a/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php +++ b/components/ILIAS/Repository/Service/Permission/CmdPermissionInterface.php @@ -66,7 +66,7 @@ public function isForwardPermitted( public function forwardPermitted( object $from_gui, object $to_gui - ): void; + ): mixed; } From c7b17aa882a688dade28a022cd48920f6c92d3f6 Mon Sep 17 00:00:00 2001 From: mjansen Date: Mon, 13 Jul 2026 15:17:17 +0200 Subject: [PATCH 083/333] [Fix] Registration: Fix empty language preference after self-registration Remove obsolete setLanguage() call that read the legacy post variable 'usr_language'. The form field was migrated to 'language' via the User Settings API, so the old call overwrote the correctly chosen language with an empty string in usr_pref. See: https://mantis.ilias.de/view.php?id=48027 --- .../Registration/classes/class.ilAccountRegistrationGUI.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ILIAS/Registration/classes/class.ilAccountRegistrationGUI.php b/components/ILIAS/Registration/classes/class.ilAccountRegistrationGUI.php index 2f2ac83172bb..c69d418373c8 100755 --- a/components/ILIAS/Registration/classes/class.ilAccountRegistrationGUI.php +++ b/components/ILIAS/Registration/classes/class.ilAccountRegistrationGUI.php @@ -500,8 +500,6 @@ protected function createUser(int $a_role): string $this->userObj->updateOwner(); // setup user preferences - $this->userObj->setLanguage($this->form->getInput('usr_language')); - global $DIC; $DIC['legalDocuments']->selfRegistration()->userCreation($this->userObj); From faa41862f2377d15ded786013382c7989f8360a0 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Thu, 16 Jul 2026 12:01:46 +0200 Subject: [PATCH 084/333] [REFACTOR] UI: add external link safeguard to roadmap --- components/ILIAS/UI/docs/ROADMAP.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/components/ILIAS/UI/docs/ROADMAP.md b/components/ILIAS/UI/docs/ROADMAP.md index 221a2c996e9c..392c064b4ee1 100755 --- a/components/ILIAS/UI/docs/ROADMAP.md +++ b/components/ILIAS/UI/docs/ROADMAP.md @@ -362,6 +362,19 @@ different to system administrators, who want to find specific data fast, than fo browse the data slowly. There could be such enumerations that are viable for all components, but also enumerations which are specific to a component. Appropriate namespaces should be chosen. +### Implement external link safeguard (beginner, ~2d) + +The `UI\Component\Link` component family should provide a possible safeguard for links that point to +external resources. The safeguard should be used whenever such a link is built and the safeguard should +prompt the user to confirm, whether its OK to open this link or not. + +Since the factory accepts `string` and `Data\URI` for resources, we cannot easily derive this information +without some obscure dependencies to ILIAS. Therefore we may introduce an additional flag inside each +factory method, that stays optional but should be required in the future. + +The safeguard itself could be implemented as prompt, modal, or even as a popover. Some research and +accessibility evaluations should provide some insight into what is most suitable there. + ## Long Term ### Mark Some Components as Internal From f7bc7f2d26c6974f0730657eacc01b57edfbb313 Mon Sep 17 00:00:00 2001 From: mjansen Date: Thu, 16 Jul 2026 12:29:05 +0200 Subject: [PATCH 085/333] [Fix] Mail: Don't manipulate password from configuration form --- components/ILIAS/Mail/classes/class.ilObjMailGUI.php | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ILIAS/Mail/classes/class.ilObjMailGUI.php b/components/ILIAS/Mail/classes/class.ilObjMailGUI.php index 20093d1c0aea..ae711061898a 100755 --- a/components/ILIAS/Mail/classes/class.ilObjMailGUI.php +++ b/components/ILIAS/Mail/classes/class.ilObjMailGUI.php @@ -448,6 +448,7 @@ protected function getExternalSettingsForm(): ilPropertyFormGUI ); $password->setRetype(false); $password->setSkipSyntaxCheck(true); + $password->setUseStripSlashes(false); $password->setDisabled(!$this->isEditingAllowed()); $password->setDisableHtmlAutoComplete(true); $smtp->addSubItem($password); From a31bd27f77747d686827a57f11f0cc96747f9e8e Mon Sep 17 00:00:00 2001 From: mjansen Date: Thu, 16 Jul 2026 11:45:33 +0200 Subject: [PATCH 086/333] [Fix] Auth: Fix issues with custom user fields --- .../LDAP/classes/class.ilLDAPAttributeToUser.php | 16 +++++++++------- .../classes/class.ilOpenIdConnectUserSync.php | 13 ++++++------- .../Saml/classes/class.ilAuthProviderSaml.php | 8 ++++---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/components/ILIAS/LDAP/classes/class.ilLDAPAttributeToUser.php b/components/ILIAS/LDAP/classes/class.ilLDAPAttributeToUser.php index 91439d4fb6bb..b1ec8e7a4072 100755 --- a/components/ILIAS/LDAP/classes/class.ilLDAPAttributeToUser.php +++ b/components/ILIAS/LDAP/classes/class.ilLDAPAttributeToUser.php @@ -28,7 +28,7 @@ */ class ilLDAPAttributeToUser { - public const MODE_INITIALIZE_ROLES = 1; + public const int MODE_INITIALIZE_ROLES = 1; private array $modes = []; private ilLDAPServer $server_settings; @@ -38,8 +38,9 @@ class ilLDAPAttributeToUser private string $new_user_auth_mode = 'ldap'; private ilLogger $logger; private ilXmlWriter $writer; + private readonly ilLanguage $lng; /** - * @var array|null + * @var array|null */ private ?array $user_defined_fields = null; @@ -53,6 +54,7 @@ public function __construct(ilLDAPServer $a_server) $this->logger = $DIC->logger()->auth(); $this->profile = $DIC['user']->getProfile(); + $this->lng = $DIC['lng']; $this->server_settings = $a_server; @@ -228,7 +230,7 @@ private function usersToXML(): void $rules = $this->mapping->getRules(true); } - $this->writer->xmlElement('Active', [], "true"); + $this->writer->xmlElement('Active', [], 'true'); $this->writer->xmlElement('TimeLimitOwner', [], 7); $this->writer->xmlElement('TimeLimitUnlimited', [], 1); $this->writer->xmlElement('TimeLimitFrom', [], time()); @@ -340,7 +342,7 @@ private function usersToXML(): void default: // Handle user defined fields - if (strpos($field, 'udf_') !== 0) { + if (!str_starts_with($field, 'udf_')) { continue 2; } $id_data = explode('_', $field); @@ -350,7 +352,7 @@ private function usersToXML(): void $this->initUserDefinedFields(); if (!isset($this->user_defined_fields[$id_data[1]])) { $this->logger->warning(sprintf( - "Invalid/Orphaned UD field mapping detected: %s", + 'Invalid/Orphaned UD field mapping detected: %s', $field )); break; @@ -360,7 +362,7 @@ private function usersToXML(): void 'UserDefinedField', [ 'Id' => $this->user_defined_fields[$id_data[1]]->getIdentifier(), - 'Name' => $this->user_defined_fields[$id_data[1]]->getLabel() + 'Name' => $this->user_defined_fields[$id_data[1]]->getLabel($this->lng) ], $value ); @@ -399,7 +401,7 @@ private function doMapping(array $user, array $rule): string { $mapping = strtolower(trim($rule['value'])); - if (strpos($mapping, ',') === false) { + if (!str_contains($mapping, ',')) { return $this->convertInput($user[$mapping] ?? ''); } // Is multiple mapping diff --git a/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectUserSync.php b/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectUserSync.php index 83a995d66ebf..4917129a549f 100755 --- a/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectUserSync.php +++ b/components/ILIAS/OpenIdConnect/classes/class.ilOpenIdConnectUserSync.php @@ -18,13 +18,12 @@ declare(strict_types=1); -use ILIAS\User\Profile\Profile; use ILIAS\Language\Language; class ilOpenIdConnectUserSync { - public const AUTH_MODE = 'oidc'; - private const UDF_STRING = 'udf_'; + public const string AUTH_MODE = 'oidc'; + private const string UDF_STRING = 'udf_'; private readonly ilLogger $logger; private readonly Language $lng; @@ -33,7 +32,7 @@ class ilOpenIdConnectUserSync private string $int_account = ''; private int $usr_id = 0; /** - * @var array|null + * @var array */ private array $user_defined_fields; @@ -255,7 +254,7 @@ private function transformToXml(): void continue 2; } - $field = $this->user_defined_fields[$id_data[1]] ?? null; + $definition = $this->user_defined_fields[$id_data[1]] ?? null; if ($definition === null) { $this->logger->warning( sprintf( @@ -269,8 +268,8 @@ private function transformToXml(): void $this->writer->xmlElement( 'UserDefinedField', [ - 'Id' => $field->getIdentifier(), - 'Name' => $field->getLabel($this->lng) + 'Id' => $definition->getIdentifier(), + 'Name' => $definition->getLabel($this->lng) ], $value ); diff --git a/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php b/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php index 81afe48f9496..7931e46c1ef7 100755 --- a/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php +++ b/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php @@ -40,7 +40,7 @@ class ilAuthProviderSaml extends ilAuthProvider implements ilAuthProviderAccount private bool $force_new_account = false; private string $migration_account = ''; /** - * @var array|null + * @var array|null */ private ?array $user_defined_fields = null; @@ -269,7 +269,7 @@ private function handleSamlAuth(ilAuthStatus $status): bool return true; } - ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug("SAML user synchronisation is not enabled, auth failed."); + ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug('SAML user synchronisation is not enabled, auth failed.'); $this->handleAuthenticationFail($status, 'err_auth_saml_no_ilias_user'); return false; @@ -345,7 +345,7 @@ private function importUser(?string $a_internal_login, string $a_external_accoun 'Action' => 'Assign' ]); - $xml_writer->xmlElement('Active', [], "true"); + $xml_writer->xmlElement('Active', [], 'true'); $xml_writer->xmlElement('TimeLimitOwner', [], USER_FOLDER_ID); $xml_writer->xmlElement('TimeLimitUnlimited', [], 1); $xml_writer->xmlElement('TimeLimitFrom', [], time()); @@ -511,7 +511,7 @@ private function buildUserAttributeXml( $field = $this->user_defined_fields[$udf_data[1]] ?? null; if ($field === null) { ilLoggerFactory::getLogger('auth')->warning(sprintf( - "Invalid/Orphaned UD field mapping detected: %s", + 'Invalid/Orphaned UD field mapping detected: %s', $rule->getAttribute() )); break; From c0e010640cb3dc67b85674e69df9947b6fefc085 Mon Sep 17 00:00:00 2001 From: kevenClausenKPG Date: Thu, 16 Jul 2026 15:43:15 +0200 Subject: [PATCH 087/333] [LSO] Streamlining of Permission Handling (remove JOIN/LEAVE, migrate to READ) (#11692) * removing join/leave * typing * Make step_1() require only the RBAC read operation and migrate permissions if either legacy op (participate/unparticipate) exists. Throw if read is missing to avoid silently skipping a broken upgrade. --------- Co-authored-by: Keven Clausen --- .../Player/class.ilLSLaunchlinksBuilder.php | 7 +- .../class.ilLearningSequenceSetupAgent.php | 6 +- ...enceStreamlinePermissionsDBUpdateSteps.php | 134 ++++++++++++++++++ .../class.ilObjLearningSequenceAccess.php | 5 - .../class.ilObjLearningSequenceGUI.php | 6 +- 5 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceStreamlinePermissionsDBUpdateSteps.php diff --git a/components/ILIAS/LearningSequence/classes/Player/class.ilLSLaunchlinksBuilder.php b/components/ILIAS/LearningSequence/classes/Player/class.ilLSLaunchlinksBuilder.php index 80b65fd879f4..6f694940cf15 100755 --- a/components/ILIAS/LearningSequence/classes/Player/class.ilLSLaunchlinksBuilder.php +++ b/components/ILIAS/LearningSequence/classes/Player/class.ilLSLaunchlinksBuilder.php @@ -25,9 +25,6 @@ */ class ilLSLaunchlinksBuilder { - public const PERM_PARTICIPATE = 'participate'; - public const PERM_UNPARTICIPATE = 'unparticipate'; - public const CMD_STANDARD = ilObjLearningSequenceLearnerGUI::CMD_STANDARD; public const CMD_EXTRO = ilObjLearningSequenceLearnerGUI::CMD_EXTRO; public const CMD_START = ilObjLearningSequenceLearnerGUI::CMD_START; @@ -49,7 +46,7 @@ public function __construct( protected function mayJoin(): bool { - return $this->access->checkAccess(self::PERM_PARTICIPATE, '', $this->lso_ref_id); + return $this->access->checkAccess('read', '', $this->lso_ref_id); } public function currentUserMayUnparticipate(): bool @@ -59,7 +56,7 @@ public function currentUserMayUnparticipate(): bool protected function mayUnparticipate(): bool { - return $this->access->checkAccess(self::PERM_UNPARTICIPATE, '', $this->lso_ref_id); + return $this->isMember() && $this->access->checkAccess('read', '', $this->lso_ref_id); } protected function isMember(): bool diff --git a/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php b/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php index cdff906c33ce..14821e4d7fd6 100755 --- a/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php +++ b/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php @@ -69,6 +69,9 @@ public function getUpdateObjective(?Setup\Config $config = null): Setup\Objectiv new ilDatabaseUpdateStepsExecutedObjective( new LSODropActivationDBUpdateSteps() ), + new ilDatabaseUpdateStepsExecutedObjective( + new ilLearningSequenceStreamlinePermissionsDBUpdateSteps() + ), ); } @@ -89,7 +92,8 @@ public function getStatusObjective(Setup\Metrics\Storage $storage): Setup\Object 'Component LearningSequence', true, new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new ilLearningSequenceRectifyPostConditionsTableDBUpdateSteps()), - new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new ilLearningSequenceRegisterNotificationType()) + new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new ilLearningSequenceRegisterNotificationType()), + new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new ilLearningSequenceStreamlinePermissionsDBUpdateSteps()) ); } diff --git a/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceStreamlinePermissionsDBUpdateSteps.php b/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceStreamlinePermissionsDBUpdateSteps.php new file mode 100644 index 000000000000..d35f72ef0034 --- /dev/null +++ b/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceStreamlinePermissionsDBUpdateSteps.php @@ -0,0 +1,134 @@ +db = $db; + } + + public function step_1(): void + { + $read_ops_id = $this->getOperationId('read'); + $participate_ops_id = $this->getOperationId('participate'); + $unparticipate_ops_id = $this->getOperationId('unparticipate'); + + if ($read_ops_id === null) { + throw new \RuntimeException('Cannot migrate learning sequence permissions: RBAC operation "read" not found.'); + } + + $old_ops_ids = array_values( + array_filter([ + $participate_ops_id, + $unparticipate_ops_id + ]) + ); + if ($old_ops_ids === []) { + return; + } + + $res = $this->db->query( + "SELECT pa.rol_id, pa.ref_id, pa.ops_id " . + "FROM rbac_pa pa " . + "JOIN object_reference ref ON ref.ref_id = pa.ref_id " . + "JOIN object_data obj ON obj.obj_id = ref.obj_id " . + "WHERE obj.type = '" . self::TYPE_TITLE . "'" + ); + + while ($row = $this->db->fetchAssoc($res)) { + $serialized = $row['ops_id'] ?? null; + if (!is_string($serialized) || $serialized === '') { + continue; + } + + $ops = @unserialize($serialized, ['allowed_classes' => false]); + if (!is_array($ops)) { + continue; + } + + $ops = array_map('intval', $ops); + $has_old = false; + foreach ($old_ops_ids as $old_ops_id) { + if (in_array((int) $old_ops_id, $ops, true)) { + $has_old = true; + break; + } + } + if (!$has_old) { + continue; + } + + $ops[] = $read_ops_id; + $ops = array_values(array_unique(array_diff($ops, array_map('intval', $old_ops_ids)))); + sort($ops); + + $this->db->manipulateF( + 'UPDATE rbac_pa SET ops_id = %s WHERE rol_id = %s AND ref_id = %s', + [\ilDBConstants::T_TEXT, \ilDBConstants::T_INTEGER, \ilDBConstants::T_INTEGER], + [serialize($ops), (int) $row['rol_id'], (int) $row['ref_id']] + ); + } + } + + public function step_2(): void + { + $participate_ops_id = $this->getOperationId('participate'); + $unparticipate_ops_id = $this->getOperationId('unparticipate'); + + if ($participate_ops_id === null && $unparticipate_ops_id === null) { + return; + } + + $ops_ids = array_values(array_filter([(int) $participate_ops_id, (int) $unparticipate_ops_id])); + if ($ops_ids === []) { + return; + } + + $in = implode(',', array_map('intval', $ops_ids)); + $sql = + "DELETE FROM rbac_ta " . + "WHERE typ_id IN (" . + "SELECT obj_id FROM object_data " . + "WHERE type = 'typ' AND title = '" . self::TYPE_TITLE . "'" . + ") " . + "AND ops_id IN (" . $in . ")"; + + $this->db->manipulate($sql); + } + + private function getOperationId(string $operation): ?int + { + $res = $this->db->queryF( + 'SELECT ops_id FROM rbac_operations WHERE operation = %s', + [\ilDBConstants::T_TEXT], + [$operation] + ); + $row = $this->db->fetchAssoc($res); + if (!is_array($row) || !isset($row['ops_id'])) { + return null; + } + return (int) $row['ops_id']; + } +} diff --git a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAccess.php b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAccess.php index a7597c60a976..3250db2a68fa 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAccess.php +++ b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAccess.php @@ -48,11 +48,6 @@ public static function _getCommands(): array 'cmd' => ilObjLearningSequenceGUI::CMD_SETTINGS, 'permission' => 'write', 'lang_var' => 'settings' - ], - [ - 'cmd' => ilObjLearningSequenceGUI::CMD_UNPARTICIPATE, - 'permission' => 'unparticipate', - 'lang_var' => 'unparticipate' ] ); } diff --git a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php index 28902c7fbe32..94dd319d9504 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php +++ b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php @@ -659,9 +659,11 @@ protected function afterSave(ilObject $new_object): void public function unparticipate(): void { - if ($this->checkAccess('unparticipate')) { + if ($this->checkAccess('read')) { $usr_id = $this->user->getId(); - $this->getObject()->getLSRoles()->leave($usr_id); + if ($this->getObject()->getLSRoles()->isMember($usr_id)) { + $this->getObject()->getLSRoles()->leave($usr_id); + } } $this->ctrl->redirectByClass('ilObjLearningSequenceLearnerGUI', self::CMD_LEARNER_VIEW); } From 31b6c30afd28320ab1dff3519e5d373497760e8a Mon Sep 17 00:00:00 2001 From: Abraham Date: Fri, 17 Jul 2026 08:37:04 +0200 Subject: [PATCH 088/333] [Survey] Preselected language handling (#11764) * [Fix] Add language parameter handling in ilObjSurveyGUI * [Fix] Added declare strict types * fix: Code format --------- Co-authored-by: ZallaxDev --- .../ILIAS/Survey/classes/class.ilObjSurveyGUI.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php b/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php index a4d333aea3d0..7baaefe561f7 100755 --- a/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php +++ b/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php @@ -16,6 +16,8 @@ * *********************************************************************/ +declare(strict_types=1); + use ILIAS\Survey\Participants; use ILIAS\Survey\InternalGUIService; use ILIAS\User\Profile\PublicProfileGUI; @@ -769,12 +771,16 @@ public static function _goto( if ($a_access_code === "" && isset($t_arr[1])) { $a_access_code = $t_arr[1]; } + $lang = $t_arr[2] ?? ""; + // see ilObjSurveyAccess::_checkGoto() if ($a_access_code !== '') { - $sess = $DIC->survey()->internal()->repo() - ->execution()->runSession(); + $sess = $DIC->survey()->internal()->repo()->execution()->runSession(); $sess->setCode(ilObject::_lookupObjId($ref_id), $a_access_code); $ctrl->setParameterByClass("ilObjSurveyGUI", "ref_id", $ref_id); + if ($lang !== "") { + $ctrl->setParameterByClass("ilObjSurveyGUI", "lang", $lang); + } $ctrl->redirectByClass("ilObjSurveyGUI", "run"); } From ac9c456b63359e917cadd3f4c05094bfb5a2f454 Mon Sep 17 00:00:00 2001 From: Fabian Schmid Date: Fri, 17 Jul 2026 10:00:09 +0200 Subject: [PATCH 089/333] [FIX] Consolidate Setup and setup_ --- .gitignore | 2 +- .../classes/Setup/ilDatabaseSetupConfig.php | 2 +- .../ILIAS/Database/classes/ilDBUpdate.php | 2 +- .../classes/class.ilFileUtils.php | 2 +- .../ILIAS/ResourceStorage/MIGRATIONS.md | 4 +- components/ILIAS/Setup/README.md | 575 ++++++++++++++++++ .../classes/class.ilCommonSetupAgent.php | 0 ...lass.ilExportMetadataGatheredObjective.php | 0 .../class.ilExportZipBuiltObjective.php | 0 .../class.ilIniFilesLoadedObjective.php | 0 .../class.ilIniFilesPopulatedObjective.php | 0 .../class.ilInstIdDefaultStoredObjective.php | 0 ....ilMakeInstallationAccessibleObjective.php | 0 .../class.ilNICKeyRegisteredObjective.php | 0 .../classes/class.ilNICKeyStoredObjective.php | 0 ...oMajorVersionSkippedConditionObjective.php | 0 ...ilNoVersionDowngradeConditionObjective.php | 0 ...verwritesExistingInstallationConfirmed.php | 0 .../class.ilOwnRiskConfirmedObjective.php | 0 .../classes/class.ilSetupConfig.php | 0 .../class.ilSetupConfigStoredObjective.php | 0 ...class.ilSetupMetricsCollectedObjective.php | 0 .../classes/class.ilSetupObjective.php | 0 .../classes/class.ilUseRootConfirmed.php | 0 ...ss.ilVersionWrittenToSettingsObjective.php | 0 .../{setup_ => Setup}/client.master.ini.php | 0 .../{setup_ => Setup}/ilias.master.ini.php | 0 .../{setup_ => Setup}/minimal-config.json | 0 .../ILIAS/{setup_ => Setup}/sql/ilias3.sql | 0 .../ILIAS/Setup/src/AbstractOfFinder.php | 2 +- components/ILIAS/setup_/PRIVACY.md | 28 - components/ILIAS/setup_/README.md | 571 ----------------- components/ILIAS/setup_/setup_.php | 37 -- composer.json | 9 +- docs/configuration/install.md | 14 +- .../development/tutorial/02-tools/03-setup.md | 2 +- scripts/PHP-CS-Fixer/code-format.php_cs | 2 +- 37 files changed, 595 insertions(+), 657 deletions(-) rename components/ILIAS/{setup_ => Setup}/classes/class.ilCommonSetupAgent.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilExportMetadataGatheredObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilExportZipBuiltObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilIniFilesLoadedObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilIniFilesPopulatedObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilInstIdDefaultStoredObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilMakeInstallationAccessibleObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilNICKeyRegisteredObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilNICKeyStoredObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilNoMajorVersionSkippedConditionObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilNoVersionDowngradeConditionObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilOverwritesExistingInstallationConfirmed.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilOwnRiskConfirmedObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilSetupConfig.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilSetupConfigStoredObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilSetupMetricsCollectedObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilSetupObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilUseRootConfirmed.php (100%) rename components/ILIAS/{setup_ => Setup}/classes/class.ilVersionWrittenToSettingsObjective.php (100%) rename components/ILIAS/{setup_ => Setup}/client.master.ini.php (100%) rename components/ILIAS/{setup_ => Setup}/ilias.master.ini.php (100%) rename components/ILIAS/{setup_ => Setup}/minimal-config.json (100%) rename components/ILIAS/{setup_ => Setup}/sql/ilias3.sql (100%) delete mode 100644 components/ILIAS/setup_/PRIVACY.md delete mode 100755 components/ILIAS/setup_/README.md delete mode 100644 components/ILIAS/setup_/setup_.php diff --git a/.gitignore b/.gitignore index 733cee9c9d6d..da9bdca6e50d 100755 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ /ilias.ini.php /ilias.log /db_template_writer.php -/components/ILIAS/setup_/sql/dbupdate_custom.php +/components/ILIAS/Setup/sql/dbupdate_custom.php /install_client.php # Directories diff --git a/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php b/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php index 06d7b651fcb2..39b8376f3592 100755 --- a/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php +++ b/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php @@ -24,7 +24,7 @@ class ilDatabaseSetupConfig implements Config { public const DEFAULT_COLLATION = "utf8_general_ci"; - public const DEFAULT_PATH_TO_DB_DUMP = "./components/ILIAS/setup_/sql/ilias3.sql"; + public const DEFAULT_PATH_TO_DB_DUMP = "./components/ILIAS/Setup/sql/ilias3.sql"; protected string $type; diff --git a/components/ILIAS/Database/classes/ilDBUpdate.php b/components/ILIAS/Database/classes/ilDBUpdate.php index 3ada9b047045..bad2feeb13ad 100755 --- a/components/ILIAS/Database/classes/ilDBUpdate.php +++ b/components/ILIAS/Database/classes/ilDBUpdate.php @@ -250,7 +250,7 @@ private function readCustomUpdatesInfo(bool $a_force = false): void } $this->custom_updates_setting = new ilSetting(); - $custom_updates_file = $this->PATH . './components/ILIAS/setup_/sql/dbupdate_custom.php'; + $custom_updates_file = $this->PATH . './components/ILIAS/Setup/sql/dbupdate_custom.php'; if (is_file($custom_updates_file)) { $this->custom_updates_content = @file($custom_updates_file); $this->custom_updates_current_version = (int) $this->custom_updates_setting->get('db_version_custom', '0'); diff --git a/components/ILIAS/FileServices/classes/class.ilFileUtils.php b/components/ILIAS/FileServices/classes/class.ilFileUtils.php index 4760da13e857..f1999a68082d 100755 --- a/components/ILIAS/FileServices/classes/class.ilFileUtils.php +++ b/components/ILIAS/FileServices/classes/class.ilFileUtils.php @@ -408,7 +408,7 @@ public static function moveUploadedFile( $target_filename = ilFileUtils::getValidFilename($target_filename); - // Make sure the target is in a valid subfolder. (e.g. no uploads to ilias/setup_/....) + // Make sure the target is in a valid subfolder. (e.g. no uploads to ilias/Setup/....) [$target_filesystem, $target_dir] = self::sanitateTargetPath($a_target); $upload = $DIC->upload(); diff --git a/components/ILIAS/ResourceStorage/MIGRATIONS.md b/components/ILIAS/ResourceStorage/MIGRATIONS.md index 2fca945a4714..1d5f57fb375c 100755 --- a/components/ILIAS/ResourceStorage/MIGRATIONS.md +++ b/components/ILIAS/ResourceStorage/MIGRATIONS.md @@ -5,7 +5,7 @@ With ILIAS 7 the possibility of migrations was introduced in the setup. Among other things, these are used to transfer files from old structures to the ILIAS Resource Storage Service (IRSS). -Information about migrations in general can be found at [setup_/README](../../setup_/README.md). +Information about migrations in general can be found at [Setup/README](../../Setup/README.md). ## General approach @@ -67,7 +67,7 @@ public function step_3(): void } ``` -The actual migration consists is structured as described in [setup_/README](../../setup_/README.md). The following two +The actual migration consists is structured as described in [Setup/README](../../Setup/README.md). The following two methods are the most relevant for the migration: - public function getPreconditions(Environment $environment): array diff --git a/components/ILIAS/Setup/README.md b/components/ILIAS/Setup/README.md index fecdde1e99d6..3e9e8265f22e 100755 --- a/components/ILIAS/Setup/README.md +++ b/components/ILIAS/Setup/README.md @@ -202,3 +202,578 @@ determine which classes to use for which task: $this->class_loader = include "vendor/ilias/Artifacts/global_screen_providers.php"; } ``` + + +--- + +# Use the Command Line to Manage ILIAS + +The ILIAS command line app can be called via `php setup\setup.php`. It contains four +main commands to manage ILIAS installations: + +* `install` will [set an installation up](#install-ilias) +* `update` will [update an installation](#update-ilias) +* `status` will [report status of an installation](#report-status-of-ilias) +* `build` [recreates static assets](#build-static-assets) of an installation +* `achieve` [a named objective](#achieve-a-named-objective) of an agent +* `migrate` will run [needed migrations](#migrations) + +`install` and `update` also supply switches and options for a granular control of the inclusion of plugins: + +* `--skip-legacy-plugin +* There are also named objectives for **import** and **export**. ` will exclude the named legacy plugin from the command +* `--no-legacy-plugins` will exclude all plugins from the command +* `install ` (or `update ` respectively) will update or install the specified legacy plugin + +`install` requires a [configuration file](#about-the-config-file) to do the job. +`update` can be used without this file for updating the installation only, but is +required to transfer any modified setting from this file to the installation. +The app also supports a `help` command that lists arguments and +options of the available commands. + + +## Install ILIAS + +To install ILIAS with all plugins from the command line, call `php cli/setup.php install config.json` +from within the ILIAS folder you checked out from GitHub (or downloaded from elsewhere). +`config.json` can be the path to some [configuration file](#about-the-config-file) +which does not need to reside in the ILIAS folder. Also, `cli/setup.php` could be +the path to the `setup.php` when the command is called from somewhere else. + +You most probably want to execute the setup with the user that also executes your +webserver to avoid problems with filesystem permissions. The installation creates +directories and files that the webserver will need to read and sometimes even modify. +If you need to run setup as another user, please make sure that the user that executes +the webserver has the necessary filesystem permissions (e.g. by using chown), to +avoid some errors which may be difficult to troubleshoot. + +The setup will ask you to confirm some assumptions during the setup process, where +you will have to type `yes` (or `no`, of course). These checks can be overwritten +with the `--yes` option, which confirm any assumption for you automatically. + +There might be cases where the setup aborts for some reasons. These reasons might +require further actions on your side which the setup cannot perform. Make sure you +read messages from the setup carefully and act accordingly. If you do not change the +config file, it is safe to execute the installation process a second time for the +same installation a during the initial setup process. + +Do not discard the `config.json` you use for the installation, you will need it later +on to update that installation. If you want to overwrite specific fields in the +configuration file you can use the `--config="="` option, even several +times. If you e.g. use `--config="database.password=XYZ"` the field `database.password` +from the original config will be overwritten with `XYZ`. This allows to use one +configuration for multiple setups and overwrite it from the CLI or even share +configs without secrets. + +The setup will also install plugins of the installation, unless the plugin explicitely +defines that it cannot be installed via CLI setup. If you still want to skip a plugin +for installation, use the skip-option: `php cli/setup.php install --skip-legacy-plugin config.json`. +The option can be repeated to cover multiple plugins. If you want to skip plugins +alltogether, use the `--no-legacy-plugins` option. If you only want to install a specific +plugin, use `php cli/setup.php install config.json `. + +The install command also offers the option to import a zip file during setup. The +zip file must have been previously exported from another instance via export +(see [a name objective](#achieve-method)). +The command `php cli/setup.php install --import-file config.json` +will install the data from the export to this instance. + +## Update ILIAS + +To update ILIAS from the command line, call `php cli/setup.php update` +from within your ILIAS folder. This will update ILIAS as well as update the +database of the installation or do other necessary task for the update. +This does not update the source code. +If there are changes in your config.json file call `php cli/setup.php update config.json` +from within your ILIAS folder. This will also update the configuration of ILIAS according +to the provided configuration. + +Plugins are updated just as the core of ILIAS (if the plugin does not exclude itself), +where the plugins can be controlled with the same options as for `install`. + +Sometimes it might happen that the database update steps detect some edge case +or warn about a possible loss of data. In this case the update is aborted with +a message and can be resumed after the messages were read carefully and acted +upon. +You may use the `--ignore-db-update-messages` at your own risk if you want +to silence the messages. + +When an update step failed, you might get a message about inconsistent order +of already performed steps when resuming the setup: +> step 2 was started last, but step 1 was finished last. +> Aborting because of that mismatch. + +You may reset the records for those steps by running: +``` +php setup/setup.php achieve database.resetFailedSteps +``` +However, be sure to understand the cause for the failing steps and tend to it before +resetting and re-running the update. + +## Report Status of ILIAS + +Via `php cli/setup.php status` you can get a status of your ILIAS installation. +The command uses a best effort approach, so according to the status of your +system the output might contain more or less fields. When calling this for a +system where ILIAS was not installed, for example, the output only contains the +information that ilias is not installed. The command also reports on the configuration +of the installation. + +The output of the command is formatted as YAML to be easily readable by people and +machines. So we encourage you to use this command for monitoring your system and +also request status information via our feature process that you are interested in. + +Like for `install` and `update`, plugins are included here, but can be controlled +via options. + + +## Build Static Assets + +There are two types of assets that ILIAS needs to function: + +* **Artifacts** are source code files that are created based on the ILIAS source tree. +* The **Public Folder** is filled with resources from the ILIAS components to be + served on the web. + +You can refresh them by calling `php cli/setup.php build` from your +installation. Make sure you run the command with the webserver user or adjust +filesystem permissions later on, because the webserver will need to access the +generated files. Please do not invoke this function unless it is explicitly stated +in update or patch instructions or you know what you are doing. + +Like for `install` and `update`, plugins are included here, but can be controlled +via options. + + +## Achieve a Named Objective + +Some components of ILIAS will publish named objectives to the setup via their +agent. The most notorious example for this is the component `UICore` which provides +the objective `buildIlCtrlArtifacts` that will generate routing information for the +GUI. To achieve a single objective from an agent, e.g. for control structure reload, +run `php cli/setup.php achieve $AGENT_NAME.$OBJECTIVE_NAME`, e.g. +`php cli/setup.php achieve uicore.buildIlCtrlArtifacts` to generate the necessary +artifacts for the control structure. The agent might need to a config file to work, +which may be added as last parameter: +`php cli/setup.php achieve uicore.buildIlCtrlArtifacts config.json` + +There is also a named objective for **export**. The command +`php cli/setup.php achieve common.buildExportZip config.json` creates a zip file 'ILIAS_EXPORT.zip' at the +location of the call. The export also changes the name of the client directory to +'default' so that the import can work with the files. The objective +'ilFileSystemClientDirectoryRenamedObjective.php' takes care of the renaming. + +The ILIAS export mechanism can be extended with ExportHooks. This allows you to influence the exported database during the export. +The ExportHooks file must be a PHP file and can be placed anywhere. It only has to be ensured that ILIAS has access to this file. +The path to the file can either be set permanently in config.json under the namespace common. +```bash +"common" : { + "client_id" : "ilias", + "master_password" : "ilias", + "server_timezone" : "Europe/Berlin", + "export_hooks_path" : "/var/ilias/export.php" + } +``` +Or you can specify it once when calling up the export command. +```bash +php cli/setup.php achieve common.buildExportZip --config="common.export_hooks_path=/var/ilias/export.php" config.json -y +``` +This [mysqldump](https://github.com/ifsnop/mysqldump-php#changing-values-when-exporting) hooks can be used in the export hooks file. +An example file could look like this (the variable $dumper is indirectly available). +```php +setTransformTableRowHook(function ($tableName, array $row) { + if ($tableName === 'write_event') { + if ($row['obj_id'] == 100) { + $row['usr_id'] = -1; + } + } + + return $row; +}); +``` +The zip file can then be imported using the install command. + +## List available objectives +Calling `php cli/setup.php achieve` without any arguments and options +or calling `php cli/setup.php achieve --list` will list all available objectives. + + +# Migrations + +Migrations are major changes in the ILIAS database or file system that are +necessary after an update. Migrations can take quite a long time, which is +why they are available separately as a command. The advantage is that you can +perform migrations after the update when the installation is already online again. +For more information, see [https://docu.ilias.de/goto_docu_wiki_wpage_6399_1357.html](https://docu.ilias.de/goto_docu_wiki_wpage_6399_1357.html) + +The command lists available migrations: + +`php cli/setup.php migrate` + + +``` +! [NOTE] There are 1 to run: + +ilFileObjectMigrationAgent.ilFileObjectToStorageMigration: Migration of File-Objects to Storage service [remaining steps: 1110] +``` + +Individual migrations can then be started as follows, e.g.: + +`php cli/setup.php migrate --run ilFileObjectMigrationAgent.ilFileObjectToStorageMigration` + +A migration must be confirmed in each case, e.g.: + +``` +Do you really want to run the following migration? Make sure you have a backup +of all your data. You will run this migration on your own risk. + +Please type 'ilFileObjectToStorageMigration' to confirm and start.: +> +``` + +With `--yes` migrations can be confirmed automatically. + +Migrations are divided into individual steps, of which there can be many depending +on the migration. A default number of steps is executed in each case; the number +can be increased or set with `--steps=...`. + +## About the Config File + +The config file is a json file with the following structure. **Mandatory fields +are printed bold**, all other fields might be omitted. A minimal example is +[here](minimal-config.json). + +* **common** (type: object) settings relevant for the complete installation, e.g.: + ``` + "common" : { + "client_id" : "test7", + "server_timezone" : "Europe/Berlin", + "register_nic" : true, + "export_hooks_path" : "/var/ilias/export_hooks.php" + } + ``` + * **client_id** (type: string) is the identifier to be used for the installation + * *server_timezone* (type: string) where the installation resides, given as `region/city`, + e.g. `Europe/Berlin`, defaults to `UTC` + * *register_nic* (boolean) sends the identification number of the installation to a server + of the ILIAS society together with some information about the installation, defaults to `false` + * *export_hooks_path* (type: string) The path to the PHP export hooks file, not required and defaults to null if absent. Setting to an empty string results in an error during export. +* *backgroundtasks* (type: object) is a service to run tasks for users in separate processes, e.g.: + ``` + "backgroundtasks" : { + "type" : "sync", + "max_number_of_concurrent_tasks" : 3 + }, + ``` + * *type* (type: string) might be `async` or `sync`, defaults to `sync`; async requires SOAP (c.f. webservices) to be enabled + * *max_number_of_concurrent_tasks* (type: number) that all users can run together, defaults to `1` +* **database** (type: object) is required to connect to the database, e.g.: + ``` + "database" : { + "type" : "innodb", + "host" : "192.168.47.11", + "port" : 3306, + "database" : "db_test7", + "user" : "test7_homer", + "password" : "homers-secret", + "create_database" : true + }, + ``` + * *type* (type: string) of the database, `innodb`, defaults + to `innodb` + * *host* (type: string) the database server runs on, defaults to `localhost` + * *port* (type: string or number) the database server uses, defaults to `3306` + * *database* (type: string) name to be used, defaults to `ilias` + * **user** (type: string) to be used to connect to the database + * *password* (type: string) to be used to connect to the database + * *create_database* (type: boolean) if a database with the given name does not exist? Defaults to `true`. +* **filesystem** (type: object) configuration, e.g.: + ``` + "filesystem" : { + "data_dir" : "/var/ilias_external_data/test7" + }, + ``` + * **data_dir** (type: string) outside the web directory where ILIAS puts some data +* *globalcache* (type: object) is a service for caching various information, e.g.: + ``` + "globalcache" : { + "service" : "static", + "components" : "all" + }, + ``` + or + ``` + "globalcache" : { + "service" : "apc", + "components" : { + "clng" : true, + "comp" : true, + "events" : true, + "global_screen" : true, + "obj_def" : true, + "ilctrl" : true, + "tpl" : true, + "tpl_blocks" : true, + "tpl_variables" : true + } + }, + ``` + or + ``` + "globalcache" : { + "service" : "memcached", + "components" : "all", + "memcached_nodes" : [ + { + "active" : true, + "host" : "example1.com", + "port" : 4711, + "weight" : 10 + }, + { + "active" : false, + "host" : "example2.com", + "port" : 4712, + "weight" : 90 + } + ] + }, + ``` + * *service* (type: string) to be used for caching. Either `none`, `static`, `memcached` + or `apc`, defaults to `static`. + * *components* (type: string or object) that should use caching. Can be `all` or any list of components that + support caching, (must be set too, if *service* is set) + * *memcached_nodes* (type: array of objects) if *service* equals `memcached` place your nodes here +* **http** (type: object) configuration, e.g.: + ``` + "http" : { + "path" : "https://test7.ilias.de/", + "https_autodetection" : { + "header_name" : "my-header-name", + "header_value" : "my-header-value" + }, + "proxy" : { + "host" : "webproxy.ilias.de", + "port" : "8088" + }, + "allowed_hosts" : [ + "red.ilias.de", + "blue.ilias.de", + "www.ilias.de" + ] + }, + ``` + * **path** (type: string) to your installation on the internet + * *https_autodetection* (type: object) allows ILIAS to be run behind a proxy that terminates ssl + connections + * *header_name* (type: string) that the proxy sets to indicate ssl connections + * *header_value* (type: string) that the proxy sets for said header + * *proxy* (type: object) for outgoing http connections + * *host* (type: string) the proxy runs on + * *port* (type: string or number) the proxy listens on + * *allowed_hosts* (type: an `array`/list of strings, or `null`) A list of valid hosts which is used to + validate the `HTTP_HOST` header of incoming web requests. If the host header does not match any of + the allowed hosts, the request is rejected. If `null` is set or an empty list is provided, the host + header is only validated against the host of the `path` setting + (stored in the "ilias.ini.php" as `http_path`), which is always considered allowed. + This also applies for the optionally configurable host used for the WSDL path definition + in the SOAP web service configuration and for "localhost". +* *logging* (type: object) configuration if logging should be used + ``` + "logging" : { + "enable" : true, + "path_to_logfile" : "/var/log/ilias_test7.log", + "errorlog_dir" : "/var/log/ilias_errorlogs/" + }, + ``` + * *enable* (type: boolean) the logging, defaults to `false` + * *path_to_logfile* (type: string) to be used for logging + * *errorlog_dir* (type: string) to put error logs in +* *preview* (type: object) contains settings for ILIAS/Preview + ``` + "preview" : { + "path_to_ghostscript" : "/usr/bin/gs" + }, + ``` + * *path_to_ghostscript* (type: string) executable +* *mediaobject* (type: object) contains settings for ILIAS/MediaObjects + ``` + "mediaobject" : { + "path_to_ffmpeg" : "/usr/bin/ffmpeg" + }, + ``` + * *path_to_ffmpeg* (type: string) executable +* *style* (type: obejct) configuration to change the ILIAS look + ``` + "style" : { + "manage_system_styles" : true, + "path_to_scss" : "/usr/bin/scss" + }, + ``` + * *manage_system_styles* (type: boolean) via a GUI in the installation, defaults to `false` + * *path_to_scss* (type: string) to compile scss to css +* **systemfolder** (type: object) settings for ILIAS/SystemFolder + ``` + "systemfolder" : { + "client" : { + "name" : "test7", + "description" : "Test Installation for ILIAS 7", + "institution" : "Atomic Powerplant Springfield" + }, + "contact" : { + "firstname" : "Homer", + "lastname" : "Simpson", + "title" : "Sir", + "position" : "Security Inspector Sector 7G", + "institution" : "Atomic Powerplant Springfield", + "street" : "742 Evergreen Terrace", + "zipcode" : "12345", + "city" : "Springfield", + "country" : "USA", + "phone" : "(939) 555-0113", + "email" : "Chunkylover53@aol.com" + } + }, + ``` + * *client* (type: string) information + * *name* (type: string) of the ILIAS installation + * *description* (type: string) of the installation + * *institution* (type: string) that provides the installation + * **contact** (type: string) to a person behind the installation + * **firstname** (type: string) of said person + * **lastname** (type: string) of said person + * *title* (type: string) of said person + * *position* (type: string) of said person + * *institution* (type: string) of said person + * *street* (type: string) of said person + * *zipcode* (type: string) of said person + * *city* (type: string) of said person + * *country* (type: string) of said person + * *phone* (type: string) of said person + * **email** (type: string) of said person +* *utilities* (type: object) contains settings for ILIAS/Utilities + ``` + "utilities" : { + "path_to_convert" : "/usr/bin/convert", + "path_to_zip" : "/usr/bin/zip", + "path_to_unzip" : "/usr/bin/unzip" + }, + ``` + * *path_to_convert* (type: string) from ImageMagick, to resize images + * *path_to_zip*" (type: string) to zip files + * *path_to_unzip*" (type: string) to unzip files +* *virusscanner* (type: object) configuration + ``` + "virusscanner" : { + "virusscanner" : "clamav", + "path_to_scan" : "/usr/bin/clamdscan", + "path_to_clean" : "/usr/bin/clamdscan --remove=yes", + }, + ``` + or + ``` + "virusscanner" : { + "virusscanner" : "icap", + "icap_host" : "192.168.47.12", + "icap_port" : 4712, + "icap_service_name" : "icap-name", + "icap_client_path" : "icap-client-path" + }, + ``` + * *virusscanner* (type: string) to be used. Either `none`, `sophos`, `antivir`, `clamav` or `icap` + * *path_to_scan* (type: string) command of the scanner + * *path_to_clean* (type: string) command of the scanner + * *icap_host* (type: string) host address of the icap scanner + * *icap_port* (type: string or number) port if the icap scanner + * *icap_service_name* (type: string) service name of the icap scanner + * *icap_client_path* (type: string) path to the `c-icap-client`, if this is left empty, a php client will be used +* *privacysecurity* (type: object) + ``` + "privacysecurity" : { + "https_enabled" : true, + "auth_duration" : 3000, + "account_assistance_duration" : 3000, + "registration_duration" : 3000, + }, + ``` + * *https_enabled* (type: boolean) forces https on login page, defaults to `false` + * *auth_duration* (type: integer) stretches the auth-duration on logins to the given amount in ms, defaults to `null` + * *account_assistance_duration* (type: integer) stretches the password- and username-assistance duration to the given amount in ms, defaults to `null` + * *registration_duration* (type: integer) stretches registration duration to the given amount in ms, defaults to `null` +* *webservices* (type: object) + ``` + "webservices" : { + "soap_user_administration" : true, + "soap_wsdl_path" : "https://test7.ilias.de/public/soap/server.php?wsdl", + "soap_connect_timeout" : 30, + "rpc_server_host" : "192.168.47.13", + "rpc_server_port" : "11112", + "soap_internal_wsdl_path": "https://localhost/public/soap/server.php?wsdl", + "soap_internal_wsdl_verify_peer": false, + "soap_internal_wsdl_verify_peer_name": false, + "soap_internal_wsdl_allow_self_signed": false + }, + ``` + * *soap_user_administration* (type: boolean) enable administration per soap, defaults to `false` + * *soap_wsdl_path* (type: string) path to the ilias wsdl file, default is `http:///public/soap/server.php?wsdl` + * *soap_connect_timeout* (type: number) maximum time in seconds until a connection attempt to the SOAP-Webservice is interrupted, defaults to `10` + * *rpc_server_host* (type: string) Java-Server host (must be set too, if *rpc_server_port* is set) + * *rpc_server_port* (type: string or number) Java-Server port (must be set too, if *rpc_server_host* is set) + * *soap_internal_wsdl_path* (type: string) path to the ilias wsdl file for internal usage (for calls from ilias to ilias itself), default is *soap_wsdl_path* + * *soap_internal_wsdl_verify_peer* (type: bool) verify peer for calls from ilias to ilias itself (see https://www.php.net/manual/en/context.ssl.php for more information) + * *soap_internal_wsdl_verify_peer_name* (type: bool) verify peer name for calls from ilias to ilias itself (see https://www.php.net/manual/en/context.ssl.php) + * *soap_internal_wsdl_allow_self_signed* (type: bool) allow self signed certificates for calls from ilias to ilias itself (see https://www.php.net/manual/en/context.ssl.php) +* *chatroom* (type: object) see also [Chat Server Setup](/components/ILIAS/Chatroom/README.md), eg.: + ``` + "chatroom" : { + "address" : "192.168.47.14", + "port" : 8081, + "sub_directory" : "/chat", + "https" : { + "cert" : "/etc/ssl/certs/server.pem", + "key" : "/etc/ssl/private/server.key", + "dhparam" : "/etc/ssl/private/dhparam.pem" + }, + "log" : "/var/log/ilias_onscreenchat/access.log", + "log_level" : "info", + "error_log" : "/var/log/ilias_onscreenchat/error.log", + "ilias_proxy" : { + "ilias_url" : "https://chat-ilias-proxy.ilias.de" + }, + "client_proxy" : { + "client_url" : "https://chat-client-proxy.ilias.de" + }, + "deletion_interval" : { + "deletion_unit" : "months", + "deletion_value" : "6", + "deletion_time" : "23:45" + } + } + ``` + * *address* (type: string) IP-Address/FQN of Chat Server + * *port* (type: string or number) of the chat server, possible value from `1` to `65535` + * *sub_directory* (type: string) http(s)://[IP/Domain]/[SUB_DIRECTORY] + * *https* (type: object) adding this enables https + * *cert* (type: string) absolute server path to the SSL certificate file e.g. `/etc/ssl/certs/server.pem` + * *key* (type: string) absolute server path to the private key file e.g. `/etc/ssl/private/server.key` + * *dhparam* (type: string) absolute server path to a file e.g. `/etc/ssl/private/dhparam.pem` + * *log* (type: string) absolute server path to the chat server's log file e.g. `/var/www/ilias/data/chat.log` + * *log_level* (type: string) possible values are `emerg`, `alert`, `crit` `error`, `warning`, `notice`, `info`, `debug`, `silly`, defaults to `warning` + * *error_log* (type: string) absolute server path to the chat server's error log file e.g. `/var/www/ilias/data/chat_error.log` + * *ilias_proxy* (type: object) ILIAS to Server Connection + * *ilias_url* (type: string) URL for the Server connection + * *client_proxy* (type: object) Client to Server Connection + * *client_url* URL for the Server connection + * *deletion_interval* (type: object) + * *deletion_unit* (type: string) possible values are `days`, `weeks`, `months`, `years` + * *deletion_value* (type: string or number) depending on `deletion_unit` possible values are `days max 31`, `weeks max 52`, `months max 12`, `years no max` + * *deletion_time* (type: string) with format `HH:MM e.g. 23:30` +* *authentication* (type: object) + ``` + "authentication" : { + "session_max_idle": 1800 + } + ``` + * *session_max_idle* (type: number) maximum session idle (in seconds) diff --git a/components/ILIAS/setup_/classes/class.ilCommonSetupAgent.php b/components/ILIAS/Setup/classes/class.ilCommonSetupAgent.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilCommonSetupAgent.php rename to components/ILIAS/Setup/classes/class.ilCommonSetupAgent.php diff --git a/components/ILIAS/setup_/classes/class.ilExportMetadataGatheredObjective.php b/components/ILIAS/Setup/classes/class.ilExportMetadataGatheredObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilExportMetadataGatheredObjective.php rename to components/ILIAS/Setup/classes/class.ilExportMetadataGatheredObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilExportZipBuiltObjective.php b/components/ILIAS/Setup/classes/class.ilExportZipBuiltObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilExportZipBuiltObjective.php rename to components/ILIAS/Setup/classes/class.ilExportZipBuiltObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilIniFilesLoadedObjective.php b/components/ILIAS/Setup/classes/class.ilIniFilesLoadedObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilIniFilesLoadedObjective.php rename to components/ILIAS/Setup/classes/class.ilIniFilesLoadedObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilIniFilesPopulatedObjective.php b/components/ILIAS/Setup/classes/class.ilIniFilesPopulatedObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilIniFilesPopulatedObjective.php rename to components/ILIAS/Setup/classes/class.ilIniFilesPopulatedObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilInstIdDefaultStoredObjective.php b/components/ILIAS/Setup/classes/class.ilInstIdDefaultStoredObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilInstIdDefaultStoredObjective.php rename to components/ILIAS/Setup/classes/class.ilInstIdDefaultStoredObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilMakeInstallationAccessibleObjective.php b/components/ILIAS/Setup/classes/class.ilMakeInstallationAccessibleObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilMakeInstallationAccessibleObjective.php rename to components/ILIAS/Setup/classes/class.ilMakeInstallationAccessibleObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilNICKeyRegisteredObjective.php b/components/ILIAS/Setup/classes/class.ilNICKeyRegisteredObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilNICKeyRegisteredObjective.php rename to components/ILIAS/Setup/classes/class.ilNICKeyRegisteredObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilNICKeyStoredObjective.php b/components/ILIAS/Setup/classes/class.ilNICKeyStoredObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilNICKeyStoredObjective.php rename to components/ILIAS/Setup/classes/class.ilNICKeyStoredObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilNoMajorVersionSkippedConditionObjective.php b/components/ILIAS/Setup/classes/class.ilNoMajorVersionSkippedConditionObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilNoMajorVersionSkippedConditionObjective.php rename to components/ILIAS/Setup/classes/class.ilNoMajorVersionSkippedConditionObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilNoVersionDowngradeConditionObjective.php b/components/ILIAS/Setup/classes/class.ilNoVersionDowngradeConditionObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilNoVersionDowngradeConditionObjective.php rename to components/ILIAS/Setup/classes/class.ilNoVersionDowngradeConditionObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilOverwritesExistingInstallationConfirmed.php b/components/ILIAS/Setup/classes/class.ilOverwritesExistingInstallationConfirmed.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilOverwritesExistingInstallationConfirmed.php rename to components/ILIAS/Setup/classes/class.ilOverwritesExistingInstallationConfirmed.php diff --git a/components/ILIAS/setup_/classes/class.ilOwnRiskConfirmedObjective.php b/components/ILIAS/Setup/classes/class.ilOwnRiskConfirmedObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilOwnRiskConfirmedObjective.php rename to components/ILIAS/Setup/classes/class.ilOwnRiskConfirmedObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilSetupConfig.php b/components/ILIAS/Setup/classes/class.ilSetupConfig.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilSetupConfig.php rename to components/ILIAS/Setup/classes/class.ilSetupConfig.php diff --git a/components/ILIAS/setup_/classes/class.ilSetupConfigStoredObjective.php b/components/ILIAS/Setup/classes/class.ilSetupConfigStoredObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilSetupConfigStoredObjective.php rename to components/ILIAS/Setup/classes/class.ilSetupConfigStoredObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilSetupMetricsCollectedObjective.php b/components/ILIAS/Setup/classes/class.ilSetupMetricsCollectedObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilSetupMetricsCollectedObjective.php rename to components/ILIAS/Setup/classes/class.ilSetupMetricsCollectedObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilSetupObjective.php b/components/ILIAS/Setup/classes/class.ilSetupObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilSetupObjective.php rename to components/ILIAS/Setup/classes/class.ilSetupObjective.php diff --git a/components/ILIAS/setup_/classes/class.ilUseRootConfirmed.php b/components/ILIAS/Setup/classes/class.ilUseRootConfirmed.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilUseRootConfirmed.php rename to components/ILIAS/Setup/classes/class.ilUseRootConfirmed.php diff --git a/components/ILIAS/setup_/classes/class.ilVersionWrittenToSettingsObjective.php b/components/ILIAS/Setup/classes/class.ilVersionWrittenToSettingsObjective.php similarity index 100% rename from components/ILIAS/setup_/classes/class.ilVersionWrittenToSettingsObjective.php rename to components/ILIAS/Setup/classes/class.ilVersionWrittenToSettingsObjective.php diff --git a/components/ILIAS/setup_/client.master.ini.php b/components/ILIAS/Setup/client.master.ini.php similarity index 100% rename from components/ILIAS/setup_/client.master.ini.php rename to components/ILIAS/Setup/client.master.ini.php diff --git a/components/ILIAS/setup_/ilias.master.ini.php b/components/ILIAS/Setup/ilias.master.ini.php similarity index 100% rename from components/ILIAS/setup_/ilias.master.ini.php rename to components/ILIAS/Setup/ilias.master.ini.php diff --git a/components/ILIAS/setup_/minimal-config.json b/components/ILIAS/Setup/minimal-config.json similarity index 100% rename from components/ILIAS/setup_/minimal-config.json rename to components/ILIAS/Setup/minimal-config.json diff --git a/components/ILIAS/setup_/sql/ilias3.sql b/components/ILIAS/Setup/sql/ilias3.sql similarity index 100% rename from components/ILIAS/setup_/sql/ilias3.sql rename to components/ILIAS/Setup/sql/ilias3.sql diff --git a/components/ILIAS/Setup/src/AbstractOfFinder.php b/components/ILIAS/Setup/src/AbstractOfFinder.php index df9ecb077656..3ccd7024a183 100755 --- a/components/ILIAS/Setup/src/AbstractOfFinder.php +++ b/components/ILIAS/Setup/src/AbstractOfFinder.php @@ -51,7 +51,7 @@ abstract class AbstractOfFinder '.*/components/ILIAS/Types/tests/', '.*/components/ILIAS/UI/tests/', '.*/components/ILIAS/VirusScanner/tests/', - '.*/components/ILIAS/setup_/', + '.*/components/ILIAS/Setup/classes/', // Classes using unknown '.*ilPDExternalFeedBlockGUI.*', ]; diff --git a/components/ILIAS/setup_/PRIVACY.md b/components/ILIAS/setup_/PRIVACY.md deleted file mode 100644 index fc5dea0d9d2a..000000000000 --- a/components/ILIAS/setup_/PRIVACY.md +++ /dev/null @@ -1,28 +0,0 @@ -# Setup Privacy - -This documentation does not warrant completeness or correctness. Please report any -missing or wrong information using the [ILIAS issue tracker](https://mantis.ilias.de) -or contribute a fix via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories). - -## Integrated Services - -- The Component component employs the following services, please consult the respective privacy.mds - - [Data](../../ILIAS/Data/PRIVACY.md) - - [Refinery](../../ILIAS/Refinery/PRIVACY.md) - - [Setup](../../ILIAS/Setup/PRIVACY.md) - - UI - - -## Data being stored - -- The Setup component itself does not store any personal data. - - -## Data presentation - -- The Setup itself does not present any personal data. - - -## Data Deletion - -- The Setup itself does not store or delete any personal data. diff --git a/components/ILIAS/setup_/README.md b/components/ILIAS/setup_/README.md deleted file mode 100755 index 5f8d87c6c2ac..000000000000 --- a/components/ILIAS/setup_/README.md +++ /dev/null @@ -1,571 +0,0 @@ -# Use the Command Line to Manage ILIAS - -The ILIAS command line app can be called via `php setup\setup.php`. It contains four -main commands to manage ILIAS installations: - -* `install` will [set an installation up](#install-ilias) -* `update` will [update an installation](#update-ilias) -* `status` will [report status of an installation](#report-status-of-ilias) -* `build` [recreates static assets](#build-static-assets) of an installation -* `achieve` [a named objective](#achieve-a-named-objective) of an agent -* `migrate` will run [needed migrations](#migrations) - -`install` and `update` also supply switches and options for a granular control of the inclusion of plugins: - -* `--skip-legacy-plugin -* There are also named objectives for **import** and **export**. ` will exclude the named legacy plugin from the command -* `--no-legacy-plugins` will exclude all plugins from the command -* `install ` (or `update ` respectively) will update or install the specified legacy plugin - -`install` requires a [configuration file](#about-the-config-file) to do the job. -`update` can be used without this file for updating the installation only, but is -required to transfer any modified setting from this file to the installation. -The app also supports a `help` command that lists arguments and -options of the available commands. - - -## Install ILIAS - -To install ILIAS with all plugins from the command line, call `php cli/setup.php install config.json` -from within the ILIAS folder you checked out from GitHub (or downloaded from elsewhere). -`config.json` can be the path to some [configuration file](#about-the-config-file) -which does not need to reside in the ILIAS folder. Also, `cli/setup.php` could be -the path to the `setup.php` when the command is called from somewhere else. - -You most probably want to execute the setup with the user that also executes your -webserver to avoid problems with filesystem permissions. The installation creates -directories and files that the webserver will need to read and sometimes even modify. -If you need to run setup as another user, please make sure that the user that executes -the webserver has the necessary filesystem permissions (e.g. by using chown), to -avoid some errors which may be difficult to troubleshoot. - -The setup will ask you to confirm some assumptions during the setup process, where -you will have to type `yes` (or `no`, of course). These checks can be overwritten -with the `--yes` option, which confirm any assumption for you automatically. - -There might be cases where the setup aborts for some reasons. These reasons might -require further actions on your side which the setup cannot perform. Make sure you -read messages from the setup carefully and act accordingly. If you do not change the -config file, it is safe to execute the installation process a second time for the -same installation a during the initial setup process. - -Do not discard the `config.json` you use for the installation, you will need it later -on to update that installation. If you want to overwrite specific fields in the -configuration file you can use the `--config="="` option, even several -times. If you e.g. use `--config="database.password=XYZ"` the field `database.password` -from the original config will be overwritten with `XYZ`. This allows to use one -configuration for multiple setups and overwrite it from the CLI or even share -configs without secrets. - -The setup will also install plugins of the installation, unless the plugin explicitely -defines that it cannot be installed via CLI setup. If you still want to skip a plugin -for installation, use the skip-option: `php cli/setup.php install --skip-legacy-plugin config.json`. -The option can be repeated to cover multiple plugins. If you want to skip plugins -alltogether, use the `--no-legacy-plugins` option. If you only want to install a specific -plugin, use `php cli/setup.php install config.json `. - -The install command also offers the option to import a zip file during setup. The -zip file must have been previously exported from another instance via export -(see [a name objective](#achieve-method)). -The command `php cli/setup.php install --import-file config.json` -will install the data from the export to this instance. - -## Update ILIAS - -To update ILIAS from the command line, call `php cli/setup.php update` -from within your ILIAS folder. This will update ILIAS as well as update the -database of the installation or do other necessary task for the update. -This does not update the source code. -If there are changes in your config.json file call `php cli/setup.php update config.json` -from within your ILIAS folder. This will also update the configuration of ILIAS according -to the provided configuration. - -Plugins are updated just as the core of ILIAS (if the plugin does not exclude itself), -where the plugins can be controlled with the same options as for `install`. - -Sometimes it might happen that the database update steps detect some edge case -or warn about a possible loss of data. In this case the update is aborted with -a message and can be resumed after the messages were read carefully and acted -upon. -You may use the `--ignore-db-update-messages` at your own risk if you want -to silence the messages. - -When an update step failed, you might get a message about inconsistent order -of already performed steps when resuming the setup: -> step 2 was started last, but step 1 was finished last. -> Aborting because of that mismatch. - -You may reset the records for those steps by running: -``` -php setup/setup.php achieve database.resetFailedSteps -``` -However, be sure to understand the cause for the failing steps and tend to it before -resetting and re-running the update. - -## Report Status of ILIAS - -Via `php cli/setup.php status` you can get a status of your ILIAS installation. -The command uses a best effort approach, so according to the status of your -system the output might contain more or less fields. When calling this for a -system where ILIAS was not installed, for example, the output only contains the -information that ilias is not installed. The command also reports on the configuration -of the installation. - -The output of the command is formatted as YAML to be easily readable by people and -machines. So we encourage you to use this command for monitoring your system and -also request status information via our feature process that you are interested in. - -Like for `install` and `update`, plugins are included here, but can be controlled -via options. - - -## Build Static Assets - -There are two types of assets that ILIAS needs to function: - -* **Artifacts** are source code files that are created based on the ILIAS source tree. -* The **Public Folder** is filled with resources from the ILIAS components to be - served on the web. - -You can refresh them by calling `php cli/setup.php build` from your -installation. Make sure you run the command with the webserver user or adjust -filesystem permissions later on, because the webserver will need to access the -generated files. Please do not invoke this function unless it is explicitly stated -in update or patch instructions or you know what you are doing. - -Like for `install` and `update`, plugins are included here, but can be controlled -via options. - - -## Achieve a Named Objective - -Some components of ILIAS will publish named objectives to the setup via their -agent. The most notorious example for this is the component `UICore` which provides -the objective `buildIlCtrlArtifacts` that will generate routing information for the -GUI. To achieve a single objective from an agent, e.g. for control structure reload, -run `php cli/setup.php achieve $AGENT_NAME.$OBJECTIVE_NAME`, e.g. -`php cli/setup.php achieve uicore.buildIlCtrlArtifacts` to generate the necessary -artifacts for the control structure. The agent might need to a config file to work, -which may be added as last parameter: -`php cli/setup.php achieve uicore.buildIlCtrlArtifacts config.json` - -There is also a named objective for **export**. The command -`php cli/setup.php achieve common.buildExportZip config.json` creates a zip file 'ILIAS_EXPORT.zip' at the -location of the call. The export also changes the name of the client directory to -'default' so that the import can work with the files. The objective -'ilFileSystemClientDirectoryRenamedObjective.php' takes care of the renaming. - -The ILIAS export mechanism can be extended with ExportHooks. This allows you to influence the exported database during the export. -The ExportHooks file must be a PHP file and can be placed anywhere. It only has to be ensured that ILIAS has access to this file. -The path to the file can either be set permanently in config.json under the namespace common. -```bash -"common" : { - "client_id" : "ilias", - "master_password" : "ilias", - "server_timezone" : "Europe/Berlin", - "export_hooks_path" : "/var/ilias/export.php" - } -``` -Or you can specify it once when calling up the export command. -```bash -php cli/setup.php achieve common.buildExportZip --config="common.export_hooks_path=/var/ilias/export.php" config.json -y -``` -This [mysqldump](https://github.com/ifsnop/mysqldump-php#changing-values-when-exporting) hooks can be used in the export hooks file. -An example file could look like this (the variable $dumper is indirectly available). -```php -setTransformTableRowHook(function ($tableName, array $row) { - if ($tableName === 'write_event') { - if ($row['obj_id'] == 100) { - $row['usr_id'] = -1; - } - } - - return $row; -}); -``` -The zip file can then be imported using the install command. - -## List available objectives -Calling `php cli/setup.php achieve` without any arguments and options -or calling `php cli/setup.php achieve --list` will list all available objectives. - - -# Migrations - -Migrations are major changes in the ILIAS database or file system that are -necessary after an update. Migrations can take quite a long time, which is -why they are available separately as a command. The advantage is that you can -perform migrations after the update when the installation is already online again. -For more information, see [https://docu.ilias.de/goto_docu_wiki_wpage_6399_1357.html](https://docu.ilias.de/goto_docu_wiki_wpage_6399_1357.html) - -The command lists available migrations: - -`php cli/setup.php migrate` - - -``` -! [NOTE] There are 1 to run: - -ilFileObjectMigrationAgent.ilFileObjectToStorageMigration: Migration of File-Objects to Storage service [remaining steps: 1110] -``` - -Individual migrations can then be started as follows, e.g.: - -`php cli/setup.php migrate --run ilFileObjectMigrationAgent.ilFileObjectToStorageMigration` - -A migration must be confirmed in each case, e.g.: - -``` -Do you really want to run the following migration? Make sure you have a backup -of all your data. You will run this migration on your own risk. - -Please type 'ilFileObjectToStorageMigration' to confirm and start.: -> -``` - -With `--yes` migrations can be confirmed automatically. - -Migrations are divided into individual steps, of which there can be many depending -on the migration. A default number of steps is executed in each case; the number -can be increased or set with `--steps=...`. - -## About the Config File - -The config file is a json file with the following structure. **Mandatory fields -are printed bold**, all other fields might be omitted. A minimal example is -[here](minimal-config.json). - -* **common** (type: object) settings relevant for the complete installation, e.g.: - ``` - "common" : { - "client_id" : "test7", - "server_timezone" : "Europe/Berlin", - "register_nic" : true, - "export_hooks_path" : "/var/ilias/export_hooks.php" - } - ``` - * **client_id** (type: string) is the identifier to be used for the installation - * *server_timezone* (type: string) where the installation resides, given as `region/city`, - e.g. `Europe/Berlin`, defaults to `UTC` - * *register_nic* (boolean) sends the identification number of the installation to a server - of the ILIAS society together with some information about the installation, defaults to `false` - * *export_hooks_path* (type: string) The path to the PHP export hooks file, not required and defaults to null if absent. Setting to an empty string results in an error during export. -* *backgroundtasks* (type: object) is a service to run tasks for users in separate processes, e.g.: - ``` - "backgroundtasks" : { - "type" : "sync", - "max_number_of_concurrent_tasks" : 3 - }, - ``` - * *type* (type: string) might be `async` or `sync`, defaults to `sync`; async requires SOAP (c.f. webservices) to be enabled - * *max_number_of_concurrent_tasks* (type: number) that all users can run together, defaults to `1` -* **database** (type: object) is required to connect to the database, e.g.: - ``` - "database" : { - "type" : "innodb", - "host" : "192.168.47.11", - "port" : 3306, - "database" : "db_test7", - "user" : "test7_homer", - "password" : "homers-secret", - "create_database" : true - }, - ``` - * *type* (type: string) of the database, `innodb`, defaults - to `innodb` - * *host* (type: string) the database server runs on, defaults to `localhost` - * *port* (type: string or number) the database server uses, defaults to `3306` - * *database* (type: string) name to be used, defaults to `ilias` - * **user** (type: string) to be used to connect to the database - * *password* (type: string) to be used to connect to the database - * *create_database* (type: boolean) if a database with the given name does not exist? Defaults to `true`. -* **filesystem** (type: object) configuration, e.g.: - ``` - "filesystem" : { - "data_dir" : "/var/ilias_external_data/test7" - }, - ``` - * **data_dir** (type: string) outside the web directory where ILIAS puts some data -* *globalcache* (type: object) is a service for caching various information, e.g.: - ``` - "globalcache" : { - "service" : "static", - "components" : "all" - }, - ``` - or - ``` - "globalcache" : { - "service" : "apc", - "components" : { - "clng" : true, - "comp" : true, - "events" : true, - "global_screen" : true, - "obj_def" : true, - "ilctrl" : true, - "tpl" : true, - "tpl_blocks" : true, - "tpl_variables" : true - } - }, - ``` - or - ``` - "globalcache" : { - "service" : "memcached", - "components" : "all", - "memcached_nodes" : [ - { - "active" : true, - "host" : "example1.com", - "port" : 4711, - "weight" : 10 - }, - { - "active" : false, - "host" : "example2.com", - "port" : 4712, - "weight" : 90 - } - ] - }, - ``` - * *service* (type: string) to be used for caching. Either `none`, `static`, `memcached` - or `apc`, defaults to `static`. - * *components* (type: string or object) that should use caching. Can be `all` or any list of components that - support caching, (must be set too, if *service* is set) - * *memcached_nodes* (type: array of objects) if *service* equals `memcached` place your nodes here -* **http** (type: object) configuration, e.g.: - ``` - "http" : { - "path" : "https://test7.ilias.de/", - "https_autodetection" : { - "header_name" : "my-header-name", - "header_value" : "my-header-value" - }, - "proxy" : { - "host" : "webproxy.ilias.de", - "port" : "8088" - }, - "allowed_hosts" : [ - "red.ilias.de", - "blue.ilias.de", - "www.ilias.de" - ] - }, - ``` - * **path** (type: string) to your installation on the internet - * *https_autodetection* (type: object) allows ILIAS to be run behind a proxy that terminates ssl - connections - * *header_name* (type: string) that the proxy sets to indicate ssl connections - * *header_value* (type: string) that the proxy sets for said header - * *proxy* (type: object) for outgoing http connections - * *host* (type: string) the proxy runs on - * *port* (type: string or number) the proxy listens on - * *allowed_hosts* (type: an `array`/list of strings, or `null`) A list of valid hosts which is used to - validate the `HTTP_HOST` header of incoming web requests. If the host header does not match any of - the allowed hosts, the request is rejected. If `null` is set or an empty list is provided, the host - header is only validated against the host of the `path` setting - (stored in the "ilias.ini.php" as `http_path`), which is always considered allowed. - This also applies for the optionally configurable host used for the WSDL path definition - in the SOAP web service configuration and for "localhost". -* *logging* (type: object) configuration if logging should be used - ``` - "logging" : { - "enable" : true, - "path_to_logfile" : "/var/log/ilias_test7.log", - "errorlog_dir" : "/var/log/ilias_errorlogs/" - }, - ``` - * *enable* (type: boolean) the logging, defaults to `false` - * *path_to_logfile* (type: string) to be used for logging - * *errorlog_dir* (type: string) to put error logs in -* *preview* (type: object) contains settings for ILIAS/Preview - ``` - "preview" : { - "path_to_ghostscript" : "/usr/bin/gs" - }, - ``` - * *path_to_ghostscript* (type: string) executable -* *mediaobject* (type: object) contains settings for ILIAS/MediaObjects - ``` - "mediaobject" : { - "path_to_ffmpeg" : "/usr/bin/ffmpeg" - }, - ``` - * *path_to_ffmpeg* (type: string) executable -* *style* (type: obejct) configuration to change the ILIAS look - ``` - "style" : { - "manage_system_styles" : true, - "path_to_scss" : "/usr/bin/scss" - }, - ``` - * *manage_system_styles* (type: boolean) via a GUI in the installation, defaults to `false` - * *path_to_scss* (type: string) to compile scss to css -* **systemfolder** (type: object) settings for ILIAS/SystemFolder - ``` - "systemfolder" : { - "client" : { - "name" : "test7", - "description" : "Test Installation for ILIAS 7", - "institution" : "Atomic Powerplant Springfield" - }, - "contact" : { - "firstname" : "Homer", - "lastname" : "Simpson", - "title" : "Sir", - "position" : "Security Inspector Sector 7G", - "institution" : "Atomic Powerplant Springfield", - "street" : "742 Evergreen Terrace", - "zipcode" : "12345", - "city" : "Springfield", - "country" : "USA", - "phone" : "(939) 555-0113", - "email" : "Chunkylover53@aol.com" - } - }, - ``` - * *client* (type: string) information - * *name* (type: string) of the ILIAS installation - * *description* (type: string) of the installation - * *institution* (type: string) that provides the installation - * **contact** (type: string) to a person behind the installation - * **firstname** (type: string) of said person - * **lastname** (type: string) of said person - * *title* (type: string) of said person - * *position* (type: string) of said person - * *institution* (type: string) of said person - * *street* (type: string) of said person - * *zipcode* (type: string) of said person - * *city* (type: string) of said person - * *country* (type: string) of said person - * *phone* (type: string) of said person - * **email** (type: string) of said person -* *utilities* (type: object) contains settings for ILIAS/Utilities - ``` - "utilities" : { - "path_to_convert" : "/usr/bin/convert", - "path_to_zip" : "/usr/bin/zip", - "path_to_unzip" : "/usr/bin/unzip" - }, - ``` - * *path_to_convert* (type: string) from ImageMagick, to resize images - * *path_to_zip*" (type: string) to zip files - * *path_to_unzip*" (type: string) to unzip files -* *virusscanner* (type: object) configuration - ``` - "virusscanner" : { - "virusscanner" : "clamav", - "path_to_scan" : "/usr/bin/clamdscan", - "path_to_clean" : "/usr/bin/clamdscan --remove=yes", - }, - ``` - or - ``` - "virusscanner" : { - "virusscanner" : "icap", - "icap_host" : "192.168.47.12", - "icap_port" : 4712, - "icap_service_name" : "icap-name", - "icap_client_path" : "icap-client-path" - }, - ``` - * *virusscanner* (type: string) to be used. Either `none`, `sophos`, `antivir`, `clamav` or `icap` - * *path_to_scan* (type: string) command of the scanner - * *path_to_clean* (type: string) command of the scanner - * *icap_host* (type: string) host address of the icap scanner - * *icap_port* (type: string or number) port if the icap scanner - * *icap_service_name* (type: string) service name of the icap scanner - * *icap_client_path* (type: string) path to the `c-icap-client`, if this is left empty, a php client will be used -* *privacysecurity* (type: object) - ``` - "privacysecurity" : { - "https_enabled" : true, - "auth_duration" : 3000, - "account_assistance_duration" : 3000, - "registration_duration" : 3000, - }, - ``` - * *https_enabled* (type: boolean) forces https on login page, defaults to `false` - * *auth_duration* (type: integer) stretches the auth-duration on logins to the given amount in ms, defaults to `null` - * *account_assistance_duration* (type: integer) stretches the password- and username-assistance duration to the given amount in ms, defaults to `null` - * *registration_duration* (type: integer) stretches registration duration to the given amount in ms, defaults to `null` -* *webservices* (type: object) - ``` - "webservices" : { - "soap_user_administration" : true, - "soap_wsdl_path" : "https://test7.ilias.de/public/soap/server.php?wsdl", - "soap_connect_timeout" : 30, - "rpc_server_host" : "192.168.47.13", - "rpc_server_port" : "11112", - "soap_internal_wsdl_path": "https://localhost/public/soap/server.php?wsdl", - "soap_internal_wsdl_verify_peer": false, - "soap_internal_wsdl_verify_peer_name": false, - "soap_internal_wsdl_allow_self_signed": false - }, - ``` - * *soap_user_administration* (type: boolean) enable administration per soap, defaults to `false` - * *soap_wsdl_path* (type: string) path to the ilias wsdl file, default is `http:///public/soap/server.php?wsdl` - * *soap_connect_timeout* (type: number) maximum time in seconds until a connection attempt to the SOAP-Webservice is interrupted, defaults to `10` - * *rpc_server_host* (type: string) Java-Server host (must be set too, if *rpc_server_port* is set) - * *rpc_server_port* (type: string or number) Java-Server port (must be set too, if *rpc_server_host* is set) - * *soap_internal_wsdl_path* (type: string) path to the ilias wsdl file for internal usage (for calls from ilias to ilias itself), default is *soap_wsdl_path* - * *soap_internal_wsdl_verify_peer* (type: bool) verify peer for calls from ilias to ilias itself (see https://www.php.net/manual/en/context.ssl.php for more information) - * *soap_internal_wsdl_verify_peer_name* (type: bool) verify peer name for calls from ilias to ilias itself (see https://www.php.net/manual/en/context.ssl.php) - * *soap_internal_wsdl_allow_self_signed* (type: bool) allow self signed certificates for calls from ilias to ilias itself (see https://www.php.net/manual/en/context.ssl.php) -* *chatroom* (type: object) see also [Chat Server Setup](/components/ILIAS/Chatroom/README.md), eg.: - ``` - "chatroom" : { - "address" : "192.168.47.14", - "port" : 8081, - "sub_directory" : "/chat", - "https" : { - "cert" : "/etc/ssl/certs/server.pem", - "key" : "/etc/ssl/private/server.key", - "dhparam" : "/etc/ssl/private/dhparam.pem" - }, - "log" : "/var/log/ilias_onscreenchat/access.log", - "log_level" : "info", - "error_log" : "/var/log/ilias_onscreenchat/error.log", - "ilias_proxy" : { - "ilias_url" : "https://chat-ilias-proxy.ilias.de" - }, - "client_proxy" : { - "client_url" : "https://chat-client-proxy.ilias.de" - }, - "deletion_interval" : { - "deletion_unit" : "months", - "deletion_value" : "6", - "deletion_time" : "23:45" - } - } - ``` - * *address* (type: string) IP-Address/FQN of Chat Server - * *port* (type: string or number) of the chat server, possible value from `1` to `65535` - * *sub_directory* (type: string) http(s)://[IP/Domain]/[SUB_DIRECTORY] - * *https* (type: object) adding this enables https - * *cert* (type: string) absolute server path to the SSL certificate file e.g. `/etc/ssl/certs/server.pem` - * *key* (type: string) absolute server path to the private key file e.g. `/etc/ssl/private/server.key` - * *dhparam* (type: string) absolute server path to a file e.g. `/etc/ssl/private/dhparam.pem` - * *log* (type: string) absolute server path to the chat server's log file e.g. `/var/www/ilias/data/chat.log` - * *log_level* (type: string) possible values are `emerg`, `alert`, `crit` `error`, `warning`, `notice`, `info`, `debug`, `silly`, defaults to `warning` - * *error_log* (type: string) absolute server path to the chat server's error log file e.g. `/var/www/ilias/data/chat_error.log` - * *ilias_proxy* (type: object) ILIAS to Server Connection - * *ilias_url* (type: string) URL for the Server connection - * *client_proxy* (type: object) Client to Server Connection - * *client_url* URL for the Server connection - * *deletion_interval* (type: object) - * *deletion_unit* (type: string) possible values are `days`, `weeks`, `months`, `years` - * *deletion_value* (type: string or number) depending on `deletion_unit` possible values are `days max 31`, `weeks max 52`, `months max 12`, `years no max` - * *deletion_time* (type: string) with format `HH:MM e.g. 23:30` -* *authentication* (type: object) - ``` - "authentication" : { - "session_max_idle": 1800 - } - ``` - * *session_max_idle* (type: number) maximum session idle (in seconds) diff --git a/components/ILIAS/setup_/setup_.php b/components/ILIAS/setup_/setup_.php deleted file mode 100644 index 5d4d38b51a31..000000000000 --- a/components/ILIAS/setup_/setup_.php +++ /dev/null @@ -1,37 +0,0 @@ -exclude(array( - __DIR__ . '/../../components/ILIAS/setup_/sql', + __DIR__ . '/../../components/ILIAS/Setup/sql', __DIR__ . '/example' )) ->in(array( From a66db21d4de13daed4ff297976d9aec97646fb07 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 7 Jul 2026 13:06:34 +0200 Subject: [PATCH 090/333] [FEATURE] UI: add `Column\Listing` component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new `UI\Component\Table\Column\Listing` component, which accepts an `UI\Component\Listing\Unordered` or `…\Ordered` component. To keep rows consistent a expand/collapse mechanism was put in place, truncating list items at some point if used inside a table column. This makes the `UI\Component\Table\Column\LinkListing` component obsolete, which has been removed during this process. Usages in ILIAS were updated to use the new listing component instead. Co-authored-by: Thibeau Fuhrer --- .../ConsultationHours/BookingTableGUI.php | 6 +- .../Grouping/Table/GroupingHandler.php | 2 +- .../Service/Table/TableAdapterGUI.php | 2 +- .../classes/class.AssignMaterialsTable.php | 2 +- components/ILIAS/UI/UI.php | 12 +++ .../resources/js/Listing/dist/listing.min.js | 15 +++ .../UI/resources/js/Listing/rollup.config.js | 43 +++++++++ .../js/Listing/src/createExpandableList.js | 74 +++++++++++++++ .../UI/resources/js/Listing/src/listing.js | 27 ++++++ .../ILIAS/UI/src/Component/Listing/Inline.php | 4 +- .../UI/src/Component/Listing/Ordered.php | 4 +- .../UI/src/Component/Listing/Unordered.php | 4 +- .../UI/src/Component/Table/Column/Factory.php | 19 +++- .../Column/{LinkListing.php => Listing.php} | 2 +- .../Component/Listing/Inline.php | 2 + .../Listing/ListingRendererFactory.php | 54 +++++++++++ .../Component/Listing/Ordered.php | 2 + .../Component/Listing/Renderer.php | 92 +++++++++---------- .../Listing/TableColumnContextRenderer.php | 80 ++++++++++++++++ .../Component/Listing/Unordered.php | 2 + .../Component/Table/Column/Factory.php | 4 +- .../Column/{LinkListing.php => Listing.php} | 7 +- .../UI/src/Implementation/Render/FSLoader.php | 6 ++ .../Column/{LinkListing => Listing}/base.php | 35 ++++--- .../default/Listing/tpl.ordered.html | 4 +- .../Listing/tpl.table_column_context.html | 10 ++ .../default/Listing/tpl.unordered.html | 4 +- components/ILIAS/UI/tests/Base.php | 10 ++ .../Table/Column/ColumnFactoryTest.php | 4 +- .../Component/Table/Column/ColumnTest.php | 64 +++++-------- components/ILIAS/UI/tests/InitUIFramework.php | 10 ++ .../ILIAS/UI/tests/Renderer/FSLoaderTest.php | 3 + .../Table/_ui-component_table.scss | 2 +- 33 files changed, 477 insertions(+), 134 deletions(-) create mode 100644 components/ILIAS/UI/resources/js/Listing/dist/listing.min.js create mode 100755 components/ILIAS/UI/resources/js/Listing/rollup.config.js create mode 100644 components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js create mode 100755 components/ILIAS/UI/resources/js/Listing/src/listing.js rename components/ILIAS/UI/src/Component/Table/Column/{LinkListing.php => Listing.php} (94%) create mode 100644 components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php rename components/ILIAS/UI/src/Implementation/Component/Table/Column/{LinkListing.php => Listing.php} (81%) rename components/ILIAS/UI/src/examples/Table/Column/{LinkListing => Listing}/base.php (69%) create mode 100644 components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html diff --git a/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php b/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php index 594321c5c83f..ff028eb872d0 100644 --- a/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php +++ b/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php @@ -207,15 +207,15 @@ protected function getColumns(): array 'booking_participant' => $this->ui_factory ->table() ->column() - ->linkListing($this->lng->txt('cal_ch_booking_participants')), + ->listing($this->lng->txt('cal_ch_booking_participants')), 'booking_comment' => $this->ui_factory ->table() ->column() - ->linkListing($this->lng->txt('cal_ch_booking_col_comments')), + ->listing($this->lng->txt('cal_ch_booking_col_comments')), 'booking_location' => $this->ui_factory ->table() ->column() - ->linkListing($this->lng->txt('cal_ch_target_object')) + ->listing($this->lng->txt('cal_ch_target_object')) ]; } diff --git a/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php b/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php index 97984f5fcfe3..7df6dd0e8543 100755 --- a/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php +++ b/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php @@ -95,7 +95,7 @@ protected function buildColumns(): array self::COL_DESCRIPTION => $f->text($this->lng->txt('description'))->withIsSortable(true), self::COL_SOURCE => $f->link($this->lng->txt('groupings_source'))->withIsSortable(true), self::COL_UNIQUE_FIELD => $f->text($this->lng->txt('unambiguousness'))->withIsSortable(true), - self::COL_ASSIGNED_OBJS => $f->linkListing($this->lng->txt('groupings_assigned_obj_' . $type))->withIsSortable(true) + self::COL_ASSIGNED_OBJS => $f->listing($this->lng->txt('groupings_assigned_obj_' . $type))->withIsSortable(true) ]; } diff --git a/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php b/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php index 15829b48e271..14e941d83a63 100755 --- a/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php +++ b/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php @@ -137,7 +137,7 @@ public function linkListingColumn( string $title, bool $sortable = false ): self { - $column = $this->ui->factory()->table()->column()->linkListing($title)->withIsSortable($sortable); + $column = $this->ui->factory()->table()->column()->listing($title)->withIsSortable($sortable); $this->addColumn($key, $column); return $this; } diff --git a/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php b/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php index f13a28acb197..d78033815bad 100755 --- a/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php +++ b/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php @@ -100,7 +100,7 @@ protected function getColumns(): array ->withIsSortable(false), "description" => $this->ui_fac->table()->column()->text($this->lng->txt("description")) ->withIsSortable(false), - "resources" => $this->ui_fac->table()->column()->linkListing($this->lng->txt("skmg_materials")) + "resources" => $this->ui_fac->table()->column()->listing($this->lng->txt("skmg_materials")) ->withIsSortable(false) ]; diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index ca489d6225c0..1fd13c1e888f 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -551,6 +551,16 @@ public function init( $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], ), + new UI\Implementation\Component\Listing\ListingRendererFactory( + $use[UI\Implementation\FactoryInternal::class], + $internal[UI\Implementation\Render\TemplateFactory::class], + $use[Language\Language::class], + $internal[UI\Implementation\Render\JavaScriptBinding::class], + $use[UI\Implementation\Render\ImagePathResolver::class], + $pull[Data\Factory::class], + $use[UI\HelpTextRetriever::class], + $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + ), ) ) ); @@ -593,6 +603,8 @@ public function init( new Component\Resource\ComponentJS($this, "js/Input/Field/input.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/Item/dist/notification.js"); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentJS($this, "js/Listing/dist/listing.min.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/MainControls/dist/mainbar.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => diff --git a/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js b/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js new file mode 100644 index 000000000000..eb2203dc7cb3 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js @@ -0,0 +1,15 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ +!function(t,e){"use strict";t.UI=t.UI||{},t.UI.Listing={createExpandableList:i=>function(t,e){const i=e.parentElement.querySelector(`[aria-controls="${e.id}"]`);if(!i)throw new Error("Could not find button associated with list.");if(!e.hasAttribute("data-max-items"))throw new Error("Could not find max items attribute.");const a=parseInt(e.getAttribute("data-max-items"),10),n=e.querySelectorAll("li");i.addEventListener("click",()=>{!function(t,e,i,a){const n=e.hasAttribute("aria-expanded")&&"true"===e.getAttribute("aria-expanded");i.forEach((t,e)=>{e>a-1&&(n?t.classList.replace("visible","hidden"):t.classList.replace("hidden","visible"))}),n?(e.setAttribute("aria-expanded","false"),e.textContent=t.txt("show_more")):(e.setAttribute("aria-expanded","true"),e.textContent=t.txt("show_less"))}(t,i,n,a)})}({txt:e=>t.Language.txt(e)},e.getElementById(i))}}(il,document); diff --git a/components/ILIAS/UI/resources/js/Listing/rollup.config.js b/components/ILIAS/UI/resources/js/Listing/rollup.config.js new file mode 100755 index 000000000000..bd092843e631 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/rollup.config.js @@ -0,0 +1,43 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ + +import terser from '@rollup/plugin-terser'; +import copyright from '../../../../../../scripts/Copyright-Checker/copyright.js'; +import preserveCopyright from '../../../../../../scripts/Copyright-Checker/preserveCopyright.js'; + +export default { + input: './src/listing.js', + external: [ + 'ilias', + 'document', + ], + output: { + // file: '../../../../../../public/assets/js/listing.min.js', + file: './dist/listing.min.js', + format: 'iife', + banner: copyright, + globals: { + ilias: 'il', + document: 'document', + }, + plugins: [ + terser({ + format: { + comments: preserveCopyright, + }, + }), + ], + }, +}; diff --git a/components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js b/components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js new file mode 100644 index 000000000000..a2008ce771f6 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js @@ -0,0 +1,74 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + * @author Thibeau Fuhrer + */ + +/** + * @param {{ txt: function(string): string }} + * @param {HTMLButtonElement} button + * @param {HTMLLIElement[]} listItems + * @param {number} maxItemCount + */ +function toggleListItems( + language, + button, + listItems, + maxItemCount, +) { + const isExpanded = button.hasAttribute('aria-expanded') + && button.getAttribute('aria-expanded') === 'true'; + + listItems.forEach((item, index) => { + if (index > (maxItemCount - 1)) { + if (isExpanded) { + item.classList.replace('visible', 'hidden'); + } else { + item.classList.replace('hidden', 'visible'); + } + } + }); + if (isExpanded) { + button.setAttribute('aria-expanded', 'false'); + button.textContent = language.txt('show_more'); + } else { + button.setAttribute('aria-expanded', 'true'); + button.textContent = language.txt('show_less'); + } +} + +/** + * @param {{ txt: function(string): string }} + * @param {HTMLUListElement|HTMLOListElement} list + */ +export default function createExpandableList(language, list) { + const button = list.parentElement.querySelector(`[aria-controls="${list.id}"]`); + if (!button) { + throw new Error('Could not find button associated with list.'); + } + if (!list.hasAttribute('data-max-items')) { + throw new Error('Could not find max items attribute.'); + } + const maxItemCount = parseInt(list.getAttribute('data-max-items'), 10); + const listItems = list.querySelectorAll('li'); + + button.addEventListener('click', () => { + toggleListItems( + language, + button, + listItems, + maxItemCount, + ); + }); +} diff --git a/components/ILIAS/UI/resources/js/Listing/src/listing.js b/components/ILIAS/UI/resources/js/Listing/src/listing.js new file mode 100755 index 000000000000..5802d3f6c917 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/src/listing.js @@ -0,0 +1,27 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ + +import il from 'ilias'; +import document from 'document'; +import createExpandableList from './createExpandableList.js'; + +il.UI = il.UI || {}; + +il.UI.Listing = { + createExpandableList: (id) => createExpandableList( + { txt: (key) => il.Language.txt(key) }, + document.getElementById(id), + ), +}; diff --git a/components/ILIAS/UI/src/Component/Listing/Inline.php b/components/ILIAS/UI/src/Component/Listing/Inline.php index a9e05e21d5cc..d38ebcc21786 100644 --- a/components/ILIAS/UI/src/Component/Listing/Inline.php +++ b/components/ILIAS/UI/src/Component/Listing/Inline.php @@ -19,6 +19,8 @@ namespace ILIAS\UI\Component\Listing; -interface Inline extends Listing +use ILIAS\UI\Component\JavaScriptBindable; + +interface Inline extends Listing, JavaScriptBindable { } diff --git a/components/ILIAS/UI/src/Component/Listing/Ordered.php b/components/ILIAS/UI/src/Component/Listing/Ordered.php index b17dc542cb46..004147c43641 100755 --- a/components/ILIAS/UI/src/Component/Listing/Ordered.php +++ b/components/ILIAS/UI/src/Component/Listing/Ordered.php @@ -20,10 +20,12 @@ namespace ILIAS\UI\Component\Listing; +use ILIAS\UI\Component\JavaScriptBindable; + /** * Interface Ordered * @package ILIAS\UI\Component\Listing */ -interface Ordered extends Listing +interface Ordered extends Listing, JavaScriptBindable { } diff --git a/components/ILIAS/UI/src/Component/Listing/Unordered.php b/components/ILIAS/UI/src/Component/Listing/Unordered.php index c58603a59bf4..49e3bb9752d9 100755 --- a/components/ILIAS/UI/src/Component/Listing/Unordered.php +++ b/components/ILIAS/UI/src/Component/Listing/Unordered.php @@ -20,10 +20,12 @@ namespace ILIAS\UI\Component\Listing; +use ILIAS\UI\Component\JavaScriptBindable; + /** * Interface Unordered * @package ILIAS\UI\Component\Listing */ -interface Unordered extends Listing +interface Unordered extends Listing, JavaScriptBindable { } diff --git a/components/ILIAS/UI/src/Component/Table/Column/Factory.php b/components/ILIAS/UI/src/Component/Table/Column/Factory.php index d97c6b92ab7f..61f211de251b 100755 --- a/components/ILIAS/UI/src/Component/Table/Column/Factory.php +++ b/components/ILIAS/UI/src/Component/Table/Column/Factory.php @@ -136,12 +136,21 @@ public function link(string $title): Link; * --- * description: * purpose: > - * The LinkListing Column features an Ordered or Unordered Listing of Standard Links. - * - * --- - * @return \ILIAS\UI\Component\Table\Column\LinkListing + * The Listing Column is used for representing lists inside a table. To + * account for large lists, the Listing Column caps the amount of items + * which are initially visible and provides a toggle to show/hide them. + * composition: > + * The Listing Column either consists of an Unordered or Ordered Listing. + * If the Listing contains many items, some of them will be initially + * hidden and a Shy Button appears below the Listing. + * effect: > + * Clicking the Shy Button will toggle the visibility of initially hidden + * items of the Listing. + * --- + * @param string $title + * @return \ILIAS\UI\Component\Table\Column\Listing */ - public function linkListing(string $title): LinkListing; + public function listing(string $title): Listing; /** * --- diff --git a/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Component/Table/Column/Listing.php similarity index 94% rename from components/ILIAS/UI/src/Component/Table/Column/LinkListing.php rename to components/ILIAS/UI/src/Component/Table/Column/Listing.php index b02c08c1cc01..fa80d9810da6 100755 --- a/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php +++ b/components/ILIAS/UI/src/Component/Table/Column/Listing.php @@ -20,6 +20,6 @@ namespace ILIAS\UI\Component\Table\Column; -interface LinkListing extends Column +interface Listing extends Column { } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php index bf6b3ebc84fc..87ac67c4aba4 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Inline.php @@ -20,7 +20,9 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; class Inline extends Listing implements C\Listing\Inline { + use JavaScriptBindable; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php new file mode 100644 index 000000000000..8ffbb8a2c766 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php @@ -0,0 +1,54 @@ + + */ +class ListingRendererFactory extends DefaultRendererFactory +{ + /** @var string[] cannonical names of table components */ + protected const array TABLE_COLUMN_CONTEXTS = [ + 'OrderingRowTable', + 'DataRowTable', + ]; + + public function getRendererInContext(Component $component, array $contexts): ComponentRenderer + { + if (!empty(array_intersect(self::TABLE_COLUMN_CONTEXTS, $contexts))) { + return new TableColumnContextRenderer( + $this->ui_factory, + $this->tpl_factory, + $this->lng, + $this->js_binding, + $this->image_path_resolver, + $this->data_factory, + $this->help_text_retriever, + $this->upload_limit_resolver, + ); + } + + return parent::getRendererInContext($component, $contexts); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php index 6bbda158f88a..6d1f2bc3c6e7 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php @@ -21,6 +21,7 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; /** * Class Listing @@ -28,4 +29,5 @@ */ class Ordered extends Listing implements C\Listing\Ordered { + use JavaScriptBindable; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index 095a93b48c8b..da00b21b1f3c 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php @@ -21,9 +21,9 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; -use ILIAS\UI\Implementation\Render\Template; use ILIAS\UI\Renderer as RendererInterface; -use ILIAS\UI\Component\Component; +use ILIAS\UI\Component; +use ILIAS\UI\Implementation\Render\Template; /** * Class Renderer @@ -37,28 +37,27 @@ class Renderer extends AbstractComponentRenderer /** * @inheritdocs */ - public function render(Component $component, RendererInterface $default_renderer): string + public function render(Component\Component $component, RendererInterface $default_renderer): string { if ($component instanceof Descriptive) { - return $this->renderDescriptive($component, $default_renderer); + return $this->renderDescriptiveList($component, $default_renderer); } + if ($component instanceof Property) { - return $this->renderProperty($component, $default_renderer); - } - if ($component instanceof Ordered) { - return $this->renderOrdered($component, $default_renderer); - } - if ($component instanceof Unordered) { - return $this->renderUnordered($component, $default_renderer); + return $this->renderPropertyList($component, $default_renderer); } - if ($component instanceof Inline) { - return $this->renderInline($component, $default_renderer); + + if ($component instanceof Unordered || + $component instanceof Ordered || + $component instanceof Inline + ) { + return $this->renderList($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function renderDescriptive( + protected function renderDescriptiveList( Descriptive $component, RendererInterface $default_renderer ): string { @@ -81,51 +80,36 @@ protected function renderDescriptive( return $tpl->get(); } - protected function renderOrdered(Ordered $component, RendererInterface $default_renderer): string - { - $tpl = $this->getTemplate("tpl.ordered.html", true, true); - - $tpl = $this->fillItems($tpl, $component, $default_renderer); - - return $tpl->get(); - } - - protected function renderUnordered(Unordered $component, RendererInterface $default_renderer): string - { - $tpl = $this->getTemplate("tpl.unordered.html", true, true); - - $tpl = $this->fillItems($tpl, $component, $default_renderer); - - return $tpl->get(); - } - - protected function renderInline(Inline $component, RendererInterface $default_renderer): string + protected function renderList(Unordered|Ordered|Inline $component, RendererInterface $default_renderer): string { - $tpl = $this->getTemplate("tpl.inline.html", true, true); - - $tpl = $this->fillItems($tpl, $component, $default_renderer); - - return $tpl->get(); - } + if ($component instanceof Unordered) { + $tpl_name = "tpl.unordered.html"; + } elseif ($component instanceof Ordered) { + $tpl_name = "tpl.ordered.html"; + } elseif ($component instanceof Inline) { + $tpl_name = "tpl.inline.html"; + } else { + $this->cannotHandleComponent($component); + } - protected function fillItems(Template $tpl, Listing $component, RendererInterface $default_renderer): Template - { - $items = $component->getItems(); + $tpl = $this->getTemplate($tpl_name, true, true); - foreach ($items as $item) { + foreach ($component->getItems() as $item) { $tpl->setCurrentBlock("item"); - if ($item instanceof Component) { - $tpl->setVariable("ITEM", $default_renderer->render($item)); - } else { + if (is_string($item)) { $tpl->setVariable("ITEM", $item); + } else { + $tpl->setVariable("ITEM", $default_renderer->render($item)); } $tpl->parseCurrentBlock(); } - return $tpl; + $this->bindAndApplyJavaScript($component, $tpl); + + return $tpl->get(); } - protected function renderProperty( + protected function renderPropertyList( Property $component, RendererInterface $default_renderer ): string { @@ -134,7 +118,7 @@ protected function renderProperty( foreach ($component->getItems() as [$label, $value, $show_label]) { $tpl->setCurrentBlock("property"); if ($show_label) { - if ($label instanceof Component) { + if ($label instanceof Component\Component) { $tpl->setVariable('LABEL', $default_renderer->render($label)); } else { $tpl->setVariable('LABEL', $this->convertSpecialCharacters($label)); @@ -148,11 +132,19 @@ protected function renderProperty( $tpl->parseCurrentBlock(); } elseif (is_string($value)) { $tpl->setVariable("SHORT_VALUE", $this->convertSpecialCharacters($value)); - } elseif ($value instanceof Component) { + } elseif ($value instanceof Component\Component) { $tpl->setVariable("SHORT_VALUE", $default_renderer->render($value)); } $tpl->parseCurrentBlock(); } return $tpl->get(); } + + protected function bindAndApplyJavaScript(Component\JavaScriptBindable $component, Template $template): void + { + $id = $this->bindJavaScript($component); + if (null !== $id) { + $template->setVariable('ID', $id); + } + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php new file mode 100644 index 000000000000..9b486c473636 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php @@ -0,0 +1,80 @@ + + */ +class TableColumnContextRenderer extends Renderer +{ + protected const int LIST_DISPLAY_LIMIT = 3; + + public function registerResources(ResourceRegistry $registry): void + { + $registry->register('assets/js/listing.min.js'); + } + + protected function renderList(Ordered|Unordered|Inline $component, RendererInterface $default_renderer): string + { + if ($component instanceof Inline || self::LIST_DISPLAY_LIMIT >= count($component->getItems())) { + return parent::renderList($component, $default_renderer); + } + + $template = $this->getTemplate('tpl.table_column_context.html', true, true); + $template->setVariable('DISPLAY_LIMIT', self::LIST_DISPLAY_LIMIT); + $template->setVariable('SHOW_MORE_LABEL', $this->txt('show_more')); + + if ($component instanceof Ordered) { + $template->setVariable('LIST_TYPE', 'ol'); + } else { + $template->setVariable('LIST_TYPE', 'ul'); + } + + // array_values() ensures we can use $index for count + foreach (array_values($component->getItems()) as $index => $item) { + $template->setCurrentBlock("item"); + if (is_string($item)) { + $template->setVariable("ITEM", $item); + } else { + $template->setVariable("ITEM", $default_renderer->render($item)); + } + if (self::LIST_DISPLAY_LIMIT > $index) { + $template->setVariable('VISIBILITY', 'visible'); + } else { + $template->setVariable('VISIBILITY', 'hidden'); + } + $template->parseCurrentBlock(); + } + + $enriched_component = $component->withAdditionalOnLoadCode( + static fn($id) => "il.UI.Listing.createExpandableList('$id');", + ); + + $this->bindAndApplyJavaScript($enriched_component, $template); + + $this->toJS('show_more'); + $this->toJS('show_less'); + + return $template->get(); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php index d983880eebf6..099c75b7c88d 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php @@ -21,6 +21,7 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; /** * Class Listing @@ -28,4 +29,5 @@ */ class Unordered extends Listing implements C\Listing\Unordered { + use JavaScriptBindable; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php index 7d8aaeb666e1..c46f15baf895 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php @@ -81,9 +81,9 @@ public function link(string $title): Link return new Link($this->lng, $title); } - public function linkListing(string $title): LinkListing + public function listing(string $title): Listing { - return new LinkListing($this->lng, $title); + return new Listing($this->lng, $title); } public function breadcrumb(string $title): I\Breadcrumb diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php similarity index 81% rename from components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php rename to components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php index 3d672128a001..64e42b060507 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php @@ -21,19 +21,16 @@ namespace ILIAS\UI\Implementation\Component\Table\Column; use ILIAS\UI\Component\Table\Column as C; -use ILIAS\UI\Component\Link\Standard; use ILIAS\UI\Component\Listing\Ordered; use ILIAS\UI\Component\Listing\Unordered; use ILIAS\UI\Component\Component; -class LinkListing extends Column implements C\LinkListing +class Listing extends Column implements C\Listing { public function format($value): string|Component { $listing = $this->toArray($value); - $this->checkArgListElements("value", $listing, [Ordered::class, Unordered::class]); - $listing_items = $value->getItems(); - $this->checkArgListElements("list items", $listing_items, Standard::class); + $this->checkArgListElements('value', $listing, [Ordered::class, Unordered::class]); return $value; } diff --git a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php index e7e5ff088705..851679081989 100755 --- a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php +++ b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php @@ -28,6 +28,7 @@ use ILIAS\UI\Implementation\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; +use ILIAS\UI\Implementation\Component\Listing\Listing; /** * Loads renderers for components from the file system. @@ -51,6 +52,7 @@ public function __construct( private RendererFactory $message_box_renderer_factory, private RendererFactory $form_renderer_factory, private RendererFactory $menu_renderer_factory, + private RendererFactory $listing_renderer_factory, ) { } @@ -84,6 +86,10 @@ public function getRendererFactoryFor(Component $component): RendererFactory if ($component instanceof Button) { return $this->button_renderer_factory; } + if ($component instanceof Listing) { + return $this->listing_renderer_factory; + } + return $this->default_renderer_factory; } } diff --git a/components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php similarity index 69% rename from components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php rename to components/ILIAS/UI/src/examples/Table/Column/Listing/base.php index 5fd76c9f9538..7dafa2327fa1 100755 --- a/components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php +++ b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\UI\examples\Table\Column\LinkListing; +namespace ILIAS\UI\examples\Table\Column\Listing; use ILIAS\UI\Component\Table as I; use ILIAS\Data\Range; @@ -32,28 +32,39 @@ */ function base(): string { + /** @var \ILIAS\DI\Container $DIC */ global $DIC; $f = $DIC->ui()->factory(); $r = $DIC->ui()->renderer(); $columns = [ - 'l1' => $f->table()->column()->linkListing("a link list column") + 'l1' => $f->table()->column()->listing('A list column') ]; - $some_link = $f->link()->standard('ILIAS Homepage', 'http://www.ilias.de'); - $some_linklisting = $f->listing()->unordered([$some_link, $some_link, $some_link]); - - $dummy_records = [ - ['l1' => $some_linklisting], - ['l1' => $some_linklisting] + $records = [ + [ + 'l1' => $f->listing()->unordered([ + 'Apples', + 'Oranges', + 'Bananas', + 'Pears' + ]) + ], + [ + 'l1' => $f->listing()->unordered([ + 'Bun', + 'Croissant', + 'Pumpernickel' + ]) + ] ]; - $data_retrieval = new class ($dummy_records) implements I\DataRetrieval { + $data_retrieval = new class ($records) implements I\DataRetrieval { protected array $records; - public function __construct(array $dummy_records) + public function __construct(array $records) { - $this->records = $dummy_records; + $this->records = $records; } public function getRows( @@ -80,7 +91,7 @@ public function getTotalRowCount( } }; - $table = $f->table()->data($data_retrieval, 'Link List Columns', $columns) + $table = $f->table()->data($data_retrieval, 'List Columns', $columns) ->withRequest($DIC->http()->request()); return $r->render($table); } diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html index c1a592268983..f0797ccc861e 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html @@ -1,5 +1,5 @@ -
      +id="{ID}">
    1. {ITEM}
    2. -
    \ No newline at end of file + diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html new file mode 100644 index 000000000000..0cbb547f6c7e --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html @@ -0,0 +1,10 @@ +
    + <{LIST_TYPE} id="{ID}" data-max-items="{DISPLAY_LIMIT}"> + +
  • {ITEM}
  • + + + +
    diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html index 594abfe9a720..3239b2ed9924 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html @@ -1,5 +1,5 @@ -
      +id="{ID}">
    • {ITEM}
    • -
    \ No newline at end of file + diff --git a/components/ILIAS/UI/tests/Base.php b/components/ILIAS/UI/tests/Base.php index a82b6f9d7ad7..0412bff2ac42 100755 --- a/components/ILIAS/UI/tests/Base.php +++ b/components/ILIAS/UI/tests/Base.php @@ -479,6 +479,16 @@ public function getDefaultRenderer( $data_factory, $help_text_retriever, $this->getUploadLimitResolver(), + ), + new I\Listing\ListingRendererFactory( + $ui_factory, + $tpl_factory, + $lng, + $js_binding, + $image_path_resolver, + $data_factory, + $help_text_retriever, + $this->getUploadLimitResolver(), ) ) ) diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php index 8d85257fbf18..ae2f7ebc5aba 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php @@ -35,8 +35,8 @@ class ColumnFactoryTest extends AbstractFactoryTestCase "statusIcon" => ["context" => false, "rules" => false], "timeSpan" => ["context" => false, "rules" => false], "link" => ["context" => false, "rules" => false], - "linkListing" => ["context" => false, "rules" => false], "breadcrumb" => ["context" => false, "rules" => false], + "listing" => ["context" => false, "rules" => false] ]; public static string $factory_title = 'ILIAS\\UI\\Component\\Table\\Column\\Factory'; @@ -66,7 +66,7 @@ public static function getColumnTypeProvider(): array [static fn($f) => [Column\StatusIcon::class, $f->statusIcon("")]], [static fn($f) => [Column\Link::class, $f->link("")]], [static fn($f) => [Column\EMail::class, $f->eMail("")]], - [static fn($f) => [Column\LinkListing::class, $f->linkListing("")]] + [static fn($f) => [Column\Listing::class, $f->listing("")]] ]; } diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php index 477cc2021d6a..648665e51adb 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php @@ -143,45 +143,41 @@ public function __construct() }; return [ [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Unordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))]), + 'column' => new Column\Link($lng, ''), + 'value' => new Link\Standard('label', '#'), 'ok' => true ], [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Unordered(['string', 'string']), + 'column' => new Column\Link($lng, ''), + 'value' => 'some string', 'ok' => false ], [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Ordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))]), + 'column' => new Column\StatusIcon($lng, ''), + 'value' => new StandardIcon('', '', 'small', false), 'ok' => true ], [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => 123, + 'column' => new Column\StatusIcon($lng, ''), + 'value' => 'some string', 'ok' => false ], [ - 'column' => new Column\Link($lng, ''), - 'value' => new Link\Standard('label', '#'), + 'column' => new Column\Listing($lng, ''), + 'value' => (new Listing\Ordered(['1', '2', '3'])), 'ok' => true ], [ - 'column' => new Column\Link($lng, ''), - 'value' => 'some string', - 'ok' => false - ], - [ - 'column' => new Column\StatusIcon($lng, ''), - 'value' => new StandardIcon('', '', 'small', false), + 'column' => new Column\Listing($lng, ''), + 'value' => (new Listing\Unordered(['1', '2', '3'])), 'ok' => true ], [ - 'column' => new Column\StatusIcon($lng, ''), - 'value' => 'some string', + 'column' => new Column\Listing($lng, ''), + 'value' => 123, 'ok' => false ], + ]; } @@ -191,42 +187,24 @@ public function testDataTableColumnAllowedFormats( mixed $value, bool $ok ): void { - if(! $ok) { + if (! $ok) { $this->expectException(\InvalidArgumentException::class); } $this->assertEquals($value, $column->format($value)); } - public function testDataTableColumnLinkListingFormat(): void + public function testDataTableColumnListingFormat(): void { - $col = new Column\LinkListing($this->lng, 'col'); + $col = new Column\Listing($this->lng, 'col'); $link = new Link\Standard('label', '#'); - $linklisting = new Listing\Unordered([$link, $link, $link]); - $this->assertEquals($linklisting, $col->format($linklisting)); - } - - public function testDataTableColumnLinkListingFormatAcceptsOnlyLinkListings(): void - { - $this->expectException(\InvalidArgumentException::class); - $col = new Column\LinkListing($this->lng, 'col'); - $linklisting_invalid = new Link\Standard('label', '#'); - $this->assertEquals($linklisting_invalid, $col->format($linklisting_invalid)); - } - - public function testDataTableColumnLinkListingItemsFormatAcceptsOnlyLinks(): void - { - $this->expectException(\InvalidArgumentException::class); - $col = new Column\LinkListing($this->lng, 'col'); - $link = 'some string'; - $linklisting_invalid = new Listing\Unordered([$link, $link, $link]); - $this->assertEquals($linklisting_invalid, $col->format($linklisting_invalid)); + $listing = (new Listing\Unordered([$link, $link, $link])); + $this->assertEquals($listing, $col->format($listing)); } public function testDataTableColumnCustomOrderingLabels(): void { - $col = (new Column\LinkListing($this->lng, 'col')) - ->withIsSortable(true) + $col = (new Column\Listing($this->lng, 'col')) ->withOrderingLabels( 'custom label ASC', 'custom label DESC', diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 0ba2a1c745e5..b5425805726e 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -381,6 +381,16 @@ public function getRefreshIntervalInMs(): int $c["help.text_retriever"], $c["ui.upload_limit_resolver"] ), + new ILIAS\UI\Implementation\Component\Listing\ListingRendererFactory( + $c["ui.factory"], + $c["ui.template_factory"], + $c["lng"], + $c["ui.javascript_binding"], + $c["ui.pathresolver"], + $c["ui.data_factory"], + $c["help.text_retriever"], + $c["ui.upload_limit_resolver"], + ), ) ) ); diff --git a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php index 689fbfd80d76..634360242a54 100755 --- a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php +++ b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php @@ -41,6 +41,7 @@ class FSLoaderTest extends TestCase protected RendererFactory & MockObject $message_box_renderer_factory; protected RendererFactory & MockObject $form_renderer_factory; protected RendererFactory & MockObject $menu_renderer_factory; + protected RendererFactory & MockObject $list_renderer_factory; protected FSLoader $fs_loader; @@ -52,6 +53,7 @@ protected function setUp(): void $this->message_box_renderer_factory = $this->createMock(RendererFactory::class); $this->form_renderer_factory = $this->createMock(RendererFactory::class); $this->menu_renderer_factory = $this->createMock(RendererFactory::class); + $this->list_renderer_factory = $this->createMock(RendererFactory::class); $this->fs_loader = new FSLoader( $this->default_renderer_factory, @@ -60,6 +62,7 @@ protected function setUp(): void $this->message_box_renderer_factory, $this->form_renderer_factory, $this->menu_renderer_factory, + $this->list_renderer_factory, ); parent::setUp(); diff --git a/templates/default/070-components/UI-framework/Table/_ui-component_table.scss b/templates/default/070-components/UI-framework/Table/_ui-component_table.scss index 7ffbab3c76d0..40fb23744475 100755 --- a/templates/default/070-components/UI-framework/Table/_ui-component_table.scss +++ b/templates/default/070-components/UI-framework/Table/_ui-component_table.scss @@ -308,7 +308,7 @@ th.c-table-data__cell:after { // Text .c-table-data__cell--link, -.c-table-data__cell--linklisting, +.c-table-data__cell--listing, .c-table-data__cell--text { .c-table-data__header__resize-wrapper { min-width: 140px; From 6679ec5d426ea93f3f437d78042fa10c8ae74721 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Fri, 17 Jul 2026 16:08:58 +0200 Subject: [PATCH 091/333] [FIX] Style: compile `templates/default/delos.css` --- templates/default/delos.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/default/delos.css b/templates/default/delos.css index b0ff73159ce8..9a0cef832343 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -12623,7 +12623,7 @@ th.c-table-data__cell { } .c-table-data__cell--link .c-table-data__header__resize-wrapper, -.c-table-data__cell--linklisting .c-table-data__header__resize-wrapper, +.c-table-data__cell--listing .c-table-data__header__resize-wrapper, .c-table-data__cell--text .c-table-data__header__resize-wrapper { min-width: 140px; resize: horizontal; From 5442a9b2fa87bd89bb3fe845c721722a3e468ffe Mon Sep 17 00:00:00 2001 From: Simon Lowe <164848325+slowe47@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:18:59 +0200 Subject: [PATCH 092/333] Test: Create PRIVACY.md See PR https://github.com/ILIAS-eLearning/ILIAS/pull/11357 * Create PRIVACY.md * replacing user with account and adding purposes * working through the feedback and revising the entire document * Add Summary and Permissions 'Score anonymously' & 'Test Results' With this commit I have tried to add a summary as table, although this is very difficult because of the complex settings at the Test component. Please have a look at it. In the process of writing the summary I came to the fact, that the two separate permissions 'Score anonymously' & 'Test Results' were not included in this document until now. Therefore I just added two little sentences - from my point of view the permissions only are relevant for the presentation of data in two tabs. Attention: After the bugfix of https://mantis.ilias.de/view.php?id=47244 the 'Edit Settings' permission is not longer able to access test results on it's own and the document probably has to be updated at a few sections. * Adresses the language-related comments --- components/ILIAS/Test/PRIVACY.md | 248 +++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 components/ILIAS/Test/PRIVACY.md diff --git a/components/ILIAS/Test/PRIVACY.md b/components/ILIAS/Test/PRIVACY.md new file mode 100644 index 000000000000..e4c1616016f5 --- /dev/null +++ b/components/ILIAS/Test/PRIVACY.md @@ -0,0 +1,248 @@ +# Test Privacy + +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** + +## General information + +The Test component is used to create, manage and run tests, which includes creating and managing questions. There are many use cases for the Test component, which is why it has so many settings. The most common scenarios are likely to be self-assessment tests and examinations. + +To ensure that this functionality meets the expectations of all stakeholders, a great deal of data needs to be stored and displayed within this component. + +The Test component and the TestQuestionPool component are still tied together in most intricate ways. The primary component of concern in regards to privacy related evaluations is the Test. As the lines between these components are blurred, it is advised to never look at only one of the components but always at both. + +## Integrated components + +The Test component employs the following components, please consult the respective privacy.mds + + - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) + - [Certificate](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Certificate/PRIVACY.md): Is used for certificate creation and uses test-specific placeholders [RESULT_PASSED], [RESULT_PERCENT], [MAX_POINTS], [RESULT_MARK_SHORT], [RESULT_MARK_LONG] + - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md): Is used for content creation/presentation within questions, Introduction and Concluding Remarks and is able to store, present and delete personal data. + - [CSV](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/CSV) + - [Excel](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Excel) + - [Export](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/Export) + - [InfoScreen](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/InfoScreen/PRIVACY.md) + - [KioskMode](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/KioskMode/PRIVACY.md) + - [LTI Provider](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/LTIProvider/PRIVACY.md): Is used to provide the Test via LTI and is able to present personal data. + - [Metadata](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/MetaData/Privacy.md): Stores the full name of the author of the test. + - [Notes](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Notes/Privacy.md) + - [Object](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/ILIASObject) + - [Skill (Competence) Service](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Skill/PRIVACY.md) + - [Taxonomy](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Taxonomy/PRIVACY.md) + - [TestQuestionPool](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/TestQuestionPool/PRIVACY.md) + - [Tracking](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/Tracking) + - [User](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/User): Provides information about the account being used using the Test component in order to store those. + +## Configuration + +### Administration > Repository and Objects > Test and Assessment + +At the Administration node for Test and Assessment accounts having 'Edit Settings' permissions are able to configure some functionality, which has an impact on personal data handling. + +At Administration > Repository and Objects > Test and Assessment > Settings it is possible to select which 'Unique user criteria' is used in test imports/exports. The selected type of personal data will be included in export type 'XML incl. Participants Results' of a test for each account. Options are: usr_id, login, email, matriculation, ext_account. This personal data is required to match accounts results when importing an export file at the same or another platform. + +At Administration > Repository and Objects > Test and Assessment > Log Data accounts with 'Edit Settings' permission can activate the History-tab via the checkbox 'Activate Test and Assessment Logging'. Additionally the setting 'Log IP' allows for logging the IP-adress of participants along with the interactions as well as specific settings of said test during the interaction. +The purpose of both options is to store important events and information for configuring and performing tests. This gives the possibility to check those information in case of issues or concerns after performing tests. + +### Test > Settings > General + +At the subtab Test > Settings > General for accounts with 'Edit Settings' permission it is possible to choose one of the following options for the field 'Privacy': +- Results with names (pre-selected) +- Results without names / anonymous test + +If the second option is selected, no personal data of test attempts is presented at the test. If the user is not logged in while performing the test attempt, additionally no personal data is stored. Please have a look at the detailled information at the following sections. + +In addition the 'Exam View' and it's sub-option 'Show Name of Participant' can be activated by accounts with 'Edit Settings' permission, which has an impact on the presented personal data. + +### Test > Settings > Scoring and Results + +At the subtab Test > Settings > Scoring and Results accounts having 'Edit Settings' permission are able to specify whether accounts have access to their own test results (and therefore to their own personal data). When activating the access to the accounts own results, it is possible to dedicated activate the presented data: + +- ‘Passed’ / ‘Failed’ Status +- Grade +- Detailed Results (these contain all given answers and points scored) + +In addition accounts with 'Edit Settings' permission are able to activate the 'Rankings' functionality, which potentially presents personal data to all participating accounts. There are several sub-settings for 'Rankings', which specify the presented personal data: + +- Mode: Own Position, Top Ranks, Own Position and Top Ranks +- Number of Top Ranks +- Without Names: prevents the presentation of accounts names +- Date / Time: additional column in the rankings table showing when the test was finished +- Point Score +- Percentage Score +- Time Spent: additional column in the ranking table showing the time taken to complete the test + +## Data being stored + +### Test > Test - data being stored while performing a test + +While an account performs a test, the following data is stored. After finishing the test, this data is presented at various other tabs (see [Data being presented](#Data being presented)). This is needed in order to provide the functionality of the test component. All listed data is at least linked to the 'User ID'. + +- User ID +- Client IP +- Starting time stamp of the test attempt +- Last access time stamp of the test attempt +- Duration of the test attempt +- Answer content and timestamp of answer content submission +- Status of questions (Not answered/Answered) +- Scoring for questions (achieved points) ? +- Mark and status of the test attempt ? +- Log Entry Type and Interaction Type + +### Test > Settings + +In general the change of settings at the tab Test > Settings is logged. Therefore the 'User ID', the 'Log Entry Type' and 'Interaction Type' is stored. + +At the creation process of a template at Test > Settings > Personal Test Settings Templates the field 'Author' is prefilled with the full name of the account, which is creating the template. If this value is not changed, the name of the account is stored. In addition the 'Creation Date' of the template and the User-ID is stored. Those information are needed in order to present the origin of a Personal Test Settings Template. + +### Test > Questions + +In genereal the creation, change and deletion of questions at the tab Test > Questions is stored as log entry. Therefore the 'User ID', the 'Log Entry Type' and 'Interaction Type' is stored. + +At the creation and editing process of questions the field 'Author' is prefilled with the full name of the account, which is creating the question. If this value is not changed, the name of the account is stored. If the value is changed and personal data is entered, it will be stored. By storing (and presenting) the value for 'Author' it is possible to contact the account, if there are problems with the question or the configuration of it. In addition it supports the collaborative development of questions. + +Owners of questions (accounts which have created a question) are stored in the Test as references to the ‘User ID’. This data is required to manage detailed access and permissions for the usage and editing of the question. + +### Test > Participants + +An account having ‘Edit Settings’ permission is able to assign accounts as participants of a test at the tab Test > Participants. In addition, accounts which perform a test are added as participants automatically. For all accounts that are assigned as participants, the ‘User ID’ is stored as a link to the test. If a ‘Client IP Range’ is set for a participant, the entered value is stored, linked to the ‘User ID’. Those pieces of information are needed in order to manage access to the test. + +For some actions, which are offered at this tab, log entries are stored. Therefore the 'User ID' (of the account with 'Edit Settings' permission), the 'Log Entry Type' and 'Interaction Type' is stored. + +### Test > Scoring + +If the Scoring of a test attempt is changed at the tab Test > Scoring, this event is logged. Therefore the 'User ID' (of the account with 'Edit Settings' permission), the 'Log Entry Type' and 'Interaction Type' is stored. This ensures the traceability of test results. + +## Data being presented + +### Test > Test - data being presented while performing a test + +While performing a test, the 'Name' of the participant himself is shown, if the 'Exam View' and it's sub-option 'Show Name of Participant' is activated. When a test is performed in person, the presentation of this information can be used for validating the logged in account. + +### Test > Settings > Personal Test Settings Templates + +The values of the field 'Author' for all Personal Test Settings Templates at the subtab Test > Settings > Personal Test Settings Templates are presented, which may contain personal data. The 'Creation Date' of the template is also presented, which is directly linked to the value for 'Author'. + +### Test > Questions + +At the overview of the questions at the tab Test > Questions, the values of the field 'Author' for all questions are shown, which may contain personal data. + +When using the 'Statistics' action from a question, the 'Author' of other tests is displayed at the table 'This question is used in the following tests', if the question is used in other tests, too. This information originates from the metadata component (see above). + +When using the 'Print Answers' action from a question, all answers to a question with the 'Name' of the participating accounts are presented here. If the test is set to anonymous (see above), this data is not presented. + +### Test > Participants + +The table ‘Participants’ at the tab Test > Participants shows the following personal data linked to the test attempt. This data is also shown if the action ‘Show Results’ is used. The purpose of the presentation is to give accounts with ‘Edit Settings’ or ‘Test Results’ permission an overview of all test attempts and the related data. + +- Name (originates from the user component) +- Login (originates from the user component) +- Matriculation Number (originates from the user component) +- Starting time stamp +- Duration +- Number of Attempts Made +- Status of the test attempt +- Scored points +- Number of questions answered +- Percentage Score +- Passed-status +- Grade +- Scoring completed- +- Last access time stamp of the test attempt + +If the test is set to anonymous (see above), 'Name', 'Login' and 'Matriculation Number' are not presented. + +### Test > My Results + +At the subtabs Test > My Results > Test Results and Test > My Results > Printable List of Answers accounts are able to access their own test results. This data contains their ‘Name’ and ‘Matriculation Number’. If the test is set to anonymous (see above), the ‘Name’ and ‘Matriculation Number’ are not presented. + +### Test > Scoring + +The tab Test > Scoring shows the following personal data linked to the test attempt for accounts with ‘Edit Settings’ permission. This is needed in order to review and possibly change the scoring of test attempts. If the test is set to anonymous (see above), the ‘Name’ and ‘Login’ are replaced by the ‘Test ID’. + +Accounts with 'Score anonymously' permission are able to access the tab Test > Scoring, but are not able to see 'Name' and 'Login'. + +- Name +- Login +- Test ID +- Number of the Scored Test Attempt +- Scored points for all questions +- Scoring completed + +### Test > History + +At the tab Test > History log entries are shown, which originate from changes to the test settings and questions or from participation in the test. The purpose of the tab ‘History’ is mainly to check events in case of issues or concerns after performing tests. The following personal data is presented: + +- Date and Time of the event +- Name and Login of Author or Participant +- Client IP (of participants) +- Log Entry Type +- Interaction Type + +Some examples for Interaction Types are 'Test Run Started', 'Question Shown', 'Answer Submitted' and 'Test Run Finished' for participating accounts and 'Main Settings Modified', 'Run of Participant Closed', 'Grading Reset' and 'Participant Data Removed' for accounts with 'Edit Settings' permission. + +If the test is set to anonymous, no entries are shown for the participation of the test. + +### Administration > Repository and Objects > Test and Assessment > Log Data > Log Data Output + +Accounts with the ‘Edit Settings’ permission for the administration node Administration > Repository and Objects > Test and Assessment > Log Data > Log Data Output are able to list the same personal data for all tests on the platform as listed in the section ‘History’ of any test. + +## Data being deleted + +In general, only accounts with 'Edit Settings' permission are able to delete data. Exceptions are explicitly listed. + +At the tab Test > Questions it is possible to delete questions, and thereby the personal data in the field ‘Author’ is deleted. + +At the tab Test > Participants the test results of participants and all linked personal data can be deleted. Additionally, the assignment of accounts as participants can be cancelled, which deletes all linked personal data. + +Accounts with the ‘Edit Settings’ permission for the tab Administration > Repository and Objects > Test and Assessment > Log Data > Log Data Output are able to delete any log entries and all linked personal data from all tests on the platform. + +Accounts with ‘Read’ permission are able to delete their own answers, which are linked to their ‘User ID’, while performing a test at the tab Test > Test. The answers are not linked to a ‘User ID’ if the test is set to anonymous and is performed without being logged in. + +If the option 'Allow Deletion of Non-Scoring Attempts' at Settings > Scoring and Results > Access to Test Results is activated, accounts with 'Read' permission are able to delete their own non-scoring test attempts and all linked personal data at the tab Test > My Results. + +## Data being exported + +In general, only accounts with the 'Edit Settings' permission are able to export data. Exceptions are explicitly listed. + +When exporting a Personal Test Settings Template at Test > Settings > Personal Test Settings Templates, the fields 'Author', 'Creation Date' and "User-ID" are exported. This secures being able to identify the origin of a Personal Test Settings Template after importing it to an account. + +At the tab Test > Participants the export files 'Scored Test Attempt', 'All test attempts' and 'as Certificate (PDF)' are available. The purpose of the different export files is to extract the dedicated test results of one or more accounts. This can, for example, be used for discussing the results with the participant. Those export files contain the following personal data of the participants, which all originate from the user component (see above): + +- Name +- Login +- E-Mail +- Matriculation Number +- Salutation +- Street +- City, State +- Zip Code / Post Code +- Country +- Institution +- Department + +At the tab Test > History the 'Export Legacy Log Data' is available, which can be used for historical purposes, and any data shown at the table can be exported (see above for details). + +At the subtabs Test > My Results > Test Results and Test > My Results > Printable List of Answers accounts with 'Read' permission are able to download their own data via the button 'Print'. This data contains their ‘Name’ and ‘Matriculation Number’. If the test is set to anonymous (see above), the ‘Name’ and ‘Matriculation Number’ are not included. + +### Test > Export + +There are three export files at the tab Test > Export which contain personal data (see below). If export files are created, they contain the personal data that is available at the time of their creation. If personal data is deleted after the creation of an export file (which contains such data), the export file must also be deleted. + +The ‘Archive file’ contains all personal data which is being stored and presented in the test (see above). Its purpose is to have this data easily accessible outside of ILIAS, e.g. for long-term archiving of the data. + +The 'XML' export contains the personal data 'Author' of the questions and of the test itself within the metadata (see above). Its purpose is to be imported into ILIAS again, although the contained personal data is easily accessible. + +The ‘XML export incl. Participant Results’ contains all personal data which is being stored and presented in the test (see above). The data in the ‘History’ tab is an exception; it is not included. Its purpose is to be imported into ILIAS again, although the contained personal data is easily accessible. This export file can be used to, e.g., provide the test results to the participants at another ILIAS installation. + +### Administration > Repository and Objects > Test and Assessment > Log Data > Log Data Output + +Accounts with the ‘Edit Settings’ permission for the administration node Administration > Repository and Objects > Test and Assessment > Log Data > Log Data Output are able to export any log entries and all linked personal data from all tests on the platform. + +## Summary + +| Data | Stored in DB | Presented to accounts with 'Read' Permission | Presented to accounts with 'Edit Settings' Permission | Presented to accounts with 'Test Results' Permission | Presented to accounts with 'Score anonymously' Permission | Exported | Deleted with removing test attempt | Deleted with removing test object | special settings | +|------|---------------|---------------------------------------------|------------------------------------------------------|----------------------------------------------------------------|------------------------------------------------------|------------------------------------------------------|------------------------------------|--------------------------------------|-------------------| +| test attempt data (e.g. account, timestamps, scoring) | reference to by ID | if activated | yes | yes | yes, but anonymised| yes | yes | yes | anonymous test without account presentation, gamification with presentation to all accounts | +| question data (e.g. author, statistics) | yes & reference to by ID | no | yes | no | no | yes | no | if question only was used at the deleted test object | value for author can be changed manually | +| assignment of account as 'Participant' | reference to by ID | if setting 'Select Participants Manually' is used | yes | yes | no | yes | no, but with deletion of assignment | yes | anonymous test without account presentation | +| Personal Test Settings Templates author | yes & reference to by ID | no | only own templates | only own templates | only own templates | yes | no | no, can be deleted separately | From 5a0c7d2921538eed8e1eadce65241129647fdbf5 Mon Sep 17 00:00:00 2001 From: Simon Lowe <164848325+slowe47@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:21:01 +0200 Subject: [PATCH 093/333] TestQuestionPool: Update PRIVACY.md See PR https://github.com/ILIAS-eLearning/ILIAS/pull/11356 * Update PRIVACY.md Update the TestQuestionPool PRIVACY.md to the latest state of the PRIVACY template and code. * replacing user with account and adding purposes * Update PRIVACY.md following up on feedback Adds more "General information", shortens the descriptions and completes the links in the section "Integrated components", expands the purpose of exporting the authorship of questions. --- components/ILIAS/TestQuestionPool/PRIVACY.md | 90 ++++++++++---------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/PRIVACY.md b/components/ILIAS/TestQuestionPool/PRIVACY.md index 73d75d63fcd9..afcf3ed4c589 100755 --- a/components/ILIAS/TestQuestionPool/PRIVACY.md +++ b/components/ILIAS/TestQuestionPool/PRIVACY.md @@ -1,50 +1,46 @@ -## TestQuestionPool Privacy -Insert caveat: *This documentation does not warrant completeness or correctness. -Please report any missing or wrong information using the ILIAS issue tracker.* +# TestQuestionPool Privacy -### Note: -The module TestQuestionPool and the module Test are still tied together in most intricate ways. The primary -component of concern in regards to privacy related evaluations is the Test. As the lines between these components -are blurred - which makes them subject for refactoring, too - it is advised to never look at only one of the components -but always at both. +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** + +## General information + +The TestQuestionPool component is used to create, continuously update and manage a collection of questions. This is useful when questions are to be reused in multiple tests. They can be categorised, e.g. using taxonomies and the lifecycle. To run tests using questions from the TestQuestionPool component, the Test component must be used. + +The TestQuestionPool component and the Test component are still tied together in most intricate ways. The primary component of concern in regards to privacy related evaluations is the Test. As the lines between these components are blurred, it is advised to never look at only one of the components but always at both. + +## Integrated components + +The TestQuestionPool component employs the following services, please consult the respective privacy.mds. + + - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) + - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md): Is used for content creation/presentation within questions and is able to store, present and delete personal data. + - [Export](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/Export) + - [InfoScreen](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/InfoScreen/PRIVACY.md) + - [Metadata](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/MetaData/Privacy.md) + - [Notes](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Notes/Privacy.md): Is used to create, edit and present comments for questions and the question pool. + - [Object](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/ILIASObject) + - [Skill (Competence) Service](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Skill/PRIVACY.md) + - [Taxonomy](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Taxonomy/PRIVACY.md) + - [Test](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Test/PRIVACY.md) + - [User](https://github.com/ILIAS-eLearning/ILIAS/tree/trunk/components/ILIAS/User): Provides information about the account being used when creating questions. ## Data being stored -- **Authorship of Questions**: - Authors of questions are stored in the TestQuestionPool as reference to the users id. - The data is required for copyright purposes as well as to enable communication with the author. - -- **Ownership of Questions**: - Owners of questions are stored in the TestQuestionPool as reference to the users id. - The data is required to manage detailed access and permissions on usage and editing of the question. - -- The TestQuestions Pool component employs the following services, please consult the respective privacy.mds: [Skill](../../ILIAS/Skill/PRIVACY.md), [Metadata](../../ILIAS/MetaData/Privacy.md), [AccessControl](../../ILIAS/AccessControl/PRIVACY.md) - - -## Data being presented -- **Authorship of Questions**: -The authors of questions are revealed in editing forms of questions as well as tabular -overviews to accounts with the edit permission to the test question pool object. -- **Ownership of Questions**: -The owners of questions are revealed in editing forms of questions as well as tabular -- overviews to accounts with the edit permission to the test question pool object. - -## Data being deleted -- **Authorship of Questions**: -The storage of this information is tied to the lifecycle of the question it is attached -to and so the deletion happens in the removal of a question by a user account with edit permissions to the question -pool the questions resides in. -- **Ownership of Questions**: -The storage of this information is tied to the lifecycle of the question it is attached - to and so the deletion happens in the removal of a question by a user account with edit permissions to the question - pool the questions resides in. - - -## Data being exported -- **Authorship of Questions**: - Authorship of questions is exported with the questions. In the test question pool, this is the case when questions or -the pool as a whole is exported by account with edit permissions on the test question pool object. -In the test object, questions can be exported by accounts with edit permission in the context of the test. -- **Ownership of Questions**: - Ownership of questions is exported with the questions. In the test question pool, this is the case when questions or - the pool as a whole is exported by account with edit permissions on the test question pool object. - In the test object, questions can be exported by accounts with edit permission in the context of the test. + +- At the creation process of questions the field 'Author' is prefilled with the full name of the account, which is creating the question. If this value is not changed, the name of the account is stored. +- At the editing of questions the field 'Author' contains the previous saved value. If the value is changed and personal data is entered, it will be stored. +- By storing (and presenting) the value for 'Author' it is possible to contact the account, if there are problems with the question or the configuration of it. In addition it supports the collaborative development of questions. +- Ownership of Questions: Owners of questions are stored in the TestQuestionPool as reference to the 'User ID'. The data is required to manage detailed access and permissions on usage and editing of the question. + +## Data being presented + +- At the overview of the questions of a question pool, the values of the field 'Author' for all questions are shown, which may contain personal data. +- At the 'Statistics' of a question, the 'Author' of tests is displayed at the table 'This question is used in the following tests', if the question is used in tests. This information originates from the metadata service (see above). + +## Data being deleted + +It is possible to delete questions. Within this the personal data at the field 'Author' and the ownership of the question is deleted. + +## Data being exported + +- The XML export of the question pool contains the personal data 'Author' of the questions and of the question pool itself within the metadata. +- It's purpose is to being imported in ILIAS again, although the contained personal data is easily accesible. Including this information ensures that the authorship of the question is not lost after import. In addition it is possible to contact the author, e.g. if there are problems with the question or the configuration of it. From 4144e952ef32976e108e013fd19c9ee4a2bd99de Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 3 Jul 2026 12:09:47 +0200 Subject: [PATCH 094/333] BookingPool: Redirect to Post-Booking Information See: https://mantis.ilias.de/view.php?id=48018 After booking from the bookable items table, users were not sent to post-booking information when an item had post text or a file attached. `BookableItemTableBookAction` now collects reservation ids and redirects to `displayPostInfo` when bookings succeed. `ProcessUtilGUI::displayPostInfo()` was refactored to resolve periods per object, render one info block per bookable item with booking information, and support multiple objects via `book_post_booking_information_for`. Added `BookingsTableBookingInformationAction` for the bookings table and handled `returnCmd` `render` in `back()`. --- .../BookingProcess/class.ProcessUtilGUI.php | 247 +++++++++++++----- .../default/tpl.booking_reservation_post.html | 14 +- .../Action/BookableItemTableBookAction.php | 17 +- .../Action/BookingTableActionsFactory.php | 16 ++ .../BookingsTableBookingInformationAction.php | 130 +++++++++ lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + 7 files changed, 346 insertions(+), 80 deletions(-) create mode 100644 components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableBookingInformationAction.php diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php index 1ddab4e7782f..3f24055f45a6 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php @@ -76,6 +76,11 @@ public function back(): void return; } + if ($retCmd === 'render') { + $this->redirectToRender(); + return; + } + if ($retCmd !== "") { $this->ctrl->redirectByClass(get_class($this->parent_gui), $retCmd); } else { @@ -184,107 +189,205 @@ public function displayPostInfo( int $user_id, string $file_deliver_cmd ): void { - $this->log->debug("displayPostInfo"); - $main_tpl = $this->gui->mainTemplate(); - $ctrl = $this->gui->ctrl(); - $lng = $this->domain->lng(); - $id = $book_obj_id; - $request = $this->gui->standardRequest(); + $this->log->debug('displayPostInfo'); + $booking_ids = $this->request->getReservationIdsFromString(); - if (!$id) { + $objects_with_periods = $this->resolveObjectsWithPeriodsForPostInfo( + $book_obj_id, + $user_id, + $booking_ids + ); + + if ($objects_with_periods === []) { + $this->ctrl->redirect($this->parent_gui, 'back'); return; } - $rsv_ids = $request->getReservationIdsFromString(); - - $tmp = []; - if ($rsv_ids === [] && $request->getReservationId() !== '') { - $reservationIdParts = explode('_', $request->getReservationId()); + $booking_objects = []; + $booking_info_filenames = []; - $user_id = (int) ($reservationIdParts[1] ?? 0); - $from = (int) ($reservationIdParts[2] ?? 0); + $objects_with_info = []; + foreach ($objects_with_periods as $object_id => $periods) { + $booking_objects[$object_id] = new \ilBookingObject($object_id); + $booking_info_filenames[$object_id] = trim($this->objects_manager->getBookingInfoFilename($object_id)); - if ($from > time()) { - $to = (int) ($reservationIdParts[3] ?? 0); - $tmp["$from-" . ($to + 1)] = 1; + if ( + trim($booking_objects[$object_id]->getPostText()) === '' + && $booking_info_filenames[$object_id] === '' + ) { + continue; } - $rsv_ids = [0]; + + $objects_with_info[$object_id] = $periods; } - // placeholder + if ($objects_with_info === []) { + $this->ctrl->redirect($this->parent_gui, 'back'); + return; + } - $book_ids = \ilBookingReservation::getObjectReservationForUser($id, $user_id); - foreach ($book_ids as $book_id) { - if (in_array($book_id, $rsv_ids) || count($rsv_ids) === 0) { - $obj = new \ilBookingReservation($book_id); - $from = $obj->getFrom(); - $to = $obj->getTo(); - if ($from > time()) { - $tmp[$from . "-" . $to + 1] = $tmp[$from . "-" . $to + 1] ?? 0; - $tmp[$from . "-" . $to + 1]++; - } + $template = new \ilTemplate( + 'tpl.booking_reservation_post.html', + true, + true, + 'components/ILIAS/BookingManager/BookingProcess' + ); + $multiple_objects = count($objects_with_info) > 1; + $submit_url = $this->ctrl->getLinkTarget($this->parent_gui, 'back'); + + foreach ($objects_with_info as $object_id => $periods) { + $booking_object = $booking_objects[$object_id]; + $post_text = trim($booking_object->getPostText()); + + $template->setCurrentBlock('info_block'); + $title = $multiple_objects + ? sprintf($this->lng->txt('book_post_booking_information_for'), $booking_object->getTitle()) + : $this->lng->txt('book_post_booking_information'); + $template->setVariable('TITLE', $title); + + if ($post_text !== '') { + $post_text = str_replace( + ['[OBJECT]', '[PERIOD]'], + [$booking_object->getTitle(), implode('
    ', $periods)], + $post_text + ); + $template->setVariable('POST_TEXT', nl2br($post_text)); + } + + if ($booking_info_filenames[$object_id] === '') { + $template->parseCurrentBlock(); + continue; } + + $this->ctrl->setParameter($this->parent_gui, 'object_id', $object_id); + $url = $this->ctrl->getLinkTarget($this->parent_gui, $file_deliver_cmd); + $this->ctrl->clearParameterByClass($this->parent_gui::class, 'object_id'); + + $template->setCurrentBlock('download'); + $template->setVariable('DOWNLOAD', $this->lng->txt('download')); + $template->setVariable('URL_FILE', $url); + $template->setVariable('TXT_FILE', $booking_info_filenames[$object_id]); + $template->parseCurrentBlock(); + $template->setCurrentBlock('info_block'); + + $template->parseCurrentBlock(); } - $olddt = \ilDatePresentation::useRelativeDates(); - \ilDatePresentation::setUseRelativeDates(false); + $template->setCurrentBlock('submit'); + $template->setVariable('TXT_SUBMIT', $this->lng->txt('ok')); + $template->setVariable('URL_SUBMIT', $submit_url); + $template->parseCurrentBlock(); - $period = array(); - ksort($tmp); - foreach ($tmp as $time => $counter) { - $time = explode("-", $time); - $time = \ilDatePresentation::formatPeriod( - new \ilDateTime($time[0], IL_CAL_UNIX), - new \ilDateTime($time[1], IL_CAL_UNIX) - ); - if ($counter > 1) { - $time .= " (" . $counter . ")"; + $this->tpl->setContent($template->get()); + } + + /** + * @return array + */ + protected function resolveObjectsWithPeriodsForPostInfo( + int $book_obj_id, + int $user_id, + array $booking_ids + ): array { + if ($booking_ids !== []) { + return $this->resolvePeriodsFromReservationIds($booking_ids); + } + + if ($book_obj_id <= 0) { + return []; + } + + $period_slots = []; + if ($this->request->getReservationId() !== '') { + $reservation_id_parts = explode('_', $this->request->getReservationId()); + $user_id = (int) ($reservation_id_parts[1] ?? 0); + $from = (int) ($reservation_id_parts[2] ?? 0); + + if ($from > time()) { + $to = (int) ($reservation_id_parts[3] ?? 0); + $period_slots["$from-$to"] = 1; } - $period[] = $time; + $booking_ids = [0]; } - $book_id = array_shift($book_ids); - \ilDatePresentation::setUseRelativeDates($olddt); + $book_ids = \ilBookingReservation::getObjectReservationForUser($book_obj_id, $user_id); + foreach ($book_ids as $book_id) { + if (!in_array($book_id, $booking_ids, true) && $booking_ids !== []) { + continue; + } - /* - #23578 since Booking pool participants. - $obj = new ilBookingReservation($book_id); - if ($obj->getUserId() != $ilUser->getId()) - { - return; + $reservation = new \ilBookingReservation($book_id); + $from = $reservation->getFrom(); + $to = $reservation->getTo(); + if ($from > time()) { + $key = "$from-$to"; + $period_slots[$key] = ($period_slots[$key] ?? 0) + 1; + } } - */ - $obj = new \ilBookingObject($id); - $pfile = $this->objects_manager->getBookingInfoFilename($id); - $ptext = $obj->getPostText(); + return $period_slots === [] + ? [] + : [$book_obj_id => $this->formatPeriodSlots($period_slots)]; + } - $mytpl = new \ilTemplate('tpl.booking_reservation_post.html', true, true, 'components/ILIAS/BookingManager/BookingProcess'); - $mytpl->setVariable("TITLE", $lng->txt('book_post_booking_information')); + /** + * @param string[] $booking_ids + * @return array + */ + protected function resolvePeriodsFromReservationIds(array $booking_ids): array + { + $period_slots_by_object = []; - if ($ptext) { - // placeholder - $ptext = str_replace( - ["[OBJECT]", "[PERIOD]"], - [$obj->getTitle(), implode("
    ", $period)], - $ptext - ); + foreach ($booking_ids as $booking_id) { + $booking_id = (int) $booking_id; + if ($booking_id <= 0) { + continue; + } - $mytpl->setVariable("POST_TEXT", nl2br($ptext)); + $reservation = new \ilBookingReservation($booking_id); + $object_id = $reservation->getObjectId(); + $key = "{$reservation->getFrom()}-{$reservation->getTo()}"; + $period_slots_by_object[$object_id][$key] = ($period_slots_by_object[$object_id][$key] ?? 0) + 1; } - if ($pfile) { - $url = $ctrl->getLinkTarget($this->parent_gui, $file_deliver_cmd); + $objects_with_periods = []; + foreach ($period_slots_by_object as $object_id => $period_slots) { + $objects_with_periods[$object_id] = $this->formatPeriodSlots($period_slots); + } + + ksort($objects_with_periods); + + return $objects_with_periods; + } + + /** + * @param array $period_slots + * @return string[] + */ + protected function formatPeriodSlots(array $period_slots): array + { + $olddt = \ilDatePresentation::useRelativeDates(); + \ilDatePresentation::setUseRelativeDates(false); + + $period = []; + ksort($period_slots); + foreach ($period_slots as $time => $counter) { + $time_parts = explode('-', $time); + $formatted = \ilDatePresentation::formatPeriod( + new \ilDateTime((int) $time_parts[0], IL_CAL_UNIX), + new \ilDateTime((int) $time_parts[1], IL_CAL_UNIX) + ); - $mytpl->setVariable("DOWNLOAD", $lng->txt('download')); - $mytpl->setVariable("URL_FILE", $url); - $mytpl->setVariable("TXT_FILE", $pfile); + if ($counter > 1) { + $formatted .= " ($counter)"; + } + + $period[] = $formatted; } - $mytpl->setVariable("TXT_SUBMIT", $lng->txt('ok')); - $mytpl->setVariable("URL_SUBMIT", $ctrl->getLinkTarget($this->parent_gui, "back")); + \ilDatePresentation::setUseRelativeDates($olddt); - $main_tpl->setContent($mytpl->get()); + return $period; } /** diff --git a/components/ILIAS/BookingManager/BookingProcess/templates/default/tpl.booking_reservation_post.html b/components/ILIAS/BookingManager/BookingProcess/templates/default/tpl.booking_reservation_post.html index 53621b1bdba5..c54fee6730f1 100755 --- a/components/ILIAS/BookingManager/BookingProcess/templates/default/tpl.booking_reservation_post.html +++ b/components/ILIAS/BookingManager/BookingProcess/templates/default/tpl.booking_reservation_post.html @@ -1,3 +1,4 @@ +

    {TITLE}

    @@ -13,9 +14,10 @@

    {TITLE}

    - -
    \ No newline at end of file +
    + + +
    + diff --git a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php index c0ae73b87720..ca6a4468b19d 100644 --- a/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php +++ b/components/ILIAS/BookingManager/src/BookableItem/Table/Action/BookableItemTableBookAction.php @@ -21,6 +21,8 @@ namespace ILIAS\BookingManager\BookableItem\Table\Action; use ilBookingObjectGUI; +use ilBookingProcessWithScheduleGUI; +use ilBookingProcessWithoutScheduleGUI; use ilBookingReservation; use ilCtrlInterface; use ilDatePresentation; @@ -43,8 +45,6 @@ use ilBookingObject; use ILIAS\BookingManager\BookingProcess\BookingProcessManager; use ilObjUser; -use DateTimeZone; -use DateTime; class BookableItemTableBookAction implements TableAction { @@ -290,6 +290,7 @@ public function onSubmit( $booked_total = 0; $unavailable = []; + $booking_ids = []; foreach ($data as $object_id => $section) { $message = $section['message'] ?? ''; @@ -318,6 +319,9 @@ public function onSubmit( if ($booked !== []) { $booked_total += count($booked); + foreach ($booked as $booking_id) { + $booking_ids[] = $booking_id; + } continue; } @@ -347,6 +351,15 @@ public function onSubmit( $this->lng->txt('book_reservation_confirmed'), true ); + + $booking_process_gui_class = $this->pool->getScheduleType() === ilObjBookingPool::TYPE_FIX_SCHEDULE + ? ilBookingProcessWithScheduleGUI::class + : ilBookingProcessWithoutScheduleGUI::class; + + $this->ctrl->setParameterByClass($booking_process_gui_class, 'rsv_ids', implode(';', array_unique($booking_ids))); + $this->ctrl->setParameterByClass($booking_process_gui_class, 'returnCmd', 'render'); + $this->ctrl->redirectByClass($booking_process_gui_class, 'displayPostInfo'); + return null; } $this->ctrl->redirectByClass(ilBookingObjectGUI::class, 'render'); diff --git a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php index 44502a36de95..c3ba8b9cf3f6 100644 --- a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php +++ b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingTableActionsFactory.php @@ -23,6 +23,7 @@ use ilCtrlInterface; use ilGlobalTemplateInterface; use ILIAS\BookingManager\Access\AccessManager; +use ILIAS\BookingManager\Bookings\Table\Action\BookingsTableBookingInformationAction; use ILIAS\BookingManager\Bookings\Table\Action\BookingsTableCancelAction; use ILIAS\BookingManager\Bookings\Table\Action\BookingsTableDeleteAction; use ILIAS\BookingManager\Bookings\Table\Action\BookingsTableMailAction; @@ -44,6 +45,8 @@ class BookingTableActionsFactory implements TableActionsFactory public const string ACTION_MAIL = 'mail'; + public const string ACTION_BOOKING_INFORMATION = 'booking_information'; + public function __construct( protected readonly ilCtrlInterface $ctrl, protected readonly ilLanguage $lng, @@ -73,6 +76,7 @@ public function getTableActions(): TableActions self::ACTION_CANCEL => $this->getCancelAction(), self::ACTION_DELETE => $this->getDeleteAction(), self::ACTION_MAIL => $this->getMailAction(), + self::ACTION_BOOKING_INFORMATION => $this->getBookingInformationAction(), ] ); } @@ -124,4 +128,16 @@ protected function getMailAction(): BookingsTableMailAction $this->bookings ); } + + protected function getBookingInformationAction(): BookingsTableBookingInformationAction + { + return new BookingsTableBookingInformationAction( + $this->ui_factory, + $this->lng, + $this->http, + $this->tpl, + $this->ctrl, + $this->bookings + ); + } } diff --git a/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableBookingInformationAction.php b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableBookingInformationAction.php new file mode 100644 index 000000000000..793cbecddfc0 --- /dev/null +++ b/components/ILIAS/BookingManager/src/Booking/Table/Action/BookingsTableBookingInformationAction.php @@ -0,0 +1,130 @@ +ui_factory->table()->action()->standard( + $this->lng->txt($this->getActionLabel()), + $url_builder + ->withParameter($action_token, $this->getActionId()) + ->withParameter($action_type_token, 'booking_information'), + $row_id_token + ); + } + + public function onExecute( + URLBuilder $url_builder, + URLBuilderToken $row_id_token, + URLBuilderToken $action_token, + URLBuilderToken $action_type_token + ): mixed { + $row_parameters = $this->http->resolveRowParameters($row_id_token->getName()); + if ($row_parameters === HttpService::ALL_OBJECTS) { + $selected_ids = null; + } elseif (is_string($row_parameters)) { + $selected_ids = [$row_parameters]; + } else { + $selected_ids = $row_parameters; + } + + $reservation_ids = $this->resolveRecords($selected_ids); + + if ($reservation_ids === []) { + $this->tpl->setOnScreenMessage( + ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('no_valid_selection'), + true + ); + return null; + } + + $this->ctrl->setParameterByClass( + ilBookingReservationsGUI::class, + 'rsv_ids', + implode(';', $reservation_ids) + ); + $this->ctrl->redirectByClass(ilBookingReservationsGUI::class, 'displayPostInfo'); + return null; + } + + /** + * @return int[] + */ + protected function resolveRecords(?array $selected_ids = null): array + { + return array_map( + 'intval', + $selected_ids ?? array_keys($this->bookings) + ); + } +} diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 97afdc5281be..16005e7adec1 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -2652,6 +2652,7 @@ book#:#book_pool_added#:#Ein Buchungspool wurde angelegt. book#:#book_pool_selection#:#Auswahl Buchungspool book#:#book_post_booking_file#:#Datei book#:#book_post_booking_information#:#Informationen zur Buchung +book#:#book_post_booking_information_for#:#Informationen zur Buchung von "%s" book#:#book_post_booking_text#:#Text book#:#book_post_booking_text_info#:#Die Platzhalter können genutzt werden, um Buchungsdetails zum Infotext hinzuzufügen.
    Der Platzhalter [OBJECT] wird durch das jeweilige Angebot ersetzt.
    Der Platzhalter [PERIOD] wird durch den gewählten Buchungszeitraum ersetzt. book#:#book_pref_book_cron#:#Buchung mit Präferenzen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 46f3ada25129..b73a0a4b4f50 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -2653,6 +2653,7 @@ book#:#book_pool_added#:#Booking pool successfully created. book#:#book_pool_selection#:#Booking Pool Selection book#:#book_post_booking_file#:#File book#:#book_post_booking_information#:#Booking Information +book#:#book_post_booking_information_for#:#Booking Information for "%s" book#:#book_post_booking_text#:#Text book#:#book_post_booking_text_info#:#Use placeholders to include booking-specific information.
    [OBJECT] will be replaced by the respective Bookable Item.
    [PERIOD] will be replaced by the respective schedule. book#:#book_pref_book_cron#:#Booking with Preferences From 1390977d0ef655262c2b6aa7bdc50e776f729152 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 20 Jul 2026 11:42:59 +0200 Subject: [PATCH 095/333] MetaData: accept imports without title, set placeholder title on md transfer if none exists (48040) --- .../xml/SchemaValidation/ilias_md_10_0.xsd | 4 +- .../IdentifierHandler/IdentifierHandler.php | 35 +++++++++-- .../IdentifierHandlerInterface.php | 4 ++ .../Repository/LOMDatabaseRepository.php | 1 + .../classes/Repository/Services/Services.php | 3 +- .../IdentifierHandlerTest.php | 61 ++++++++++++++++++- 6 files changed, 99 insertions(+), 9 deletions(-) diff --git a/components/ILIAS/Export/xml/SchemaValidation/ilias_md_10_0.xsd b/components/ILIAS/Export/xml/SchemaValidation/ilias_md_10_0.xsd index df685edf75cd..fccfea2a4956 100644 --- a/components/ILIAS/Export/xml/SchemaValidation/ilias_md_10_0.xsd +++ b/components/ILIAS/Export/xml/SchemaValidation/ilias_md_10_0.xsd @@ -197,7 +197,7 @@ - + @@ -215,7 +215,7 @@ - + diff --git a/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandler.php b/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandler.php index cd5ccae84ead..461515282559 100644 --- a/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandler.php +++ b/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandler.php @@ -26,20 +26,37 @@ use ILIAS\MetaData\Paths\PathInterface; use ILIAS\MetaData\Paths\FactoryInterface as PathFactory; use ILIAS\MetaData\Paths\Filters\FilterType; +use ILIAS\MetaData\Paths\Navigator\NavigatorFactoryInterface as NavigatorFactory; +// TODO rename to something more appropriate class IdentifierHandler implements IdentifierHandlerInterface { - protected ManipulatorInterface $manipulator; - protected PathFactory $path_factory; + protected const PLACEHOLDER_TITLE = 'PLACEHOLDER'; public function __construct( - ManipulatorInterface $manipulator, - PathFactory $path_factory + protected ManipulatorInterface $manipulator, + protected PathFactory $path_factory, + protected NavigatorFactory $navigator_factory ) { $this->manipulator = $manipulator; $this->path_factory = $path_factory; } + public function preparePlaceholderTitleIfEmpty( + SetInterface $set + ): SetInterface { + $path_to_title = $this->getPathToTitle(); + $navigator = $this->navigator_factory->navigator($path_to_title, $set->getRoot()); + if ($navigator->lastElementAtFinalStep() !== null) { + return $set; + } + return $this->manipulator->prepareCreateOrUpdate( + $set, + $path_to_title, + self::PLACEHOLDER_TITLE + ); + } + public function prepareUpdateOfIdentifier( SetInterface $set, RessourceIDInterface $ressource_id @@ -71,6 +88,16 @@ protected function generateIdentifierCatalog(): string return 'ILIAS'; } + protected function getPathToTitle(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('title') + ->withNextStep('string') + ->get(); + } + protected function getPathToFirstIdentifierEntry(): PathInterface { return $this->path_factory diff --git a/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php b/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php index 4d8bbc7e96e9..eb5a3443a106 100644 --- a/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php +++ b/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php @@ -29,4 +29,8 @@ public function prepareUpdateOfIdentifier( SetInterface $set, RessourceIDInterface $ressource_id ): SetInterface; + + public function preparePlaceholderTitleIfEmpty( + SetInterface $set + ): SetInterface; } diff --git a/components/ILIAS/MetaData/classes/Repository/LOMDatabaseRepository.php b/components/ILIAS/MetaData/classes/Repository/LOMDatabaseRepository.php index b87e01ddf535..27ceb26f696f 100755 --- a/components/ILIAS/MetaData/classes/Repository/LOMDatabaseRepository.php +++ b/components/ILIAS/MetaData/classes/Repository/LOMDatabaseRepository.php @@ -116,6 +116,7 @@ public function transferMD( $this->processor->cleanMarkers($from_set); } $from_set = $this->identifier_handler->prepareUpdateOfIdentifier($from_set, $to_ressource_id); + $from_set = $this->identifier_handler->preparePlaceholderTitleIfEmpty($from_set); $this->manipulator->deleteAllMD($to_ressource_id); $this->manipulator->transferMD($from_set, $to_ressource_id); } diff --git a/components/ILIAS/MetaData/classes/Repository/Services/Services.php b/components/ILIAS/MetaData/classes/Repository/Services/Services.php index aa930f8b2690..ad2b9c721392 100755 --- a/components/ILIAS/MetaData/classes/Repository/Services/Services.php +++ b/components/ILIAS/MetaData/classes/Repository/Services/Services.php @@ -159,7 +159,8 @@ public function repository(): RepositoryInterface ), new IdentifierHandler( $this->manipulator_services->manipulator(), - $this->path_services->pathFactory() + $this->path_services->pathFactory(), + $this->path_services->navigatorFactory() ) ); } diff --git a/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php b/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php index 0d5d481b4bf9..d4d9cb37da52 100644 --- a/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php +++ b/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php @@ -32,6 +32,11 @@ use ILIAS\MetaData\Paths\NullBuilder as NullPathBuilder; use ILIAS\MetaData\Paths\Filters\FilterType; use ILIAS\MetaData\Paths\NullPath; +use ILIAS\MetaData\Paths\Navigator\NullNavigatorFactory; +use ILIAS\MetaData\Paths\Navigator\NavigatorInterface; +use ILIAS\MetaData\Elements\ElementInterface; +use ILIAS\MetaData\Paths\Navigator\NullNavigator; +use ILIAS\MetaData\Elements\NullElement; class IdentifierHandlerTest extends TestCase { @@ -69,7 +74,7 @@ public function type(): string }; } - protected function getIdentifierHandler(): IdentifierHandler + protected function getIdentifierHandler(bool $title_exists = false): IdentifierHandler { $manipulator = new class () extends NullManipulator { public function prepareCreateOrUpdate( @@ -149,7 +154,33 @@ public function custom(): PathBuilder } }; - return new class ($manipulator, $path_factory) extends IdentifierHandler { + $navigator_factory = new class ($title_exists) extends NullNavigatorFactory { + public function __construct( + protected bool $title_exists + ) { + } + + public function navigator(PathInterface $path, ElementInterface $start_element): NavigatorInterface + { + return new class ($path, $this->title_exists) extends NullNavigator { + public function __construct( + protected PathInterface $path, + protected bool $title_exists + ) { + } + + public function lastElementAtFinalStep(): ?ElementInterface + { + if ($this->title_exists && $this->path->toString() === '~start~%general%title%string') { + return new NullElement(); + } + return null; + } + }; + } + }; + + return new class ($manipulator, $path_factory, $navigator_factory) extends IdentifierHandler { protected function getInstallID(): string { return 'MockInstID'; @@ -200,4 +231,30 @@ public function testPrepareUpdateOfIdentifierForSubIDZero(): void $this->assertContains($expected_entry_changes, $prepared_changes); $this->assertContains($expected_catalog_changes, $prepared_changes); } + + public function testPreparePlaceholderTitleIfEmptyWhenNotEmpty(): void + { + $set = $this->getSet(); + $identifier_handler = $this->getIdentifierHandler(true); + + $prepared_set = $identifier_handler->preparePlaceholderTitleIfEmpty($set); + + $prepared_changes = $prepared_set->prepared_changes; + $this->assertEmpty($prepared_changes); + } + + public function testPreparePlaceholderTitleIfEmptyWhenEmpty(): void + { + $set = $this->getSet(); + $identifier_handler = $this->getIdentifierHandler(false); + + $prepared_set = $identifier_handler->preparePlaceholderTitleIfEmpty($set); + + $expected_change = [ + 'path' => '~start~%general%title%string', + 'values' => ['PLACEHOLDER'] + ]; + $prepared_changes = $prepared_set->prepared_changes; + $this->assertSame([$expected_change], $prepared_changes); + } } From 6423671390d685611a515bde8f55950c90491168 Mon Sep 17 00:00:00 2001 From: Abraham Date: Mon, 20 Jul 2026 12:27:30 +0200 Subject: [PATCH 096/333] [FIX] Survey: Correct mean calculation (#11773) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Saúl Díaz --- .../class.SurveyMatrixQuestionEvaluation.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php index 0916eb896527..e39cf4885fda 100755 --- a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php +++ b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php @@ -22,6 +22,23 @@ */ class SurveyMatrixQuestionEvaluation extends SurveyQuestionEvaluation { + protected function parseResults( + ilSurveyEvaluationResults $a_results, + array $a_answers, + SurveyCategories $a_categories = null + ): void { + parent::parseResults($a_results, $a_answers, $a_categories); + + $total = $sum = 0; + foreach ($a_results->getAnswers() as $answer) { + $total++; + $sum += $answer->value; + } + if ($total > 0) { + $a_results->setMean($sum / $total); + } + } + // // RESULTS // From c77554d6ea3c1b4ddec0426335b5277008273cd8 Mon Sep 17 00:00:00 2001 From: Aaron Bidzan Date: Mon, 15 Dec 2025 14:34:37 +0100 Subject: [PATCH 097/333] User: Fix Feed Hash Null Error See: https://mantis.ilias.de/view.php?id=46260 Changed the check in `ilObjUser::getFeedHash()` to use `$rec['feed_hash'] ?? ''` instead of `$rec['feed_hash']` before calling `strlen()`, so a `null` value no longer causes an error. --- components/ILIAS/User/classes/class.ilObjUser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/User/classes/class.ilObjUser.php b/components/ILIAS/User/classes/class.ilObjUser.php index 9a30e81a24a4..f7e6d356f8ea 100755 --- a/components/ILIAS/User/classes/class.ilObjUser.php +++ b/components/ILIAS/User/classes/class.ilObjUser.php @@ -3123,7 +3123,7 @@ public static function _lookupFeedHash( [$a_user_id] ); if ($rec = $ilDB->fetchAssoc($set)) { - if (strlen($rec['feed_hash']) == 32) { + if (strlen($rec['feed_hash'] ?? '') == 32) { return $rec['feed_hash']; } elseif ($a_create) { $hash = md5(random_int(1, 9999999) + str_replace(' ', '', microtime())); From 72626bc3067457fdcd1f905ecd94a3298b8637d7 Mon Sep 17 00:00:00 2001 From: Fabian Schmid Date: Fri, 17 Jul 2026 10:29:31 +0200 Subject: [PATCH 098/333] [REFACTOR] Move master.ini templates and their reader to Init component The ilias.master.ini.php / client.master.ini.php templates and their sole consumer ilIniFilesPopulatedObjective lived in the Setup component (formerly setup_). They seed the ilias.ini.php / client.ini.php written during installation - an Init/environment concern - so both the templates and the objective belong in the Init component. Moved: - components/ILIAS/Setup/{ilias,client}.master.ini.php -> components/ILIAS/Init/resources/ - components/ILIAS/Setup/classes/class.ilIniFilesPopulatedObjective.php -> components/ILIAS/Init/classes/ The objective now reads its templates same-component via dirname(__DIR__) . '/resources/...'. All other __DIR__ based paths keep working because Init/classes has the same nesting depth as Setup/classes. The class stays in the global namespace, so its many callers across components resolve unchanged via the classmap. composer.json classmap excludes updated to the new template locations. --- .../classes/class.ilIniFilesPopulatedObjective.php | 4 ++-- .../ILIAS/{Setup => Init/resources}/client.master.ini.php | 0 .../ILIAS/{Setup => Init/resources}/ilias.master.ini.php | 0 composer.json | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename components/ILIAS/{Setup => Init}/classes/class.ilIniFilesPopulatedObjective.php (94%) rename components/ILIAS/{Setup => Init/resources}/client.master.ini.php (100%) rename components/ILIAS/{Setup => Init/resources}/ilias.master.ini.php (100%) diff --git a/components/ILIAS/Setup/classes/class.ilIniFilesPopulatedObjective.php b/components/ILIAS/Init/classes/class.ilIniFilesPopulatedObjective.php similarity index 94% rename from components/ILIAS/Setup/classes/class.ilIniFilesPopulatedObjective.php rename to components/ILIAS/Init/classes/class.ilIniFilesPopulatedObjective.php index 75d3cc184ca7..877696fb0668 100755 --- a/components/ILIAS/Setup/classes/class.ilIniFilesPopulatedObjective.php +++ b/components/ILIAS/Init/classes/class.ilIniFilesPopulatedObjective.php @@ -65,7 +65,7 @@ public function achieve(Setup\Environment $environment): Setup\Environment $path = $this->getILIASIniPath(); if (!file_exists($path)) { $ini = new ilIniFile($path); - $ini->GROUPS = parse_ini_file(__DIR__ . "/../ilias.master.ini.php", true); + $ini->GROUPS = parse_ini_file(dirname(__DIR__) . "/resources/ilias.master.ini.php", true); $ini->write(); $environment = $environment ->withResource(Setup\Environment::RESOURCE_ILIAS_INI, $ini); @@ -74,7 +74,7 @@ public function achieve(Setup\Environment $environment): Setup\Environment $path = $this->getClientIniPath($client_id); if (!file_exists($path)) { $client_ini = new ilIniFile($path); - $client_ini->GROUPS = parse_ini_file(__DIR__ . "/../client.master.ini.php", true); + $client_ini->GROUPS = parse_ini_file(dirname(__DIR__) . "/resources/client.master.ini.php", true); $client_ini->write(); $environment = $environment ->withResource(Setup\Environment::RESOURCE_CLIENT_INI, $client_ini); diff --git a/components/ILIAS/Setup/client.master.ini.php b/components/ILIAS/Init/resources/client.master.ini.php similarity index 100% rename from components/ILIAS/Setup/client.master.ini.php rename to components/ILIAS/Init/resources/client.master.ini.php diff --git a/components/ILIAS/Setup/ilias.master.ini.php b/components/ILIAS/Init/resources/ilias.master.ini.php similarity index 100% rename from components/ILIAS/Setup/ilias.master.ini.php rename to components/ILIAS/Init/resources/ilias.master.ini.php diff --git a/composer.json b/composer.json index e6356cde6bf7..147736fa56c4 100755 --- a/composer.json +++ b/composer.json @@ -98,8 +98,8 @@ "./public/Customizing/**/vendor", "./components/ILIAS/Setup/sql", "./cli/setup.php", - "./components/ILIAS/Setup/client.master.ini.php", - "./components/ILIAS/Setup/ilias.master.ini.php" + "./components/ILIAS/Init/resources/client.master.ini.php", + "./components/ILIAS/Init/resources/ilias.master.ini.php" ] }, "extra": { From 6ff1b1793ee52400c4c7be9407515143086801e1 Mon Sep 17 00:00:00 2001 From: Abraham Date: Mon, 20 Jul 2026 13:58:43 +0200 Subject: [PATCH 099/333] [Language] update: T&A spanish language variables (#11778) --- lang/ilias_es.lang | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lang/ilias_es.lang b/lang/ilias_es.lang index 52c4dd49cd9a..9a18757e8edc 100644 --- a/lang/ilias_es.lang +++ b/lang/ilias_es.lang @@ -1484,7 +1484,7 @@ assessment#:#tst_exam_modal_message_conditions#:#Por favor, confirme las condici assessment#:#tst_exam_modal_message_conditions_and_password#:#Por favor, confirme las condiciones e introduzca la contraseña para iniciar la prueba. assessment#:#tst_exam_modal_message_password#:#Por favor, introduzca la contraseña para iniciar la prueba. assessment#:#tst_exam_not_assigned_participant_disclaimer#:#No puede iniciar esta prueba, ya que no está asignado como participante. -assessment#:#tst_exam_password#:#Contraseña del examen +assessment#:#tst_exam_password#:#Contraseña de la prueba assessment#:#tst_exam_password_invalid_message#:#¡La contraseña proporcionada no es válida! assessment#:#tst_exam_password_label#:#Contraseña assessment#:#tst_exam_required_fields_not_filled_message#:#¡Debe completar todos los campos obligatorios! @@ -1509,7 +1509,13 @@ assessment#:#tst_final_information#:#Finalizar la prueba assessment#:#tst_finish_confirm_button#:#Sí, quiero finalizar la prueba assessment#:#tst_finish_confirm_cancel_button#:#No, volver a la pregunta anterior assessment#:#tst_finish_confirmation_question#:#Va a finalizar esta prueba. No podrá volver a acceder a este intento de prueba para cambiar sus respuestas. ¿Realmente desea finalizar la prueba? -assessment#:#tst_finish_confirmation_question_no_attempts_left#:#Va a finalizar esta prueba y alcanzará el número máximo de intentos permitidos. No podrá volver a entrar en esta prueba para cambiar sus respuestas. ¿Realmente desea finalizar la prueba? +assessment#:#tst_finish_confirmation_question_no_attempts_left#:#Va a finalizar esta prueba y alcanzará el número máximo de intentos permitidos. No podrá volver a acceder a esta prueba para cambiar sus respuestas. ¿Realmente desea finalizarla? +assessment#:#tst_finish_notification#:#Notificación +assessment#:#tst_finish_notification_advanced#:#Enviar resultado completo de la prueba +assessment#:#tst_finish_notification_content_type#:#Contenido del correo electrónico +assessment#:#tst_finish_notification_desc#:#Envía un correo electrónico al propietario de la prueba por cada usuario que haya completado la prueba. +assessment#:#tst_finish_notification_no#:#Sin correo electrónico +assessment#:#tst_finish_notification_simple#:#Enviar nombre de usuario y fecha de finalización assessment#:#tst_finished#:#Finalizado assessment#:#tst_form_dynamic_question_set_config#:#Continuar selección de preguntas assessment#:#tst_gap_analysis#:#Análisis de brechas @@ -1594,9 +1600,9 @@ assessment#:#tst_introduction_text#:#Mensaje introductorio assessment#:#tst_invited_nobody#:#No se han añadido usuarios, grupos ni roles como participantes fijos del test assessment#:#tst_invited_selected_users#:#Los usuarios seleccionados se han añadido como participantes fijos del test assessment#:#tst_launcher_button_label_passes_limit_reached#:#Has alcanzado el límite de intentos posibles del test -assessment#:#tst_launcher_status_message_conditions#:#Se te pedirá que aceptes las condiciones del examen cuando inicies el test. -assessment#:#tst_launcher_status_message_conditions_and_password#:#Se te pedirá la contraseña y que aceptes las condiciones del examen cuando inicies el test. -assessment#:#tst_launcher_status_message_password#:#Se te pedirá la contraseña cuando inicies el test. +assessment#:#tst_launcher_status_message_conditions#:#Se le pedirá que acepte las condiciones del examen cuando inicie la prueba. +assessment#:#tst_launcher_status_message_conditions_and_password#:#Se le pedirá la contraseña y que acepte las condiciones del examen cuando inicie la prueba. +assessment#:#tst_launcher_status_message_password#:#Se le pedirá la contraseña cuando inicie la prueba. assessment#:#tst_level#:#Nivel de competencia assessment#:#tst_limit_nr_of_tries#:#Limitar el número de intentos del test assessment#:#tst_link_only_unassigned#:#Has seleccionado al menos una pregunta que ya está vinculada a un banco de preguntas. Solo se pueden añadir preguntas no asignadas a un banco de preguntas. @@ -5805,15 +5811,15 @@ common#:#trash#:#Papelera common#:#tree#:#Árbol common#:#tree_frame#:#Marco del árbol common#:#treeview#:#Mostrar barra lateral -common#:#tst#:#Test -common#:#tst_add#:#Añadir test +common#:#tst#:#Prueba +common#:#tst_add#:#Añadir prueba common#:#tst_edit_questions#:#Editar preguntas common#:#tst_history_read#:#Ver historial -common#:#tst_new#:#Nuevo test -common#:#tst_results#:#Resultados del examen +common#:#tst_new#:#Nueva prueba +common#:#tst_results#:#Resultados de la prueba common#:#tst_run#:#Ejecución -common#:#tst_user_not_invited#:#No está autorizado para realizar este examen. -common#:#tst_warning_test_not_complete#:#¡El examen no está completo! +common#:#tst_user_not_invited#:#No está autorizado para realizar esta prueba. +common#:#tst_warning_test_not_complete#:#¡La prueba no está completa! common#:#tutors#:#Tutores common#:#txt_registered#:#Se ha registrado correctamente en ILIAS. Por favor, haga clic en el botón de abajo para iniciar sesión con su cuenta de usuario. common#:#txt_registered_passw_gen#:#Se ha registrado correctamente en ILIAS. En breve recibirá un correo electrónico con su contraseña generada. @@ -15146,8 +15152,8 @@ rep#:#rep_export_limitation_limited#:#Limitar exportación rep#:#rep_export_limitation_unlimited#:#Exportación ilimitada rep#:#rep_failure_trashed_trash#:#Ha seleccionado objetos que no pueden restaurarse a su ubicación original porque sus objetos superiores fueron eliminados. Por favor, desmarque el objeto correspondiente en la tabla o seleccione Restaurar en una ubicación nueva en su lugar. rep#:#rep_fav_intro1#:#Aún no ha seleccionado ningún favorito. Para ello, debe seguir dos pasos: -rep#:#rep_fav_intro2#:#Haga clic en '%s' y seleccione un objeto de aprendizaje de la oferta disponible, por ejemplo, un módulo de aprendizaje o un foro. -rep#:#rep_fav_intro3#:#Cuando haya encontrado algo que le interese, puede añadirlo fácilmente a sus favoritos. Seleccione el elemento deseado en el menú Acciones y elija "Añadir a favoritos". +rep#:#rep_fav_intro2#:#Haga clic en '%s' y seleccione un objeto de aprendizaje entre las opciones disponibles, por ejemplo, un módulo de aprendizaje o un foro. +rep#:#rep_fav_intro3#:#Cuando encuentre algo que le interese, puede añadirlo fácilmente a sus favoritos. En el menú Acciones del elemento deseado, seleccione «Añadir a favoritos». rep#:#rep_favourites#:#Favoritos rep#:#rep_favourites_info#:#Los usuarios pueden marcar elementos individuales del repositorio como favoritos. Las listas de favoritos se pueden activar y configurar en los ajustes del tablero y del menú. rep#:#rep_input_not_empty#:#Este campo no puede estar vacío, por favor proporcione un valor. From 9968eaa7efff90bcb52331a40c8ea3a7c37a4a11 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 20 Jul 2026 13:57:04 +0200 Subject: [PATCH 100/333] WebLink: truncate too long list title before writing to db (47980) --- .../classes/Repository/class.ilWebLinkDatabaseRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/WebResource/classes/Repository/class.ilWebLinkDatabaseRepository.php b/components/ILIAS/WebResource/classes/Repository/class.ilWebLinkDatabaseRepository.php index 5bd4ff628c2b..014da3954889 100755 --- a/components/ILIAS/WebResource/classes/Repository/class.ilWebLinkDatabaseRepository.php +++ b/components/ILIAS/WebResource/classes/Repository/class.ilWebLinkDatabaseRepository.php @@ -134,7 +134,7 @@ public function createList(ilWebLinkDraftList $list): ilWebLinkList self::LISTS_TABLE, [ 'webr_id' => ['integer', $new_list->getWebrId()], - 'title' => ['text', $new_list->getTitle()], + 'title' => ['text', mb_substr($new_list->getTitle(), 0, 127)], 'description' => ['text', $new_list->getDescription() ?? ''], 'create_date' => ['integer', $new_list->getCreateDate() ->getTimestamp()], @@ -422,7 +422,7 @@ public function updateList( $this->db->update( self::LISTS_TABLE, [ - 'title' => ['text', $drafted_list->getTitle()], + 'title' => ['text', mb_substr($drafted_list->getTitle(), 0, 127)], 'description' => ['text', $drafted_list->getDescription() ?? ''], 'last_update' => ['integer', $this->getCurrentTime()] ], From 0f04fd9208d654fa4cccc99c4399a6a42a936a4e Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 20 Jul 2026 14:53:54 +0200 Subject: [PATCH 101/333] Membership: remove unused code (47970) --- .../Membership/classes/class.ilMembershipNotifications.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/ILIAS/Membership/classes/class.ilMembershipNotifications.php b/components/ILIAS/Membership/classes/class.ilMembershipNotifications.php index c77bde4656da..100be39ee1b5 100755 --- a/components/ILIAS/Membership/classes/class.ilMembershipNotifications.php +++ b/components/ILIAS/Membership/classes/class.ilMembershipNotifications.php @@ -448,9 +448,6 @@ public static function addToSettingsForm( ); $option->addSubItem($changeable); } - } elseif ($noti->isValidMode(self::MODE_ALL)) { - $option = new ilRadioOption($lng->txt("mem_force_notification_mode_all"), (string) self::MODE_ALL); - $force_noti->addOption($option); } // set current mode From 48696e69e77282a0e38bda49ca660db4e22a9c0b Mon Sep 17 00:00:00 2001 From: Fabian Schmid Date: Fri, 17 Jul 2026 10:28:10 +0200 Subject: [PATCH 102/333] [REFACTOR] Move ilias3.sql database template to Database component The ilias3.sql database dump template lived in the Setup component (formerly setup_). It is the database template consumed exclusively by the Database setup (ilDatabaseSetupConfig / ilDatabasePopulatedObjective), so it belongs in the Database component. Moved components/ILIAS/Setup/sql/ilias3.sql to components/ILIAS/Database/sql/ilias3.sql and updated all path references: ilDatabaseSetupConfig::DEFAULT_PATH_TO_DB_DUMP, ilDBUpdate custom updates path, composer.json classmap exclude, PHP-CS-Fixer exclude and .gitignore. --- .gitignore | 2 +- .../ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php | 2 +- components/ILIAS/Database/classes/ilDBUpdate.php | 2 +- components/ILIAS/{Setup => Database}/sql/ilias3.sql | 0 composer.json | 2 +- scripts/PHP-CS-Fixer/code-format.php_cs | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename components/ILIAS/{Setup => Database}/sql/ilias3.sql (100%) diff --git a/.gitignore b/.gitignore index da9bdca6e50d..81929aa66c58 100755 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ /ilias.ini.php /ilias.log /db_template_writer.php -/components/ILIAS/Setup/sql/dbupdate_custom.php +/components/ILIAS/Database/sql/dbupdate_custom.php /install_client.php # Directories diff --git a/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php b/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php index 39b8376f3592..226b7d2f0213 100755 --- a/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php +++ b/components/ILIAS/Database/classes/Setup/ilDatabaseSetupConfig.php @@ -24,7 +24,7 @@ class ilDatabaseSetupConfig implements Config { public const DEFAULT_COLLATION = "utf8_general_ci"; - public const DEFAULT_PATH_TO_DB_DUMP = "./components/ILIAS/Setup/sql/ilias3.sql"; + public const DEFAULT_PATH_TO_DB_DUMP = "./components/ILIAS/Database/sql/ilias3.sql"; protected string $type; diff --git a/components/ILIAS/Database/classes/ilDBUpdate.php b/components/ILIAS/Database/classes/ilDBUpdate.php index bad2feeb13ad..926a6fa0df31 100755 --- a/components/ILIAS/Database/classes/ilDBUpdate.php +++ b/components/ILIAS/Database/classes/ilDBUpdate.php @@ -250,7 +250,7 @@ private function readCustomUpdatesInfo(bool $a_force = false): void } $this->custom_updates_setting = new ilSetting(); - $custom_updates_file = $this->PATH . './components/ILIAS/Setup/sql/dbupdate_custom.php'; + $custom_updates_file = $this->PATH . './components/ILIAS/Database/sql/dbupdate_custom.php'; if (is_file($custom_updates_file)) { $this->custom_updates_content = @file($custom_updates_file); $this->custom_updates_current_version = (int) $this->custom_updates_setting->get('db_version_custom', '0'); diff --git a/components/ILIAS/Setup/sql/ilias3.sql b/components/ILIAS/Database/sql/ilias3.sql similarity index 100% rename from components/ILIAS/Setup/sql/ilias3.sql rename to components/ILIAS/Database/sql/ilias3.sql diff --git a/composer.json b/composer.json index 147736fa56c4..5dacbc8bd126 100755 --- a/composer.json +++ b/composer.json @@ -96,7 +96,7 @@ "./components/ILIAS/Migration", "./*/*/lib", "./public/Customizing/**/vendor", - "./components/ILIAS/Setup/sql", + "./components/ILIAS/Database/sql", "./cli/setup.php", "./components/ILIAS/Init/resources/client.master.ini.php", "./components/ILIAS/Init/resources/ilias.master.ini.php" diff --git a/scripts/PHP-CS-Fixer/code-format.php_cs b/scripts/PHP-CS-Fixer/code-format.php_cs index 6dc6ea125f33..277457a725ba 100755 --- a/scripts/PHP-CS-Fixer/code-format.php_cs +++ b/scripts/PHP-CS-Fixer/code-format.php_cs @@ -2,7 +2,7 @@ $finder = PhpCsFixer\Finder::create() ->exclude(array( - __DIR__ . '/../../components/ILIAS/Setup/sql', + __DIR__ . '/../../components/ILIAS/Database/sql', __DIR__ . '/example' )) ->in(array( From 2d04678ae6307a71eec951cd9704c0e7b2f72453 Mon Sep 17 00:00:00 2001 From: Ilja Lukin Date: Thu, 12 Feb 2026 16:26:46 +0100 Subject: [PATCH 103/333] ECS: Restore HTTP protocol option in GUI (cherry picked from commit acc0f4a19224fbd00ab3b8df4c770cc7d5b5dc17) --- .../WebServices/ECS/classes/class.ilECSSettingsGUI.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/WebServices/ECS/classes/class.ilECSSettingsGUI.php b/components/ILIAS/WebServices/ECS/classes/class.ilECSSettingsGUI.php index c9e71df0c9f6..e5f9beede947 100755 --- a/components/ILIAS/WebServices/ECS/classes/class.ilECSSettingsGUI.php +++ b/components/ILIAS/WebServices/ECS/classes/class.ilECSSettingsGUI.php @@ -302,10 +302,10 @@ protected function initSettingsForm($a_mode = 'update'): void $this->form->addItem($ser); $pro = new ilSelectInputGUI($this->lng->txt('ecs_protocol'), 'protocol'); - // fixed to https - #$pro->setOptions(array(ilECSSetting::PROTOCOL_HTTP => $this->lng->txt('http'), - # ilECSSetting::PROTOCOL_HTTPS => $this->lng->txt('https'))); - $pro->setOptions(array(ilECSSetting::PROTOCOL_HTTPS => 'HTTPS')); + $pro->setOptions(array( + ilECSSetting::PROTOCOL_HTTPS => 'HTTPS', + ilECSSetting::PROTOCOL_HTTP => 'HTTP' + )); $pro->setValue($this->settings->getProtocol()); $pro->setRequired(true); $this->form->addItem($pro); From 4f12cffc501e1c293de067247c50bd1b23d0a9f8 Mon Sep 17 00:00:00 2001 From: selyesa Date: Mon, 20 Jul 2026 14:32:37 +0200 Subject: [PATCH 104/333] Create PRIVACY.md for COPage --- components/ILIAS/Imprint/PRIVACY.md | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 components/ILIAS/Imprint/PRIVACY.md diff --git a/components/ILIAS/Imprint/PRIVACY.md b/components/ILIAS/Imprint/PRIVACY.md new file mode 100644 index 000000000000..023c4cbc0cb6 --- /dev/null +++ b/components/ILIAS/Imprint/PRIVACY.md @@ -0,0 +1,43 @@ +# Imprint Privacy + +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** + +## General Information + +The Imprint component provides a single, installation-wide legal notice page (also referred to as "Legal Notice" or "Impressum"). It is accessible through the ILIAS footer without authentication, meaning any visitor -- whether logged in or not -- can view the imprint content when it is activated. + +The imprint page uses the ILIAS page editor (COPage) for content management. There is exactly one imprint page per ILIAS installation (page ID 1, parent type `impr`). Since the imprint is a page object, all personal data associated with its creation and editing is managed by the COPage component, not by the Imprint component itself. + +## Integrated Components + +- The Imprint component employs the following components, please consult the respective PRIVACY.md files: + - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md) -- the page editor manages all content storage, including page history, author tracking (creator user ID, last change user ID), and page versioning for the imprint page. + - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) -- manages permissions for editing the imprint page via the RBAC system (`ilPermissionGUI`). + - ILIASObject -- provides the base object framework (`ilObject2`, `ilObject2GUI`) for the legal notice administration object. + +## Data being stored + +The Imprint component does not store personal data in its own database tables. All data storage is delegated to the COPage component, which stores the following for the imprint page in the `page_object` table: + +- **User ID of the page creator**: stored as `create_user` when the imprint page is first created. +- **User ID of the last editor**: stored as `last_change_user` each time the imprint page is saved. +- **Page history entries**: each edit creates a history record including the **user ID** of the editor and a **timestamp**, stored in the `page_history` table. + +For details on how this data is handled, see the [COPage PRIVACY.md](../COPage/PRIVACY.md](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md). + +## Data being presented + +- **Any visitor** (including unauthenticated users) can view the imprint page content when it is activated. The content itself is static text entered by persons with editing permissions and does not display system-managed personal data of other users. +- **Persons with the "Write" permission** on the Legal Notice administration object can edit the imprint page. Through the page editor, they have access to the **page history**, which includes **timestamps** and the **user IDs** of persons who made previous edits (as documented by COPage). +- If the imprint page is **not activated**, a preview is shown only to persons with the "Write" permission, along with a notice that the imprint is inactive. + +## Data being deleted + +- The Imprint component does not provide its own deletion mechanism for personal data. The imprint is a system object that exists exactly once per installation and is not intended to be deleted. +- **Page history** entries for the imprint page are managed by the COPage component. For details on how page history and author information are handled upon user account deletion, see the [COPage PRIVACY.md](../COPage/PRIVACY.md). +- **When a user account is deleted**, the COPage component retains the page content but no longer displays the account name, first name, or last name of the deleted user in page history or authorship information. + +## Data being exported + +- The Imprint component does not provide any export functionality for the imprint page. +- No personal data is exported through this component. From dc19ab1bf98e13bb8e7da41bfb6f63e170241490 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 20 Jul 2026 16:38:43 +0200 Subject: [PATCH 105/333] Calendar: fix mismatched type (48096) --- .../ILIAS/Calendar/classes/class.ilCalendarUserSettings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Calendar/classes/class.ilCalendarUserSettings.php b/components/ILIAS/Calendar/classes/class.ilCalendarUserSettings.php index 21c4269d1fe8..b7cf132393c1 100755 --- a/components/ILIAS/Calendar/classes/class.ilCalendarUserSettings.php +++ b/components/ILIAS/Calendar/classes/class.ilCalendarUserSettings.php @@ -210,7 +210,7 @@ protected function read(): void { $this->timezone = (string) $this->user->getTimeZone(); $this->export_tz_type = (int) ( - ($this->user->getPref('export_tz_type') !== false) ? + ($this->user->getPref('export_tz_type') !== null) ? $this->user->getPref('export_tz_type') : $this->export_tz_type ); @@ -218,7 +218,7 @@ protected function read(): void $this->user->getDateFormat() ); $this->time_format = (int) $this->user->getTimeFormat(); - if (($weekstart = $this->user->getPref('weekstart')) === false) { + if (($weekstart = $this->user->getPref('weekstart')) === null) { $weekstart = $this->settings->getDefaultWeekStart(); } $this->calendar_selection_type = (int) $this->user->getPref('calendar_selection_type') ? From 24da053e905c650675bc2968d636bc32ac371b42 Mon Sep 17 00:00:00 2001 From: Abraham Date: Tue, 21 Jul 2026 14:33:12 +0200 Subject: [PATCH 106/333] [Survey] fix: Questions parameters nullable (#11791) --- .../Questions/class.SurveyMatrixQuestionEvaluation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php index e39cf4885fda..fed06fa1a1d5 100755 --- a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php +++ b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMatrixQuestionEvaluation.php @@ -25,7 +25,7 @@ class SurveyMatrixQuestionEvaluation extends SurveyQuestionEvaluation protected function parseResults( ilSurveyEvaluationResults $a_results, array $a_answers, - SurveyCategories $a_categories = null + ?SurveyCategories $a_categories = null ): void { parent::parseResults($a_results, $a_answers, $a_categories); From f7bdba70b94cf04d3c4284febd034b351fd33b85 Mon Sep 17 00:00:00 2001 From: Abraham Date: Tue, 21 Jul 2026 15:07:13 +0200 Subject: [PATCH 107/333] [Survey] fix: Get maximum directly (#11796) --- .../Questions/class.SurveyMetricQuestion.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMetricQuestion.php b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMetricQuestion.php index c9a1f174b96d..febea6415cc3 100755 --- a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMetricQuestion.php +++ b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMetricQuestion.php @@ -176,11 +176,7 @@ public function saveToDb(int $original_id = 0): int array($this->getId()) ); - if (preg_match("/[\D]/", $this->maximum) or (strcmp($this->maximum, "∞") == 0)) { - $max = -1; - } else { - $max = $this->getMaximum(); - } + $max = $this->getMaximum(); $next_id = $ilDB->nextId('svy_variable'); $ilDB->manipulateF( "INSERT INTO svy_variable (variable_id, category_fi, question_fi, value1, value2, sequence, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s)", From 6c76d50372aacf752cc787a8713444d703ac6db1 Mon Sep 17 00:00:00 2001 From: Abraham Date: Wed, 22 Jul 2026 09:19:47 +0200 Subject: [PATCH 108/333] [Survey] fix: Access to appraisee ID (#11803) --- .../ILIAS/Survey/Evaluation/class.ilSurveyEvaluationGUI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Survey/Evaluation/class.ilSurveyEvaluationGUI.php b/components/ILIAS/Survey/Evaluation/class.ilSurveyEvaluationGUI.php index f34921b212bb..77bd940b0f2c 100755 --- a/components/ILIAS/Survey/Evaluation/class.ilSurveyEvaluationGUI.php +++ b/components/ILIAS/Survey/Evaluation/class.ilSurveyEvaluationGUI.php @@ -296,7 +296,7 @@ public function exportCumulatedResults( ): void { $finished_ids = null; if ($this->object->get360Mode()) { - $appr_id = $this->request->getAppraiseeId(); + $appr_id = $this->getAppraiseeId(); if (!$appr_id) { $this->ctrl->redirect($this, $details ? "evaluationdetails" : "evaluation"); } @@ -1069,7 +1069,7 @@ public function exportEvaluationUser(): void $finished_ids = null; if ($this->object->get360Mode()) { - $appr_id = $this->request->getAppraiseeId(); + $appr_id = $this->getAppraiseeId(); if (!$appr_id) { $this->ctrl->redirect($this, "evaluationuser"); } From bb73acc32f5b2a488ddf31f6444781bbf0e18dc3 Mon Sep 17 00:00:00 2001 From: Abraham Date: Wed, 22 Jul 2026 10:20:12 +0200 Subject: [PATCH 109/333] [Survey] fix: Handle non auth access to Survey (#11806) --- components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php b/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php index 7baaefe561f7..12117831cb8a 100755 --- a/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php +++ b/components/ILIAS/Survey/classes/class.ilObjSurveyGUI.php @@ -308,7 +308,7 @@ public function executeCommand(): void protected function noPermission(): void { - throw new ilObjectException($this->lng->txt("permission_denied")); + $this->checkPermission("read"); } protected function addToNavigationHistory(): void From 4f39560e9f7def61a6a1e7d1fccb99e73f336c77 Mon Sep 17 00:00:00 2001 From: Sagun Karki <51822939+sKarki999@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:56:37 +0200 Subject: [PATCH 110/333] fix: preserve blocked status of course members in updateCourse SOAP call (0040575) (#11808) --- .../ILIAS/soap/classes/class.ilSoapCourseAdministration.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ILIAS/soap/classes/class.ilSoapCourseAdministration.php b/components/ILIAS/soap/classes/class.ilSoapCourseAdministration.php index 5ee22de949dc..25394d1111b2 100755 --- a/components/ILIAS/soap/classes/class.ilSoapCourseAdministration.php +++ b/components/ILIAS/soap/classes/class.ilSoapCourseAdministration.php @@ -371,8 +371,6 @@ public function updateCourse(string $sid, int $course_id, string $xml) return $this->raiseError('Check access failed. No permission to write course', 'Server'); } - ilCourseParticipants::_deleteAllEntries($tmp_course->getId()); - ilCourseWaitingList::_deleteAll($tmp_course->getId()); From 769d42fa69af1ebf99c296c4eaf22ff0a0ec0748 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 26 Jun 2026 16:21:16 +0200 Subject: [PATCH 111/333] 47954: Notes: Edit form for private notes accessible by all users Signed-off-by: Fabian Wolf --- components/ILIAS/Notes/Note/class.ilNoteGUI.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Notes/Note/class.ilNoteGUI.php b/components/ILIAS/Notes/Note/class.ilNoteGUI.php index 11dbb52618a6..ec823422d266 100755 --- a/components/ILIAS/Notes/Note/class.ilNoteGUI.php +++ b/components/ILIAS/Notes/Note/class.ilNoteGUI.php @@ -1051,7 +1051,12 @@ public function updateNote(): void public function editNoteForm( bool $a_init_form = true ): string { - $this->edit_note_form = true; + $this->edit_note_form = false; + if ($this->notes_access->canEdit( + $this->manager->getById($this->requested_note_id) + )) { + $this->edit_note_form = true; + } return $this->getListHTML($a_init_form); } From 327fc7f7069369d5375f39ac9864d51e966a489b Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 26 May 2026 08:30:08 +0200 Subject: [PATCH 112/333] Course: Transform participant selection string See: https://mantis.ilias.de/view.php?id=47800 Signed-off-by: Fabian Wolf --- .../classes/class.ilCourseParticipantsTableGUI.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php b/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php index 47663169ca2e..25815b5d43ac 100755 --- a/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php +++ b/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php @@ -38,6 +38,7 @@ class ilCourseParticipantsTableGUI extends ilParticipantTableGUI protected ilRbacReview $rbacReview; protected ilObjUser $user; protected Profile $profile; + protected \ILIAS\Refinery $refinery; protected array $cached_user_names = []; public function __construct( @@ -72,6 +73,7 @@ public function __construct( $this->rbacReview = $DIC->rbac()->review(); $this->user = $DIC->user(); $this->profile = $DIC['user']->getProfile(); + $this->refinery = $DIC->refinery(); $this->setId('crs_' . $this->getRepositoryObject()->getId()); parent::__construct($a_parent_obj, 'participants'); @@ -146,7 +148,12 @@ protected function fillRow(array $a_set): void { $this->tpl->setVariable('VAL_ID', $a_set['usr_id']); $this->tpl->setVariable('VAL_NAME', $a_set['lastname'] . ', ' . $a_set['firstname']); - $this->tpl->setVariable('SELECT_PARTICIPANT', $this->lng->txt("select") . ' ' . $a_set['lastname'] . ', ' . $a_set['firstname']); + $this->tpl->setVariable( + 'SELECT_PARTICIPANT', + $this->refinery->encode()->htmlAttributeValue()->transform( + "{$this->lng->txt('select')} {$a_set['lastname']}, {$a_set['firstname']}" + ) + ); if ( !$this->access->checkAccessOfUser($a_set['usr_id'], 'read', '', $this->getRepositoryObject()->getRefId()) && From cb105803da1d1c9f53af6a59c60e094ea436d688 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 17 Jun 2026 13:59:56 +0200 Subject: [PATCH 113/333] Object: Do Not Show Debug Info on Import Failure See: https://mantis.ilias.de/view.php?id=47952 Signed-off-by: Fabian Wolf --- components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php | 2 +- lang/ilias_de.lang | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php index c2569917934a..1178081271b2 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php @@ -1436,7 +1436,7 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f $this->tmp_import_dir = $imp->getTemporaryImportDir(); $this->tpl->setOnScreenMessage( 'failure', - $this->lng->txt('obj_import_file_error') . '
    ' . $e->getMessage() + $this->lng->txt('obj_import_file_error') ); $this->deleteUploadedImportFile($path_to_uploaded_file_in_temp_dir); return; diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 16005e7adec1..312925c280f4 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -13271,7 +13271,7 @@ obj#:#obj_deactivate_multilang#:#Mehrsprachigkeit deaktivieren obj#:#obj_deactivate_multilang_conf#:#Wollen Sie die Unterstützung für mehrsprachige Inhalte wirklich deaktivieren? Nur die Inhalte der Standardsprache werden verfügbar bleiben. obj#:#obj_fallback_lang#:#Standardsprache obj#:#obj_features#:#Zusätzliche Funktionen -obj#:#obj_import_file_error#:#Die Datei konnte nicht importiert werden. Bitte stellen Sie sicher, dass es sich um eine ILIAS-Exportdatei (XML-Export) des gleichen Objekttyps handelt und der Dateiname unverändert ist. Fehlermeldung: +obj#:#obj_import_file_error#:#Die Datei konnte nicht importiert werden. Bitte stellen Sie sicher, dass es sich um eine ILIAS-Exportdatei (XML-Export) des gleichen Objekttyps handelt und der Dateiname unverändert ist. obj#:#obj_insert_into_clipboard#:#In Zwischenablage übernehmen obj#:#obj_inserted_clipboard#:#Die Objekte wurden in die Zwischenablage übernommen. obj#:#obj_master_lang#:#Basissprache From 41840d62d98c44b89867f1ce42f53aa9f635af03 Mon Sep 17 00:00:00 2001 From: Sagun Karki Date: Wed, 10 Jun 2026 12:14:24 +0200 Subject: [PATCH 114/333] Fix 0047887: SOAP IDOR by enforcing session-user read access in ilSoapObjectAdministration Signed-off-by: Fabian Wolf --- .../class.ilSoapObjectAdministration.php | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/soap/classes/class.ilSoapObjectAdministration.php b/components/ILIAS/soap/classes/class.ilSoapObjectAdministration.php index 6ca6cb56e500..4c09817df2dc 100755 --- a/components/ILIAS/soap/classes/class.ilSoapObjectAdministration.php +++ b/components/ILIAS/soap/classes/class.ilSoapObjectAdministration.php @@ -168,13 +168,17 @@ public function getObjectByReference(string $sid, int $a_ref_id, ?int $user_id = return $this->raiseError("Object with ID $a_ref_id has been deleted.", 'Client'); } + global $DIC; + $access = $DIC['ilAccess']; + $xml_writer = new ilObjectXMLWriter(); $xml_writer->enablePermissionCheck(true); if (is_int($user_id)) { $xml_writer->setUserId($user_id); $xml_writer->enableOperations(true); } - $xml_writer->setObjects(array($tmp_obj)); + $objs = $access->checkAccess("read", "", $a_ref_id) ? array($tmp_obj) : array(); + $xml_writer->setObjects($objs); if ($xml_writer->start()) { return $xml_writer->getXML(); } @@ -220,6 +224,9 @@ public function getObjectsByTitle(string $sid, string $a_title, ?int $user_id = $res->filter(ROOT_FOLDER_ID, true); + global $DIC; + $access = $DIC['ilAccess']; + $objs = array(); foreach ($res->getUniqueResults() as $entry) { if ($entry['type'] === 'role' || $entry['type'] === 'rolt') { @@ -228,7 +235,8 @@ public function getObjectsByTitle(string $sid, string $a_title, ?int $user_id = } continue; } - if ($tmp = ilObjectFactory::getInstanceByRefId($entry['ref_id'], false)) { + if (($tmp = ilObjectFactory::getInstanceByRefId($entry['ref_id'], false)) && + $access->checkAccess("read", "", (int) $entry['ref_id'])) { $objs[] = $tmp; } } @@ -268,6 +276,9 @@ public function searchObjects(string $sid, ?array $types, string $key, string $c ); } + global $DIC; + $access = $DIC['ilAccess']; + $highlighter = null; if (ilSearchSettings::getInstance()->enabledLucene()) { ilSearchSettings::getInstance()->setMaxHits(25); @@ -300,7 +311,7 @@ public function searchObjects(string $sid, ?array $types, string $key, string $c $objs[ROOT_FOLDER_ID] = ilObjectFactory::getInstanceByRefId(ROOT_FOLDER_ID, false); foreach ($result_ids as $ref_id => $obj_id) { $obj = ilObjectFactory::getInstanceByRefId($ref_id, false); - if ($obj instanceof ilObject) { + if ($obj instanceof ilObject && $access->checkAccess("read", "", (int) $ref_id)) { $objs[] = $obj; } } @@ -332,7 +343,7 @@ public function searchObjects(string $sid, ?array $types, string $key, string $c $objs = array(); foreach ($res->getUniqueResults() as $entry) { $obj = ilObjectFactory::getInstanceByRefId($entry['ref_id'], false); - if ($obj instanceof ilObject) { + if ($obj instanceof ilObject && $access->checkAccess("read", "", (int) $entry['ref_id'])) { $objs[] = $obj; } } @@ -381,6 +392,7 @@ public function getTreeChilds(string $sid, int $ref_id, ?array $types = null, ?i global $DIC; $tree = $DIC['tree']; + $access = $DIC['ilAccess']; if (!$target_obj = ilObjectFactory::getInstanceByRefId($ref_id, false)) { return $this->raiseError( @@ -403,7 +415,8 @@ public function getTreeChilds(string $sid, int $ref_id, ?array $types = null, ?i foreach ($tree->getChilds($ref_id, 'title') as $child) { if ($all || in_array($child['type'], $types, true)) { - if ($tmp = ilObjectFactory::getInstanceByRefId($child['ref_id'], false)) { + if (($tmp = ilObjectFactory::getInstanceByRefId($child['ref_id'], false)) && + $access->checkAccess("read", "", (int) $child['ref_id'])) { $objs[] = $tmp; } } From 820be2adbe592457518433425d1a6a9bae5ae885 Mon Sep 17 00:00:00 2001 From: kevenClausenKPG Date: Mon, 27 Jul 2026 13:45:19 +0200 Subject: [PATCH 115/333] Enable LOM metadata support for Learning Sequences (#11675) Feature Metadata --- .../classes/class.ilObjectMetaDataGUI.php | 3 +- ...ss.InitLOMForLearningSequenceMigration.php | 36 ++++++++ ...class.ilObjLearningSequenceSettingsGUI.php | 77 +++++++++++++++-- .../class.ilLearningSequenceSetupAgent.php | 5 +- .../class.ilLearningSequenceExporter.php | 67 +++++++++++++++ .../class.ilLearningSequenceImporter.php | 34 ++++++++ .../classes/class.ilObjLearningSequence.php | 9 +- .../class.ilObjLearningSequenceGUI.php | 82 ++++++++++++++++++- .../class.ilObjLearningSequenceListGUI.php | 2 - components/ILIAS/LearningSequence/module.xml | 1 + 10 files changed, 300 insertions(+), 16 deletions(-) create mode 100644 components/ILIAS/LearningSequence/Setup/class.InitLOMForLearningSequenceMigration.php diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectMetaDataGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectMetaDataGUI.php index 3ee6b6716b1b..f42859c75b45 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectMetaDataGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectMetaDataGUI.php @@ -321,7 +321,8 @@ protected function isLOMAvailable(): bool 'cmix', 'mep', 'mep:mpg', - 'wiki' + 'wiki', + 'lso' ]) ); } diff --git a/components/ILIAS/LearningSequence/Setup/class.InitLOMForLearningSequenceMigration.php b/components/ILIAS/LearningSequence/Setup/class.InitLOMForLearningSequenceMigration.php new file mode 100644 index 000000000000..569ba1be25d2 --- /dev/null +++ b/components/ILIAS/LearningSequence/Setup/class.InitLOMForLearningSequenceMigration.php @@ -0,0 +1,36 @@ + $this->lng->txt($id); $shift_trafo = $this->refinery->custom()->transformation( static fn(array $v) => current($v) @@ -125,13 +127,13 @@ protected function buildFormElements( $formElements['online'] = $if->field()->section( [ - $props->getPropertyIsOnline() + 'online' => $props->getPropertyIsOnline() ->toForm( $this->lng, $if->field(), $this->refinery ), - $ref_props->getPropertyAvailabilityPeriod() + 'availability_period' => $ref_props->getPropertyAvailabilityPeriod() ->toForm( $this->lng, $if->field(), @@ -154,9 +156,38 @@ protected function buildFormElements( $this->refinery->always(false) ]) ); + // Metadata + $custom_md = $if->field()->checkbox($this->lng->txt('obj_tool_setting_custom_metadata')) + ->withValue((bool) ilContainer::_lookupContainerSetting( + $lso->getId(), + ilObjectServiceSettingsGUI::CUSTOM_METADATA, + '0' + )) + ->withAdditionalTransformation( + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->bool(), + $this->refinery->always(false) + ]) + ); + //Taxonomies + $taxonomies = $if->field()->checkbox($this->lng->txt('obj_tool_setting_taxonomies')) + ->withValue((bool) ilContainer::_lookupContainerSetting( + $lso->getId(), + ilObjectServiceSettingsGUI::TAXONOMIES, + '0' + )) + ->withAdditionalTransformation( + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->bool(), + $this->refinery->always(false) + ]) + ); + $section_additional = $if->field()->section( [ - self::PROP_GALLERY => $gallery + self::PROP_GALLERY => $gallery, + ilObjectServiceSettingsGUI::CUSTOM_METADATA => $custom_md, + ilObjectServiceSettingsGUI::TAXONOMIES => $taxonomies ], $txt('obj_features') ); @@ -211,14 +242,42 @@ protected function update(): ?string $lso = $this->obj; $obj_props = $lso->getObjectProperties(); + $ref_props = $lso->getObjectReferenceProperties(); + + $title_and_description = $data['object'] ?? null; + if ($title_and_description instanceof TitleAndDescription) { + $obj_props->storePropertyTitleAndDescription($title_and_description); + } + + ilContainer::_writeContainerSetting( + $lso->getId(), + ilObjectServiceSettingsGUI::CUSTOM_METADATA, + ($data['additional'][ilObjectServiceSettingsGUI::CUSTOM_METADATA] ?? false) ? '1' : '0' + ); + ilContainer::_writeContainerSetting( + $lso->getId(), + ilObjectServiceSettingsGUI::TAXONOMIES, + ($data['additional'][ilObjectServiceSettingsGUI::TAXONOMIES] ?? false) ? '1' : '0' + ); + + $obj_props->storePropertyIsOnline( + $data['online']['online'] ?? $obj_props->getPropertyIsOnline()->withOffline() + ); - $obj_props->storePropertyTitleAndDescription($data['object']); - list($online, $availability) = $data['online']; - $obj_props->storePropertyIsOnline($online); - $lso->storeAvailabilityPeriod($availability); + $availability_period = $data['online']['availability_period'] ?? null; + if ($availability_period instanceof AvailabilityPeriod) { + $lso->storeAvailabilityPeriod( + $availability_period->withObjectReferenceId($lso->getRefId()) + ); + } elseif ($ref_props !== null) { + // keep current value (no-op), but ensure it stays bound to current ref id + $lso->storeAvailabilityPeriod( + $ref_props->getPropertyAvailabilityPeriod()->withObjectReferenceId($lso->getRefId()) + ); + } $settings = $lso->getLSSettings() - ->withMembersGallery($data['additional'][self::PROP_GALLERY]); + ->withMembersGallery($data['additional'][self::PROP_GALLERY] ?? false); $lso->updateSettings($settings); $obj_props->storePropertyTitleAndIconVisibility($data['common']['icon']); diff --git a/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php b/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php index 14821e4d7fd6..dc84be7b3625 100755 --- a/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php +++ b/components/ILIAS/LearningSequence/classes/Setup/class.ilLearningSequenceSetupAgent.php @@ -20,6 +20,7 @@ use ILIAS\Setup; use ILIAS\Refinery; +use ILIAS\LearningSequence\Setup\InitLOMForLearningSequenceMigration; class ilLearningSequenceSetupAgent implements Setup\Agent { @@ -102,6 +103,8 @@ public function getStatusObjective(Setup\Metrics\Storage $storage): Setup\Object */ public function getMigrations(): array { - return []; + return [ + new InitLOMForLearningSequenceMigration(), + ]; } } diff --git a/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceExporter.php b/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceExporter.php index c9122c7e633d..6383a7b78352 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceExporter.php +++ b/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceExporter.php @@ -91,12 +91,55 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ { $res = []; if ($a_entity == "lso") { + // advanced metadata + $advmd_ids = []; + foreach ($a_ids as $id) { + $rec_ids = $this->getActiveAdvMDRecords((int) $id); + foreach ($rec_ids as $rec_id) { + $advmd_ids[] = $id . ":" . $rec_id; + } + } + if ($advmd_ids !== []) { + $res[] = [ + "component" => "components/ILIAS/AdvancedMetaData", + "entity" => "advmd", + "ids" => $advmd_ids + ]; + } + // service settings $res[] = [ "component" => "components/ILIAS/ILIASObject", "entity" => "common", "ids" => $a_ids ]; + + // metadata + $md_ids = []; + foreach ($a_ids as $id) { + $md_ids[] = $id . ":0:lso"; + } + $res[] = [ + "component" => "components/ILIAS/MetaData", + "entity" => "md", + "ids" => $md_ids + ]; + + // taxonomies + $tax_ids = []; + foreach ($a_ids as $id) { + $t_ids = ilObjTaxonomy::getUsageOfObject((int) $id); + foreach ($t_ids as $t_id) { + $tax_ids[$t_id] = $t_id; + } + } + if ($tax_ids !== []) { + $res[] = [ + "component" => "components/ILIAS/Taxonomy", + "entity" => "tax", + "ids" => $tax_ids + ]; + } } // container pages @@ -120,4 +163,28 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ return $res; } + + protected function getActiveAdvMDRecords(int $a_id): array + { + $active = []; + + foreach (ilAdvancedMDRecord::_getActivatedRecordsByObjectType('lso') as $record_obj) { + foreach ($record_obj->getAssignedObjectTypes() as $obj_info) { + if ($obj_info['obj_type'] === 'lso' && (int) $obj_info['optional'] === 0) { + $active[] = $record_obj->getRecordId(); + } + + // local activation + if ( + $obj_info['obj_type'] === 'lso' && + (int) $obj_info['optional'] === 1 && + $a_id === $record_obj->getParentObject() + ) { + $active[] = $record_obj->getRecordId(); + } + } + } + + return $active; + } } diff --git a/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php b/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php index fdadca6e450c..28f7a11d4364 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php +++ b/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php @@ -59,6 +59,26 @@ public function importXmlRepresentation(string $a_entity, string $a_id, string $ LSOPageType::EXTRO->value . ':' . $a_id, LSOPageType::EXTRO->value . ':' . (string) $this->obj->getId() ); + + $a_mapping->addMapping( + 'components/ILIAS/MetaData', + 'md', + $a_id . ':0:lso', + (string) $this->obj->getId() . ':0:lso' + ); + + $a_mapping->addMapping( + "components/ILIAS/Taxonomy", + "tax_item", + "lso:obj:" . $a_id, + (string) $this->obj->getId() + ); + $a_mapping->addMapping( + "components/ILIAS/Taxonomy", + "tax_item_obj_id", + "lso:obj:" . $a_id, + (string) $this->obj->getId() + ); } public function finalProcessing(ilImportMapping $a_mapping): void @@ -77,6 +97,20 @@ public function finalProcessing(ilImportMapping $a_mapping): void $new_obj_id = $this->obj->getId(); ilPageObject::_writeParentId($pg_type, (int) $new_pg_id, (int) $new_obj_id); } + + // taxonomy usages + $maps = $a_mapping->getMappingsOfEntity("components/ILIAS/LearningSequence", "lso"); + foreach ($maps as $old => $new) { + if ($old !== "new_id" && (int) $old > 0) { + $new_tax_ids = $a_mapping->getMapping("components/ILIAS/Taxonomy", "tax_usage_of_obj", (string) $old); + if ($new_tax_ids !== "") { + $tax_ids = explode(":", (string) $new_tax_ids); + foreach ($tax_ids as $tid) { + ilObjTaxonomy::saveUsage((int) $tid, (int) $new); + } + } + } + } } public function afterContainerImportProcessing(ilImportMapping $mapping): void diff --git a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequence.php b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequence.php index 9f22bfc7dc38..4064699026e9 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequence.php +++ b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequence.php @@ -97,7 +97,8 @@ public function create(): int if (!$id) { return 0; } - $this->ref_props = $this->repo_ref_props->getFor(null); + + $this->createMetaData(); $this->raiseEvent(self::E_CREATE); return $this->getId(); @@ -108,6 +109,8 @@ public function update(): bool if (!parent::update()) { return false; } + + $this->updateMetaData(); $this->raiseEvent(self::E_UPDATE); return true; @@ -115,6 +118,8 @@ public function update(): bool public function delete(): bool { + $this->deleteMetaData(); + if (!parent::delete()) { return false; } @@ -123,6 +128,8 @@ public function delete(): bool $this->getSettingsDB()->delete($this->getId()); $this->getStateDB()->deleteFor($this->getRefId()); + ilObjTaxonomy::deleteUsagesOfObject($this->getId()); + $this->raiseEvent(self::E_DELETE); return true; diff --git a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php index 94dd319d9504..107215124c05 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php +++ b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceGUI.php @@ -51,9 +51,11 @@ * @ilCtrl_Calls ilObjLearningSequenceGUI: ilObjSurveyGUI * @ilCtrl_Calls ilObjLearningSequenceGUI: ilObjFileUploadHandlerGUI * @ilCtrl_Calls ilObjLearningSequenceGUI: ilObjLearningSequenceEditIntroGUI, ilObjLearningSequenceEditExtroGUI + * @ilCtrl_Calls ilObjLearningSequenceGUI: ilObjectMetaDataGUI, ilTaxonomySettingsGUI, ilObjTaxonomyGUI */ -class ilObjLearningSequenceGUI extends ilContainerGUI implements ilCtrlBaseClassInterface +class ilObjLearningSequenceGUI extends ilContainerGUI implements ilCtrlBaseClassInterface, \ILIAS\Taxonomy\Settings\ModifierGUIInterface { + protected \ILIAS\Taxonomy\Service $taxonomy; public const CMD_VIEW = "view"; public const CMD_LEARNER_VIEW = "learnerView"; public const CMD_CONTENT = "manageContent"; @@ -228,6 +230,7 @@ public function __construct() $this->post_wrapper = $DIC->http()->wrapper()->post(); $this->refinery = $DIC->refinery(); $this->content_style = $DIC->contentStyle(); + $this->taxonomy = $DIC->taxonomy(); $this->help->setScreenIdComponent($this->type); $this->lng->loadLanguageModule($this->type); @@ -292,6 +295,7 @@ public function executeCommand(): void } $this->tabs->activateTab(self::TAB_SETTINGS); + $this->setEditTabs(); $this->ctrl->forwardCommand($this->getGUISettings()); break; case "ilobjlearningsequencecontentgui": @@ -350,6 +354,23 @@ public function executeCommand(): void $gui = $this->object->getLocalDI()["gui.learner.lp"]; $this->ctrl->forwardCommand($gui); break; + case "ilobjectmetadatagui": + $this->tabs->activateTab("meta_data"); + $mdgui = new ilObjectMetaDataGUI($this->object); + $this->ctrl->forwardCommand($mdgui); + break; + case "iltaxonomysettingsgui": + case "ilobjtaxonomygui": + $this->tabs->activateTab(self::TAB_SETTINGS); + $this->setEditTabs("taxonomy"); + $tax_gui = $this->taxonomy->gui()->getSettingsGUI( + $this->object->getId(), + $this->lng->txt("cntr_tax_settings_info"), + true, + $this + ); + $this->ctrl->forwardCommand($tax_gui); + break; case "ilobjlearningsequenceeditintrogui": $which_page = LSOPageType::INTRO; $which_tab = self::TAB_EDIT_INTRO; @@ -519,7 +540,9 @@ public function addToNavigationHistory(): void protected function getGUIInfo(): ilInfoScreenGUI { - return new ilInfoScreenGUI($this); + $info = new ilInfoScreenGUI($this); + $info->addMetaDataSections($this->object->getId(), 0, 'lso'); + return $info; } protected function getGUIPermissions(): ilPermissionGUI @@ -668,6 +691,20 @@ public function unparticipate(): void $this->ctrl->redirectByClass('ilObjLearningSequenceLearnerGUI', self::CMD_LEARNER_VIEW); } + protected function getSubServices(): array + { + $subs = [ + ilObjectServiceSettingsGUI::CUSTOM_METADATA, + ilObjectServiceSettingsGUI::TAXONOMIES, + ilObjectServiceSettingsGUI::CALENDAR_CONFIGURATION, + ilObjectServiceSettingsGUI::TAG_CLOUD, + ilObjectServiceSettingsGUI::BADGES, + ilObjectServiceSettingsGUI::SKILLS + ]; + + return $subs; + } + /** * Initializes the header action for the object list GUI with optional subtype and sub-ID. * Disables multi-download functionality for the object list GUI if initialized. @@ -686,6 +723,26 @@ protected function initHeaderAction(?string $sub_type = null, ?int $sub_id = nul } + protected function setEditTabs(string $active_tab = "settings_misc"): void + { + $this->tabs->addSubTab( + "settings_misc", + $this->lng->txt("general"), + $this->ctrl->getLinkTargetByClass("ilobjlearningsequencesettingsgui", "settings") + ); + + if (ilContainer::_lookupContainerSetting( + $this->object->getId(), + ilObjectServiceSettingsGUI::TAXONOMIES, + '0' + )) { + $this->taxonomy->gui()->addSettingsSubTab($this->object->getId()); + } + + $this->tabs->activateTab(self::TAB_SETTINGS); + $this->tabs->activateSubTab($active_tab); + } + protected function getTabs(): void { if ($this->checkAccess("read")) { @@ -711,6 +768,17 @@ protected function getTabs(): void $this->lng->txt(self::TAB_SETTINGS), $this->getLinkTarget(self::CMD_SETTINGS) ); + + // metadata + $mdgui = new ilObjectMetaDataGUI($this->object); + $mdtab = $mdgui->getTab(); + if ($mdtab) { + $this->tabs->addTab( + "meta_data", + $this->lng->txt("meta_data"), + $mdtab + ); + } } if ($this->checkAccess("read")) { @@ -793,6 +861,16 @@ protected function addSubTabsForContent(string $active): void $this->tabs->activateSubTab($active); } + public function getProperties(int $tax_id): array + { + return []; + } + + public function getActions(int $tax_id): array + { + return []; + } + protected function checkAccess(string $which): bool { return $this->access->checkAccess($which, "", $this->ref_id); diff --git a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceListGUI.php b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceListGUI.php index 345ff97a2806..c47ed4bbfd8c 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceListGUI.php +++ b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceListGUI.php @@ -18,8 +18,6 @@ declare(strict_types=1); -declare(strict_types=1); - class ilObjLearningSequenceListGUI extends ilObjectListGUI { public function __construct() diff --git a/components/ILIAS/LearningSequence/module.xml b/components/ILIAS/LearningSequence/module.xml index 51840c7d7923..7af63f1e5882 100755 --- a/components/ILIAS/LearningSequence/module.xml +++ b/components/ILIAS/LearningSequence/module.xml @@ -25,6 +25,7 @@ repository="1" group="lso" offline_handling="1" + amet="1" > rolf htlm From 1b54123d2e3474dee02c0fa467b067dec894744d Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Thu, 9 Jul 2026 10:27:35 +0200 Subject: [PATCH 116/333] News: Sort Timeline Items Newest First See: https://mantis.ilias.de/view.php?id=47969 `NewsCollection` sorted news items by creation date ascending, so the timeline listed older entries first. Reversed the `uasort` comparator to order by newest creation date first. --- components/ILIAS/News/src/Data/NewsCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/News/src/Data/NewsCollection.php b/components/ILIAS/News/src/Data/NewsCollection.php index 4630ce467cee..5e078ee1c63e 100644 --- a/components/ILIAS/News/src/Data/NewsCollection.php +++ b/components/ILIAS/News/src/Data/NewsCollection.php @@ -442,7 +442,7 @@ public function orderByDate(): static uasort( $ordered->news_items, - fn(NewsItem $a, NewsItem $b): int => $a->getCreationDate() <=> $b->getCreationDate() + fn(NewsItem $a, NewsItem $b): int => $b->getCreationDate() <=> $a->getCreationDate() ); return $ordered; From c59e2e0b3ae48dca61bfb208ae7dfbb20290b17d Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 27 Jul 2026 16:45:24 +0200 Subject: [PATCH 117/333] User: Fix Redirect in Security See: https://mantis.ilias.de/view.php?id=48039 --- .../ILIAS/User/classes/class.ilObjUserFolderGUI.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/User/classes/class.ilObjUserFolderGUI.php b/components/ILIAS/User/classes/class.ilObjUserFolderGUI.php index 4c2c429bc94b..260c86ee91a9 100755 --- a/components/ILIAS/User/classes/class.ilObjUserFolderGUI.php +++ b/components/ILIAS/User/classes/class.ilObjUserFolderGUI.php @@ -58,6 +58,8 @@ class ilObjUserFolderGUI extends ilObjectGUI { use ilTableCommandHelper; + private const string REDIRECT_TO_SETTINGS_CMD = 'showSettings'; + public const USER_FIELD_TRANSLATION_MAPPING = [ 'visible' => 'user_visible_in_profile', 'changeable' => 'changeable', @@ -335,6 +337,10 @@ public function executeCommand(): void $this->ctrl->forwardCommand($perm_gui); break; default: + if ($cmd === self::REDIRECT_TO_SETTINGS_CMD) { + $this->ctrl->redirectByClass(AdminSettingsGUI::class, 'show'); + } + if (!$cmd) { $cmd = 'view'; } @@ -815,8 +821,8 @@ public function confirmdeleteObject(): void 'show' ); } else { - $this->ctrl->redirect( - $this, + $this->ctrl->redirectByClass( + self::class, 'view' ); } @@ -2315,7 +2321,7 @@ public function addToExternalSettingsForm(int $a_form_id): array // Missing arra ]; $fields['ps_security_protection'] = [null, null, $subitems]; - return [['generalSettings', $fields]]; + return [[self::REDIRECT_TO_SETTINGS_CMD, $fields]]; } return []; } From c76fdbad49c2c57d1f563a9290027c7697d5253b Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 27 Jul 2026 17:06:05 +0200 Subject: [PATCH 118/333] Course: fix member view --- .../ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php b/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php index 25815b5d43ac..396b55a71b17 100755 --- a/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php +++ b/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php @@ -38,7 +38,7 @@ class ilCourseParticipantsTableGUI extends ilParticipantTableGUI protected ilRbacReview $rbacReview; protected ilObjUser $user; protected Profile $profile; - protected \ILIAS\Refinery $refinery; + protected \ILIAS\Refinery\Factory $refinery; protected array $cached_user_names = []; public function __construct( From 2a0d208cf0a74f5938c44fa86336ee4611cadac2 Mon Sep 17 00:00:00 2001 From: mjansen Date: Mon, 27 Jul 2026 20:26:19 +0200 Subject: [PATCH 119/333] [FIX] User: Preserve SAML UDF attribute mappings when migrating field IDs to UUIDs Rewrite auth_ext_attr_mapping entries in-place during the User DB update so SAML custom field references are not lost after the integer-to-UUID migration (48095). --- components/ILIAS/User/src/Setup/DBUpdateSteps11.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/ILIAS/User/src/Setup/DBUpdateSteps11.php b/components/ILIAS/User/src/Setup/DBUpdateSteps11.php index 65af574b8c4a..d74ced0d5bf4 100755 --- a/components/ILIAS/User/src/Setup/DBUpdateSteps11.php +++ b/components/ILIAS/User/src/Setup/DBUpdateSteps11.php @@ -170,6 +170,9 @@ public function step_3(): void $this->db->manipulate( "UPDATE ldap_attribute_mapping SET keyword = 'udf_{$uuid}' WHERE keyword = 'udf_{$row->old_field_id}'" ); + $this->db->manipulate( + "UPDATE auth_ext_attr_mapping SET attribute = 'udf_{$uuid}' WHERE attribute = 'udf_{$row->old_field_id}'" + ); $this->db->manipulate( "UPDATE settings SET keyword = 'pmap_udf_{$uuid}' WHERE keyword = 'pmap_udf_{$row->old_field_id}'" ); From 6b3debd4adff0b962c7c5e27e42c63517b9cb703 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 28 Jul 2026 08:54:22 +0200 Subject: [PATCH 120/333] User: Drop Temp Table for UDF Updates --- components/ILIAS/User/src/Setup/Agent.php | 4 +- .../ILIAS/User/src/Setup/DBUpdateSteps11.php | 589 ------------------ .../ILIAS/User/src/Setup/DBUpdateSteps12.php | 42 ++ 3 files changed, 44 insertions(+), 591 deletions(-) delete mode 100755 components/ILIAS/User/src/Setup/DBUpdateSteps11.php create mode 100755 components/ILIAS/User/src/Setup/DBUpdateSteps12.php diff --git a/components/ILIAS/User/src/Setup/Agent.php b/components/ILIAS/User/src/Setup/Agent.php index dde989898e59..ab90067f3602 100755 --- a/components/ILIAS/User/src/Setup/Agent.php +++ b/components/ILIAS/User/src/Setup/Agent.php @@ -79,7 +79,7 @@ public function getUpdateObjective(?Setup\Config $config = null): Setup\Objectiv 'Updates for User', false, new \ilDatabaseUpdateStepsExecutedObjective( - new DBUpdateSteps11() + new DBUpdateSteps12() ), new AddReadAllAccountsPermissionObjective(), new CollectSettingsObjective($this->user_settings_contributions), @@ -92,7 +92,7 @@ public function getStatusObjective(Setup\Metrics\Storage $storage): Setup\Object { return new \ilDatabaseUpdateStepsMetricsCollectedObjective( $storage, - new DBUpdateSteps11() + new DBUpdateSteps12() ); } diff --git a/components/ILIAS/User/src/Setup/DBUpdateSteps11.php b/components/ILIAS/User/src/Setup/DBUpdateSteps11.php deleted file mode 100755 index d74ced0d5bf4..000000000000 --- a/components/ILIAS/User/src/Setup/DBUpdateSteps11.php +++ /dev/null @@ -1,589 +0,0 @@ -db = $db; - $this->uuid_factory = new UUIDFactory(); - } - - private function insertSetting( - string $keyword, - string $value - ): void { - if ($this->db->fetchObject( - $this->db->query( - "SELECT COUNT(*) cnt FROM settings WHERE module = 'common' AND keyword='{$keyword}'" - ) - )?->cnt > 0 - ) { - return; - } - - $this->db->insert( - 'settings', - [ - 'module' => [ - \ilDBConstants::T_TEXT, - 'common' - ], - 'keyword' => [ - \ilDBConstants::T_TEXT, - $keyword - ], - 'value' => [ - \ilDBConstants::T_TEXT, - $value - ], - ] - ); - } - - private function migrateBadges( - string $old_value, - string $new_value - ): void { - $query = $this->db->query("SELECT id, conf FROM badge_badge WHERE type_id='user/profile' AND conf LIKE '%$old_value%'"); - while (($badge = $this->db->fetchObject($query)) !== null) { - $config_array = unserialize($badge->conf, ['allowed_classes' => false]); - if (!array_key_exists('profile', $config_array)) { - continue; - } - $config_array['profile'] = array_map( - static function (string $v) use ($old_value, $new_value): string { - if ($v !== "chk_{$old_value}") { - return $v; - } - - return "chk_{$new_value}"; - }, - $config_array['profile'] - ); - $this->db->update( - 'badge_badge', - [ - 'conf' => [\ilDBConstants::T_TEXT, serialize($config_array)] - ], - [ - 'id' => [\ilDBConstants::T_INTEGER, $badge->id] - ] - ); - } - } - - public function step_1(): void - { - if (!$this->db->tableColumnExists('mail_template', 'att_rid')) { - $this->db->addTableColumn( - 'mail_template', - 'att_rid', - [ - 'type' => \ilDBConstants::T_TEXT, - 'length' => 64 - ] - ); - } - } - - public function step_2(): void - { - if ($this->db->tableExists('usr_data_multi') - && !$this->db->tableExists('usr_profile_data')) { - $this->db->renameTable( - 'usr_data_multi', - 'usr_profile_data' - ); - } - if ($this->db->tableExists('usr_profile_data')) { - $this->db->modifyTableColumn('usr_profile_data', 'value', ['type' => \ilDBConstants::T_CLOB]); - } - if ($this->db->sequenceExists('usr_profile_data')) { - $this->db->dropSequence('usr_profile_data'); - } - if ($this->db->tableExists('usr_profile_data') - && $this->db->tableColumnExists('usr_profile_data', 'id')) { - $this->db->dropTableColumn('usr_profile_data', 'id'); - } - if ($this->db->tableExists('usr_profile_data') - && !$this->db->indexExistsByFields('usr_profile_data', ['usr_id', 'field_id'])) { - $this->db->addIndex('usr_profile_data', ['usr_id', 'field_id'], 'uf'); - } - } - - public function step_3(): void - { - if ($this->db->tableExists('udf_data')) { - $this->db->dropTable('udf_data'); - } - - if ($this->db->sequenceExists('udf_definition')) { - /* - * 2025-07-17, sk: This needs to be done here, as we absolutely need - * the change to be ready for the next steps - */ - $this->db->modifyTableColumn('ldap_attribute_mapping', 'keyword', ['length' => 68]); - $this->db->modifyTableColumn('settings', 'keyword', ['length' => 74]); - - $this->db->renameTableColumn('udf_definition', 'field_id', 'old_field_id'); - $this->db->manipulate('ALTER TABLE udf_definition ADD COLUMN field_id VARCHAR(64) NOT NULL FIRST'); - $this->db->modifyTableColumn('udf_clob', 'field_id', ['type' => 'text', 'length' => 64]); - $this->db->modifyTableColumn('udf_text', 'field_id', ['type' => 'text', 'length' => 64]); - $fields_query = $this->db->query('SELECT old_field_id FROM udf_definition'); - while (($row = $this->db->fetchObject($fields_query))) { - $uuid = $this->uuid_factory->uuid4AsString(); - $this->db->manipulate( - "UPDATE udf_definition SET field_id = '{$uuid}' WHERE old_field_id = '{$row->old_field_id}'" - ); - $this->db->manipulate( - "UPDATE udf_clob SET field_id = '{$uuid}' WHERE field_id = '{$row->old_field_id}'" - ); - $this->db->manipulate( - "UPDATE udf_text SET field_id = '{$uuid}' WHERE field_id = '{$row->old_field_id}'" - ); - $this->db->manipulate( - "UPDATE ldap_attribute_mapping SET keyword = 'udf_{$uuid}' WHERE keyword = 'udf_{$row->old_field_id}'" - ); - $this->db->manipulate( - "UPDATE auth_ext_attr_mapping SET attribute = 'udf_{$uuid}' WHERE attribute = 'udf_{$row->old_field_id}'" - ); - $this->db->manipulate( - "UPDATE settings SET keyword = 'pmap_udf_{$uuid}' WHERE keyword = 'pmap_udf_{$row->old_field_id}'" - ); - $this->db->manipulate( - "UPDATE settings SET keyword = 'pumap_udf_{$uuid}' WHERE keyword = 'pumap_udf_{$row->old_field_id}'" - ); - - $this->migrateBadges("udf_{$row->old_field_id}", $uuid); - } - $this->db->dropTableColumn('udf_definition', 'old_field_id'); - $this->db->addPrimaryKey('udf_definition', ['field_id']); - $this->db->dropSequence('udf_definition'); - } - } - - public function step_4(): void - { - if ($this->db->tableExists('udf_text')) { - $this->db->manipulate('INSERT INTO usr_profile_data SELECT * FROM udf_text'); - $this->db->dropTable('udf_text'); - } - - if ($this->db->tableExists('udf_clob')) { - $this->db->manipulate('INSERT INTO usr_profile_data SELECT * FROM udf_clob'); - $this->db->dropTable('udf_clob'); - } - } - - public function step_5(): void - { - if (!$this->db->tableExists('usr_field_config')) { - $this->db->createTable( - 'usr_field_config', - [ - 'field_id' => [ - 'type' => \ilDBConstants::T_TEXT, - 'length' => 64, - 'notnull' => true - ], - 'visible_in_registration' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'visible_to_user' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'visible_in_lua' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'visible_in_crss' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'visible_in_grps' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'visible_in_prgs' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'changeable_by_user' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'changeable_in_lua' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'required' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'export' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'searchable' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - 'available_in_certs' => [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true - ], - ] - ); - $this->db->addPrimaryKey('usr_field_config', ['field_id']); - $this->db->insert( - 'usr_field_config', - [ - 'field_id' => [\ilDBConstants::T_TEXT, 'location'], - 'visible_in_registration' => [\ilDBConstants::T_INTEGER, 0], - 'visible_to_user' => [\ilDBConstants::T_INTEGER, 1], - 'visible_in_lua' => [\ilDBConstants::T_INTEGER, 0], - 'visible_in_crss' => [\ilDBConstants::T_INTEGER, 0], - 'visible_in_grps' => [\ilDBConstants::T_INTEGER, 0], - 'visible_in_prgs' => [\ilDBConstants::T_INTEGER, 0], - 'changeable_by_user' => [\ilDBConstants::T_INTEGER, 1], - 'changeable_in_lua' => [\ilDBConstants::T_INTEGER, 0], - 'required' => [\ilDBConstants::T_INTEGER, 0], - 'export' => [\ilDBConstants::T_INTEGER, 0], - 'searchable' => [\ilDBConstants::T_INTEGER, 0], - 'available_in_certs' => [\ilDBConstants::T_INTEGER, 0] - ] - ); - } - } - - public function step_6(): void - { - $this->db->modifyTableColumn( - 'udf_definition', - 'field_type', - [ - 'type' => \ilDBConstants::T_TEXT, - 'length' => 4000 - ] - ); - $this->db->update( - 'udf_definition', - [ - 'field_type' => [ - \ilDBConstants::T_TEXT, - \ILIAS\User\Profile\Fields\Custom\Text::class - ] - ], - [ - 'field_type' => [ - \ilDBConstants::T_TEXT, - '1' - ] - ] - ); - $this->db->update( - 'udf_definition', - [ - 'field_type' => [ - \ilDBConstants::T_TEXT, - \ILIAS\User\Profile\Fields\Custom\Select::class - ] - ], - [ - 'field_type' => [ - \ilDBConstants::T_TEXT, - '2' - ] - ] - ); - $this->db->update( - 'udf_definition', - [ - 'field_type' => [ - \ilDBConstants::T_TEXT, - \ILIAS\User\Profile\Fields\Custom\TextArea::class - ] - ], - [ - 'field_type' => [ - \ilDBConstants::T_TEXT, - '3' - ] - ] - ); - } - - public function step_7(): void - { - if (!$this->db->tableColumnExists('udf_definition', 'section')) { - $this->db->addTableColumn( - 'udf_definition', - 'section', - [ - 'type' => \ilDBConstants::T_TEXT, - 'length' => 64, - 'notnull' => true, - 'default' => AvailableSections::Other->value - ] - ); - } - } - - public function step_8(): void - { - if ($this->db->fetchObject( - $this->db->query( - 'SELECT COUNT(*) cnt FROM settings WHERE module = "common"' . PHP_EOL - . 'AND keyword="usr_settings_changeable_by_user_new_mail_notification"' - ) - )?->cnt <= 0 - ) { - $this->db->manipulate( - 'INSERT INTO settings (module, keyword, value) VALUES ' - . '("common", "usr_settings_changeable_by_user_new_mail_notification", "1"), ' - . '("common", "usr_settings_changeable_lua_new_mail_notification", "1"), ' - . '("common", "usr_settings_export_new_mail_notification", "1")' - ); - } - - $renamed_fields = [ - 'hide_own_online_status' => 'awrn_user_show', - 'bs_allow_to_contact_me' => 'allow_contact_request', - 'mail_incoming_mail' => 'incoming_mail', - 'skin_style' => 'style', - 'upload' => 'avatar', - 'sel_country' => 'selcountry', - 'country' => 'old_country', - 'selcountry' => 'country' - ]; - - foreach ($renamed_fields as $old_id => $new_id) { - $settings_query = $this->db->query( - "SELECT keyword FROM settings WHERE {$this->db->like('keyword', \ilDBConstants::T_TEXT, "%_{$old_id}")}" - ); - while (($row = $this->db->fetchObject($settings_query)) !== null) { - if ($row->keyword === 'admin_country') { - continue; - } - $this->db->update( - 'settings', - [ - 'keyword' => [ - \ilDBConstants::T_TEXT, - str_replace("_{$old_id}", "_{$new_id}", $row->keyword) - ] - ], - [ - 'module' => [ - \ilDBConstants::T_TEXT, - 'common' - ], - 'keyword' => [ - \ilDBConstants::T_TEXT, - $row->keyword - ] - ] - ); - } - - $user_query = $this->db->query( - "SELECT DISTINCT keyword FROM usr_pref WHERE {$this->db->like('keyword', \ilDBConstants::T_TEXT, "%_{$old_id}")}" - ); - while (($row = $this->db->fetchObject($user_query)) !== null) { - $this->db->update( - 'usr_pref', - [ - 'keyword' => [ - \ilDBConstants::T_TEXT, - str_replace("_{$old_id}", "_{$new_id}", $row->keyword) - ] - ], - [ - 'keyword' => [ - \ilDBConstants::T_TEXT, - $row->keyword - ] - ] - ); - } - } - - $this->migrateBadges('upload', 'avatar'); - $this->migrateBadges('selcountry', 'country'); - } - - public function step_9(): void - { - $query = $this->db->query( - "SELECT usr_id, keyword FROM usr_pref WHERE {$this->db->like('keyword', \ilDBConstants::T_TEXT, 'public_udf_%')}" - ); - while (($row = $this->db->fetchObject($query)) !== null) { - $this->db->update( - 'usr_pref', - [ - 'keyword' => [ - \ilDBConstants::T_TEXT, - str_replace('public_udf_', 'public_', $row->keyword) - ] - ], - [ - 'usr_id' => [ - \ilDBConstants::T_INTEGER, - $row->usr_id - ], - 'keyword' => [ - \ilDBConstants::T_TEXT, - $row->keyword - ] - ] - ); - } - } - - public function step_10(): void - { - if ($this->db->tableColumnExists('usr_data', 'sel_country')) { - $this->db->renameTableColumn('usr_data', 'country', 'old_country'); - $this->db->renameTableColumn('usr_data', 'sel_country', 'country'); - } - } - - public function step_11(): void - { - $this->db->modifyTableColumn('usr_pref', 'keyword', ['length' => 74]); - } - - public function step_12(): void - { - if ($this->db->fetchObject( - $this->db->query( - 'SELECT COUNT(*) cnt FROM settings WHERE module = "common"' . PHP_EOL - . 'AND keyword="usr_settings_changeable_by_user_starting_point"' - ) - )?->cnt <= 0 - ) { - $this->db->update( - 'settings', - [ - 'keyword' => [ - \ilDBConstants::T_TEXT, - 'usr_settings_changeable_by_user_starting_point' - ] - ], - [ - 'module' => [ - \ilDBConstants::T_TEXT, - 'common' - ], - 'keyword' => [ - \ilDBConstants::T_TEXT, - 'usr_starting_point_personal' - ] - ] - ); - } else { - $this->db->manipulate( - 'DELETE FROM settings WHERE module = "common" AND keyword= "usr_starting_point_personal"' - ); - } - - $this->insertSetting('usr_settings_changeable_lua_starting_point', '1'); - $this->insertSetting('usr_settings_export_starting_point', '1'); - - foreach ([ - 'last_visited', - 'timezone', - 'date_format', - 'time_format' - ] as $setting) { - $this->insertSetting("usr_settings_changeable_by_user_{$setting}", '1'); - $this->insertSetting("usr_settings_changeable_lua_{$setting}", '1'); - $this->insertSetting("usr_settings_export_{$setting}", '1'); - } - } - - public function step_13(): void - { - $this->db->update( - 'usr_pref', - ['keyword' => [\ilDBConstants::T_TEXT, 'public_avatar']], - ['keyword' => [\ilDBConstants::T_TEXT, 'public_upload']] - ); - } - - public function step_14(): void - { - if (!$this->db->tableColumnExists('usr_data', 'expiration_reminder_sent')) { - $this->db->addTableColumn( - 'usr_data', - 'expiration_reminder_sent', - [ - 'type' => \ilDBConstants::T_INTEGER, - 'length' => 1, - 'notnull' => true, - 'default' => 0 - ] - ); - } - - if ($this->db->tableColumnExists('usr_data', 'time_limit_message')) { - $this->db->update( - 'usr_data', - [ - 'expiration_reminder_sent' => [ - \ilDBConstants::T_INTEGER, - 1 - ] - ], - [ - 'time_limit_message' => [ - \ilDBConstants::T_TEXT, - 1 - ] - ] - ); - $this->db->dropTableColumn('usr_data', 'time_limit_message'); - } - - } -} diff --git a/components/ILIAS/User/src/Setup/DBUpdateSteps12.php b/components/ILIAS/User/src/Setup/DBUpdateSteps12.php new file mode 100755 index 000000000000..ae13dd802dc6 --- /dev/null +++ b/components/ILIAS/User/src/Setup/DBUpdateSteps12.php @@ -0,0 +1,42 @@ +db = $db; + $this->uuid_factory = new UUIDFactory(); + } + + public function step_1(): void + { + if ($this->db->tableExists('udf_field_id_map')) { + $this->db->dropTable('udf_field_id_map'); + } + } +} From 5c75bf31304d81b08940232e120abae23671f590 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Wed, 22 Jul 2026 17:15:19 +0200 Subject: [PATCH 121/333] ILIASObject: consistently use long description on object creation so that multilingualism does not get confused (48105) --- components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php index 1178081271b2..c397b8f1f1da 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectGUI.php @@ -1066,7 +1066,7 @@ public function saveObject(): void $new_obj->setType($this->requested_new_type); $new_obj->processAutoRating(); $new_obj->setTitle($data['title_and_description']->getTitle()); - $new_obj->setDescription($data['title_and_description']->getDescription()); + $new_obj->setDescription($data['title_and_description']->getLongDescription()); $new_obj->create(); $new_obj->getObjectProperties()->storePropertyTitleAndDescription( From 5d0ce63b54b806bb5aef7b7cbb1d4f0fe938e44a Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 28 Jul 2026 09:24:38 +0200 Subject: [PATCH 122/333] Object: Add Default Value for Info-Tab visibility See: https://mantis.ilias.de/view.php?id=46397 --- .../ILIASObject/classes/class.ilObjectServiceSettingsGUI.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectServiceSettingsGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectServiceSettingsGUI.php index c523483f4e47..897981e92bb3 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectServiceSettingsGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectServiceSettingsGUI.php @@ -108,7 +108,8 @@ public static function initServiceSettingsForm( $info->setValue("1"); $info->setChecked((bool) ilContainer::_lookupContainerSetting( $obj_id, - self::INFO_TAB_VISIBILITY + self::INFO_TAB_VISIBILITY, + '1' )); //$info->setOptionTitle($lng->txt('obj_tool_setting_info_tab')); $info->setInfo($lng->txt('obj_tool_setting_info_tab_info')); From a2ac8fd58c2a55d77eec57a977b5a385a03d95f3 Mon Sep 17 00:00:00 2001 From: Tim Schmitz <104776863+schmitz-ilias@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:35:00 +0200 Subject: [PATCH 123/333] User: skip broken users in gallery See: https://mantis.ilias.de/view.php?id=47791 --- .../User/classes/Gallery/class.ilUsersGalleryParticipants.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/ILIAS/User/classes/Gallery/class.ilUsersGalleryParticipants.php b/components/ILIAS/User/classes/Gallery/class.ilUsersGalleryParticipants.php index c1843a755106..9ee49ceb882e 100755 --- a/components/ILIAS/User/classes/Gallery/class.ilUsersGalleryParticipants.php +++ b/components/ILIAS/User/classes/Gallery/class.ilUsersGalleryParticipants.php @@ -43,6 +43,10 @@ protected function getUsers(array $usr_ids): array continue; } + if (!ilObjUser::userExists([$usr_id])) { + continue; + } + if (!($user = ilObjectFactory::getInstanceByObjId($usr_id, false)) || !($user instanceof ilObjUser)) { continue; } From d6138c3f3273f191f6da6234b1c37ad6bae1005a Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 28 Jul 2026 09:56:29 +0200 Subject: [PATCH 124/333] Form: Hidden Tag Needs String See: https://mantis.ilias.de/view.php?id=47931 --- components/ILIAS/Form/classes/class.ilTextInputGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Form/classes/class.ilTextInputGUI.php b/components/ILIAS/Form/classes/class.ilTextInputGUI.php index 4f19b4cc9df0..2fe62fd1db55 100755 --- a/components/ILIAS/Form/classes/class.ilTextInputGUI.php +++ b/components/ILIAS/Form/classes/class.ilTextInputGUI.php @@ -341,7 +341,7 @@ public function render(string $a_mode = ""): string } } } else { - $hidden = $this->getHiddenTag($postvar, $this->getValue()); + $hidden = $this->getHiddenTag($postvar, (string) $this->getValue()); } if ($hidden) { $tpl->setVariable("HIDDEN_INPUT", $hidden); From a6d70b8549ac3a045ea71d0a818d47e8a6632f1a Mon Sep 17 00:00:00 2001 From: Michael Jansen Date: Tue, 28 Jul 2026 13:55:01 +0200 Subject: [PATCH 125/333] [Docs] Rewrite and expand supported versions policy (#11740) --- docs/development/supported-versions.md | 290 +++++++++++++++++++++---- 1 file changed, 244 insertions(+), 46 deletions(-) diff --git a/docs/development/supported-versions.md b/docs/development/supported-versions.md index 077328007741..db14aedf7099 100755 --- a/docs/development/supported-versions.md +++ b/docs/development/supported-versions.md @@ -1,50 +1,239 @@ # Supported Versions -Every ILIAS version will be **fully supported** until the end of the year after -the year it was released in. **Fully supported** means that every kind of issue -that is reported for the release according to our bugfixing process is eligible -for a fix. *E.g.: A usability issue, reported for ILIAS 10 (released 2025), can -be reported in August 2026 and is eligible for a fix then.* - -Every ILIAS version will then gain **security support** for an additional year -after that. **Security support** means that we are fixing security issues only. -*E.g.: A security issue, reported for ILIAS 10 (release 2025), can be reported -in August 2027 and is eligible for a fix then. A malfunction that does make the -program crash, reported at the same moment, won't be eligible for a fix.* - - -## Timeline per Version - -With that support schedule, every version will have (roughly) the following timeline: - -| Date | ILIAS X | ILIAS (X+1) | ILIAS (X+2) | -|-----------|-------------------------|-------------------------|-------------------------| -| 20X4, Nov | Project Jour Fixe | | | -| 20X5, Oct | Coding Completed | | | -| 20X5, Nov | Start of Beta Phase | Project Jour Fixe | | -| 20X6, Mar | Release | | | -| 20X6, Oct | | Coding Completed | | -| 20X6, Nov | | Start of Beta Phase | Project Jour Fixe | -| 20X7, Mar | | Release | | -| 20X7, Oct | | | Coding Completed | -| 20X7, Dec | End of Full Support | | Start of Beta Phase | -| 20X8, Mar | | | Release | -| 20X8, Dec | End of Security Support | End of Full Support | | -| 20X9, Dec | | End of Security Support | End of Full Support | -| 20Y0, Dec | | | End of Security Support | - - -## Implications - -* If we follow this optimal timeline, users have roughly 3/4 year to update to the - next fully supported version. This can be expanded to 1 3/4 year if users skip - every other fully supported version. -* From the project planning jour fixe to the end of security support, every version is - active for a little more then four years. -* Most of the time, the community will need to keep track of four different version - in different states of their life cycle. -* Most of the changes that fix issues will need to be included in three branches, - fixes for security issues will need to be included in four branches. +> **In a nutshell** +> A new ILIAS version is released roughly every year (usually in spring). +> After its release, each version goes through **two support phases**: +> +> 1. **Full support** – *every* reported issue may be fixed +> (bugs, usability problems, security problems …). +> 2. **Security support** – *only security* issues are fixed. +> +> A simple rule of thumb: +> **Full support covers the release year plus the following year; after that +> comes one more year of security-only support.** +> Then the version reaches its end of life. + +This document explains the exact rules, shows how several versions overlap over +time, and summarises what it means for the different people who work with ILIAS. + + +## How long is a version supported? + +The two phases are defined precisely as follows: + +| Phase | Lasts until … | What gets fixed | +|-----------------------|----------------------------------------------------------|------------------------------------------| +| **Full support** | the **end of the year *after* the release year** | any kind of issue (bug, usability, security …) | +| **Security support** | the **end of the following year** (one more year) | security issues **only** | +| **End of life** | after security support ends | nothing – please upgrade | + +**Examples (ILIAS 10, released in 2025):** + +* A **usability** issue reported in **August 2026** *is* eligible for a fix – + ILIAS 10 is still in **full support** (until the end of 2026). +* A **security** issue reported in **August 2027** *is* eligible for a fix – + ILIAS 10 is then in **security support** (until the end of 2027). +* A **crash / malfunction** reported in **August 2027** is **not** eligible – + in the security phase only security issues are fixed. + + +## The life cycle of a single version + +Every version passes through the same milestones. The months below are given +**relative to its release** (releases usually happen in **March**): + +```mermaid +flowchart LR + JF["Project
    Jour Fixe
    ~16 months before"] --> CC["Coding
    Completed
    ~5 months before"] + CC --> BETA["Beta Phase
    starts
    ~4 months before"] + BETA --> REL["RELEASE
    March"] + REL --> FS["Full Support
    until end of
    next year
    "] + FS --> SEC["Security only
    + 1 more year"] + SEC --> EOL["End of Life"] + + style REL fill:#2e7d32,color:#fff + style FS fill:#1565c0,color:#fff + style SEC fill:#c62828,color:#fff + style EOL fill:#616161,color:#fff +``` + +| Milestone | When (relative to release) | Meaning | +|-------------------------|----------------------------|------------------------------------------------------| +| Project Jour Fixe | ~16 months before release | Planning of the version starts | +| Coding Completed | ~5 months before release | No new features are added; stabilisation begins | +| Start of Beta Phase | ~4 months before release | Testable pre-release available | +| **Release** | **month 0 (March)** | Full support starts | +| End of Full Support | December of *release year + 1* | Switch to security-only support | +| End of Security Support | December of *release year + 2* | End of life | + +From the first planning meeting to the end of security support, a version stays +active for **a little more than four years**. + +### How consecutive versions overlap + +A new version is already being planned and built while the previous one is still +supported, so **several life cycles run in parallel**. The table below is the +classic overview, but with concrete example versions instead of abstract +placeholders. It follows **ILIAS 12, 13 and 14** through every milestone. +Read it **top to bottom for time**; **each column is one version** +*(months are illustrative — the pattern repeats for every version)*: + +| When | ILIAS 12 | ILIAS 13 | ILIAS 14 | +|------------|--------------------------|--------------------------|--------------------------| +| 2025, Nov | Project Jour Fixe | | | +| 2026, Oct | Coding Completed | | | +| 2026, Nov | Start of Beta Phase | Project Jour Fixe | | +| 2027, Mar | **Release** | | | +| 2027, Oct | | Coding Completed | | +| 2027, Nov | | Start of Beta Phase | Project Jour Fixe | +| 2028, Mar | | **Release** | | +| 2028, Oct | | | Coding Completed | +| 2028, Nov | | | Start of Beta Phase | +| 2028, Dec | End of Full Support | | | +| 2029, Mar | | | **Release** | +| 2029, Dec | End of Security Support | End of Full Support | | +| 2030, Dec | | End of Security Support | End of Full Support | +| 2031, Dec | | | End of Security Support | + +### Which version is in which phase each year + +The same information, but viewed **per calendar year** — this is usually the +question administrators and project managers actually ask: *"What is supported +**this** year?"* + +| Year | ILIAS 12 | ILIAS 13 | ILIAS 14 | +|------|-------------------|-------------------|-------------------| +| 2026 | Beta (from Nov) | – | – | +| 2027 | **Full support** | Beta (from Nov) | – | +| 2028 | **Full support** | **Full support** | Beta (from Nov) | +| 2029 | Security only | **Full support** | **Full support** | +| 2030 | End of life | Security only | **Full support** | +| 2031 | End of life | End of life | Security only | + +Reading a single row shows the recurring pattern: in a typical year there are +**two versions in full support** plus **one older version in security-only +support**. For example, in **2029** both ILIAS 13 and ILIAS 14 are fully +supported, while ILIAS 12 only receives security fixes. + + +## Concrete example with real versions + +To avoid abstract placeholders, here is how consecutive versions line up on the +calendar. *(Exact dates are illustrative; the binding rule is the two-phase +definition above.)* + +| Version | Released | Full support until | Security support until | +|-----------|----------|--------------------|------------------------| +| ILIAS 12 | 2027 | end of **2028** | end of **2029** | +| ILIAS 13 | 2028 | end of **2029** | end of **2030** | +| ILIAS 14 | 2029 | end of **2030** | end of **2031** | +| ILIAS 15 | 2030 | end of **2031** | end of **2032** | + +Because the phases overlap, **several versions are supported at the same time**. +The Gantt chart below makes this visible — read it top to bottom (versions) and +left to right (years): + +```mermaid +gantt + title ILIAS Support Phases Over Time (illustrative) + dateFormat YYYY-MM-DD + axisFormat %Y + + section ILIAS 12 + Beta :done, 2026-11-01, 2027-03-01 + Full support :active, 2027-03-01, 2028-12-31 + Security only :crit, 2029-01-01, 2029-12-31 + + section ILIAS 13 + Beta :done, 2027-11-01, 2028-03-01 + Full support :active, 2028-03-01, 2029-12-31 + Security only :crit, 2030-01-01, 2030-12-31 + + section ILIAS 14 + Beta :done, 2028-11-01, 2029-03-01 + Full support :active, 2029-03-01, 2030-12-31 + Security only :crit, 2031-01-01, 2031-12-31 + + section ILIAS 15 + Beta :done, 2029-11-01, 2030-03-01 + Full support :active, 2030-03-01, 2031-12-31 + Security only :crit, 2032-01-01, 2032-12-31 +``` + +**Legend:** grey = beta / pre-release · blue = full support · red = security +only. Draw an imaginary vertical line at any year and you will usually cross +**two versions in full support** and **one version in security-only support**. + + +## What this means for you + +Two very different groups work with these support phases. **Authorities** build +and safeguard ILIAS itself; **Users** run and operate ILIAS in their +organisations. Each group cares about a different part of the cycle. + +### Authorities (Code, Concept, Test Case Curation, Security) + +These are the roles responsible for *producing* and *safeguarding* the software. +Their key concern is **into how many versions a change has to flow**. + +* **Code (maintainers / developers)** + * A normal bug fix typically has to be applied to **three branches** — trunk + plus the **two fully supported versions**. + * Write fixes so they can be cleanly back-ported across these branches. + +* **Concept (product & feature planning)** + * Plan features around the milestones of the *upcoming* version — from + *Project Jour Fixe* to *Coding Completed* (see the life-cycle chapter). + +* **Test Case Curation** + * You decide which scenarios, workflows and edge cases must be covered before a + release can be considered safe. Your test cases should reflect what + institutions actually do with ILIAS — not only what developers build. + * The **Beta Phase** (from ~November before the March release) is when your + curated test cases matter most: new and changed behaviour is available for + verification, but the release is not yet final. + +* **Security** + * A security fix has the widest reach: it typically has to be applied to + **four branches** — trunk plus the two fully supported versions **plus** the + version that is still in security-only support. + * Security is the *only* kind of fix that a version keeps receiving after it + has left full support. + +### Users (Organisation Administrators, Help Desk, Domain Experts) + +These are the roles that *operate* ILIAS at an institution and support its +day-to-day use. Their key concern is **which version to run and when to upgrade**. + +* **Organisation administrators (system operations)** + * You have roughly **¾ of a year** to move to the next fully supported version + before your current one leaves full support. + * If you upgrade only every *other* version, you still stay within full + support, but your planning runway across two cycles shrinks to about + **1¾ years**. + * Once a version is **security only**, plan the upgrade — normal bugs will no + longer be fixed. After end of life, upgrade as soon as possible. + +* **Help Desk / support staff** + * Before promising/requesting a fix, check the version's phase: + * **Full support** → any issue may be fixed. + * **Security only** → only security issues are fixed; other reports are + moved to a still fully supported version (see below). + * **End of life** → the user must upgrade. + +* **Domain experts of institutions** + * You design and run e-learning scenarios (courses, assessments, collaboration + workflows …) and often depend on **new ILIAS features** to realise them. + * New features appear in the **upcoming release** — first in beta, then in the + March release. Plan your scenarios around that rhythm: what you need may not + exist in your current version yet. + * If you stay on an older version (especially one in **security-only** + support), you will **not** receive new features — only security fixes. To + use new capabilities, your institution must upgrade to a fully supported + version. + * Use the beta phase to try out whether a planned scenario works with the new + features before your institution upgrades. + ## Transition to Security Support @@ -52,6 +241,15 @@ When a version transitions from **full support** to **security support** (at the end of the year), open issues in the Mantis bug tracker for this version are handled as follows to ensure that reported problems are not lost: +```mermaid +flowchart TD + A["Version enters
    security-only phase"] --> B{"Issue in an eligible
    status?"} + B -->|no| C["Leave as is"] + B -->|yes| D["Move 'Target Version' to the next
    still fully supported version"] + D --> E["Assignee re-checks whether the
    issue still exists in maintained versions"] + E --> F["Add a comment informing
    the reporter"] +``` + 1. **Eligibility for Transition**: All issues with the following status are considered: `open`, `unassigned`, `feedback`, `needs JF decision`, `postponed`, `funding needed`, `assigned`, `fixing acc to prio`. @@ -68,4 +266,4 @@ are handled as follows to ensure that reported problems are not lost: entered the security-fix-only phase. We have moved this issue to the next maintained version for further investigation." -This applies to issues for ILIAS 10 or greater. \ No newline at end of file +This applies to issues for ILIAS 10 or greater. From 674a07bdb237105b437fce08e57dc50e9ba85f61 Mon Sep 17 00:00:00 2001 From: Fabian Schmid Date: Tue, 28 Jul 2026 12:02:08 +0200 Subject: [PATCH 126/333] =?UTF-8?q?[FIX]=2048047:=20=C3=84nderungen=20im?= =?UTF-8?q?=20Content=20Style=20wirken=20sich=20nicht=20aus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entries inside a container are stored relative since Mantis 45580 / 47237, e.g. "style.css". Containers written before that hold the same file as "/style.css", and once such a container is written again both variants exist side by side. Streams::ofFileInsideZIP() looked up the variant with the leading slash first, so consumers kept receiving the outdated legacy entry: a content style is written correctly but never changes for the user. Newly created styles are not affected, copies inherit the problem with the cloned ZIP, and export/import produces a clean container. ofFileInsideZIP() now looks up the relative entry first and falls back to the legacy one, which fixes existing containers without touching them - the ZIP stream wrapper matches entry names literally, so no container can be resolved to the wrong entry by accident. Additionally the legacy entries are cleaned up over time: addUploadToContainer() no longer produces entries with a leading slash itself when the parent path inside the container is empty, and removePathInsideContainer() removes both variants of the given path. --- .../ILIAS/Filesystem/src/Stream/Streams.php | 28 +++-- .../tests/Stream/FileInsideZIPTest.php | 105 ++++++++++++++++++ .../src/Resource/ResourceBuilder.php | 39 ++++++- .../tests/Resource/EnsurePathInZipTest.php | 24 ++++ 4 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 components/ILIAS/Filesystem/tests/Stream/FileInsideZIPTest.php diff --git a/components/ILIAS/Filesystem/src/Stream/Streams.php b/components/ILIAS/Filesystem/src/Stream/Streams.php index 81f55368948b..c9674633f9d7 100755 --- a/components/ILIAS/Filesystem/src/Stream/Streams.php +++ b/components/ILIAS/Filesystem/src/Stream/Streams.php @@ -83,16 +83,24 @@ public static function ofReattachableResource($resource): ReattachableStream public static function ofFileInsideZIP(string $path_to_zip, string $path_inside_zip): ZIPStream { - // we try to open the zip file with the path inside the zip file, once with a leading slash and once without - try { - $resource = @fopen('zip://' . $path_to_zip . '#/' . $path_inside_zip, 'rb'); - } catch (\Throwable) { - $resource = null; - } - try { - $resource = $resource ?: @fopen('zip://' . $path_to_zip . '#' . $path_inside_zip, 'rb'); - } catch (\Throwable) { - $resource = null; + // Entries are stored relative since Mantis 45580 / 47237, therefore that variant + // is tried first. Containers written before still hold entries with a leading + // slash, they are covered by the second candidate. If a container holds both + // variants of a file, the relative one is the up to date one (Mantis 48047): + // the ZIP stream wrapper matches names literally, so the entry we do not ask for + // is never returned by accident. + $relative_path = ltrim($path_inside_zip, '/'); + $resource = null; + + foreach ([$relative_path, '/' . $relative_path] as $candidate) { + try { + $resource = @fopen('zip://' . $path_to_zip . '#' . $candidate, 'rb') ?: null; + } catch (\Throwable) { + $resource = null; + } + if ($resource !== null) { + break; + } } if (!is_resource($resource)) { diff --git a/components/ILIAS/Filesystem/tests/Stream/FileInsideZIPTest.php b/components/ILIAS/Filesystem/tests/Stream/FileInsideZIPTest.php new file mode 100644 index 000000000000..8f580c88e1d8 --- /dev/null +++ b/components/ILIAS/Filesystem/tests/Stream/FileInsideZIPTest.php @@ -0,0 +1,105 @@ + + */ +final class FileInsideZIPTest extends TestCase +{ + private string $zip_file; + + protected function setUp(): void + { + parent::setUp(); + $this->zip_file = tempnam(sys_get_temp_dir(), 'irss_zip_read_test_'); + } + + protected function tearDown(): void + { + parent::tearDown(); + @unlink($this->zip_file); + } + + /** + * @param array $entries + */ + private function buildZip(array $entries): void + { + $zip = new ZipArchive(); + $zip->open($this->zip_file, ZipArchive::OVERWRITE); + foreach ($entries as $name => $content) { + $zip->addFromString($name, $content); + } + $zip->close(); + } + + public function testRelativeEntryIsRead(): void + { + $this->buildZip(['style.css' => 'relative']); + + $stream = Streams::ofFileInsideZIP($this->zip_file, 'style.css'); + + $this->assertSame('relative', (string) $stream); + } + + public function testLegacyEntryIsStillRead(): void + { + $this->buildZip(['/style.css' => 'legacy']); + + $stream = Streams::ofFileInsideZIP($this->zip_file, 'style.css'); + + $this->assertSame('legacy', (string) $stream); + } + + public function testRelativeEntryWinsOverLegacyEntry(): void + { + $this->buildZip(['/style.css' => 'legacy', 'style.css' => 'relative']); + + $stream = Streams::ofFileInsideZIP($this->zip_file, 'style.css'); + + $this->assertSame('relative', (string) $stream); + } + + public function testRelativeEntryWinsAlsoIfTheRequestedPathHasALeadingSlash(): void + { + $this->buildZip(['/style.css' => 'legacy', 'style.css' => 'relative']); + + $stream = Streams::ofFileInsideZIP($this->zip_file, '/style.css'); + + $this->assertSame('relative', (string) $stream); + } + + public function testUnknownEntryThrows(): void + { + $this->buildZip(['style.css' => 'relative']); + + $this->expectException(\InvalidArgumentException::class); + Streams::ofFileInsideZIP($this->zip_file, 'does_not_exist.css'); + } +} diff --git a/components/ILIAS/ResourceStorage/src/Resource/ResourceBuilder.php b/components/ILIAS/ResourceStorage/src/Resource/ResourceBuilder.php index 00dd9737e7ed..03aedc87c737 100755 --- a/components/ILIAS/ResourceStorage/src/Resource/ResourceBuilder.php +++ b/components/ILIAS/ResourceStorage/src/Resource/ResourceBuilder.php @@ -633,10 +633,33 @@ private function ensurePathInZIP(\ZipArchive $zip, string $path, bool $is_file): return $path_inside_container . '/' . $filename; } + /** + * Entries have been stored with a leading slash before Mantis 45580 / 47237, + * e.g. "/style.css" instead of "style.css". Both variants of an entry can exist + * side by side in containers of that age, so removing an entry has to cover both - + * otherwise the legacy entry stays in the container forever (Mantis 48047). + * + * @return string[] empty if the given path does not address an entry at all + */ + private function pathVariantsInZIP(string $path): array + { + $path = ltrim($path, '/'); + if ($path === '') { + return []; + } + + return [$path, '/' . $path]; + } + public function removePathInsideContainer( StorableContainerResource $container, string $path_inside_container, ): bool { + $paths_to_remove = $this->pathVariantsInZIP($path_inside_container); + if ($paths_to_remove === []) { + return false; + } + $revision = $container->getCurrentRevisionIncludingDraft(); $stream = $this->extractStream($revision); @@ -645,15 +668,21 @@ public function removePathInsideContainer( $zip = new \ZipArchive(); $zip->open($stream->getMetadata()['uri']); - $return = $zip->deleteName($path_inside_container); + $return = false; + foreach ($paths_to_remove as $path_to_remove) { + $return = $zip->deleteName($path_to_remove) || $return; + } // remove all files inside the directory for ($i = 0; $i < $zip->numFiles; $i++) { $path = $zip->getNameIndex($i); if ($path === false) { continue; } - if (str_starts_with($path, $path_inside_container)) { - $zip->deleteIndex($i); + foreach ($paths_to_remove as $path_to_remove) { + if (str_starts_with($path, $path_to_remove)) { + $zip->deleteIndex($i); + break; + } } } @@ -686,7 +715,9 @@ public function addUploadToContainer( $parent_path_inside_container = $this->ensurePathInZIP($zip, $parent_path_inside_container, false); - $path_inside_zip = rtrim($parent_path_inside_container, '/') . '/' . $result->getName(); + // an empty parent path must not result in an entry like "/file.txt", + // entries are stored relative since Mantis 45580 / 47237 + $path_inside_zip = ltrim(rtrim($parent_path_inside_container, '/') . '/' . $result->getName(), '/'); $return = $zip->addFile( $result->getPath(), diff --git a/components/ILIAS/ResourceStorage/tests/Resource/EnsurePathInZipTest.php b/components/ILIAS/ResourceStorage/tests/Resource/EnsurePathInZipTest.php index d7829479466a..844c7d43d088 100644 --- a/components/ILIAS/ResourceStorage/tests/Resource/EnsurePathInZipTest.php +++ b/components/ILIAS/ResourceStorage/tests/Resource/EnsurePathInZipTest.php @@ -108,4 +108,28 @@ public function testWritingRootFileProducesRelativeEntry(): void } $this->assertContains('index.html', $names); } + + /** + * Mantis 48047: containers written before the fix above still contain entries + * like "/style.css" next to the relative entry of the same file. Removing an + * entry has to cover both variants, otherwise the legacy one stays forever. + */ + #[DataProvider('pathVariantProvider')] + public function testPathVariantsCoverLegacyEntries(string $input, array $expected): void + { + $method = new \ReflectionMethod(ResourceBuilder::class, 'pathVariantsInZIP'); + $builder = (new \ReflectionClass(ResourceBuilder::class))->newInstanceWithoutConstructor(); + + $this->assertSame($expected, $method->invoke($builder, $input)); + } + + public static function pathVariantProvider(): \Iterator + { + yield 'root file' => ['style.css', ['style.css', '/style.css']]; + yield 'root file, leading slash' => ['/style.css', ['style.css', '/style.css']]; + yield 'nested file' => ['images/header.png', ['images/header.png', '/images/header.png']]; + yield 'directory' => ['images/', ['images/', '/images/']]; + yield 'empty path' => ['', []]; + yield 'slash only' => ['/', []]; + } } From 2fcbcd069ab29b2bb4e4717bf6009664eb77d116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schweigert=20=28FAU=29?= <162575681+andreschweigert@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:04:34 +0200 Subject: [PATCH 127/333] Document SOAP interface access restrictions Document SOAP interface access restrictions --- docs/configuration/secure.md | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/configuration/secure.md b/docs/configuration/secure.md index 277452b95268..0ccda71d1715 100755 --- a/docs/configuration/secure.md +++ b/docs/configuration/secure.md @@ -17,6 +17,7 @@ * [Suppress server signature and PHP version information](#suppress-server-signature-and-php-version-information) * [deny access or restrict to several files or locations](#deny-access-or-restrict-to-several-files-or-locations) + [ILIAS setup](#ilias-setup) + + [Restrict access to the SOAP interface](#restrict-access-to-the-soap-interface) + [Prevent blacklisted files of beeing served by the webserver](#prevent-blacklisted-files-of-beeing-served-by-the-webserver) + [Prevent execution of PHP-Code in data-directory](#prevent-execution-of-php-code-in-data-directory) + [Deny Access to local Git-Directory](#deny-access-to-local-git-directory) @@ -46,6 +47,7 @@ For a better identification, they will describe here: | %EXTERNALDATA% | ILIAS data directory outside of the web document root | | %LOGDIR% | path to the directory containing log files | | %CLIENTID% | the client name of the ILIAS installation | +| %UPSTREAM% | name or address of the ILIAS backend/upstream | ## Firewall @@ -547,6 +549,45 @@ Nginx: Please add the whitelisted ip address (%IPADDRESS%) to grant access to ILIAS setup here. +### Restrict access to the SOAP interface + +ILIAS provides a SOAP web service interface (`/soap/server.php`) which exposes a number of web service functions. To reduce the attack surface, you SHOULD restrict access to this endpoint on the web server level so that only trusted hosts can reach it. If you don't make use of the SOAP interface at all, you MAY additionally disable the SOAP user administration via the ILIAS configuration file and by running the ILIAS update command afterwards. Note that this ILIAS setting does NOT block the `server.php` endpoint itself; a web server restriction is required for that. + +ILIAS uses SOAP internally for some asynchronous operations, most notably the cloning/copying of object trees (`ilClone` via `ilSoapUtils::callNextNode`). These calls are issued against the server's own configured (public) URL and, depending on name resolution on the host, originate either from the loopback address (`127.0.0.1`) or from the server's own IP address. You MUST whitelist this internal source address as well, otherwise these background operations will silently fail while the web interface keeps working. + +Apache2: + +``` + + Require all denied + Require ip %IPADDRESS% + +``` + +Nginx: + +``` + location /soap/ { + allow %IPADDRESS%; + deny all; + } +``` + +Please add all whitelisted ip addresses (%IPADDRESS%) here, including the internal loopback/server address as well as any external consumers (e.g. campus/identity management provisioning, cron-triggered SOAP jobs). + +**note:** +With Nginx, place the `allow`/`deny` directives in the same proxied `location` block as the `proxy_pass` directive. With Apache2, the `` block above also applies to reverse-proxied requests (`ProxyPass`), as access control is evaluated before the request is handed to the proxy handler — no separate configuration is required. + +Nginx: + +``` + location /soap/ { + allow %IPADDRESS%; + deny all; + proxy_pass http://%UPSTREAM%; + } +``` + ### Prevent blacklisted files of beeing served by the webserver If somebody tries to upload a file with a filetype blacklisted by the upload settings, the upload will take place, but the file will be renamed to `filename.sec`. The webserver should not serve this file anymore to it's visitors as the file may consists of malicious software. From 75573473a33c0035a0978de35a2f5195826cf369 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Tue, 28 Jul 2026 16:28:11 +0200 Subject: [PATCH 128/333] copage: added maintenance json example --- components/ILIAS/COPage/code_maintenance.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 components/ILIAS/COPage/code_maintenance.json diff --git a/components/ILIAS/COPage/code_maintenance.json b/components/ILIAS/COPage/code_maintenance.json new file mode 100644 index 000000000000..ec95d9a6c0da --- /dev/null +++ b/components/ILIAS/COPage/code_maintenance.json @@ -0,0 +1,27 @@ +{ + "issues": { + "static-method-declarations": { + "desc": "The component declares > 100 static methods that reduce dependency management capabilities and testability. The goal is to reduce this number significantly.", + "min": "2000", + "max": "10000", + "funding": [ + { + "release": "12", + "amount": "2000", + "summary": "The number of static methods has been reduced to 70." + } + ] + }, + "large-classes": { + "desc": "The component includes > 20 code classes with > 500 lines of code each. This usually violates single responsibility principles and makes the code harder to read and test. The goal is to reduce this number significantly.", + "min": "2000", + "max": "10000", + "funding": [ + { + "release": "12", + "amount": "3000" + } + ] + } + } +} From 48d07425281a0833188eaedb584d5a419a32fd45 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Tue, 28 Jul 2026 17:06:02 +0200 Subject: [PATCH 129/333] copage: fixed example --- components/ILIAS/COPage/code_maintenance.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/COPage/code_maintenance.json b/components/ILIAS/COPage/code_maintenance.json index ec95d9a6c0da..d5f1f74400c3 100644 --- a/components/ILIAS/COPage/code_maintenance.json +++ b/components/ILIAS/COPage/code_maintenance.json @@ -1,6 +1,7 @@ { - "issues": { - "static-method-declarations": { + "issues": [ + { + "title": "Static Method Declarations", "desc": "The component declares > 100 static methods that reduce dependency management capabilities and testability. The goal is to reduce this number significantly.", "min": "2000", "max": "10000", @@ -12,16 +13,18 @@ } ] }, - "large-classes": { + { + "title": "Large Classes", "desc": "The component includes > 20 code classes with > 500 lines of code each. This usually violates single responsibility principles and makes the code harder to read and test. The goal is to reduce this number significantly.", "min": "2000", "max": "10000", "funding": [ { "release": "12", - "amount": "3000" + "amount": "3000", + "summary": "The number of large classes been reduced to 15." } ] } - } + ] } From 3eba6169d04b4dc1db81e93a4497f22ebfc132b9 Mon Sep 17 00:00:00 2001 From: Rob Falkenstein Date: Tue, 28 Jul 2026 17:14:09 +0200 Subject: [PATCH 130/333] picked 11 installation changes to trunk for further revision --- components/ILIAS/Setup/minimal-config.json | 8 +- docs/configuration/install.md | 372 ++++++++++----------- 2 files changed, 188 insertions(+), 192 deletions(-) diff --git a/components/ILIAS/Setup/minimal-config.json b/components/ILIAS/Setup/minimal-config.json index 41d9260ed3d5..fb56a291e991 100755 --- a/components/ILIAS/Setup/minimal-config.json +++ b/components/ILIAS/Setup/minimal-config.json @@ -9,13 +9,13 @@ "data_dir" : "/var/lib/ilias" }, "http" : { - "path" : "http://demo1.cat06.de" + "path" : "http://www.example.com" }, "systemfolder" : { "contact" : { - "firstname" : "Richard", - "lastname" : "Klees", - "email" : "richard.klees@concepts-and-training.de" + "firstname" : "Lucy", + "lastname" : "Snowe", + "email" : "lucysnowe@ilias.de" } } } diff --git a/docs/configuration/install.md b/docs/configuration/install.md index ba021b2fc26c..4d75b7f932a4 100755 --- a/docs/configuration/install.md +++ b/docs/configuration/install.md @@ -1,8 +1,8 @@ # ILIAS Installation -This is the installation guide for ILIAS 11, providing step-by-step instructions to set up all necessary components, -including the web server, database, and ILIAS code. Follow these instructions carefully to ensure a successful -installation of the e-learning software. Each section will guide you through the required configurations and setups +This is the installation guide for ILIAS 11, providing step-by-step instructions to set up all necessary components, +including the web server, database, and ILIAS code. Follow these instructions carefully to ensure a successful +installation of the e-learning software. Each section will guide you through the required configurations and setups for a fully functional ILIAS environment. # Table of Contents @@ -10,36 +10,36 @@ for a fully functional ILIAS environment. - [System Requirements](#system-requirements) - * [Hardware](#hardware) - * [Supported Software Setup and Reference System](#supported-system) + * [Hardware](#hardware) + * [Supported Software Setup and Reference System](#supported-system) - [Installation on Ubuntu 24.04](#installation-on-linux) - * [Install Dependencies](#install-dependencies) - * [Webserver Installation/Configuration](#install-webserver) - * [Database Installation/Configuration](#install-database) - * [Get the Code and Install ILIAS](#get-code) - * [Install ILIAS](#install-ilias) - * [Install Further Components](#install-further) - * [Install Plugins and Styles](#install-plugins-and-styles) + * [Install Dependencies](#install-dependencies) + * [Webserver Installation/Configuration](#install-webserver) + * [Database Installation/Configuration](#install-database) + * [Get the Code and Install ILIAS](#get-code) + * [Install ILIAS](#install-ilias) + * [Install Further Components](#install-further) + * [Install Plugins and Styles](#install-plugins-and-styles) - [Backup ILIAS](#backup-ilias) - [Upgrading ILIAS](#upgrading-ilias) - * [Minor Upgrade](#minor-upgrade) - * [Major Upgrade](#major-upgrade) - * [Update the Database](#update-the-database) - * [Information on Updates](#information-updates) + * [Minor Upgrade](#minor-upgrade) + * [Major Upgrade](#major-upgrade) + * [Update the Database](#update-the-database) + * [Information on Updates](#information-updates) - [Connect and Contribute](#connect-and-contribute) - [Appendix](#appendix) - * [Upgrading Dependencies](#upgrading-dependencies) - * [Configure Cron Jobs](#configurate-cron) - * [Configure WebDAV](#webdav-configuration) - * [Hardening and Security Guidance](#hardening-and-security-guidance) - * [MySQL Strict Mode (5.7+)](#mysql-strict-mode-57) + * [Upgrading Dependencies](#upgrading-dependencies) + * [Configure Cron Jobs](#configure-cron) + * [Configure WebDAV](#webdav-configuration) + * [Hardening and Security Guidance](#hardening-and-security-guidance) + * [MySQL Strict Mode (5.7+)](#mysql-strict-mode-57) # System Requirements -The necessary hardware to run an ILIAS installation is always dependent from the number of users and the kind of usage. +The necessary hardware to run an ILIAS installation always depends on the number of users and the kind of usage. ## Hardware @@ -58,16 +58,16 @@ few GBs for the database. ## Supported Software Setup and Reference System -The following software versions are required/supported for ILIAS 11. The table below lists these versions alongside the +The following software versions are required/supported for ILIAS 11. The table below lists these versions alongside the current configuration of the [ILIAS test server](https://test11.ilias.de), which serves as a reference system. | Package | Version | Reference System | |--------------|--------------------------------------------------------|------------------| | Distribution | current version of Debian GNU Linux, Ubuntu or RHEL | Ubuntu 22.04 LTS | -| Database | MySQL 8.4 - 9.7 or MariaDB 11.4 - 12.3 | MariaDB 11.8 | -| PHP | 8.4, 8.5 | 8.5 | +| Database | MySQL >8.0.21 or MariaDB 10.5 - 11.8 | MariaDB 10.6.18 | +| PHP | 8.3, 8.4 | 8.4 | | Webserver | nginx: 1.12.x – 1.18.x, Apache: ≥ 2.4.x | Apache 2.4.52 | -| JDK | Open JDK Runtime 17, 21 or 25 LTS | OpenJDK 21 | +| JDK | Open JDK Runtime 11, 17 or 21 LTS | OpenJDK 17 | | Node.js | 22 (LTS), 24 Recommended: 24 | v24.10.0 | | Ghostscript | 10.x | 9.55 | | Imagemagick | 6.9.x | 6.9.11 | @@ -84,31 +84,31 @@ Package names may vary depending on the Linux distribution. # Installation on Ubuntu 24.04 Depending on your Linux Distribution, you have several ways to install the required -dependencies. We recommend to always use your distributions package manager to -easily keep your packages up to date avoiding security issues. +dependencies. We recommend that you always use your distribution's package manager in order +to keep your packages up to date and avoid security issues. -In this guide we choose Ubuntu 24.04 because it already meets the recommended PHP version 8.3. -For other Ubuntu or Debian-based Linux systems, we recommend using [DEB.SURY.ORG](https://deb.sury.org/) to install the correct +In this guide we have chosen Ubuntu 24.04 because it already meets the recommended PHP version 8.3. +For other Ubuntu or Debian-based Linux systems, we recommend using [DEB.SURY.ORG](https://deb.sury.org/) to install the correct PHP version later on. ## Install Dependencies -`openjdk-21-jdk` and `maven` are optional and are used for the ILIAS RPC server for search indexing and certificate generation. +`openjdk-17-jdk` and `maven` are optional and are used for the ILIAS RPC server for search indexing and certificate generation. `git` is required if the source code is obtained directly from GitHub. `nodejs` and `npm` are required as well if you get the source directly to download the javascript dependencies in the installation process. -Alternatively, they can be obtained directly from the distribution package at [Nodesource](https://deb.nodesource.com/) to select appropriate nodejs versions according to the [Recommended Setup for Running ILIAS](#recommended-setup-for-running-ilias). +Alternatively, they can be obtained directly from the distribution package at [Nodesource](https://nodejs.org/en/download) to select appropriate nodejs versions according to the [Recommended Setup for Running ILIAS](#recommended-setup-for-running-ilias). `ffmpeg` is optionally used to optimise media files, and `ghostscript` is optionally used to create file previews. ```shell apt update -apt update zip unzip openjdk-21-jdk maven ffmpeg git ghostscript nodejs npm +apt update zip unzip openjdk-17-jdk maven ffmpeg git ghostscript nodejs npm ``` ## Webserver Installation/Configuration -In this guide, we use Apache2 as the web server, utilizing `libapache2-mod-php` to process PHP files. +In this guide, we use Apache2 as the web server, utilizing `libapache2-mod-php` to process PHP files. Other web servers capable of processing PHP, such as Nginx or Apache2 with FCGI and PHP-FPM, can also be used. **Required PHP Extensions:** @@ -118,27 +118,29 @@ gd, dom, xsl, pdo, pdo_mysql, curl, json, simplexml, libxml, xml, zip, imagick, **Optional PHP Extensions:** -* `soap` for SOAP user administration +* `soap` for SOAP user administration * `ldap` for LDAP user authentication -The PHP packet manager Composer is necessary to download all external PHP dependencies and for PHP autoloading. -Alternatively, it can be obtained directly from [getcomposer.org](https://getcomposer.org/download/). +The PHP packet manager Composer is necessary to download all external PHP dependencies and for PHP autoloading. +Alternatively, it can be obtained directly from [getcomposer.org](https://getcomposer.org/download/). Composer may be optional when using the prepacked ILIAS from [Download & Releases](https://docu.ilias.de/go/pg/197851_35), but it is necessary when using plugins to rebuild the PHP autoload classmap. ```shell apt install apache2 libapache2-mod-php php php-gd php-xsl php-imagick php-curl php-mysql php-soap php-ldap composer ``` -Create a directory for the html sources (e.g. `/var/www/ilias`) which is referenced in the apache2 vhost and also a directory outside the web servers docroot (e.g. `/var/www/files`) for files stored by ILIAS. -Make sure that the web server is the owner of the files and directories that were created by changing the group and owner to www-data (on Debian/Ubuntu) or apache (on RHEL). +Create a directory for the html sources (e.g. `/var/www/ilias`) which is referenced in the apache2 vhost (the apache2 vhost will use `/var/www/ilias/public`) and also a directory outside the web server's docroot (e.g. `/var/www/files`) for files stored by ILIAS. +Make sure that the web server user is the owner of the files and directories that were created by changing the group and owner to `www-data` (on Debian/Ubuntu), `apache` (on RHEL) or `wwwrun` (on SLES). In addition to the file folder, ILIAS also needs a place to create the log files (e.g. `/var/www/logs`). The 'ilias.log' can be viewed there later, as well as all error_log files, which are created in case of errors and are referenced in ILIAS by an errorcode. -Also, to store the ILIAS configuration, which is later used to configurate ILIAS, we create the folder /var/www/config. To prevent future issues with npm we create /var/www/.npm with webowner rights. +Also, to store the ILIAS configuration, which is later used to configure ILIAS, +we need to create the folder `/var/www/config`. To prevent future issues with npm we +also need to create `/var/www/.npm` which needs to be writable by the web server user. ```shell mkdir /var/www/ilias @@ -153,7 +155,7 @@ chown www-data:www-data /var/www/.npm ``` Usually Apache ships with a default configuration (e.g. `/etc/apache2/sites-enabled/000-default.conf` on Debian based -systems). A minimal configuration for ILIAS may look as follows: +systems). A minimal configuration for ILIAS may look like this: ```apacheconf @@ -182,17 +184,18 @@ a2enmod rewrite systemctl restart apache2.service ``` -To check if the installation was successfull create the file `/var/www/ilias/phpinfo.php` with the following contents: +To check if the installation was successful, create the file `/var/www/ilias/public/phpinfo.php` with the following contents: ``` ## Database Installation/Configuration @@ -251,7 +254,7 @@ apt install mariadb-server > Please note that installing ILIAS in utf8mb4-collations is currently not supported! > ILIAS supports utf8-collations with 3 bytes per character, such as `utf8_general_ci`, only. -We **strongly recommend** to use MariaDB with the following settings: +We **strongly recommend** using MariaDB with the following settings: * InnoDB storage engine (default) * `character-set-server` = `utf8mb3` @@ -269,7 +272,7 @@ is set to `COMPACT`. systemctl restart mariadb.service ``` -We recommend to create a dedicated database user for ILIAS: +We recommend creating a dedicated database user for ILIAS: ```shell mysql -e "CREATE DATABASE ilias CHARACTER SET utf8 COLLATE utf8_general_ci;" @@ -287,22 +290,22 @@ ILIAS release or clone it from [GitHub](https://github.com/ILIAS-eLearning/ILIAS For production use make sure to checkout the latest stable release, not the trunk, which is the development branch of the repository. -We recommend to clone from GitHub and use git to update the code, since this simplifies +We recommend cloning from GitHub and using git to update the code, since this simplifies the update to future releases and versions. -Clone the code to the web servers docroot (e.g. `/var/www/html`) with the following +Clone the code to the web server's docroot (e.g. `/var/www/ilias`) with the following commands: ```shell -cd /var/www/ilias/ -sudo -uwww-data git clone https://github.com/ILIAS-eLearning/ILIAS.git . --single-branch -b release_11 +cd /var/www +sudo -u www-data git clone -b release_11 https://github.com/ILIAS-eLearning/ILIAS.git ilias ```
    If you use tags to directly reference ILIAS versions ```shell -cd /var/www/ilias/ -sudo -uwww-data git clone https://github.com/ILIAS-eLearning/ILIAS.git . --single-branch -b v11.X +cd /var/www +sudo -u www-data git clone -b v11.X https://github.com/ILIAS-eLearning/ILIAS.git ilias ```
    @@ -312,20 +315,22 @@ sudo -uwww-data git clone https://github.com/ILIAS-eLearning/ILIAS.git . --singl Download the file from the [Download & Releases](https://docu.ilias.de/go/lm/35) page. ```shell -sudo -uwww-data wget https://github.com/ILIAS-eLearning/ILIAS/releases/download/v11.X/ILIAS-11.X.tar.gz -sudo -uwww-data tar -xzf ILIAS-11.X.tar.gz -C /var/www/ilias --strip-components=1 ILIAS-11.X/ +sudo -u www-data wget https://github.com/ILIAS-eLearning/ILIAS/releases/download/v11.X/ILIAS-11.X.tar.gz +sudo -u www-data tar -xzf ILIAS-11.X.tar.gz -C /var/www/ilias --strip-components=1 ILIAS-11.X/ ``` -The GitHub repository of ILIAS doesn't contain all code that is required to run. To download the required PHP-dependencies and -to create static artifacts from the source, run the following in your ILIAS folder: +The GitHub repository of ILIAS doesn't contain all code that is required to run. To download the required dependencies and +to create static artifacts from the source, run the following in your ILIAS directory (the directory that contains +the composer.lock and package-lock.json). This will create a new directory called public which is the root +directory of your webserver: ```shell -sudo -uwww-data npm clean-install --omit=dev --ignore-scripts -sudo -uwww-data composer install --no-dev +sudo -u www-data npm clean-install --omit=dev --ignore-scripts +sudo -u www-data composer install --no-dev ``` > [!IMPORTANT] -> We recommend restricting the rights of the ILIAS code in the production system so that the web server only has +> We recommend restricting the permissions of the ILIAS code in the production system so that the web server only has > read access to the code. For this and other important security considerations, please refer to the security > instructions in the [Security Guide](./secure.md). @@ -333,15 +338,15 @@ sudo -uwww-data composer install --no-dev ## Install ILIAS After having all dependencies installed and configured you should be able to run -the [ILIAS Setup on the command-line](../../components/ILIAS/Setup/README.md). +the [ILIAS Setup on the command-line](../../components/ILIAS/setup_/README.md). -To do so, create a configuration file for the setup by copying the [minimal-config.json](../../components/ILIAS/Setup/minimal-config.json) +To do so, create a configuration file for the setup by copying the [minimal-config.json](../../components/ILIAS/setup_/minimal-config.json) to a location outside your docroot. Fill in the configuration fields that are already -contained in the minimal config. Have a look into the [list of available config options](../../components/ILIAS/Setup/README.md#about-the-config-file) +contained in the minimal config. Have a look at the [list of available config options](../../components/ILIAS/setup_/README.md#about-the-config-file) and add the fields that your environment and installation requires. ```shell -cp /var/www/ilias/components/ILIAS/Setup/minimal-config.json /var/www/config/ilias.json +cp /var/www/ilias/components/ILIAS/setup_/minimal-config.json /var/www/config/ilias.json ``` A typical configuration might look like this afterwards: @@ -353,7 +358,7 @@ A typical configuration might look like this afterwards: }, "database" : { "user" : "ilias", - "password": ">", + "password": "", "database": "ilias", "create_database": true }, @@ -370,9 +375,9 @@ A typical configuration might look like this afterwards: }, "systemfolder" : { "contact" : { - "firstname" : "Richard", - "lastname" : "Klees", - "email" : "richard.klees@concepts-and-training.de" + "firstname" : "Lucy", + "lastname" : "Snowe", + "email" : "lucysnowe@ilias.de" } }, "utilities" : { @@ -391,23 +396,16 @@ Run the ILIAS command line setup from within your ILIAS folder with your configu file (located outside your doc-root!) as a parameter: ```shell -sudo -uwww-data php cli/setup.php install /var/www/config/ilias.json +sudo -u www-data php cli/setup.php install /var/www/config/ilias.json ``` -The installation will display what currently happens and might prompt you with -questions. You might want to have a look into the [documentation of the command line setup](../../components/ILIAS/Setup/README.md) -or into the help of the program itself `php cli/setup.php help`. It is the tool +The installation will display what is currently happening and might prompt you with +questions. You might want to have a look at the [documentation of the command line setup](../../components/ILIAS/setup_/README.md) +or at the help of the program itself `php cli/setup.php help`. It is the tool to manage and monitor your ILIAS installation. -If you are installing from Git, it is possible that ILIAS will already require a few migrations to the initial -database. Run the setup migration and follow the steps shown. This is also necessary whenever you update your code. - -```shell -php cli/setup.php migrate -``` - -Now that you have ILIAS installed, you can start by logging in as root. Go to your http path and log in with the -username `root` and password `homer`. ILIAS will ask you for a new password after the first login. +Now that you have ILIAS installed, you can start by logging in as root. Go to your http path and log in with the +username `root` and password `homer`. ILIAS will ask you for a new password on the first login. ## Install Further Components @@ -415,19 +413,19 @@ username `root` and password `homer`. ILIAS will ask you for a new password afte Optionally you can continue with the installation of further components to get the full functionality of ILIAS: 1. **ILIAS Cron Job** -A cron job can be automatically executed to perform recurring tasks, such as sending notifications or deleting inactive user accounts. -For details on how to configure the automatic execution of cron jobs, see [Configure Cron Jobs](#configurate-cron). + A cron job can be automatically executed to perform recurring tasks, such as sending notifications or deleting inactive user accounts. + For details on how to configure the automatic execution of cron jobs, see [Configure Cron Jobs](#configure-cron). 2. **ILIAS Java RPC server** -It is used for certain optional functions such as Lucene Search -or generating PDF Certificates. See [Lucene RPC-Server](../../components/ILIAS/WebServices/RPC/lib/README.md) for details -on how to install the RPC server. -3. **ILIAS Chat Server** -It is used to provide an interactive chat experience between users. -See [Chat Server](../../components/ILIAS/Chatroom/README.md) for details on how to install the chat server. + This is used for certain optional functions such as Lucene Search + or generating PDF Certificates. See [Lucene RPC-Server](../../components/ILIAS/WebServices/RPC/lib/README.md) for details + on how to install the RPC server. +3. **ILIAS Chat Server** + This is used to provide an interactive chat experience between users. + See [Chat Server](../../components/ILIAS/Chatroom/README.md) for details on how to install the chat server. 4. **E-Mail** -You either use a MTA of your liking to send e-mail generated by ILIAS or configure a SMPT Connection in ILIAS -"Administration > Communication > Settings > Extern" by activating and configuring "Send via SMTP". We recommend -to use a MTA installed to your OS like `postfix`. On Debian/Ubuntu execute and configure it according to their instructions: + You either use an MTA of your liking to send e-mail generated by ILIAS or configure an SMTP Connection in ILIAS + "Administration > Communication > Settings > Extern" by activating and configuring "Send via SMTP". We recommend + to use a MTA installed to your OS like `postfix`. On Debian/Ubuntu execute and configure it according to their instructions: ```shell apt-get install postfix ``` @@ -441,27 +439,27 @@ A variety of free plugins is provided from our community via the [ILIAS Plugin R To develop plugins, you can get started in our [Development Guide](https://docu.ilias.de/go/pg/27030_42). Custom styles are the way to modify the look of your ILIAS installation. Have -a look in the [documentation of the System Styles and Custom Styles](../../templates/Readme.md) +a look at the [documentation of the System Styles and Custom Styles](../../templates/Readme.md) to learn how to build and install them. # Backup ILIAS -There are three places where the ILIAS core system stores data that needs to be backed up in order to restore your +There are three places where the ILIAS core system stores data that needs to be backed up in order to restore your system in case of failure. * Internal data within your webroot `/public/data` in our case `/var/www/ilias/public/data`. * External data, configured in `ilias.json` within `filesystem.data_dir`, in our case `/var/www/files/ilias`. * The database, which can easily be done by running `mysqldump'. ```shell -mysqldump --lock-tables=false -u -p > /path/to/your/backup/folder/ilias-backup.sql +mysqldump --lock-tables=false -u -p > /path/to/your/backup/folder/ilias-backup.sql # Prompt for password ``` -When restoring the ILIAS files to the designated folder, remember to set the correct permissions and ownership of your -web server. To restore the database, drop the old database with `DROP DATABASE ;`, create an empty +When restoring the ILIAS files to the designated folder, remember to set the correct permissions and ownership of your +web server. To restore the database, drop the old database with `DROP DATABASE ;`, create an empty database according to [Database Installation/Configuration](#install-database) and write the database dump to this database: ```shell -mysql -u -p < /path/to/your/backup/folder/ilias-backup.sql +mysql -u -p < /path/to/your/backup/folder/ilias-backup.sql # Prompt for password ``` @@ -472,7 +470,7 @@ The easiest way to update ILIAS is using Git. Please note that this is only poss if you installed ILIAS via Git as advised in this document. If Git wasn't used you can also [download](https://docu.ilias.de/go/lm/35) new releases. -Before you start you should consider to [backup](#backup-ilias). +Before you start we strongly advise you to make a [backup](#backup-ilias). ## Minor Upgrade @@ -481,19 +479,19 @@ To apply a minor update (e.g. v11.1 to v11.2) execute the following command in your ILIAS basepath (e.g. `/var/www/ilias/`): ```shell -sudo -uwww-data git pull origin release_11 -sudo -uwww-data npm clean-install --omit-dev --ignore-scripts -sudo -uwww-data composer install --no-dev +sudo -u www-data git pull origin release_11 +sudo -u www-data npm clean-install --omit-dev --ignore-scripts +sudo -u www-data composer install --no-dev ```
    If you use tags to directly reference ILIAS versions ```shell -sudo -uwww-data git fetch origin v11.X:v11.X -sudo -uwww-data git checkout v11.X -sudo -uwww-data npm clean-install --omit-dev --ignore-scripts -sudo -uwww-data composer install --no-dev +sudo -u www-data git fetch origin v11.X:v11.X +sudo -u www-data git checkout v11.X +sudo -u www-data npm clean-install --omit-dev --ignore-scripts +sudo -u www-data composer install --no-dev ```
    @@ -503,17 +501,17 @@ sudo -uwww-data composer install --no-dev Download the archive of the newest ILIAS minor version on the [Download & Releases](https://docu.ilias.de/go/lm/35) page. ```shell -sudo -uwww-data wget https://github.com/ILIAS-eLearning/ILIAS/releases/download/v11.X/ILIAS-11.X.tar.gz -sudo -uwww-data mkdir /tmp/ilias_update -sudo -uwww-data tar -xzf ILIAS-11.X.tar.gz -C /tmp/ilias_update --strip-components=1 ILIAS-11.X/ -sudo -uwww-data rsync -av --exclude='/public/' --exclude='/Customizing/' --exclude='/data/' --exclude='ilias.ini.php' /tmp/ilias_update/ /var/www/ilias/ +sudo -u www-data wget https://github.com/ILIAS-eLearning/ILIAS/releases/download/v11.X/ILIAS-11.X.tar.gz +sudo -u www-data mkdir /tmp/ilias_update +sudo -u www-data tar -xzf ILIAS-11.X.tar.gz -C /tmp/ilias_update --strip-components=1 ILIAS-11.X/ +sudo -u www-data rsync -av --exclude='/public/' --exclude='/Customizing/' --exclude='/data/' --exclude='ilias.ini.php' /tmp/ilias_update/ /var/www/ilias/ rm -rf /tmp/ilias_update ``` -If you have plugins installed, you will need to rebuild the classmap, including all plugins. You will need to call +If you have plugins installed, you will need to rebuild the classmap, including all plugins. You will need to call `composer du` to do this. ```shell -sudo -uwww-data composer du +sudo -u www-data composer du ``` @@ -526,24 +524,27 @@ Then complete the update by [updating the database](#update-the-database). ## Major Upgrade -To apply a major upgrade (e.g. v11.13 to v12.1) please check that your OS has the +Before running a major upgrade please make sure that you have run all migrations +of your current ILIAS release as explained in the +[Update the Database and run the migrations](#update-the-database) section. +To apply a major upgrade (e.g. v10.13 to v11.1) please check that your OS has the [proper dependency versions](#upgrading-dependencies) installed. Note that no major -version can be omitted during the upgrade process. You can upgrade from 11 to 12, -but not directly from 10 to 12. If everything is fine, change your default skin to +version can be omitted during the upgrade process. You can upgrade from 10 to 11, +but not directly from 9 to 11. If everything is fine, change your default skin to Delos and apply this change at least to your root user. Otherwise ILIAS might become unusable due to changes in the layout templates. Then execute the following commands in your ILIAS basepath (e.g. `/var/www/ilias`). ```shell -sudo -uwww-data git fetch origin release_11:release_11 -sudo -uwww-data git checkout release_11 +sudo -u www-data git fetch origin release_11:release_11 +sudo -u www-data git checkout release_11 ```
    If you use tags to directly reference ILIAS versions ```shell -sudo -uwww-data git fetch origin v11.X:v11.X -sudo -uwww-data git checkout v11.X +sudo -u www-data git fetch origin v11.X:v11.X +sudo -u www-data git checkout v11.X ```
    @@ -553,74 +554,71 @@ sudo -uwww-data git checkout v11.X Download the archive of the newest ILIAS minor version from the [Download & Releases](https://docu.ilias.de/go/lm/35) page. ```shell -sudo -uwww-data wget https://github.com/ILIAS-eLearning/ILIAS/releases/download/v11.X/ILIAS-11.X.tar.gz -sudo -uwww-data mkdir /tmp/ilias_update -sudo -uwww-data tar -xzf ILIAS-11.X.tar.gz -C /tmp/ilias_update --strip-components=1 ILIAS-11.X/ -sudo -uwww-data rsync -av --exclude='/public/' --exclude='/Customizing/' --exclude='/data/' --exclude='ilias.ini.php' /tmp/ilias_update/ /var/www/ilias/ +sudo -u www-data wget https://github.com/ILIAS-eLearning/ILIAS/releases/download/v11.X/ILIAS-11.X.tar.gz +sudo -u www-data mkdir /tmp/ilias_update +sudo -u www-data tar -xzf ILIAS-11.X.tar.gz -C /tmp/ilias_update --strip-components=1 ILIAS-11.X/ +sudo -u www-data rsync -av --exclude='/public/' --exclude='/Customizing/' --exclude='/data/' --exclude='ilias.ini.php' /tmp/ilias_update/ /var/www/ilias/ rm -rf /tmp/ilias_update ``` -After upgrading the code from ILIAS 10 to ILIAS 11 due to structural changes, you need to move the `/Customizing/global/plugins` -and `/data` folder to its new destination. Both are now located in the newly created `public` folder. - -```shell -sudo -uwww-data mkdir -p public/Customizing/plugins -mv data public/ -mv Customizing/global/plugins/Services/* public/Customizing/plugins/ -mv Customizing/global/plugins/Modules/* public/Customizing/plugins/ -``` Then update the code of your plugins according to their documentation to ensure they are compatible with the new ILIAS version. If you are **not** using the tar.gz archive to upgrade your release, update your javascript and php dependencies. If you are using a tar.gz archive and are using plugins, reload your php classmap with `composer du`. ```shell -sudo -uwww-data npm clean-install --omit-dev --ignore-scripts -sudo -uwww-data composer install --no-dev +sudo -u www-data npm clean-install --omit-dev --ignore-scripts +sudo -u www-data composer install --no-dev ``` Complete the update of the base system by [updating the database](#update-the-database). As a last step, you should log in with a User using your custom skin. If everything -works fine, change back from Delos to your custom system style. If not, you probably +works correctly, change back from Delos to your custom system style. If not, you probably will need to update your style to match the new release. -## Update the Database +## Update the Database and carrying out migrations to IRSS Database updates must be done for both minor and major updates, the schema and content of the database probably won't match the code otherwise. Database updates are performed -via the [command line setup program](../../components/ILIAS/Setup/README.md). The required updates +via the [command line setup program](../../components/ILIAS/setup_/README.md). The required updates are split into two groups. **Updates** are tasks that need to be run immediately to -make your installation work properly. **Migrations** are tasks, that potentially take -some time, but which can also be executed while the installation is in productive use. +make your installation work properly. **Migrations** are tasks that potentially take +some time, but which can also be executed while the installation is live. Run the `status` command on the command line to check if there are any updates available and if ILIAS is responding. After this you need to perform the update. -``` -php cli/setup.php update -``` - -To check if there are migrations, run in your ILIAS folder. +Please make sure to check for migrations before you run the update, especially if it's a +major upgrade. If there are migrations left please make sure to run these before updating. ``` -php cli/setup.php migrate +cd /var/www/ilias +sudo -u www-data php cli/setup.php migrate ``` -The command will show you if there are migrations that need to be run for you -installation. Run them by using the `--run` parameter and have a look into +The command will show you if there are migrations that need to be run for your +installation. Run them by using the `--run` parameter and refering to the help of the command for more details: `php cli/setup.php migrate --help`. -Both commands will display what currently happens and might prompt you with -questions. You might want to have a look into the [documentation of the command line setup](../../components/ILIAS/Setup/README.md) -or into the help of the program itself `php cli/setup.php help`. It is the tool -to manage and monitor your ILIAS installation. +Both commands will display what is currently happening and might prompt you with +questions. You might want to have a look into the [documentation of the command line setup](../../components/ILIAS/setup_/README.md) +or into the help of the program itself `php cli/setup.php help`. This is the tool +for managing and monitoring your ILIAS installation. + +As soon as all migrations are done you can do the database update: +``` +cd /var/www/ilias +sudo -u www-data npm clean-install --omit=dev --ignore-scripts +sudo -u www-data composer install --no-dev +sudo -u www-data php cli/setup.php update +``` Database updates are performed in steps; it might happen that a step fails, e.g. due to some edge case or inconsistency in existing data, files, etc. -In this case, a concecutive command `php setup/setup.php update` will error with -a message like +In this case, a consecutive command `php setup/setup.php update` will indicate +an error error with a message like > step 2 was started last, but step 1 was finished last. > Aborting because of that mismatch. @@ -628,17 +626,17 @@ You may reset the records for those steps by running: ```shell php cli/setup.php achieve database.resetFailedSteps ``` -However, be sure to understand the cause for the failing steps and tend to it before -resetting and running update again. +However, be sure to understand why the steps failed and address the issue +before resetting and rerunning the update. ## Information on Updates To keep your ILIAS Installation secure and healthy it is important that you keep -it up to date. To get informations about updates and security fixes you should -consider subscribing to the [ILIAS Admin Mailing-List](http://lists.ilias.de/cgi-bin/mailman/listinfo/ilias-admins). Information on the -new versions, such as Important Changes, Known Issues, Changed Behaviour and Fixed -Issues, can be found in the release notes in [Download & Releases](https://docu.ilias.de/go/lm/35). +it up to date. To get information about updates and security fixes you should +consider subscribing to the [ILIAS Admin Mailing-List](http://lists.ilias.de/cgi-bin/mailman/listinfo/ilias-admins). Information on the +new versions, such as Important Changes, Known Issues, Changed Behaviour and Fixed +Issues can be found in the release notes in [Download & Releases](https://docu.ilias.de/go/lm/35). # Connect and Contribute @@ -651,7 +649,7 @@ or [ILIAS Development Conferences](https://docu.ilias.de/goto_docu_grp_3721.html We are also looking for [contributions of code](../development/contributing.md), [reports of issues](http://mantis.ilias.de) or [requests in our Feature Wiki](https://docu.ilias.de/goto.php?target=wiki_5307&client_id=docu#ilPageTocA119). -If you have any questions about the installation or the community, please visit us on our [Discord server](https://discord.gg/H9v2v2Ar2T) and join +If you have any questions about the installation or the community, please visit us on our [Discord server](https://discord.gg/H9v2v2Ar2T) and join the discussion! @@ -666,31 +664,29 @@ each ILIAS release. **PHP:** -| ILIAS Version | PHP Version | -|---------------|---------------------| -| 12.x | 8.4.x, 8.5.x | -| 11.x | 8.3.x, 8.4.x | -| 10.x | 8.2.x, 8.3.x | -| 9.x | 8.1.x, 8.2.x | -| 8.x | 7.4.x, 8.0.x | -| 7.x | 7.3.x, 7.4.x | -| 6.x | 7.2.x, 7.3.x, 7.4.x | +| ILIAS Version | PHP Version | +|----------------|-----------------------------| +| 11.x | 8.3.x, 8.4.x | +| 10.x | 8.2.x, 8.3.x | +| 9.x | 8.1.x, 8.2.x | +| 8.x | 7.4.x, 8.0.x | +| 7.x | 7.3.x, 7.4.x | +| 6.x | 7.2.x, 7.3.x, 7.4.x | **DBMS:** We strongly recommend using MariaDB instead of MySQL due to performance, licensing and compatibility in the future. -| ILIAS Version | MySQL Version | MariaDB Version | -|---------------|---------------------|------------------------| -| 12.0 - 12.x | 8.4.x | 11.4, 11.8, 12.3 | -| 11.0 - 11.x | 8.0.x | 10.4, 10.5, 10.6 | -| 10.0 - 10.x | 8.0.x | 10.4, 10.5, 10.6 | -| 9.0 - 9.x | 8.0.x | 10.3, 10.4, 10.5, 10.6 | -| 8.0 - 8.x | 5.7.x, 8.0.x | 10.2, 10.3, 10.4 | -| 7.0 - 7.x | 5.7.x, 8.0.x | 10.1, 10.2, 10.3 | -| 6.0 - 6.x | 5.6.x, 5.7.x, 8.0.x | 10.0, 10.1, 10.2, 10.3 | - - +| ILIAS Version | MySQL Version | MariaDB Version | +|---------------|---------------------|------------------------------| +| 11.0 - 11.x | 8.0.x | 10.4, 10.5, 10.6, 11.4, 11.8 | +| 10.0 - 10.x | 8.0.x | 10.4, 10.5, 10.6 | +| 9.0 - 9.x | 8.0.x | 10.3, 10.4, 10.5, 10.6 | +| 8.0 - 8.x | 5.7.x, 8.0.x | 10.2, 10.3, 10.4 | +| 7.0 - 7.x | 5.7.x, 8.0.x | 10.1, 10.2, 10.3 | +| 6.0 - 6.x | 5.6.x, 5.7.x, 8.0.x | 10.0, 10.1, 10.2, 10.3 | + + ## Configure Cron Jobs This step configures the execution of the ILIAS Cron Jobs, which can be set to perform tasks, such as sending notifications. @@ -705,16 +701,16 @@ php /var/www/ilias/cli/cron.php run-jobs The `` is a valid, arbitrary user account within the ILIAS installation. The `` corresponds to the client ID of the ILIAS installation. -To configure automated Cron Jobs in your system, you need to create an user in ILIAS, for example named `cron`. -Then create a new file in the Linux Cron configuration for ILIAS at `/etc/cron.d/ilias`, -including a line to execute `./cli/cron.php` every 5 minutes. +To configure automated Cron Jobs in your system, you need to create a user in ILIAS, for example named `cron`. +Then create a new file in the Linux Cron configuration for ILIAS at `/etc/cron.d/ilias`, +including a line to execute `./cli/cron.php` every 5 minutes. Other methods for executing Linux cron tasks, such as using the user crontab, can also be utilized. ```cron */5 * * * * www-data /usr/bin/php /var/www/ilias/cli/cron.php run-jobs cron myilias > /dev/null 2>&1 ``` -You can verify the proper automatic execution in the ILIAS Administration section by checking the timestamp +You can verify the proper automatic execution in the ILIAS Administration section by checking the timestamp displayed at `Last Start of the Cron Job` after some time. From 1b25bbe59223d09412ec827286de2fd9337b2232 Mon Sep 17 00:00:00 2001 From: Rob Falkenstein Date: Tue, 28 Jul 2026 17:32:54 +0200 Subject: [PATCH 131/333] added all changes from trunk/r12 to the pick from release_11 --- docs/configuration/install.md | 48 ++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/docs/configuration/install.md b/docs/configuration/install.md index 4d75b7f932a4..781696c54381 100755 --- a/docs/configuration/install.md +++ b/docs/configuration/install.md @@ -64,10 +64,10 @@ current configuration of the [ILIAS test server](https://test11.ilias.de), which | Package | Version | Reference System | |--------------|--------------------------------------------------------|------------------| | Distribution | current version of Debian GNU Linux, Ubuntu or RHEL | Ubuntu 22.04 LTS | -| Database | MySQL >8.0.21 or MariaDB 10.5 - 11.8 | MariaDB 10.6.18 | -| PHP | 8.3, 8.4 | 8.4 | +| Database | MySQL 8.4 - 9.7 or MariaDB 11.4 - 12.3 | MariaDB 11.8 | +| PHP | 8.4, 8.5 | 8.5 | | Webserver | nginx: 1.12.x – 1.18.x, Apache: ≥ 2.4.x | Apache 2.4.52 | -| JDK | Open JDK Runtime 11, 17 or 21 LTS | OpenJDK 17 | +| JDK | Open JDK Runtime 17, 21 or 25 LTS | OpenJDK 21 | | Node.js | 22 (LTS), 24 Recommended: 24 | v24.10.0 | | Ghostscript | 10.x | 9.55 | | Imagemagick | 6.9.x | 6.9.11 | @@ -94,7 +94,7 @@ PHP version later on. ## Install Dependencies -`openjdk-17-jdk` and `maven` are optional and are used for the ILIAS RPC server for search indexing and certificate generation. +`openjdk-21-jdk` and `maven` are optional and are used for the ILIAS RPC server for search indexing and certificate generation. `git` is required if the source code is obtained directly from GitHub. `nodejs` and `npm` are required as well if you get the source directly to download the javascript dependencies in the installation process. Alternatively, they can be obtained directly from the distribution package at [Nodesource](https://nodejs.org/en/download) to select appropriate nodejs versions according to the [Recommended Setup for Running ILIAS](#recommended-setup-for-running-ilias). @@ -102,7 +102,7 @@ Alternatively, they can be obtained directly from the distribution package at [N ```shell apt update -apt update zip unzip openjdk-17-jdk maven ffmpeg git ghostscript nodejs npm +apt update zip unzip openjdk-21-jdk maven ffmpeg git ghostscript nodejs npm ``` @@ -338,15 +338,15 @@ sudo -u www-data composer install --no-dev ## Install ILIAS After having all dependencies installed and configured you should be able to run -the [ILIAS Setup on the command-line](../../components/ILIAS/setup_/README.md). +the [ILIAS Setup on the command-line](../../components/ILIAS/Setup/README.md). -To do so, create a configuration file for the setup by copying the [minimal-config.json](../../components/ILIAS/setup_/minimal-config.json) +To do so, create a configuration file for the setup by copying the [minimal-config.json](../../components/ILIAS/Setup/minimal-config.json) to a location outside your docroot. Fill in the configuration fields that are already -contained in the minimal config. Have a look at the [list of available config options](../../components/ILIAS/setup_/README.md#about-the-config-file) +contained in the minimal config. Have a look at the [list of available config options](../../components/ILIAS/Setup/README.md#about-the-config-file) and add the fields that your environment and installation requires. ```shell -cp /var/www/ilias/components/ILIAS/setup_/minimal-config.json /var/www/config/ilias.json +cp /var/www/ilias/components/ILIAS/Setup/minimal-config.json /var/www/config/ilias.json ``` A typical configuration might look like this afterwards: @@ -400,7 +400,7 @@ sudo -u www-data php cli/setup.php install /var/www/config/ilias.json ``` The installation will display what is currently happening and might prompt you with -questions. You might want to have a look at the [documentation of the command line setup](../../components/ILIAS/setup_/README.md) +questions. You might want to have a look at the [documentation of the command line setup](../../components/ILIAS/Setup/README.md) or at the help of the program itself `php cli/setup.php help`. It is the tool to manage and monitor your ILIAS installation. @@ -527,10 +527,10 @@ Then complete the update by [updating the database](#update-the-database). Before running a major upgrade please make sure that you have run all migrations of your current ILIAS release as explained in the [Update the Database and run the migrations](#update-the-database) section. -To apply a major upgrade (e.g. v10.13 to v11.1) please check that your OS has the +To apply a major upgrade (e.g. v11.13 to v12.1) please check that your OS has the [proper dependency versions](#upgrading-dependencies) installed. Note that no major -version can be omitted during the upgrade process. You can upgrade from 10 to 11, -but not directly from 9 to 11. If everything is fine, change your default skin to +version can be omitted during the upgrade process. You can upgrade from 11 to 12, +but not directly from 10 to 12. If everything is fine, change your default skin to Delos and apply this change at least to your root user. Otherwise ILIAS might become unusable due to changes in the layout templates. Then execute the following commands in your ILIAS basepath (e.g. `/var/www/ilias`). @@ -582,7 +582,7 @@ will need to update your style to match the new release. Database updates must be done for both minor and major updates, the schema and content of the database probably won't match the code otherwise. Database updates are performed -via the [command line setup program](../../components/ILIAS/setup_/README.md). The required updates +via the [command line setup program](../../components/ILIAS/Setup/README.md). The required updates are split into two groups. **Updates** are tasks that need to be run immediately to make your installation work properly. **Migrations** are tasks that potentially take some time, but which can also be executed while the installation is live. @@ -603,7 +603,7 @@ installation. Run them by using the `--run` parameter and refering to the help of the command for more details: `php cli/setup.php migrate --help`. Both commands will display what is currently happening and might prompt you with -questions. You might want to have a look into the [documentation of the command line setup](../../components/ILIAS/setup_/README.md) +questions. You might want to have a look into the [documentation of the command line setup](../../components/ILIAS/Setup/README.md) or into the help of the program itself `php cli/setup.php help`. This is the tool for managing and monitoring your ILIAS installation. @@ -664,14 +664,15 @@ each ILIAS release. **PHP:** -| ILIAS Version | PHP Version | -|----------------|-----------------------------| -| 11.x | 8.3.x, 8.4.x | -| 10.x | 8.2.x, 8.3.x | -| 9.x | 8.1.x, 8.2.x | -| 8.x | 7.4.x, 8.0.x | -| 7.x | 7.3.x, 7.4.x | -| 6.x | 7.2.x, 7.3.x, 7.4.x | +| ILIAS Version | PHP Version | +|---------------|---------------------| +| 12.x | 8.4.x, 8.5.x | +| 11.x | 8.3.x, 8.4.x | +| 10.x | 8.2.x, 8.3.x | +| 9.x | 8.1.x, 8.2.x | +| 8.x | 7.4.x, 8.0.x | +| 7.x | 7.3.x, 7.4.x | +| 6.x | 7.2.x, 7.3.x, 7.4.x | **DBMS:** @@ -679,6 +680,7 @@ We strongly recommend using MariaDB instead of MySQL due to performance, licensi | ILIAS Version | MySQL Version | MariaDB Version | |---------------|---------------------|------------------------------| +| 12.0 - 12.x | 8.4.x | 11.4, 11.8, 12.3 | | 11.0 - 11.x | 8.0.x | 10.4, 10.5, 10.6, 11.4, 11.8 | | 10.0 - 10.x | 8.0.x | 10.4, 10.5, 10.6 | | 9.0 - 9.x | 8.0.x | 10.3, 10.4, 10.5, 10.6 | From afde68d88ba683fcad3f4be167ba931a6384cdd3 Mon Sep 17 00:00:00 2001 From: Rob Falkenstein Date: Mon, 13 Jul 2026 13:21:29 +0200 Subject: [PATCH 132/333] changed github handle for maintenance.md --- docs/development/maintenance.md | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/development/maintenance.md b/docs/development/maintenance.md index 233228e767ff..f4ad2da39d30 100755 --- a/docs/development/maintenance.md +++ b/docs/development/maintenance.md @@ -218,12 +218,12 @@ of ILIAS. The file contains the following fields: [//]: # (BEGIN BackgroundTasks) * **BackgroundTasks** - * Authority to Sign off on Conceptual Changes: [tjoussen](https://docu.ilias.de/go/usr/103745), [mjansen](https://docu.ilias.de/go/usr/8784) - * Authority to Sign off on Code Changes: [tjoussen](https://docu.ilias.de/go/usr/103745), [mjansen](https://docu.ilias.de/go/usr/8784) + * Authority to Sign off on Conceptual Changes: [thojou](https://docu.ilias.de/go/usr/103745), [mjansen](https://docu.ilias.de/go/usr/8784) + * Authority to Sign off on Code Changes: [thojou](https://docu.ilias.de/go/usr/103745), [mjansen](https://docu.ilias.de/go/usr/8784) * Authority to Curate Test Cases: MISSING - * Authority to (De-)Assign Authorities: [tjoussen (Databay AG)](https://docu.ilias.de/go/usr/103745) - * Assignee for Issues: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Assignee for Security Reports: [tjoussen](https://docu.ilias.de/go/usr/103745) + * Authority to (De-)Assign Authorities: [thojou (Databay AG)](https://docu.ilias.de/go/usr/103745) + * Assignee for Issues: [thojou](https://docu.ilias.de/go/usr/103745) + * Assignee for Security Reports: [thojou](https://docu.ilias.de/go/usr/103745) * Unit-specific Guidelines, Rules, and Regulations: [LINK MISSING]('') [//]: # (END BackgroundTasks) @@ -285,11 +285,11 @@ of ILIAS. The file contains the following fields: * **Booking Pool** * Authority to Sign off on Conceptual Changes: [simon.lowe](https://docu.ilias.de/go/usr/79091), [oliver.samoila](https://docu.ilias.de/go/usr/26160) - * Authority to Sign off on Code Changes: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Authority to Curate Test Cases: [simon.lowe](https://docu.ilias.de/go/usr/79091), [tjoussen](https://docu.ilias.de/go/usr/103745) + * Authority to Sign off on Code Changes: [thojou](https://docu.ilias.de/go/usr/103745) + * Authority to Curate Test Cases: [simon.lowe](https://docu.ilias.de/go/usr/79091), [thojou](https://docu.ilias.de/go/usr/103745) * Authority to (De-)Assign Authorities: [simon.lowe (Databay AG)](https://docu.ilias.de/go/usr/79091), [oliver.samoila (Databay AG)](https://docu.ilias.de/go/usr/26160) - * Assignee for Issues: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Assignee for Security Reports: [tjoussen](https://docu.ilias.de/go/usr/103745) + * Assignee for Issues: [thojou](https://docu.ilias.de/go/usr/103745) + * Assignee for Security Reports: [thojou](https://docu.ilias.de/go/usr/103745) * Unit-specific Guidelines, Rules, and Regulations: [LINK MISSING]('') [//]: # (END BookingPool) @@ -556,7 +556,7 @@ of ILIAS. The file contains the following fields: * **ECS Interface** * Authority to Sign off on Conceptual Changes: [bogen](https://docu.ilias.de/go/usr/13815), [mglaubitz](https://docu.ilias.de/go/usr/28309) - * Authority to Sign off on Code Changes: [sdyhr](https://docu.ilias.de/go/usr/102107), [tjoussen](https://docu.ilias.de/go/usr/103745) + * Authority to Sign off on Code Changes: [sdyhr](https://docu.ilias.de/go/usr/102107), [thojou](https://docu.ilias.de/go/usr/103745) * Authority to Curate Test Cases: [jheim](https://docu.ilias.de/go/usr/40167), [SIG CampusConnect und ECS(A)](https://docu.ilias.de/go/grp/7893) * Authority to (De-)Assign Authorities: [bogen](https://docu.ilias.de/go/usr/13815), [mglaubitz](https://docu.ilias.de/go/usr/28309) * Assignee for Issues: [sdyhr](https://docu.ilias.de/go/usr/102107) @@ -806,11 +806,11 @@ of ILIAS. The file contains the following fields: * **ItemGroup** * Authority to Sign off on Conceptual Changes: [oliver.samoila](https://docu.ilias.de/go/usr/26160) - * Authority to Sign off on Code Changes: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Authority to Curate Test Cases: [oliver.samoila](https://docu.ilias.de/go/usr/26160), [tjoussen](https://docu.ilias.de/go/usr/103745) + * Authority to Sign off on Code Changes: [thojou](https://docu.ilias.de/go/usr/103745) + * Authority to Curate Test Cases: [oliver.samoila](https://docu.ilias.de/go/usr/26160), [thojou](https://docu.ilias.de/go/usr/103745) * Authority to (De-)Assign Authorities: [oliver.samoila (Databay AG)](https://docu.ilias.de/go/usr/26160) - * Assignee for Issues: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Assignee for Security Reports: [tjoussen](https://docu.ilias.de/go/usr/103745) + * Assignee for Issues: [thojou](https://docu.ilias.de/go/usr/103745) + * Assignee for Security Reports: [thojou](https://docu.ilias.de/go/usr/103745) * Unit-specific Guidelines, Rules, and Regulations: [LINK MISSING]('') [//]: # (END ItemGroup) @@ -911,8 +911,8 @@ of ILIAS. The file contains the following fields: * **Like** * Authority to Sign off on Conceptual Changes: [oliver.samoila](https://docu.ilias.de/go/usr/26160) - * Authority to Sign off on Code Changes: [fhelfer](https://docu.ilias.de/go/usr/93367), [tjoussen](https://docu.ilias.de/go/usr/103745) - * Authority to Curate Test Cases: [fhelfer](https://docu.ilias.de/go/usr/93367), [tjoussen](https://docu.ilias.de/go/usr/103745), [oliver.samoila](https://docu.ilias.de/go/usr/26160) + * Authority to Sign off on Code Changes: [fhelfer](https://docu.ilias.de/go/usr/93367), [thojou](https://docu.ilias.de/go/usr/103745) + * Authority to Curate Test Cases: [fhelfer](https://docu.ilias.de/go/usr/93367), [thojou](https://docu.ilias.de/go/usr/103745), [oliver.samoila](https://docu.ilias.de/go/usr/26160) * Authority to (De-)Assign Authorities: [oliver.samoila (Databay AG)](https://docu.ilias.de/go/usr/26160) * Assignee for Issues: [fhelfer](https://docu.ilias.de/go/usr/93367) * Assignee for Security Reports: [fhelfer](https://docu.ilias.de/go/usr/93367) @@ -937,9 +937,9 @@ of ILIAS. The file contains the following fields: * **Login, Auth & Registration** * Authority to Sign off on Conceptual Changes: [mjansen](https://docu.ilias.de/go/usr/8784) - , [tjoussen](https://docu.ilias.de/go/usr/103745) + , [thojou](https://docu.ilias.de/go/usr/103745) * Authority to Sign off on Code Changes: [mjansen](https://docu.ilias.de/go/usr/8784) - , [tjoussen](https://docu.ilias.de/go/usr/103745) + , [thojou](https://docu.ilias.de/go/usr/103745) * Authority to Curate Test Cases: [mjansen](https://docu.ilias.de/go/usr/8784) * Authority to (De-)Assign Authorities: [mjansen (Databay AG)](https://docu.ilias.de/go/usr/8784) * Assignee for Issues: [mjansen](https://docu.ilias.de/go/usr/8784) @@ -1095,11 +1095,11 @@ of ILIAS. The file contains the following fields: * **News** * Authority to Sign off on Conceptual Changes: [oliver.samoila](https://docu.ilias.de/go/usr/26160) - * Authority to Sign off on Code Changes: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Authority to Curate Test Cases: [tjoussen](https://docu.ilias.de/go/usr/103745), [oliver.samoila](https://docu.ilias.de/go/usr/26160) + * Authority to Sign off on Code Changes: [thojou](https://docu.ilias.de/go/usr/103745) + * Authority to Curate Test Cases: [thojou](https://docu.ilias.de/go/usr/103745), [oliver.samoila](https://docu.ilias.de/go/usr/26160) * Authority to (De-)Assign Authorities: [oliver.samoila (Databay AG)](https://docu.ilias.de/go/usr/26160) - * Assignee for Issues: [tjoussen](https://docu.ilias.de/go/usr/103745) - * Assignee for Security Reports: [tjoussen](https://docu.ilias.de/go/usr/103745) + * Assignee for Issues: [thojou](https://docu.ilias.de/go/usr/103745) + * Assignee for Security Reports: [thojou](https://docu.ilias.de/go/usr/103745) * Unit-specific Guidelines, Rules, and Regulations: [LINK MISSING]('') [//]: # (END News) @@ -1479,7 +1479,7 @@ of ILIAS. The file contains the following fields: * Authority to Sign off on Conceptual Changes: [dstrassner](https://docu.ilias.de/go/usr/48931) * Authority to Sign off on Code Changes: [skergomard](https://docu.ilias.de/go/usr/44474) , [dstrassner](https://docu.ilias.de/go/usr/48931) - , [tjoussen](https://docu.ilias.de/go/usr/103745) + , [thojou](https://docu.ilias.de/go/usr/103745) * Authority to Curate Test Cases: [dstrassner](https://docu.ilias.de/go/usr/48931) * Authority to (De-)Assign Authorities: [dstrassner](https://docu.ilias.de/go/usr/48931) * Assignee for Issues: [dstrassner](https://docu.ilias.de/go/usr/48931) From d7ee16396cbe830521b48ea88ac156a1367ea56a Mon Sep 17 00:00:00 2001 From: Matthias Kunkel Date: Wed, 29 Jul 2026 09:15:28 +0200 Subject: [PATCH 133/333] Fixed Mantis #0048133: -glo_remove_glossary- // Missing translation (cherry picked from commit 03829adff6225c2854f0e27f9decfab9b86a230e) --- lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + 2 files changed, 2 insertions(+) diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 312925c280f4..370ae8f87e44 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -10330,6 +10330,7 @@ glo#:#glo_really_remove_from_collection#:#Sind Sie sich sicher, dass Sie das fol glo#:#glo_reference#:#Link glo#:#glo_reference_terms#:#Begriffe verlinken glo#:#glo_referenced_term#:#Verlinkte Begriffe +glo#:#glo_remove_glossary#:##Sind Sie sich sicher, dass Sie das folgende Glossar aus der Liste der automatisch verlinkten Glossare entfernen wollen? glo#:#glo_removed_from_collection_info#:#Das Glossar wurde aus dem Sammelglossar entfernt. glo#:#glo_reset_all_boxes#:#Alle Fächer zurücksetzen glo#:#glo_save_and_continue#:#Speichern und weiter diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index b73a0a4b4f50..e0b0b754d3b1 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -10304,6 +10304,7 @@ glo#:#glo_really_remove_from_collection#:#Are you sure you want to remove the fo glo#:#glo_reference#:#Reference glo#:#glo_reference_terms#:#Reference Terms glo#:#glo_referenced_term#:#Referenced Term +glo#:#glo_remove_glossary#:##Are you sure you want to remove the following glossary from the list of auto-linked glossaries? glo#:#glo_removed_from_collection_info#:#The glossary has been removed from the collection glossary. glo#:#glo_reset_all_boxes#:#Reset All Boxes glo#:#glo_save_and_continue#:#Save and Continue From e3305c5d484f5ca57ac352fb33aed3b70ce0ea4d Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 09:04:27 +0200 Subject: [PATCH 134/333] Object: Redirect on (De)Activation of Translations See: https://conceptsandtraining.testmo.net/runs/view/68?test_id=269806 --- .../src/Properties/Translations/class.TranslationGUI.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/ILIASObject/src/Properties/Translations/class.TranslationGUI.php b/components/ILIAS/ILIASObject/src/Properties/Translations/class.TranslationGUI.php index b9eb39c4a766..f8b4c2915ec6 100755 --- a/components/ILIAS/ILIASObject/src/Properties/Translations/class.TranslationGUI.php +++ b/components/ILIAS/ILIASObject/src/Properties/Translations/class.TranslationGUI.php @@ -169,6 +169,7 @@ public function activateContentTranslation(): void ->getData(); if ($data === null) { + $this->listTranslations(); return; } @@ -185,7 +186,7 @@ public function activateContentTranslation(): void $this->object->getObjectProperties()->storePropertyTranslations( $this->translations ); - $this->listTranslations(); + $this->ctrl->redirectByClass(self::class); } public function deactivateContentTranslation(): void @@ -196,7 +197,7 @@ public function deactivateContentTranslation(): void ); $this->tpl->setOnScreenMessage('success', $this->lng->txt('obj_cont_transl_deactivated'), true); - $this->listTranslations(); + $this->ctrl->redirectByClass(self::class); } private function addAddLanguagesToolbarActionAndRetrieveModal( From ea453a6dfa4e412551675d8bd0d00cbe7f152d0a Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 09:11:36 +0200 Subject: [PATCH 135/333] Object: Remove Unneeded Includes in Translations --- .../ILIASObject/src/Properties/Translations/Translations.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php b/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php index 4486d11e61bb..89f7f242eb97 100755 --- a/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php +++ b/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php @@ -20,11 +20,6 @@ namespace ILIAS\ILIASObject\Properties\Translations; -use ILIAS\ILIASObject\Properties\Property; -use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -use ILIAS\UI\Component\Input\Container\Form\FormInput; -use ILIAS\Refinery\Factory as Refinery; - /** * Class handles translation mode for an object. * From 0bab03d47243a28e6f4e42be8a1306451db1b7db Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 11:13:22 +0200 Subject: [PATCH 136/333] Object: Fix Ordering of Translations --- .../src/Properties/Translations/Language.php | 52 +++++++++++++------ .../Translations/TranslationsTable.php | 37 ++++++++++--- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/components/ILIAS/ILIASObject/src/Properties/Translations/Language.php b/components/ILIAS/ILIASObject/src/Properties/Translations/Language.php index 7d9fb4263a47..a0e08c4a8456 100755 --- a/components/ILIAS/ILIASObject/src/Properties/Translations/Language.php +++ b/components/ILIAS/ILIASObject/src/Properties/Translations/Language.php @@ -28,6 +28,12 @@ class Language { + public const KEY_LANGUAGE = 'language'; + public const KEY_BASE = 'base'; + public const KEY_DEFAULT = 'default'; + public const KEY_TITLE = 'title'; + public const KEY_DESCRIPTION = 'description'; + public function __construct( private readonly string $language_code, private string $title, @@ -97,22 +103,22 @@ public function toForm( ): array { return [ $field_factory->group([ - 'language' => $field_factory->hidden()->withValue($this->language_code), - 'title' => $field_factory->text($language->txt('title')) + self::KEY_LANGUAGE => $field_factory->hidden()->withValue($this->language_code), + self::KEY_TITLE => $field_factory->text($language->txt('title')) ->withRequired(true) ->withValue($this->title), - 'description' => $field_factory->textarea($language->txt('description')) + self::KEY_DESCRIPTION => $field_factory->textarea($language->txt('description')) ->withValue($this->description), - 'default' => $field_factory->hidden()->withValue($this->isDefault()), - 'base' => $field_factory->hidden()->withValue($this->isBase()), + self::KEY_DEFAULT => $field_factory->hidden()->withValue($this->isDefault()), + self::KEY_BASE => $field_factory->hidden()->withValue($this->isBase()), ])->withAdditionalTransformation( $refinery->custom()->transformation( static fn(array $vs): self => new self( - $vs['language'], - $vs['title'], - $vs['description'], - $vs['default'] === '1', - $vs['base'] === '1' + $vs[self::KEY_LANGUAGE], + $vs[self::KEY_TITLE], + $vs[self::KEY_DESCRIPTION], + $vs[self::KEY_DEFAULT] === '1', + $vs[self::KEY_BASE] === '1' ) ) ) @@ -126,16 +132,32 @@ public function toRow( return $row_builder->buildDataRow( $this->language_code, [ - 'language' => $this->getTranslatedLanguageName($lng, $this->language_code), - 'base' => $this->isBase(), - 'default' => $this->isDefault(), - 'title' => $this->getTitle(), - 'description' => $this->getDescription() + self::KEY_LANGUAGE => $this->getTranslatedLanguageName($lng, $this->language_code), + self::KEY_BASE => $this->isBase(), + self::KEY_DEFAULT => $this->isDefault(), + self::KEY_TITLE => $this->getTitle(), + self::KEY_DESCRIPTION => $this->getDescription() ] )->withDisabledAction(TranslationsTable::ACTION_DELETE, $this->isBase() || $this->isDefault()) ->withDisabledAction(TranslationsTable::ACTION_MAKE_DEFAULT, $this->isDefault()); } + public function getDisplayValueForKey( + SystemLanguage $lng, + string $key + ): string|bool { + return match($key) { + self::KEY_LANGUAGE => $this->getTranslatedLanguageName( + $lng, + $this->language_code + ), + self::KEY_BASE => $this->base, + self::KEY_DEFAULT => $this->default, + self::KEY_TITLE => $this->title, + self::KEY_DESCRIPTION => $this->description + }; + } + private function getTranslatedLanguageName( SystemLanguage $lng, string $language_code diff --git a/components/ILIAS/ILIASObject/src/Properties/Translations/TranslationsTable.php b/components/ILIAS/ILIASObject/src/Properties/Translations/TranslationsTable.php index fb9a6a8f4cad..e0506504ad1b 100644 --- a/components/ILIAS/ILIASObject/src/Properties/Translations/TranslationsTable.php +++ b/components/ILIAS/ILIASObject/src/Properties/Translations/TranslationsTable.php @@ -132,7 +132,10 @@ public function getRows( mixed $filter_data, mixed $additional_parameters ): \Generator { - foreach ($this->translations->getLanguages() as $langauge) { + foreach ($this->orderLanguages( + $this->translations->getLanguages(), + $order + ) as $langauge) { yield $langauge->toRow($row_builder, $this->lng); } } @@ -150,10 +153,10 @@ private function getColumns(): array { $cf = $this->ui_factory->table()->column(); $columns = [ - 'language' => $cf->text($this->lng->txt('language')), + Language::KEY_LANGUAGE => $cf->text($this->lng->txt('language')), ]; if ($this->translations->getContentTranslationActivated()) { - $columns['base'] = $cf->boolean( + $columns[Language::KEY_BASE] = $cf->boolean( $this->lng->txt('obj_base_lang'), $this->ui_factory->symbol()->icon()->custom('assets/images/standard/icon_checked.svg', '', 'small'), $this->ui_factory->symbol()->icon()->custom('assets/images/standard/icon_unchecked.svg', '', 'small') @@ -161,13 +164,13 @@ private function getColumns(): array } return $columns + [ - 'default' => $cf->boolean( + Language::KEY_DEFAULT => $cf->boolean( $this->lng->txt('default'), $this->ui_factory->symbol()->icon()->custom('assets/images/standard/icon_checked.svg', '', 'small'), $this->ui_factory->symbol()->icon()->custom('assets/images/standard/icon_unchecked.svg', '', 'small') ), - 'title' => $cf->text($this->lng->txt('title')), - 'description' => $cf->text($this->lng->txt('description')), + Language::KEY_TITLE => $cf->text($this->lng->txt('title')), + Language::KEY_DESCRIPTION => $cf->text($this->lng->txt('description')), ]; } @@ -379,4 +382,26 @@ private function sendAsync(UIComponent $response): void $this->http->sendResponse(); $this->http->close(); } + + private function orderLanguages( + array $languages, + Order $order + ): array { + return $order->join( + $languages, + function (array $langs, string $field, string $direction): array { + usort( + $langs, + fn(Language $a, Language $b) => $a->getDisplayValueForKey($this->lng, $field) + <=> $b->getDisplayValueForKey($this->lng, $field) + ); + + if ($direction === 'DESC') { + return array_reverse($langs); + } + + return $langs; + } + ); + } } From 9724606af6dd1da27fe62219f153b3ee78d1606b Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 14:09:59 +0200 Subject: [PATCH 137/333] Test: Remove Usage of DIC --- .../classes/class.assClozeTest.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index 34adaf3e51e6..c404ba488c4e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -889,12 +889,8 @@ public function deleteGap($gap_index): void */ public function getTextgapPoints($a_original, $a_entered, $max_points): float { - global $DIC; - $refinery = $DIC->refinery(); - $result = 0; - $gaprating = $this->getTextgapRating(); - - switch ($gaprating) { + $result = 0.0; + switch ($this->textgap_rating) { case assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE: if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) { $result = $max_points; @@ -906,19 +902,19 @@ public function getTextgapPoints($a_original, $a_entered, $max_points): float } break; case assClozeGap::TEXTGAP_RATING_LEVENSHTEIN1: - $transformation = $refinery->string()->levenshtein()->standard($a_original, 1); + $transformation = $this->refinery->string()->levenshtein()->standard($a_original, 1); break; case assClozeGap::TEXTGAP_RATING_LEVENSHTEIN2: - $transformation = $refinery->string()->levenshtein()->standard($a_original, 2); + $transformation = $this->refinery->string()->levenshtein()->standard($a_original, 2); break; case assClozeGap::TEXTGAP_RATING_LEVENSHTEIN3: - $transformation = $refinery->string()->levenshtein()->standard($a_original, 3); + $transformation = $this->refinery->string()->levenshtein()->standard($a_original, 3); break; case assClozeGap::TEXTGAP_RATING_LEVENSHTEIN4: - $transformation = $refinery->string()->levenshtein()->standard($a_original, 4); + $transformation = $this->refinery->string()->levenshtein()->standard($a_original, 4); break; case assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5: - $transformation = $refinery->string()->levenshtein()->standard($a_original, 5); + $transformation = $this->refinery->string()->levenshtein()->standard($a_original, 5); break; } From 0f8cf623c05b9414c7e612ca97d95f62576292b5 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 14:10:30 +0200 Subject: [PATCH 138/333] Test: Fix Feedback in Cloze See: https://mantis.ilias.de/view.php?id=48135 --- .../classes/class.assClozeTest.php | 44 +++++++++++++++++++ .../feedback/class.ilAssClozeTestFeedback.php | 19 ++++---- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index c404ba488c4e..a7934c1a21c2 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -925,6 +925,50 @@ public function getTextgapPoints($a_original, $a_entered, $max_points): float return $result; } + /** + * + * @param array $answer_options + */ + public function getAnswerOptionIndexForTextGapAnswer( + array $answer_options, + string $response + ): ?int { + $levenshtein_distance = match ($this->textgap_rating) { + assClozeGap::TEXTGAP_RATING_LEVENSHTEIN1 => 1, + assClozeGap::TEXTGAP_RATING_LEVENSHTEIN2 => 2, + assClozeGap::TEXTGAP_RATING_LEVENSHTEIN3 => 3, + assClozeGap::TEXTGAP_RATING_LEVENSHTEIN4 => 4, + assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5 => 5, + default => null + }; + + if ($levenshtein_distance !== null) { + foreach ($answer_options as $answer_index => $answer_option) { + if ($this->refinery->string()->levenshtein()->standard( + $answer_option->getAnswertext(), + $levenshtein_distance + )->transform($response) >= 0) { + return $answer_index; + } + } + } elseif ($this->textgap_rating === assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE) { + $response_to_lower = strtolower($response); + foreach ($answer_options as $answer_index => $answer_option) { + if (strtolower($answer_option->getAnswertext()) === $response_to_lower) { + return $answer_index; + } + } + } elseif ($this->textgap_rating === assClozeGap::TEXTGAP_RATING_CASESENSITIVE) { + foreach ($answer_options as $answer_index => $answer_option) { + if ($answer_option->getAnswertext() === $response) { + return $answer_index; + } + } + } + + return null; + } + /** * Returns the points for a text gap and compares the given solution with diff --git a/components/ILIAS/TestQuestionPool/classes/feedback/class.ilAssClozeTestFeedback.php b/components/ILIAS/TestQuestionPool/classes/feedback/class.ilAssClozeTestFeedback.php index 904101ec42f9..9534acfe456e 100755 --- a/components/ILIAS/TestQuestionPool/classes/feedback/class.ilAssClozeTestFeedback.php +++ b/components/ILIAS/TestQuestionPool/classes/feedback/class.ilAssClozeTestFeedback.php @@ -821,17 +821,16 @@ public function determineAnswerIndexForAnswerValue(assClozeGap $gap, string $ans return self::FB_TEXT_GAP_EMPTY_INDEX; } - $items = $gap->getItems($this->randomGroup()->dontShuffle()); - - foreach ($items as $answerIndex => $answer) { - /* @var assAnswerCloze $answer */ - - if ($answer->getAnswertext() == $answerValue) { - return $answerIndex; - } - } + $index = $this->questionOBJ->getAnswerOptionIndexForTextGapAnswer( + $gap->getItems( + $this->randomGroup()->dontShuffle() + ), + $answerValue + ); - return self::FB_TEXT_GAP_NOMATCH_INDEX; + return $index === null + ? self::FB_TEXT_GAP_NOMATCH_INDEX + : $index; case assClozeGap::TYPE_SELECT: if ($answerValue !== '') { From 6c98630094681bab2ecfecc943ef726460378d00 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 15:08:26 +0200 Subject: [PATCH 139/333] Test: Fix Display of SpecialChars in Essay See: https://mantis.ilias.de/view.php?id=48138 --- .../classes/class.assTextQuestionGUI.php | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestionGUI.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestionGUI.php index 5cc1f00df162..09a5fd051801 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestionGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestionGUI.php @@ -449,7 +449,7 @@ public function getPreview( if (is_object($this->getPreviewSession())) { $template->setVariable( "ESSAY", - ilLegacyFormElementsUtil::prepareFormOutput( + $this->prepareUserSolutionForAnswerFormOutput( (string) $this->getPreviewSession()->getParticipantsSolution() ) ); @@ -481,14 +481,11 @@ public function getTestOutput( if ($active_id) { $solutions = $this->object->getUserSolutionPreferingIntermediate($active_id, $pass); foreach ($solutions as $solution_value) { - $user_solution = $solution_value["value1"]; + $raw_user_solution = $solution_value["value1"]; } - - if ($this->tiny_mce_enabled) { - $user_solution = htmlentities($user_solution); - } - - $user_solution = str_replace(['{', '}', '\\'], ['{', '}', '\'], $user_solution); + $user_solution = $this->prepareUserSolutionForAnswerFormOutput( + $raw_user_solution + ); } $template = new ilTemplate("tpl.il_as_qpl_text_question_output.html", true, true, "components/ILIAS/TestQuestionPool"); @@ -799,4 +796,14 @@ public function saveCorrectionsFormProperties(ilPropertyFormGUI $form): void $this->writeQuestionSpecificPostData($form); $this->writeAnswerSpecificPostData($form); } + + private function prepareUserSolutionForAnswerFormOutput( + string $solution + ): string { + if ($this->tiny_mce_enabled) { + $solution = htmlentities($solution); + } + + return str_replace(['{', '}', '\\'], ['{', '}', '\'], $solution); + } } From 0dd9998c8f3c3c498f184edf89990557f11a422a Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 29 Jul 2026 16:17:07 +0200 Subject: [PATCH 140/333] Object: There Might be no Default Translation --- .../ILIASObject/src/Properties/Translations/Translations.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php b/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php index 89f7f242eb97..87ed33319e1d 100755 --- a/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php +++ b/components/ILIAS/ILIASObject/src/Properties/Translations/Translations.php @@ -79,7 +79,9 @@ public function getDefaultLanguage(): string public function withDefaultLanguage(string $default_language): self { $clone = clone $this; - $clone->languages[$clone->default_language] = $clone->languages[$clone->default_language]->withDefault(false); + if (isset($clone->languages[$clone->default_language])) { + $clone->languages[$clone->default_language] = $clone->languages[$clone->default_language]->withDefault(false); + } $clone->languages[$default_language] = $clone->languages[$default_language]->withDefault(true); $clone->default_language = $default_language; return $clone; From 216d86f8d04d488fcc52c5bdfc5ca39053fd1c80 Mon Sep 17 00:00:00 2001 From: Fabian Wolf Date: Wed, 29 Jul 2026 17:28:12 +0200 Subject: [PATCH 141/333] Update link to the coding guidelines. --- docs/development/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 5e5c630f8388..63c71da31f31 100755 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -69,7 +69,7 @@ code please make sure: * that your code is understandable and is documented - this will help reviewers as well * that your commit follows the [ILIAS coding - guidelines](https://docu.ilias.de/goto_docu_pg_202_42.html) - this is a + guidelines](./README.md#coding) - this is a bare minimum when it comes to style that we require for new code * you don't introduce new code violations which could have been easily found by importing and running our From e0afe45d2e0fad8b3d91bbd06400467162a735ef Mon Sep 17 00:00:00 2001 From: mjansen Date: Mon, 20 Jul 2026 13:32:38 +0200 Subject: [PATCH 142/333] Chat/ContentPage/Imprint: Fix links in PRIVACY.md references --- components/ILIAS/Chatroom/PRIVACY.md | 10 +++++----- components/ILIAS/ContentPage/PRIVACY.md | 20 ++++++++++---------- components/ILIAS/Imprint/PRIVACY.md | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/components/ILIAS/Chatroom/PRIVACY.md b/components/ILIAS/Chatroom/PRIVACY.md index 2b6f713d29ea..cdf243338811 100644 --- a/components/ILIAS/Chatroom/PRIVACY.md +++ b/components/ILIAS/Chatroom/PRIVACY.md @@ -30,20 +30,20 @@ Several features affect the scope of personal data processing: - The Chatroom component employs the following components, please consult the respective PRIVACY.md files: - - AccessControl – manages permissions for chatroom access, + - [AccessControl](../../ILIAS/AccessControl/PRIVACY.md) – manages permissions for chatroom access, moderation, and settings. - User – resolves user names and profile information for display in the chat. The Chatroom component listens to the User component's `deleteUser` event to clean up ban records. - - Notifications – delivers on-screen notifications for chat + - [Notifications](../../ILIAS/Notifications/PRIVACY.md) – delivers on-screen notifications for chat invitations. - ILIASObject – the Object service stores the account which created a chatroom object and its timestamps. - - InfoScreen – provides the info screen tab for chatroom objects. + - [InfoScreen](../../ILIAS/InfoScreen/PRIVACY.md) – provides the info screen tab for chatroom objects. - OnScreenChat – manages direct messaging conversations between users. The Chatroom component provides user settings for On-Screen Chat participation. - - Export – provides the export framework. Chatroom objects can be + - [Export](../../ILIAS/Export/PRIVACY.md) – provides the export framework. Chatroom objects can be exported as XML including message history. - - Mail – used for sending chat invitation notifications via email. + - [Mail](../../ILIAS/Mail/PRIVACY.md) – used for sending chat invitation notifications via email. ## Data being stored diff --git a/components/ILIAS/ContentPage/PRIVACY.md b/components/ILIAS/ContentPage/PRIVACY.md index 876d5ceccfdd..cf9b1c0cb1ef 100644 --- a/components/ILIAS/ContentPage/PRIVACY.md +++ b/components/ILIAS/ContentPage/PRIVACY.md @@ -1,6 +1,6 @@ # Content Page Privacy -> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories).** ## General Information @@ -24,21 +24,21 @@ property of the page content, not of individual users. - The Content Page component employs the following components, please consult the respective PRIVACY.md files: - - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md) — provides the page editor and page rendering engine. The + - [COPage](../../ILIAS/COPage/PRIVACY.md) — provides the page editor and page rendering engine. The COPage component manages the page content, its edit history, and internal media objects. - - [MetaData](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/MetaData/PRIVACY.md) — stores metadata (e.g., author information) associated + - [MetaData](../../ILIAS/MetaData/PRIVACY.md) — stores metadata (e.g., author information) associated with the Content Page object. - - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) — manages permissions for reading, editing, and + - [AccessControl](../../ILIAS/AccessControl/PRIVACY.md) — manages permissions for reading, editing, and administering Content Page objects. - - [Export](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Export/PRIVACY.md) — provides the XML export functionality for Content Page + - [Export](../../ILIAS/Export/PRIVACY.md) — provides the XML export functionality for Content Page objects, including page content, metadata, and styles. - - [InfoScreen](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/InfoScreen/PRIVACY.md) — renders the Info tab, which may display metadata + - [InfoScreen](../../ILIAS/InfoScreen/PRIVACY.md) — renders the Info tab, which may display metadata and allows private notes. The Info tab can be enabled or disabled per Content Page. - - [Notes](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Notes/PRIVACY.md) — enables private notes for users on the Content Page Info + - [Notes](../../ILIAS/Notes/PRIVACY.md) — enables private notes for users on the Content Page Info screen. - - [KioskMode](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/KioskMode/PRIVACY.md) — provides an embedded presentation mode used when a + - [KioskMode](../../ILIAS/KioskMode/PRIVACY.md) — provides an embedded presentation mode used when a Content Page is viewed inside a course or learning sequence context. - - Style — manages content styles applied to the Content Page. No personal data is handled by + - [Style](../../ILIAS/Style/Content/PRIVACY.md) — manages content styles applied to the Content Page. No personal data is handled by the Style component in this context. - ILIASObject — the Object service stores the account which created the Content Page object and its timestamps. @@ -46,7 +46,7 @@ property of the page content, not of individual users. deactivated, manual completion, and content visited. In manual mode, users can toggle their completion status. In content-visited mode, progress is recorded automatically when the page is viewed. - - [Container](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/Container/PRIVACY.md) — used to store per-object settings such as Info tab visibility via container + - [Container](../../ILIAS/Container/PRIVACY.md) — used to store per-object settings such as Info tab visibility via container settings. ## Data being stored diff --git a/components/ILIAS/Imprint/PRIVACY.md b/components/ILIAS/Imprint/PRIVACY.md index 023c4cbc0cb6..e591e32f55f6 100644 --- a/components/ILIAS/Imprint/PRIVACY.md +++ b/components/ILIAS/Imprint/PRIVACY.md @@ -1,6 +1,6 @@ # Imprint Privacy -> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** ## General Information @@ -11,8 +11,8 @@ The imprint page uses the ILIAS page editor (COPage) for content management. The ## Integrated Components - The Imprint component employs the following components, please consult the respective PRIVACY.md files: - - [COPage](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md) -- the page editor manages all content storage, including page history, author tracking (creator user ID, last change user ID), and page versioning for the imprint page. - - [AccessControl](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/AccessControl/PRIVACY.md) -- manages permissions for editing the imprint page via the RBAC system (`ilPermissionGUI`). + - [COPage](../COPage/PRIVACY.md) -- the page editor manages all content storage, including page history, author tracking (creator user ID, last change user ID), and page versioning for the imprint page. + - [AccessControl](../AccessControl/PRIVACY.md) -- manages permissions for editing the imprint page via the RBAC system (`ilPermissionGUI`). - ILIASObject -- provides the base object framework (`ilObject2`, `ilObject2GUI`) for the legal notice administration object. ## Data being stored @@ -23,7 +23,7 @@ The Imprint component does not store personal data in its own database tables. A - **User ID of the last editor**: stored as `last_change_user` each time the imprint page is saved. - **Page history entries**: each edit creates a history record including the **user ID** of the editor and a **timestamp**, stored in the `page_history` table. -For details on how this data is handled, see the [COPage PRIVACY.md](../COPage/PRIVACY.md](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/components/ILIAS/COPage/PRIVACY.md). +For details on how this data is handled, see the [COPage PRIVACY.md](../COPage/PRIVACY.md). ## Data being presented From aa38cf1f66d03a07c1b83eeebb07bad86e5b3797 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 30 Jul 2026 11:11:04 +0200 Subject: [PATCH 143/333] Test: Fix Import on Missing User --- .../ILIAS/Test/classes/class.ilTestResultsImportParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php b/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php index 86e4e3c68364..ce95d671e3cc 100755 --- a/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php +++ b/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php @@ -135,7 +135,7 @@ public function handlerBeginTag($a_xml_parser, $a_name, $a_attribs): void $this->db->insert('tst_active', [ 'active_id' => [ilDBConstants::T_INTEGER, $next_id], 'user_fi' => [ilDBConstants::T_INTEGER, $usr_id], - 'anonymous_id' => [ilDBConstants::T_TEXT, $a_attribs['anonymous_id'] ?: null], + 'anonymous_id' => [ilDBConstants::T_TEXT, $a_attribs['anonymous_id'] ?? null], 'test_fi' => [ilDBConstants::T_INTEGER, $this->test_obj->getTestId()], 'lastindex' => [ilDBConstants::T_INTEGER, $a_attribs['lastindex']], 'tries' => [ilDBConstants::T_INTEGER, $a_attribs['tries']], From fb701b0c948cddfe9f5e9fe843d40be847832ef0 Mon Sep 17 00:00:00 2001 From: Abraham Date: Thu, 30 Jul 2026 13:30:13 +0200 Subject: [PATCH 144/333] [Survey] update: Change survey assignee for issues (#11837) --- docs/development/maintenance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/maintenance.md b/docs/development/maintenance.md index f4ad2da39d30..3dfabbc1512d 100755 --- a/docs/development/maintenance.md +++ b/docs/development/maintenance.md @@ -1401,7 +1401,7 @@ of ILIAS. The file contains the following fields: * Authority to Sign off on Code Changes: [abrahammordev](https://docu.ilias.de/go/usr/110909), [juanma1331](https://docu.ilias.de/go/usr/107249) * Authority to Curate Test Cases: [jcopado](https://docu.ilias.de/go/usr/30511) * Authority to (De-)Assign Authorities: [jcopado](https://docu.ilias.de/go/usr/30511) - * Assignee for Issues: [jcopado](https://docu.ilias.de/go/usr/30511) + * Assignee for Issues: [abrahammordev](https://docu.ilias.de/go/usr/110909) * Assignee for Security Reports: [jcopado](https://docu.ilias.de/go/usr/30511) * Unit-specific Guidelines, Rules, and Regulations: [LINK MISSING]('') From a7d20b89d29a750b2660124fc03be81ad2139f08 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Fri, 5 Jun 2026 13:40:15 +0200 Subject: [PATCH 145/333] Logging: refactor component, abandon browser log, cache, memory usage, root log level --- ....ilComponentDefinitionsStoredObjective.php | 1 - components/ILIAS/Logging/Logging.php | 49 ++++- components/ILIAS/Logging/README.md | 72 ++++--- components/ILIAS/Logging/ROADMAP.md | 13 +- .../ILIAS/Logging/classes/NullLogger.php | 7 +- .../class.ilLoggingDefinitionProcessor.php | 70 ------- .../classes/class.ilComponentLogger.php | 4 + .../classes/class.ilLogComponentLevel.php | 81 -------- .../classes/class.ilLogComponentLevels.php | 100 --------- .../classes/class.ilLogComponentTableGUI.php | 122 ++++++----- .../ILIAS/Logging/classes/class.ilLogger.php | 30 +-- .../classes/class.ilLoggingDBSettings.php | 169 ++------------- .../classes/class.ilLoggingSetupSettings.php | 109 ---------- .../classes/class.ilObjLoggingSettingsGUI.php | 168 ++++----------- .../classes/public/class.ilLogLevel.php | 1 + .../classes/public/class.ilLoggerFactory.php | 196 ++---------------- .../exceptions/class.ilLogException.php | 2 + .../interface.ilLoggingSettings.php | 29 +-- .../ILIAS/Logging/src/Config/Basic/Config.php | 64 ++++++ .../Config/Basic/ConfigInterface.php} | 21 +- .../Logging/src/Config/Basic/IniReader.php | 51 +++++ .../Config/Basic/IniReaderInterface.php} | 21 +- .../Logging/src/Config/ByComponent/Config.php | 51 +++++ .../Config/ByComponent/ConfigInterface.php | 28 +++ .../src/Config/ByComponent/DBRepository.php | 98 +++++++++ .../ByComponent/RepositoryInterface.php | 41 ++++ .../ILIAS/Logging/src/Config/Config.php | 43 ++++ .../Logging/src/Config/ConfigInterface.php | 31 +++ .../ILIAS/Logging/src/ILIASLogLevel.php | 57 +++++ .../ILIAS/Logging/src/LegacyInitiator.php | 119 +++++++++++ .../src/Logger/DefaultConfigLoggerFactory.php | 43 ++++ .../DefaultConfigLoggerFactoryInterface.php | 33 +++ .../src/Logger/LazyInternalFactory.php | 74 +++++++ .../Logger/LazyInternalFactoryInterface.php | 32 +++ .../LevelFetcher/ComponentLevelFetcher.php} | 31 ++- .../LevelFetcher/DefaultLevelFetcher.php | 37 ++++ .../LevelFetcher/LevelFetcherFactory.php | 40 ++++ .../LevelFetcherFactoryInterface.php | 36 ++++ .../LevelFetcher/LevelFetcherInterface.php | 36 ++++ .../ILIAS/Logging/src/Logger/Logger.php | 65 ++++++ .../Logging/src/Logger/LoggerFactory.php | 43 ++++ .../src/Logger/LoggerFactoryInterface.php | 26 +++ .../Logging/src/Logger/LoggerInterface.php | 44 ++++ .../Logging/src/Logger/Monolog/Factory.php | 69 ++++++ .../src/Logger/Monolog/FactoryInterface.php | 31 +++ .../Logger/Monolog/ILIASLineFormatter.php} | 27 ++- .../Logger/Monolog/ILIASTraceProcessor.php} | 26 +-- .../Setup/Agent.php} | 68 +++--- .../Setup/Config.php} | 26 ++- .../Setup/ConfigStoredObjective.php} | 22 +- .../Setup/DefaultLevelMigratedObjective.php | 92 ++++++++ .../Setup/MetricsCollectedObjective.php} | 12 +- .../src/Setup/Steps/DBUpdateSteps12.php | 58 ++++++ .../Logging/tests/Config/Basic/ConfigTest.php | 180 ++++++++++++++++ .../tests/Config/ByComponent/ConfigTest.php | 129 ++++++++++++ .../Logger/DefaultConfigLoggerFactoryTest.php | 64 ++++++ .../tests/Logger/LazyInternalFactoryTest.php | 156 ++++++++++++++ .../LevelFetcher/LevelFetcherFactoryTest.php | 87 ++++++++ .../tests/Logger/LoggerFactoryTest.php | 64 ++++++ .../Logging/tests/ilLogComponentLevelTest.php | 67 ------ lang/ilias_de.lang | 11 +- lang/ilias_en.lang | 11 +- 62 files changed, 2420 insertions(+), 1168 deletions(-) delete mode 100755 components/ILIAS/Logging/classes/Setup/class.ilLoggingDefinitionProcessor.php delete mode 100755 components/ILIAS/Logging/classes/class.ilLogComponentLevel.php delete mode 100755 components/ILIAS/Logging/classes/class.ilLogComponentLevels.php delete mode 100755 components/ILIAS/Logging/classes/class.ilLoggingSetupSettings.php create mode 100755 components/ILIAS/Logging/src/Config/Basic/Config.php rename components/ILIAS/Logging/{Testrail/throwException.php => src/Config/Basic/ConfigInterface.php} (61%) create mode 100755 components/ILIAS/Logging/src/Config/Basic/IniReader.php rename components/ILIAS/Logging/{Testrail/raiseError.php => src/Config/Basic/IniReaderInterface.php} (58%) create mode 100755 components/ILIAS/Logging/src/Config/ByComponent/Config.php create mode 100755 components/ILIAS/Logging/src/Config/ByComponent/ConfigInterface.php create mode 100755 components/ILIAS/Logging/src/Config/ByComponent/DBRepository.php create mode 100755 components/ILIAS/Logging/src/Config/ByComponent/RepositoryInterface.php create mode 100755 components/ILIAS/Logging/src/Config/Config.php create mode 100755 components/ILIAS/Logging/src/Config/ConfigInterface.php create mode 100755 components/ILIAS/Logging/src/ILIASLogLevel.php create mode 100755 components/ILIAS/Logging/src/LegacyInitiator.php create mode 100755 components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactory.php create mode 100755 components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactoryInterface.php create mode 100755 components/ILIAS/Logging/src/Logger/LazyInternalFactory.php create mode 100755 components/ILIAS/Logging/src/Logger/LazyInternalFactoryInterface.php rename components/ILIAS/Logging/{classes/Setup/class.ilLoggingUpdateSteps8.php => src/Logger/LevelFetcher/ComponentLevelFetcher.php} (51%) create mode 100755 components/ILIAS/Logging/src/Logger/LevelFetcher/DefaultLevelFetcher.php create mode 100755 components/ILIAS/Logging/src/Logger/LevelFetcher/LevelFetcherFactory.php create mode 100755 components/ILIAS/Logging/src/Logger/LevelFetcher/LevelFetcherFactoryInterface.php create mode 100755 components/ILIAS/Logging/src/Logger/LevelFetcher/LevelFetcherInterface.php create mode 100755 components/ILIAS/Logging/src/Logger/Logger.php create mode 100755 components/ILIAS/Logging/src/Logger/LoggerFactory.php create mode 100755 components/ILIAS/Logging/src/Logger/LoggerFactoryInterface.php create mode 100755 components/ILIAS/Logging/src/Logger/LoggerInterface.php create mode 100755 components/ILIAS/Logging/src/Logger/Monolog/Factory.php create mode 100755 components/ILIAS/Logging/src/Logger/Monolog/FactoryInterface.php rename components/ILIAS/Logging/{classes/extensions/class.ilLineFormatter.php => src/Logger/Monolog/ILIASLineFormatter.php} (68%) mode change 100755 => 100644 rename components/ILIAS/Logging/{classes/extensions/class.ilTraceProcessor.php => src/Logger/Monolog/ILIASTraceProcessor.php} (75%) mode change 100755 => 100644 rename components/ILIAS/Logging/{classes/Setup/class.ilLoggingSetupAgent.php => src/Setup/Agent.php} (56%) rename components/ILIAS/Logging/{classes/Setup/class.ilLoggingSetupConfig.php => src/Setup/Config.php} (71%) rename components/ILIAS/Logging/{classes/Setup/class.ilLoggingConfigStoredObjective.php => src/Setup/ConfigStoredObjective.php} (82%) create mode 100755 components/ILIAS/Logging/src/Setup/DefaultLevelMigratedObjective.php rename components/ILIAS/Logging/{classes/Setup/class.ilLoggingMetricsCollectedObjective.php => src/Setup/MetricsCollectedObjective.php} (82%) create mode 100644 components/ILIAS/Logging/src/Setup/Steps/DBUpdateSteps12.php create mode 100755 components/ILIAS/Logging/tests/Config/Basic/ConfigTest.php create mode 100755 components/ILIAS/Logging/tests/Config/ByComponent/ConfigTest.php create mode 100755 components/ILIAS/Logging/tests/Logger/DefaultConfigLoggerFactoryTest.php create mode 100755 components/ILIAS/Logging/tests/Logger/LazyInternalFactoryTest.php create mode 100755 components/ILIAS/Logging/tests/Logger/LevelFetcher/LevelFetcherFactoryTest.php create mode 100755 components/ILIAS/Logging/tests/Logger/LoggerFactoryTest.php delete mode 100755 components/ILIAS/Logging/tests/ilLogComponentLevelTest.php diff --git a/components/ILIAS/Component/classes/Setup/class.ilComponentDefinitionsStoredObjective.php b/components/ILIAS/Component/classes/Setup/class.ilComponentDefinitionsStoredObjective.php index 1cabac44eaff..167582786681 100755 --- a/components/ILIAS/Component/classes/Setup/class.ilComponentDefinitionsStoredObjective.php +++ b/components/ILIAS/Component/classes/Setup/class.ilComponentDefinitionsStoredObjective.php @@ -143,7 +143,6 @@ public function write(): void new \ilBadgeDefinitionProcessor($db), new \ilCOPageDefinitionProcessor($db), new \ilComponentInfoDefinitionProcessor(), - new \ilLoggingDefinitionProcessor($db), new \ILIAS\Cron\Setup\DefinitionProcessor( $db, $settings_factory->settingsFor(), diff --git a/components/ILIAS/Logging/Logging.php b/components/ILIAS/Logging/Logging.php index ea5202974232..4fbc0bad6559 100644 --- a/components/ILIAS/Logging/Logging.php +++ b/components/ILIAS/Logging/Logging.php @@ -32,9 +32,52 @@ public function init( array | \ArrayAccess &$pull, array | \ArrayAccess &$internal, ): void { - $contribute[\ILIAS\Setup\Agent::class] = static fn() => - new \ilLoggingSetupAgent( - $pull[\ILIAS\Refinery\Factory::class] + /*$define[] = Logging\Logger\LoggerFactoryInterface::class; + $define[] = Logging\Logger\DefaultConfigLoggerFactoryInterface::class; + $define[] = Logging\Config\ConfigInterface::class; + + $internal[Logging\Config\Basic\ConfigInterface::class] = static fn() => + new Logging\Config\Basic\Config( + new Logging\Config\Basic\IniReader( + $use[\ilIniFile::class] // TODO change to whatever this is now called + ) + ); + $internal[Logging\Config\ByComponent\ConfigInterface::class] = static fn() => + new Logging\Config\ByComponent\Config( + new Logging\Config\ByComponent\DBRepository( + $use[\ilDBInterface::class] // TODO change to whatever this is now called + ), + $internal[Logging\Config\Basic\ConfigInterface::class] + ); + $internal[Logging\Logger\LevelFetcher\LevelFetcherFactory::class] = static fn() => + new Logging\Logger\LevelFetcher\LevelFetcherFactory(); + $internal[Logging\Logger\LazyInternalFactoryInterface::class] = static fn() => + new Logging\Logger\LazyInternalFactory( + new Logging\Logger\Monolog\Factory(), + $internal[Logging\Config\Basic\ConfigInterface::class] + ); + + $implement[Logging\Logger\LoggerFactoryInterface::class] = static fn() => + new Logging\Logger\LoggerFactory( + $internal[Logging\Logger\LazyInternalFactoryInterface::class], + $internal[Logging\Config\ByComponent\ConfigInterface::class], + $internal[Logging\Logger\LevelFetcher\LevelFetcherFactory::class] + ); + $implement[Logging\Logger\DefaultConfigLoggerFactoryInterface::class] = static fn() => + new Logging\Logger\DefaultConfigLoggerFactory( + $internal[Logging\Logger\LazyInternalFactoryInterface::class], + $internal[Logging\Config\Basic\ConfigInterface::class], + $internal[Logging\Logger\LevelFetcher\LevelFetcherFactory::class] + ); + $implement[Logging\Config\ConfigInterface::class] = static fn() => + new Logging\Config\Config( + $internal[Logging\Config\Basic\ConfigInterface::class], + $internal[Logging\Config\ByComponent\ConfigInterface::class] + );*/ + + $contribute[Setup\Agent::class] = static fn() => + new Logging\Setup\Agent( + $pull[Refinery\Factory::class] ); } } diff --git a/components/ILIAS/Logging/README.md b/components/ILIAS/Logging/README.md index 93f84584dc19..fa30a4723558 100755 --- a/components/ILIAS/Logging/README.md +++ b/components/ILIAS/Logging/README.md @@ -1,60 +1,58 @@ -# Logging Service +# Logging -Starting with release 5.1 a new logging service based on [Monolog](https://github.com/Seldaek/monolog) is available. +The Logging components providers loggers to other components in ILIAS. Those Loggers use [Monolog](https://github.com/Seldaek/monolog), +and are [PSR-3](https://www.php-fig.org/psr/psr-3/) compliant. -The service provides support for different log levels per component. +## Configuration -## Activate Logging for Components +The basic configuration of Logging is done in the `ilias.ini.php` (or alternatively using the Setup). All relevant fields +are in the section `log`. `path` and `file` determine the location of the log directory and the name of the log file, +and `default_level` defines the default log level. -To use different log levels for your component, you have to enable "logging" in your module.xml or service.xml. +Every component and plugin of an ILIAS installation also has its own log level, configurable in the Logging administration. +If no log level is set explicitly for a component, the default log level is used. -```php - - -... - - - -``` - -Call `composer du` to trigger the reading of the XML files. - -Different log levels for components can be defined in "ILIAS -> Administration -> Logging -> Components". -If no component specific log level is given, the the global log level is used. +Your `ilias.ini.php` may also contain an additional field `level` under `log`. That field doesn't do anything, and +can be removed. ## Definition of Log Levels -ILIAS (monolog) support the following log levels defines in [RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424): +ILIAS (via Monolog) supports the following log levels defined in [RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424): -- DEBUG: Detailed debug information -- INFO: Interesting event. E.g user logs in -- NOTICE: Normal but significant events -- WARNING: Exceptional occurences that are no errors. E.g calls of deprecated methods -- ERROR: Runtime errors that do not require immediate action -- CRITICAL: Critical conditions - e.g. a module service is unasable due to missing librarys. -- ALERT: Immediate action is required. E.g. no database connection +- DEBUG: Detailed debug information. +- INFO: Interesting event, e.g. a user logs in. +- NOTICE: Normal but significant events. +- WARNING: Exceptional occurences that are no errors, e.g. calls of deprecated methods. +- ERROR: Runtime errors that do not require immediate action. +- CRITICAL: Critical conditions, e.g. a service is unusable due to missing libraries. +- ALERT: Immediate action is required, e.g. no database connection. - EMERGENCY: The system is unusable. ## Using the Logging Service -An instance of the logging service is available via `$DIC->logger()`. You should reveice the logger for your component by calling a method with your component id on this object. +work in progress -```php +Loggers are available via their ID through the [`LoggerFactory`](src/Logger/LoggerFactoryInterface.php). To get the +logger for your component or plugin, with its own log level, use its respective ID. The factory also gives out loggers +for any other ID, the default log level is then used. -// Get component logger -$grp_logger = $DIC->logger->grp(); - -// write a message with info log level -$grp_logger->info('info message'); +```php +$logger = $factory->getLazy('crs'); +$logger->info('Lorem ipsum'); ``` -## Using Placeholders +Logging also offers a [`DefaultConfigLoggerFactory`](src/Logger/DefaultConfigLoggerFactoryInterface.php), which does not +depend on the Database component. You should only use it if you need to log anything before the database is initialized. +As a tradeoff, its loggers will always use the default log level, no matter the ID. -The ilLogger exposes the placeholder feature given by the monolog bundle, which implements a PSR-3 compliant logger interface. +Note that both factories share the same cache, so it's not possible to get two different loggers with the same ID. If +your component needs to use both a database-unaware logger and a logger with the correct log level, use a different ID +for the former (e.g. `crs_default`). -Placeholders should be used to allow escaping of user input just as `$database->quote(...)` is used to escape user input in SQL queries. +### Using Placeholders -### Example usage +The Logger exposes the placeholder feature of Monolog. Placeholders should be used to allow escaping of user input just +as `$database->quote(...)` is used to escape user input in SQL queries. ```php $logger->debug('Lorem ipsum {foo} dolor {bar}.', [ diff --git a/components/ILIAS/Logging/ROADMAP.md b/components/ILIAS/Logging/ROADMAP.md index 416c2baa6312..c259d6039e94 100755 --- a/components/ILIAS/Logging/ROADMAP.md +++ b/components/ILIAS/Logging/ROADMAP.md @@ -2,11 +2,20 @@ ## Short Term -... +### Move UI to KS, activities + +The Logging administration should be moved to KS, and the GUI classes +refactored. This can be done with the help of activities. ## Mid Term -... +### Allow Components register and configure loggers + +Instead of giving every component and plugin a logger by default, there +should be a mechanism for components to configure their loggers (e.g. +should the log level be changeable in the GUI, for loggers that can't +depend on the database). They should also be able to register additional +loggers. ## Long Term diff --git a/components/ILIAS/Logging/classes/NullLogger.php b/components/ILIAS/Logging/classes/NullLogger.php index 532f8715bf18..3fb5f2012d50 100755 --- a/components/ILIAS/Logging/classes/NullLogger.php +++ b/components/ILIAS/Logging/classes/NullLogger.php @@ -22,9 +22,12 @@ use ilLogger; use ilLogLevel; -use Monolog\Logger; +use ILIAS\Logging\Logger\LoggerInterface; use Exception; +/** + * @deprecated If a null version of the new logger would be handy, let the Logging authorities know. + */ class NullLogger extends ilLogger { public function __construct() @@ -77,7 +80,7 @@ public function emergency(string $message, array $context = []): void } /** @noinspection \PhpInconsistentReturnPointsInspection */ - public function getLogger(): Logger + protected function getLogger(): LoggerInterface { throw new Exception('Can not return monolog logger from a null logger.'); } diff --git a/components/ILIAS/Logging/classes/Setup/class.ilLoggingDefinitionProcessor.php b/components/ILIAS/Logging/classes/Setup/class.ilLoggingDefinitionProcessor.php deleted file mode 100755 index f2c46a9f3f9c..000000000000 --- a/components/ILIAS/Logging/classes/Setup/class.ilLoggingDefinitionProcessor.php +++ /dev/null @@ -1,70 +0,0 @@ -db = $db; - } - - public function purge(): void - { - } - - public function beginComponent(string $component, string $type): void - { - $this->component_id = ''; - } - - public function endComponent(string $component, string $type): void - { - $this->component_id = ''; - } - - public function beginTag(string $name, array $attributes): void - { - if ($name === "module" || $name === "service") { - $this->component_id = $attributes["id"] ?? ''; - return; - } - - if ($name !== "logging") { - return; - } - - if ($this->component_id === '') { - throw new \RuntimeException( - "Found $name-tag outside of module or service in {$this->component_id}." - ); - } - ilLogComponentLevels::updateFromXML($this->component_id); - } - - public function endTag(string $name): void - { - if ($name === "module" || $name === "service") { - $this->component_id = ''; - } - } -} diff --git a/components/ILIAS/Logging/classes/class.ilComponentLogger.php b/components/ILIAS/Logging/classes/class.ilComponentLogger.php index 75af11e69bbf..5a250bf95b67 100755 --- a/components/ILIAS/Logging/classes/class.ilComponentLogger.php +++ b/components/ILIAS/Logging/classes/class.ilComponentLogger.php @@ -19,6 +19,10 @@ declare(strict_types=1); /** * Component logger with individual log levels by component id + * + * @deprecated Please use {@see \ILIAS\Logging\Logger\LoggerInterface} via + * {@see \ILIAS\Logging\Logger\LoggerFactoryInterface} instead. + * * @author Stefan Meyer */ class ilComponentLogger extends ilLogger diff --git a/components/ILIAS/Logging/classes/class.ilLogComponentLevel.php b/components/ILIAS/Logging/classes/class.ilLogComponentLevel.php deleted file mode 100755 index 3943cdbc6de7..000000000000 --- a/components/ILIAS/Logging/classes/class.ilLogComponentLevel.php +++ /dev/null @@ -1,81 +0,0 @@ - - * @version $Id$ - * - */ -class ilLogComponentLevel -{ - private string $compontent_id = ''; - private ?int $component_level = null; - - protected ilDBInterface $db; - - public function __construct(string $a_component_id, ?int $a_level = null) - { - global $DIC; - - $this->db = $DIC->database(); - $this->compontent_id = $a_component_id; - if ($a_level === null) { - $this->read(); - } else { - $this->setLevel($a_level); - } - } - - public function getComponentId(): string - { - return $this->compontent_id; - } - - public function setLevel(?int $a_level): void - { - $this->component_level = $a_level; - } - - public function getLevel(): ?int - { - return $this->component_level; - } - - public function update(): void - { - $this->db->replace( - 'log_components', - array('component_id' => array('text',$this->getComponentId())), - array('log_level' => array('integer',$this->getLevel())) - ); - } - - public function read(): void - { - $query = 'SELECT * FROM log_components ' . - 'WHERE component_id = ' . $this->db->quote($this->getComponentId(), 'text'); - - $res = $this->db->query($query); - while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) { - $this->component_level = (int) $row->log_level; - } - } -} diff --git a/components/ILIAS/Logging/classes/class.ilLogComponentLevels.php b/components/ILIAS/Logging/classes/class.ilLogComponentLevels.php deleted file mode 100755 index 12764f4866af..000000000000 --- a/components/ILIAS/Logging/classes/class.ilLogComponentLevels.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @version $Id$ - * - */ -class ilLogComponentLevels -{ - protected static ?ilLogComponentLevels $instance = null; - /** - * @var ilLogComponentLevel[] - */ - protected array $components = array(); - - protected ilDBInterface $db; - - /** - * constructor - */ - protected function __construct() - { - global $DIC; - $this->db = $DIC->database(); - $this->read(); - } - - public static function getInstance(): ilLogComponentLevels - { - if (!self::$instance) { - self::$instance = new self(); - } - return self::$instance; - } - - /** - * @param string $a_component_id - */ - public static function updateFromXML($a_component_id): bool - { - global $DIC; - - $ilDB = $DIC->database(); - if (!$a_component_id) { - return false; - } - - $query = 'SELECT * FROM log_components ' . - 'WHERE component_id = ' . $ilDB->quote($a_component_id, 'text'); - $res = $ilDB->query($query); - if (!$res->numRows()) { - $query = 'INSERT INTO log_components (component_id) ' . - 'VALUES (' . - $ilDB->quote($a_component_id, 'text') . - ')'; - $ilDB->manipulate($query); - } - return true; - } - - /** - * Get component levels - * @return ilLogComponentLevel[] - */ - public function getLogComponents(): array - { - return $this->components; - } - - public function read(): void - { - $query = 'SELECT * FROM log_components '; - $res = $this->db->query($query); - - $this->components = array(); - while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) { - $this->components[] = new ilLogComponentLevel((string) $row->component_id, (int) $row->log_level); - } - } -} diff --git a/components/ILIAS/Logging/classes/class.ilLogComponentTableGUI.php b/components/ILIAS/Logging/classes/class.ilLogComponentTableGUI.php index 335f3a31a5ef..ecdd5cc81d1e 100755 --- a/components/ILIAS/Logging/classes/class.ilLogComponentTableGUI.php +++ b/components/ILIAS/Logging/classes/class.ilLogComponentTableGUI.php @@ -17,32 +17,28 @@ *********************************************************************/ declare(strict_types=1); + +use ILIAS\Logging\Config\Basic\ConfigInterface as BasicConfig; +use ILIAS\Logging\Config\ByComponent\RepositoryInterface as ComponentConfigRepo; +use ILIAS\Logging\ILIASLogLevel; + /** * Component logger with individual log levels by component id */ class ilLogComponentTableGUI extends ilTable2GUI { - protected ilComponentRepository $component_repo; - protected ?ilLoggingDBSettings $settings = null; - protected bool $editable = true; - - public function __construct(object $a_parent_obj, string $a_parent_cmd = "") - { - global $DIC; - $this->component_repo = $DIC["component.repository"]; - + public function __construct( + protected bool $editable, + protected ilComponentRepository $component_repo, + protected BasicConfig $basic_log_config, + protected ComponentConfigRepo $component_config_repo, + object $a_parent_obj, + string $a_parent_cmd = "" + ) { $this->setId('il_log_component'); parent::__construct($a_parent_obj, $a_parent_cmd); } - /** - * Set ediatable (write permission granted) - */ - public function setEditable(bool $a_status): void - { - $this->editable = $a_status; - } - /** * Check if ediatable (write permission granted) */ @@ -57,7 +53,6 @@ public function isEditable(): bool public function init(): void { $this->setFormAction($this->ctrl->getFormAction($this->getParentObject())); - $this->settings = ilLoggingDBSettings::getInstance(); $this->setRowTemplate('tpl.log_component_row.html', 'components/ILIAS/Logging'); $this->addColumn($this->lng->txt('log_component_col_component'), 'component_sortable'); @@ -74,63 +69,76 @@ public function init(): void $this->setLimit(500); } - /** - * Get settings - */ - public function getSettings(): ilLoggingDBSettings - { - return $this->settings; - } - /** * Parse table */ public function parse(): void { - $components = ilLogComponentLevels::getInstance()->getLogComponents(); - $rows = array(); - foreach ($components as $component) { - $row['id'] = $component->getComponentId(); - if ($component->getComponentId() == 'log_root') { - $row['component'] = 'Root'; - $row['component_sortable'] = '_' . $row['component']; - } else { - if ($this->component_repo->hasComponentId( - $component->getComponentId() - )) { - $row['component'] = $this->component_repo->getComponentById( - $component->getComponentId() - )->getName(); - } else { - $row['component'] = "Unknown (" . $component->getComponentId() . ")"; - } - $row['component_sortable'] = $row['component']; - } - $row['level'] = (int) $component->getLevel(); + $levels_by_component = $this->component_config_repo->getAllLevelsForComponents(); + + $rows = []; + foreach ($this->component_repo->getComponents() as $id => $component) { + $row = []; + $row['id'] = $id; + $row['component'] = $row['component_sortable'] = $component->getName(); + $row['level'] = $levels_by_component[$id]?->value ?? 0; + unset($levels_by_component[$id]); + $rows[] = $row; + } + foreach ($this->component_repo->getPlugins() as $id => $plugin) { + $row = []; + $row['id'] = $id; + $row['component'] = $row['component_sortable'] = $plugin->getName(); + $row['level'] = $levels_by_component[$id]?->value ?? 0; + unset($levels_by_component[$id]); + $rows[] = $row; + } + foreach ($levels_by_component as $id => $level) { + $row = []; + $row['id'] = $id; + $row['component'] = $row['component_sortable'] = sprintf( + $this->lng->txt('log_component_unknown'), + $id + ); + $row['level'] = $level->value; + unset($levels_by_component[$id]); $rows[] = $row; } $this->setMaxCount(count($rows)); $this->setData($rows); } - /** - * @inheritDoc - */ protected function fillRow(array $a_set): void { $this->tpl->setVariable('CNAME', $a_set['component']); - if ($a_set['id'] == 'log_root') { - $this->tpl->setVariable('TXT_DESC', $this->lng->txt('log_component_root_desc')); - } - $default_option_value = ilLoggingDBSettings::getInstance()->getLevel(); - $array_options = ilLogLevel::getLevelOptions(); - $default_option = array( 0 => $this->lng->txt('default') . " (" . $array_options[$default_option_value] . ")"); - $array_options = $default_option + $array_options; + $default_label = sprintf( + $this->lng->txt('log_level_default'), + $this->presentableLogLevel($this->basic_log_config->defaultLevel()) + ); + $options = [0 => $default_label]; + foreach (ILIASLogLevel::cases() as $level) { + $options[$level->value] = $this->presentableLogLevel($level); + } $levels = new ilSelectInputGUI('', 'level[' . $a_set['id'] . ']'); - $levels->setOptions($array_options); + $levels->setOptions($options); $levels->setValue($a_set['level']); $this->tpl->setVariable('C_SELECT_LEVEL', $levels->render()); } + + protected function presentableLogLevel(ILIASLogLevel $level): string + { + return match ($level) { + ILIASLogLevel::DEBUG => $this->lng->txt('log_level_debug'), + ILIASLogLevel::INFO => $this->lng->txt('log_level_info'), + ILIASLogLevel::NOTICE => $this->lng->txt('log_level_notice'), + ILIASLogLevel::WARNING => $this->lng->txt('log_level_warning'), + ILIASLogLevel::ERROR => $this->lng->txt('log_level_error'), + ILIASLogLevel::CRITICAL => $this->lng->txt('log_level_critical'), + ILIASLogLevel::ALERT => $this->lng->txt('log_level_alert'), + ILIASLogLevel::EMERGENCY => $this->lng->txt('log_level_emergency'), + ILIASLogLevel::OFF => $this->lng->txt('log_level_off') + }; + } } diff --git a/components/ILIAS/Logging/classes/class.ilLogger.php b/components/ILIAS/Logging/classes/class.ilLogger.php index 94c684c4b42f..62d1002dd378 100755 --- a/components/ILIAS/Logging/classes/class.ilLogger.php +++ b/components/ILIAS/Logging/classes/class.ilLogger.php @@ -18,16 +18,20 @@ declare(strict_types=1); -use Monolog\Logger; +use ILIAS\Logging\Logger\LoggerInterface; use Monolog\Processor\MemoryPeakUsageProcessor; /** * Component logger with individual log levels by component id + * + * @deprecated Please use {@see \ILIAS\Logging\Logger\LoggerInterface} via + * {@see \ILIAS\Logging\Logger\LoggerFactoryInterface} instead. + * * @author Stefan Meyer */ abstract class ilLogger { - public function __construct(private readonly Logger $logger) + public function __construct(private readonly LoggerInterface $logger) { } @@ -36,7 +40,7 @@ public function __construct(private readonly Logger $logger) */ public function isHandling(int $level): bool { - return $this->getLogger()->isHandling($level); + return $this->getLogger()->isHandlingLogLevel($level); } public function log(string $message, int $level = ilLogLevel::INFO, array $context = []): void @@ -91,7 +95,7 @@ public function emergency(string $message, array $context = []): void $this->getLogger()->emergency($message, $context); } - public function getLogger(): Logger + protected function getLogger(): LoggerInterface { return $this->logger; } @@ -128,22 +132,6 @@ public function logStack(?int $level = null, string $message = '', array $contex $level = ilLogLevel::INFO; } - - try { - throw new Exception($message); - } catch (Exception $ex) { - $this->getLogger()->log($level, $message . "\n" . $ex->getTraceAsString(), $context); - } - } - - /** - * Write memory peak usage - * Automatically called at end of script - */ - public function writeMemoryPeakUsage(int $level): void - { - $this->getLogger()->pushProcessor(new MemoryPeakUsageProcessor()); - $this->getLogger()->log($level, 'Memory usage: '); - $this->getLogger()->popProcessor(); + $this->getLogger()->logStack($level, $message, $context); } } diff --git a/components/ILIAS/Logging/classes/class.ilLoggingDBSettings.php b/components/ILIAS/Logging/classes/class.ilLoggingDBSettings.php index fc25a9694818..8c3ea9e0c9fc 100755 --- a/components/ILIAS/Logging/classes/class.ilLoggingDBSettings.php +++ b/components/ILIAS/Logging/classes/class.ilLoggingDBSettings.php @@ -17,39 +17,31 @@ *********************************************************************/ declare(strict_types=1); + +use ILIAS\Logging\Config\Basic\ConfigInterface as BasicConfig; +use ILIAS\Logging\Config\ByComponent\ConfigInterface as ComponentConfig; +use ILIAS\Logging\Logger\LegacyInitiator; + /** -* @defgroup ServicesLogging Services/Logging -* -* @author Stefan Meyer -* @ingroup ServicesLogging -*/ + * @deprecated Please use {@see \ILIAS\Logging\Config\ConfigInterface} instead. + * + * @defgroup ServicesLogging Services/Logging + * + * @author Stefan Meyer + * @ingroup ServicesLogging + */ class ilLoggingDBSettings implements ilLoggingSettings { protected static ?ilLoggingDBSettings $instance = null; - private bool $enabled = false; - private ilSetting $storage; - - private int $level; - private bool $cache = false; - private int $cache_level; - private bool $memory_usage = false; - private bool $browser = false; - /** - * @var string[] - */ - private array $browser_users = array(); - - + protected BasicConfig $basic_config; + protected ComponentConfig $component_config; private function __construct() { - $this->enabled = (bool) ILIAS_LOG_ENABLED; - $this->level = ilLogLevel::INFO; - $this->cache_level = ilLogLevel::DEBUG; - - $this->storage = new ilSetting('logging'); - $this->read(); + $initiator = LegacyInitiator::getInstance(); + $this->basic_config = $initiator->basicConfig(); + $this->component_config = $initiator->componentConfig(); } public static function getInstance(): self @@ -66,143 +58,24 @@ public static function getInstance(): self */ public function getLevelByComponent(string $a_component_id): int { - $levels = ilLogComponentLevels::getInstance()->getLogComponents(); - foreach ($levels as $level) { - if ($level->getComponentId() == $a_component_id) { - if ($level->getLevel()) { - return $level->getLevel(); - } - } - } - return $this->getLevel(); - } - - /** - * @return ilSetting - */ - protected function getStorage(): ilSetting - { - return $this->storage; + return $this->component_config->level($a_component_id)->value; } /** * Check if logging is enabled - * @return bool */ public function isEnabled(): bool { - return $this->enabled; + return $this->basic_config->isLoggingEnabled(); } public function getLogDir(): string { - return ILIAS_LOG_DIR; - } - - public function getLogFile(): string - { - return ILIAS_LOG_FILE; + return $this->basic_config->pathToLogDirectory(); } public function getLevel(): int { - return $this->level; - } - - public function setLevel(int $a_level): void - { - $this->level = $a_level; - } - - public function setCacheLevel(int $a_level): void - { - $this->cache_level = $a_level; - } - - public function getCacheLevel(): int - { - return $this->cache_level; - } - - public function enableCaching(bool $a_status): void - { - $this->cache = $a_status; - } - - public function isCacheEnabled(): bool - { - return $this->cache; - } - - public function enableMemoryUsage(bool $a_stat): void - { - $this->memory_usage = $a_stat; - } - - public function isMemoryUsageEnabled(): bool - { - return $this->memory_usage; - } - - public function isBrowserLogEnabled(): bool - { - return $this->browser; - } - - - public function isBrowserLogEnabledForUser(string $a_login): bool - { - if (!$this->isBrowserLogEnabled()) { - return false; - } - if (in_array($a_login, $this->getBrowserLogUsers())) { - return true; - } - return false; - } - - public function enableBrowserLog(bool $a_stat): void - { - $this->browser = $a_stat; - } - - public function getBrowserLogUsers(): array - { - return $this->browser_users; - } - - public function setBrowserUsers(array $users): void - { - $this->browser_users = $users; - } - - - /** - * Update setting - */ - public function update(): void - { - $this->getStorage()->set('level', (string) $this->getLevel()); - $this->getStorage()->set('cache', (string) $this->isCacheEnabled()); - $this->getStorage()->set('cache_level', (string) $this->getCacheLevel()); - $this->getStorage()->set('memory_usage', (string) $this->isMemoryUsageEnabled()); - $this->getStorage()->set('browser', (string) $this->isBrowserLogEnabled()); - $this->getStorage()->set('browser_users', serialize($this->getBrowserLogUsers())); - } - - - /** - * Read settings - * - * @access private - */ - private function read(): void - { - $this->setLevel((int) $this->getStorage()->get('level', (string) $this->level)); - $this->enableCaching((bool) $this->getStorage()->get('cache', (string) $this->cache)); - $this->setCacheLevel((int) $this->getStorage()->get('cache_level', (string) $this->cache_level)); - $this->enableMemoryUsage((bool) $this->getStorage()->get('memory_usage', (string) $this->memory_usage)); - $this->enableBrowserLog((bool) $this->getStorage()->get('browser', (string) $this->browser)); - $this->setBrowserUsers((array) unserialize($this->getStorage()->get('browser_users', serialize($this->browser_users)))); + return $this->basic_config->defaultLevel()->value; } } diff --git a/components/ILIAS/Logging/classes/class.ilLoggingSetupSettings.php b/components/ILIAS/Logging/classes/class.ilLoggingSetupSettings.php deleted file mode 100755 index 11288c80e180..000000000000 --- a/components/ILIAS/Logging/classes/class.ilLoggingSetupSettings.php +++ /dev/null @@ -1,109 +0,0 @@ - -* @ingroup ServicesLogging -*/ -class ilLoggingSetupSettings implements ilLoggingSettings -{ - private bool $enabled = false; - private string $log_dir = ''; - private string $log_file = ''; - - - public function init(): void - { - $ilIliasIniFile = new ilIniFile("./ilias.ini.php"); - $ilIliasIniFile->read(); - - $enabled = $ilIliasIniFile->readVariable('log', 'enabled'); - $this->enabled = $enabled == '1'; - $this->log_dir = (string) $ilIliasIniFile->readVariable('log', 'path'); - $this->log_file = (string) $ilIliasIniFile->readVariable('log', 'file'); - } - - /** - * Logging enabled - * @return bool - */ - public function isEnabled(): bool - { - return $this->enabled; - } - - public function getLogDir(): string - { - return $this->log_dir; - } - - public function getLogFile(): string - { - return $this->log_file; - } - - /** - * Get log Level - * @return int - */ - public function getLevel(): int - { - return ilLogLevel::INFO; - } - - public function getLevelByComponent(string $a_component_id): int - { - return $this->getLevel(); - } - - /** - * Get log Level - * @return int - */ - public function getCacheLevel(): int - { - return ilLogLevel::INFO; - } - - public function isCacheEnabled(): bool - { - return false; - } - - public function isMemoryUsageEnabled(): bool - { - return false; - } - - public function isBrowserLogEnabled(): bool - { - return false; - } - - public function isBrowserLogEnabledForUser(string $a_login): bool - { - return false; - } - - public function getBrowserLogUsers(): array - { - return array(); - } -} diff --git a/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php b/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php index fc91f1e100ff..00b286407990 100755 --- a/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php +++ b/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php @@ -17,9 +17,15 @@ *********************************************************************/ declare(strict_types=1); + use ILIAS\DI\Container; use ILIAS\Refinery\Factory as Refinery; use ILIAS\HTTP\Services as Services; +use ILIAS\Logging\Config\Basic\ConfigInterface as BasicConfig; +use ILIAS\Logging\Config\ByComponent\ConfigInterface as ByComponentConfig; +use ILIAS\Logging\Config\ByComponent\RepositoryInterface as ComponentConfigRepo; +use ILIAS\Logging\ILIASLogLevel; +use ILIAS\Logging\Logger\LegacyInitiator; /** * @@ -31,21 +37,16 @@ */ class ilObjLoggingSettingsGUI extends ilObjectGUI { - protected const SECTION_SETTINGS = 'settings'; - protected const SUB_SECTION_MAIN = 'log_general_settings'; - protected const SUB_SECTION_COMPONENTS = 'log_components'; - protected const SUB_SECTION_ERROR = 'log_error_settings'; + protected const string SECTION_SETTINGS = 'settings'; + protected const string SUB_SECTION_COMPONENTS = 'log_components'; + protected const string SUB_SECTION_ERROR = 'log_error_settings'; - protected ilLoggingDBSettings $log_settings; - protected ilLogger $log; protected ilLoggingErrorSettings $error_settings; protected Refinery $refinery; + protected ilComponentRepository $component_repo; + protected BasicConfig $basic_log_config; + protected ComponentConfigRepo $component_config_repo; - /** - * - * @param mixed $a_data - * @param boolean $a_prepare_output - */ public function __construct($a_data, int $a_id, bool $a_call_by_reference, bool $a_prepare_output = true) { global $DIC; @@ -54,19 +55,17 @@ public function __construct($a_data, int $a_id, bool $a_call_by_reference, bool parent::__construct($a_data, $a_id, $a_call_by_reference, $a_prepare_output); $this->lng = $DIC->language(); + $this->component_repo = $DIC["component.repository"]; - $this->initSettings(); $this->initErrorSettings(); $this->lng->loadLanguageModule('logging'); $this->lng->loadLanguageModule('log'); - $this->log = ilLoggerFactory::getLogger('log'); $this->refinery = $DIC->refinery(); - } - public function getLogger(): ilLogger - { - return $this->log; + $initiator = LegacyInitiator::getInstance(); + $this->basic_log_config = $initiator->basicConfig(); + $this->component_config_repo = $initiator->componentConfigRepository(); } public function executeCommand(): void @@ -74,6 +73,7 @@ public function executeCommand(): void $next_class = $this->ctrl->getNextClass($this); $cmd = $this->ctrl->getCmd(); $this->prepareOutput(); + $this->checkPermission('read'); switch ($next_class) { case 'ilpermissiongui': @@ -84,7 +84,7 @@ public function executeCommand(): void default: if ($cmd == "" || $cmd == "view") { - $cmd = "settings"; + $cmd = "errorSettings"; } $this->$cmd(); @@ -111,11 +111,6 @@ public function getAdminTabs(): void public function setSubTabs(string $a_section): void { - $this->tabs_gui->addSubTab( - static::SUB_SECTION_MAIN, - $this->lng->txt(static::SUB_SECTION_MAIN), - $this->ctrl->getLinkTarget($this, 'settings') - ); $this->tabs_gui->addSubTab( static::SUB_SECTION_ERROR, $this->lng->txt(static::SUB_SECTION_ERROR), @@ -129,108 +124,6 @@ public function setSubTabs(string $a_section): void $this->tabs_gui->activateSubTab($a_section); } - protected function initSettings() - { - $this->log_settings = ilLoggingDBSettings::getInstance(); - } - - public function getSettings(): ilLoggingDBSettings - { - return $this->log_settings; - } - - public function settings(?ilPropertyFormGUI $form = null) - { - if (!$this->rbac_system->checkAccess("read", $this->object->getRefId())) { - $this->error->raiseError($this->lng->txt('permission_denied'), $this->error->MESSAGE); - } - - $this->tabs_gui->setTabActive(static::SECTION_SETTINGS); - $this->setSubTabs(static::SUB_SECTION_MAIN); - - if (!$form instanceof ilPropertyFormGUI) { - $form = $this->initFormSettings(); - } - $this->tpl->setContent($form->getHTML()); - $this->getLogger()->debug('Currrent level is ' . $this->getSettings()->getLevel()); - return true; - } - - public function updateSettings(): void - { - if (!$this->rbac_system->checkAccess('write', $this->object->getRefId())) { - $this->ilias->raiseError($this->lng->txt("permission_denied"), $this->ilias->error_obj->MESSAGE); - } - $form = $this->initFormSettings(); - if ($form->checkInput()) { - $this->getSettings()->setLevel((int) $form->getInput('level')); - $this->getSettings()->enableCaching((bool) $form->getInput('cache')); - $this->getSettings()->setCacheLevel((int) $form->getInput('cache_level')); - $this->getSettings()->enableMemoryUsage((bool) $form->getInput('memory')); - $this->getSettings()->enableBrowserLog((bool) $form->getInput('browser')); - $this->getSettings()->setBrowserUsers($form->getInput('browser_users')); - - $this->getLogger()->info(print_r($form->getInput('browser_users'), true)); - - $this->getSettings()->update(); - - $this->tpl->setOnScreenMessage('success', $this->lng->txt('settings_saved'), true); - $this->ctrl->redirect($this, 'settings'); - return; - } - - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('err_check_input')); - $form->setValuesByPost(); - $this->settings($form); - } - - protected function initFormSettings(): ilPropertyFormGUI - { - $form = new ilPropertyFormGUI(); - $form->setTitle($this->lng->txt('logs_settings')); - $form->setFormAction($this->ctrl->getFormAction($this)); - - if ($this->access->checkAccess('write', '', $this->object->getRefId())) { - $form->addCommandButton('updateSettings', $this->lng->txt('save')); - } - - $level = new ilSelectInputGUI($this->lng->txt('log_log_level'), 'level'); - $level->setOptions(ilLogLevel::getLevelOptions()); - $level->setValue($this->getSettings()->getLevel()); - $form->addItem($level); - - $cache = new ilCheckboxInputGUI($this->lng->txt('log_cache_'), 'cache'); - $cache->setInfo($this->lng->txt('log_cache_info')); - $cache->setValue('1'); - $cache->setChecked($this->getSettings()->isCacheEnabled()); - $form->addItem($cache); - - $cache_level = new ilSelectInputGUI($this->lng->txt('log_cache_level'), 'cache_level'); - $cache_level->setOptions(ilLogLevel::getLevelOptions()); - $cache_level->setValue($this->getSettings()->getCacheLevel()); - $cache->addSubItem($cache_level); - - $memory = new ilCheckboxInputGUI($this->lng->txt('log_memory'), 'memory'); - $memory->setValue('1'); - $memory->setChecked($this->getSettings()->isMemoryUsageEnabled()); - $form->addItem($memory); - - // Browser handler - $browser = new ilCheckboxInputGUI($this->lng->txt('log_browser'), 'browser'); - $browser->setValue('1'); - $browser->setChecked($this->getSettings()->isBrowserLogEnabled()); - $form->addItem($browser); - - // users - $users = new ilTextInputGUI($this->lng->txt('log_browser_users'), 'browser_users'); - $users->setValue(current($this->getSettings()->getBrowserLogUsers())); - $users->setMulti(true); - $users->setMultiValues($this->getSettings()->getBrowserLogUsers()); - $this->getLogger()->debug(print_r($this->getSettings()->getBrowserLogUsers(), true)); - $browser->addSubItem($users); - return $form; - } - /** * Show components @@ -240,8 +133,14 @@ protected function components(): void $this->tabs_gui->activateTab(static::SECTION_SETTINGS); $this->setSubTabs(static::SUB_SECTION_COMPONENTS); - $table = new ilLogComponentTableGUI($this, 'components'); - $table->setEditable($this->checkPermissionBool('write')); + $table = new ilLogComponentTableGUI( + $this->checkPermissionBool('write'), + $this->component_repo, + $this->basic_log_config, + $this->component_config_repo, + $this, + 'components' + ); $table->init(); $table->parse(); $this->tpl->setContent($table->getHTML()); @@ -277,8 +176,14 @@ static function ($k, $v): array { ); } foreach ($levels as $component_id => $value) { - $level = new ilLogComponentLevel($component_id, $value); - $level->update(); + if ($value === 0) { + $this->component_config_repo->resetLevelForComponent($component_id); + } + $level = ILIASLogLevel::tryFrom($value); + if ($level === null) { + continue; + } + $this->component_config_repo->updateLevelForComponent($component_id, $level); } $this->tpl->setOnScreenMessage('success', $this->lng->txt('settings_saved'), true); $this->ctrl->redirect($this, 'components'); @@ -287,10 +192,7 @@ static function ($k, $v): array { protected function resetComponentLevels(): void { $this->checkPermission('write'); - foreach (ilLogComponentLevels::getInstance()->getLogComponents() as $component) { - $component->setLevel(null); - $component->update(); - } + $this->component_config_repo->resetLevelsForAllComponents(); $this->tpl->setOnScreenMessage('success', $this->lng->txt('settings_saved'), true); $this->ctrl->redirect($this, 'components'); } diff --git a/components/ILIAS/Logging/classes/public/class.ilLogLevel.php b/components/ILIAS/Logging/classes/public/class.ilLogLevel.php index 1678c78701eb..308468a3b339 100755 --- a/components/ILIAS/Logging/classes/public/class.ilLogLevel.php +++ b/components/ILIAS/Logging/classes/public/class.ilLogLevel.php @@ -26,6 +26,7 @@ * * @author Stefan Meyer * + * @deprecated Please use {@see \ILIAS\Logging\ILIASLogLevel} instead. */ class ilLogLevel { diff --git a/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php b/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php index e865f2b34aca..afe412644d8b 100755 --- a/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php +++ b/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php @@ -18,70 +18,46 @@ declare(strict_types=1); -use Monolog\Logger; -use Monolog\Handler\StreamHandler; -use Monolog\Handler\BrowserConsoleHandler; -use Monolog\Formatter\LineFormatter; -use Monolog\Handler\FingersCrossedHandler; -use Monolog\Handler\NullHandler; -use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; -use ILIAS\DI\Container; -use Monolog\Processor\PsrLogMessageProcessor; +use ILIAS\Logging\Logger\LoggerFactoryInterface; +use ILIAS\Logging\Logger\DefaultConfigLoggerFactoryInterface; +use ILIAS\Logging\Logger\LegacyInitiator; /** * Logging factory * + * @deprecated Please use {@see \ILIAS\Logging\Logger\LoggerInterface} via + * {@see \ILIAS\Logging\Logger\LoggerFactoryInterface} instead. + * * @author Stefan Meyer * */ class ilLoggerFactory { - protected const DEFAULT_FORMAT = "[%extra.suid%] [%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; - - protected const ROOT_LOGGER = 'root'; - protected const COMPONENT_ROOT = 'log_root'; - protected const SETUP_LOGGER = 'setup'; - private static ?ilLoggerFactory $instance = null; - private ilLoggingSettings $settings; - protected Container $dic; - - private bool $enabled = false; //ToDo PHP8 Review: This is a private var never read only written and should probably be removed. + private LoggerFactoryInterface $logger_factory; + private DefaultConfigLoggerFactoryInterface $default_config_logger_factory; /** * @var array */ - private array $loggers = array(); + private array $loggers = []; - protected function __construct(ilLoggingSettings $settings) + protected function __construct() { - global $DIC; - - $this->dic = $DIC; - $this->settings = $settings; - $this->enabled = $this->getSettings()->isEnabled(); + $initiator = LegacyInitiator::getInstance(); + $this->logger_factory = $initiator->loggerFactory(); + $this->default_config_logger_factory = $initiator->defaultConfigLoggerFactory(); } public static function getInstance(): ilLoggerFactory { if (!static::$instance instanceof ilLoggerFactory) { - $settings = ilLoggingDBSettings::getInstance(); - static::$instance = new ilLoggerFactory($settings); + static::$instance = new ilLoggerFactory(); } return static::$instance; } - public static function newInstance(ilLoggingSettings $settings): ilLoggerFactory - { - return static::$instance = new self($settings); - } - - public function isLoggingEnabled(): bool - { - return $this->enabled; - } - /** * Get component logger @@ -98,7 +74,7 @@ public static function getLogger(string $a_component_id): ilLogger public static function getRootLogger(): ilLogger { $factory = self::getInstance(); - return $factory->getComponentLogger(self::ROOT_LOGGER); + return $factory->getComponentLogger('root'); } @@ -107,64 +83,11 @@ public static function getRootLogger(): ilLogger */ public function initUser(string $a_login): void { - if (!$this->getSettings()->isBrowserLogEnabledForUser($a_login)) { - return; - } - - foreach ($this->loggers as $a_component_id => $logger) { - if ($this->isConsoleAvailable()) { - $browser_handler = new BrowserConsoleHandler(); - $browser_handler->setLevel($this->getSettings()->getLevelByComponent($a_component_id)); - $browser_handler->setFormatter(new ilLineFormatter(static::DEFAULT_FORMAT, 'Y-m-d H:i:s.u', true, true)); - $logger->getLogger()->pushHandler($browser_handler); - } - } - } - - /** - * Check if console handler is available - */ - protected function isConsoleAvailable(): bool - { - if (ilContext::getType() !== ilContext::CONTEXT_WEB) { - return false; - } - - if (($this->dic->isDependencyAvailable('ctrl') && $this->dic->ctrl()->isAsynch()) || - ( - $this->dic->isDependencyAvailable('http') && - strtolower( - $this->dic->http()->request()->getServerParams()['HTTP_X_REQUESTED_WITH'] ?? '' - ) === 'xmlhttprequest' - ) - ) { - return false; - } - - if ($this->dic->isDependencyAvailable('http') && - str_contains($this->dic->http()->request()->getServerParams()['HTTP_ACCEPT'] ?? '', 'text/html')) { - return true; - } - - if ($this->dic->isDependencyAvailable('http') && - str_contains($this->dic->http()->request()->getServerParams()['HTTP_ACCEPT'] ?? '', 'application/json')) { - return false; - } - - return true; } public function getSettings(): ilLoggingSettings { - return $this->settings; - } - - /** - * @return ilComponentLogger[] - */ - protected function getLoggers(): array - { - return $this->loggers; + return ilLoggingDBSettings::getInstance(); } public function getComponentLogger(string $a_component_id): ilLogger @@ -173,88 +96,13 @@ public function getComponentLogger(string $a_component_id): ilLogger return $this->loggers[$a_component_id]; } - $loggerNamePrefix = ''; - if (defined('CLIENT_ID')) { - $loggerNamePrefix = CLIENT_ID . '_'; - } - - switch ($a_component_id) { - case 'root': - $logger = new Logger($loggerNamePrefix . 'root'); - break; - - default: - $logger = new Logger($loggerNamePrefix . $a_component_id); - break; - } - - if (!$this->isLoggingEnabled()) { - $null_handler = new NullHandler(); - $logger->pushHandler($null_handler); - - return $this->loggers[$a_component_id] = new ilComponentLogger($logger); - } - - - // standard stream handler - $stream_handler = new StreamHandler( - $this->getSettings()->getLogDir() . '/' . $this->getSettings()->getLogFile(), - Logger::DEBUG, // default minimum level, will be overwritten by component log level - true - ); - - if ($a_component_id == self::ROOT_LOGGER) { - $stream_handler->setLevel($this->getSettings()->getLevelByComponent(self::COMPONENT_ROOT)); - } else { - $stream_handler->setLevel($this->getSettings()->getLevelByComponent($a_component_id)); - } - - // format lines - $line_formatter = new ilLineFormatter(static::DEFAULT_FORMAT, 'Y-m-d H:i:s.u', true, true); - $stream_handler->setFormatter($line_formatter); - - if ($this->getSettings()->isCacheEnabled()) { - // add new finger crossed handler - $finger_crossed_handler = new FingersCrossedHandler( - $stream_handler, - new ErrorLevelActivationStrategy($this->getSettings()->getCacheLevel()), - 1000 + if ($a_component_id === 'root') { + return $this->loggers['root'] = new ilComponentLogger( + $this->default_config_logger_factory->getLazy('legacy_root') ); - $logger->pushHandler($finger_crossed_handler); - } else { - $logger->pushHandler($stream_handler); } - - if ( - $this->dic->offsetExists('ilUser') && - $this->dic->user() instanceof ilObjUser - ) { - if ($this->getSettings()->isBrowserLogEnabledForUser($this->dic->user()->getLogin())) { - if ($this->isConsoleAvailable()) { - $browser_handler = new BrowserConsoleHandler(); - $browser_handler->setLevel($this->getSettings()->getLevel()); - $browser_handler->setFormatter($line_formatter); - $logger->pushHandler($browser_handler); - } - } - } - - - // suid log - $logger->pushProcessor(function ($record) { - $record['extra']['suid'] = substr(session_id(), 0, 5); - return $record; - }); - - // append trace - $logger->pushProcessor(new ilTraceProcessor(ilLogLevel::DEBUG)); - - // Interpolate context variables. - $logger->pushProcessor(new PsrLogMessageProcessor()); - - // register new logger - $this->loggers[$a_component_id] = new ilComponentLogger($logger); - - return $this->loggers[$a_component_id]; + return $this->loggers[$a_component_id] = new ilComponentLogger( + $this->logger_factory->getLazy($a_component_id) + ); } } diff --git a/components/ILIAS/Logging/exceptions/class.ilLogException.php b/components/ILIAS/Logging/exceptions/class.ilLogException.php index c032ac963262..5dce492d0adf 100755 --- a/components/ILIAS/Logging/exceptions/class.ilLogException.php +++ b/components/ILIAS/Logging/exceptions/class.ilLogException.php @@ -21,6 +21,8 @@ /** * ILIAS Log exception class * + * @deprecated + * * @author Alex Killing * @version $Id:$ * @ingroup ServicesLogging diff --git a/components/ILIAS/Logging/interfaces/interface.ilLoggingSettings.php b/components/ILIAS/Logging/interfaces/interface.ilLoggingSettings.php index bc7c51932f4a..856268fb3543 100755 --- a/components/ILIAS/Logging/interfaces/interface.ilLoggingSettings.php +++ b/components/ILIAS/Logging/interfaces/interface.ilLoggingSettings.php @@ -17,34 +17,21 @@ *********************************************************************/ /** -* -* @author Stefan Meyer -* @version $Id$ -* -* -* @ingroup ServicesLogging -*/ + * @deprecated Please use {@see \ILIAS\Logging\Config\ConfigInterface} instead. + * + * @author Stefan Meyer + * @version $Id$ + * + * + * @ingroup ServicesLogging + */ interface ilLoggingSettings { public function isEnabled(): bool; public function getLogDir(): string; - public function getLogFile(): string; - public function getLevel(): int; public function getLevelByComponent(string $a_component_id): int; - - public function getCacheLevel(): int; - - public function isCacheEnabled(): bool; - - public function isMemoryUsageEnabled(): bool; - - public function isBrowserLogEnabled(): bool; - - public function isBrowserLogEnabledForUser(string $a_login): bool; - - public function getBrowserLogUsers(): array; } diff --git a/components/ILIAS/Logging/src/Config/Basic/Config.php b/components/ILIAS/Logging/src/Config/Basic/Config.php new file mode 100755 index 000000000000..7ddd4d7f6fd8 --- /dev/null +++ b/components/ILIAS/Logging/src/Config/Basic/Config.php @@ -0,0 +1,64 @@ +is_enabled ??= (bool) $this->reader->isLoggingEnabled(); + } + + public function pathToLogFile(): string + { + return $this->log_file ??= + rtrim($this->reader->logPath(), '/') . '/' . ltrim($this->reader->logFile(), '/'); + } + + public function pathToLogDirectory(): string + { + return $this->log_directory ??= $this->reader->logPath(); + } + + public function defaultLevel(): ILIASLogLevel + { + return $this->default_level ??= $this->logLevelFromString($this->reader->defaultLevel()); + } + + protected function logLevelFromString(string $raw_level): ILIASLogLevel + { + return ILIASLogLevel::tryFromString(strtoupper($raw_level)) ?? + ILIASLogLevel::tryFrom((int) $raw_level) ?? + ILIASLogLevel::INFO; + } +} diff --git a/components/ILIAS/Logging/Testrail/throwException.php b/components/ILIAS/Logging/src/Config/Basic/ConfigInterface.php similarity index 61% rename from components/ILIAS/Logging/Testrail/throwException.php rename to components/ILIAS/Logging/src/Config/Basic/ConfigInterface.php index 1fb407e9a5d0..24873aea2d3f 100755 --- a/components/ILIAS/Logging/Testrail/throwException.php +++ b/components/ILIAS/Logging/src/Config/Basic/ConfigInterface.php @@ -16,16 +16,19 @@ * *********************************************************************/ -chdir("./../../../"); -$ini = new ilIniFile("ilias.ini.php"); -$ini->read(); +declare(strict_types=1); -$http = $ini->readVariable("server", "http_path"); -$http = preg_replace("/^(https:\/\/)|(http:\/\/)+/", "", $http); +namespace ILIAS\Logging\Config\Basic; -$_SERVER['HTTP_HOST'] = $http; -$_SERVER['REQUEST_URI'] = ""; +use ILIAS\Logging\ILIASLogLevel; -ilInitialisation::initILIAS(); +interface ConfigInterface +{ + public function isLoggingEnabled(): bool; -throw new Exception("This is your error message"); + public function pathToLogFile(): string; + + public function pathToLogDirectory(): string; + + public function defaultLevel(): ILIASLogLevel; +} diff --git a/components/ILIAS/Logging/src/Config/Basic/IniReader.php b/components/ILIAS/Logging/src/Config/Basic/IniReader.php new file mode 100755 index 000000000000..6221c175c13d --- /dev/null +++ b/components/ILIAS/Logging/src/Config/Basic/IniReader.php @@ -0,0 +1,51 @@ +ilias_ini_file->readVariable('log', 'enabled'); + } + + public function logFile(): string + { + return $this->ilias_ini_file->readVariable('log', 'file'); + } + + public function logPath(): string + { + return $this->ilias_ini_file->readVariable('log', 'path'); + } + + public function defaultLevel(): string + { + return $this->ilias_ini_file->readVariable('log', 'default_level'); + } +} diff --git a/components/ILIAS/Logging/Testrail/raiseError.php b/components/ILIAS/Logging/src/Config/Basic/IniReaderInterface.php similarity index 58% rename from components/ILIAS/Logging/Testrail/raiseError.php rename to components/ILIAS/Logging/src/Config/Basic/IniReaderInterface.php index 375ef8508ade..27904aba94bd 100755 --- a/components/ILIAS/Logging/Testrail/raiseError.php +++ b/components/ILIAS/Logging/src/Config/Basic/IniReaderInterface.php @@ -16,20 +16,17 @@ * *********************************************************************/ -chdir("./../../../"); -$ini = new ilIniFile("ilias.ini.php"); -$ini->read(); +declare(strict_types=1); -$http = $ini->readVariable("server", "http_path"); -$http = preg_replace("/^(https:\/\/)|(http:\/\/)+/", "", $http); +namespace ILIAS\Logging\Config\Basic; -$_SERVER['HTTP_HOST'] = $http; -$_SERVER['REQUEST_URI'] = ""; +interface IniReaderInterface +{ + public function isLoggingEnabled(): string; -ilInitialisation::initILIAS(); + public function logFile(): string; -global $DIC; + public function logPath(): string; -$ilErr = $DIC['ilErr']; - -$ilErr->raiseError("This is your error message", $ilErr->FATAL); + public function defaultLevel(): string; +} diff --git a/components/ILIAS/Logging/src/Config/ByComponent/Config.php b/components/ILIAS/Logging/src/Config/ByComponent/Config.php new file mode 100755 index 000000000000..821d439305ef --- /dev/null +++ b/components/ILIAS/Logging/src/Config/ByComponent/Config.php @@ -0,0 +1,51 @@ + + */ + protected array $levels_by_component_id; + + public function __construct( + protected RepositoryInterface $repo, + protected BasicConfigInterface $basic_config + ) { + } + + public function level(string $component_id): ILIASLogLevel + { + return $this->getLevels()[$component_id] ?? $this->basic_config->defaultLevel(); + } + + /** + * @return array + */ + protected function getLevels(): array + { + return $this->levels_by_component_id ??= $this->repo->getAllLevelsForComponents(); + } +} diff --git a/components/ILIAS/Logging/src/Config/ByComponent/ConfigInterface.php b/components/ILIAS/Logging/src/Config/ByComponent/ConfigInterface.php new file mode 100755 index 000000000000..f0387e2cd7ca --- /dev/null +++ b/components/ILIAS/Logging/src/Config/ByComponent/ConfigInterface.php @@ -0,0 +1,28 @@ +db->queryF( + 'SELECT * FROM log_components WHERE component_id = %s', + [ilDBConstants::T_TEXT], + [$component_id] + ); + if (!$res->numRows()) { + $this->db->insert( + 'log_components', + ['component_id' => [ilDBConstants::T_TEXT, $component_id]] + ); + } + } + + /** + * @return array + */ + public function getAllLevelsForComponents(): array + { + $levels_by_components = []; + $res = $this->db->query('SELECT * FROM log_components'); + while (($row = $res->fetchAssoc())) { + $levels_by_components[(string) $row['component_id']] = isset($row['log_level']) ? + ILIASLogLevel::tryFrom((int) $row['log_level']) : null; + } + return $levels_by_components; + } + + public function getLevelForComponent(string $component_id): ?ILIASLogLevel + { + $res = $this->db->queryF( + 'SELECT * FROM log_components WHERE component_id = %s', + [ilDBConstants::T_TEXT], + [$component_id] + ); + if (($row = $res->fetchAssoc()) && isset($row['log_level'])) { + return ILIASLogLevel::tryFrom((int) $row['log_level']); + } + return null; + } + + public function updateLevelForComponent(string $component_id, ILIASLogLevel $level): void + { + $this->db->replace( + 'log_components', + ['component_id' => [ilDBConstants::T_TEXT, $component_id]], + ['log_level' => [ilDBConstants::T_INTEGER, $level->value]] + ); + } + + public function resetLevelForComponent(string $component_id): void + { + $this->db->manipulateF( + 'DELETE FROM log_components WHERE component_id = %s', + [ilDBConstants::T_TEXT], + [$component_id] + ); + } + + public function resetLevelsForAllComponents(): void + { + $this->db->manipulate('DELETE FROM log_components'); + } +} diff --git a/components/ILIAS/Logging/src/Config/ByComponent/RepositoryInterface.php b/components/ILIAS/Logging/src/Config/ByComponent/RepositoryInterface.php new file mode 100755 index 000000000000..61ff5f2f8e9f --- /dev/null +++ b/components/ILIAS/Logging/src/Config/ByComponent/RepositoryInterface.php @@ -0,0 +1,41 @@ + + */ + public function getAllLevelsForComponents(): array; + + public function getLevelForComponent(string $component_id): ?ILIASLogLevel; + + public function updateLevelForComponent(string $component_id, ILIASLogLevel $level): void; + + public function resetLevelForComponent(string $component_id): void; + + public function resetLevelsForAllComponents(): void; +} diff --git a/components/ILIAS/Logging/src/Config/Config.php b/components/ILIAS/Logging/src/Config/Config.php new file mode 100755 index 000000000000..7c3faf839a22 --- /dev/null +++ b/components/ILIAS/Logging/src/Config/Config.php @@ -0,0 +1,43 @@ +basic; + } + + public function byComponent(): ByComponentConfigInterface + { + return $this->by_component; + } +} diff --git a/components/ILIAS/Logging/src/Config/ConfigInterface.php b/components/ILIAS/Logging/src/Config/ConfigInterface.php new file mode 100755 index 000000000000..a44fb8d20b0b --- /dev/null +++ b/components/ILIAS/Logging/src/Config/ConfigInterface.php @@ -0,0 +1,31 @@ + self::DEBUG, + 'INFO' => self::INFO, + 'NOTICE' => self::NOTICE, + 'WARNING' => self::WARNING, + 'ERROR' => self::ERROR, + 'CRITICAL' => self::CRITICAL, + 'ALERT' => self::ALERT, + 'EMERGENCY' => self::EMERGENCY, + 'OFF' => self::OFF, + ]; + + case DEBUG = 100; + case INFO = 200; + case NOTICE = 250; + case WARNING = 300; + case ERROR = 400; + case CRITICAL = 500; + case ALERT = 550; + case EMERGENCY = 600; + + case OFF = 1000; + + public static function tryFromString(string $value): ?self + { + return self::STRING_MAP[$value] ?? null; + } + + public function toString(): string + { + return array_search($this, self::STRING_MAP, true); + } +} diff --git a/components/ILIAS/Logging/src/LegacyInitiator.php b/components/ILIAS/Logging/src/LegacyInitiator.php new file mode 100755 index 000000000000..471a7ba90b3b --- /dev/null +++ b/components/ILIAS/Logging/src/LegacyInitiator.php @@ -0,0 +1,119 @@ +dic = $DIC; + } + + public static function getInstance(): self + { + return self::$instance ??= new self(); + } + + public function basicConfig(): BasicConfigInterface + { + return $this->basic_config ??= new BasicConfig( + new IniReader($this->dic->iliasIni()) + ); + } + + public function componentConfigRepository(): ComponentConfigRepoInterface + { + return $this->component_config_repo ??= new ComponentConfigRepo( + $this->dic->database() + ); + } + + protected function componentConfig(): ComponentConfigInterface + { + return $this->component_config ??= new ComponentConfig( + $this->componentConfigRepository(), + $this->basicConfig() + ); + } + + protected function lazyInternalFactory(): LazyInternalFactoryInterface + { + return $this->lazy_internal_factory ??= new LazyInternalFactory( + new MonologFactory(), + $this->basicConfig() + ); + } + + protected function levelFetcherFactory(): LevelFetcherFactoryInterface + { + return $this->level_fetcher_factory ??= new LevelFetcherFactory(); + } + + public function loggerFactory(): LoggerFactoryInterface + { + return $this->logger_factory ??= new LoggerFactory( + $this->lazyInternalFactory(), + $this->componentConfig(), + $this->levelFetcherFactory() + ); + } + + public function defaultConfigLoggerFactory(): DefaultConfigLoggerFactoryInterface + { + return $this->default_config_logger_factory ??= new DefaultConfigLoggerFactory( + $this->lazyInternalFactory(), + $this->basicConfig(), + $this->levelFetcherFactory() + ); + } +} diff --git a/components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactory.php b/components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactory.php new file mode 100755 index 000000000000..f1e60c859e9d --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactory.php @@ -0,0 +1,43 @@ +internal_factory->getLazyGhost( + $component_id, + $this->level_fetcher_factory->defaultLevelFetcher($this->basic_config) + ); + } +} diff --git a/components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactoryInterface.php b/components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactoryInterface.php new file mode 100755 index 000000000000..156ac46579d6 --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/DefaultConfigLoggerFactoryInterface.php @@ -0,0 +1,33 @@ + + */ + protected array $loggers = []; + + public function __construct( + protected MonologFactoryInterface $monolog_factory, + protected BasicConfigInterface $basic_config + ) { + } + + public function getLazyGhost( + string $component_id, + LevelFetcherInterface $level_fetcher + ): LoggerInterface { + if (isset($this->loggers[$component_id])) { + return $this->loggers[$component_id]; + } + + /** @var Logger $lazy_logger */ + $lazy_logger = new ReflectionClass(Logger::class)->newLazyGhost( + function (Logger $logger) use ($component_id, $level_fetcher): void { + $monolog_logger = $this->buildMonologLogger($component_id, $level_fetcher); + $logger->__construct($monolog_logger); + } + ); + return $this->loggers[$component_id] = $lazy_logger; + } + + protected function buildMonologLogger( + string $component_id, + LevelFetcherInterface $level_fetcher + ): MonologLogger { + if (!$this->basic_config->isLoggingEnabled()) { + return $this->monolog_factory->nullLogger($component_id); + } + return $this->monolog_factory->logger( + $component_id, + $level_fetcher->fetchLevel(), + $this->basic_config->pathToLogFile() + ); + } +} diff --git a/components/ILIAS/Logging/src/Logger/LazyInternalFactoryInterface.php b/components/ILIAS/Logging/src/Logger/LazyInternalFactoryInterface.php new file mode 100755 index 000000000000..b678c887f9c8 --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/LazyInternalFactoryInterface.php @@ -0,0 +1,32 @@ + - */ -class ilLoggingUpdateSteps8 implements ilDatabaseUpdateSteps -{ - protected ilDBInterface $db; - public function prepare(ilDBInterface $db): void - { - $this->db = $db; +namespace ILIAS\Logging\Logger\LevelFetcher; + +use ILIAS\Logging\ILIASLogLevel; +use ILIAS\Logging\Config\ByComponent\ConfigInterface as ConfigByComponentInterface; + +class ComponentLevelFetcher implements LevelFetcherInterface +{ + public function __construct( + protected ConfigByComponentInterface $config_by_component, + protected string $component_id + ) { } - /** - * Add consent table - */ - public function step_1(): void + public function fetchLevel(): ILIASLogLevel { - $query = 'DELETE from log_components ' . - 'WHERE component_id = ' . $this->db->quote('lchk', ilDBConstants::T_TEXT); - $this->db->manipulate($query); + return $this->config_by_component->level($this->component_id); } } diff --git a/components/ILIAS/Logging/src/Logger/LevelFetcher/DefaultLevelFetcher.php b/components/ILIAS/Logging/src/Logger/LevelFetcher/DefaultLevelFetcher.php new file mode 100755 index 000000000000..20d0b5445b44 --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/LevelFetcher/DefaultLevelFetcher.php @@ -0,0 +1,37 @@ +basic_config->defaultLevel(); + } +} diff --git a/components/ILIAS/Logging/src/Logger/LevelFetcher/LevelFetcherFactory.php b/components/ILIAS/Logging/src/Logger/LevelFetcher/LevelFetcherFactory.php new file mode 100755 index 000000000000..b2596eedcf01 --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/LevelFetcher/LevelFetcherFactory.php @@ -0,0 +1,40 @@ +logger->isHandling($level); + } + + public function dump(mixed $value, mixed $level = ILIASLogLevel::INFO): void + { + $this->log($level, '{dump}', ['dump' => print_r($value, true)]); + } + + public function logStack(mixed $level = ILIASLogLevel::INFO, string|Stringable $message = '', array $context = []): void + { + try { + throw new Exception($message); + } catch (Exception $ex) { + $this->log($level, $message . "\n" . $ex->getTraceAsString(), $context); + } + } + + public function log(mixed $level, Stringable|string $message, array $context = []): void + { + if ($level instanceof ILIASLogLevel) { + $level = $level->value; + } + $this->logger->log($level, $message, $context); + } +} diff --git a/components/ILIAS/Logging/src/Logger/LoggerFactory.php b/components/ILIAS/Logging/src/Logger/LoggerFactory.php new file mode 100755 index 000000000000..5a8d801011ea --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/LoggerFactory.php @@ -0,0 +1,43 @@ +internal_factory->getLazyGhost( + $component_id, + $this->level_fetcher_factory->componentLevelFetcher($this->config_by_component, $component_id) + ); + } +} diff --git a/components/ILIAS/Logging/src/Logger/LoggerFactoryInterface.php b/components/ILIAS/Logging/src/Logger/LoggerFactoryInterface.php new file mode 100755 index 000000000000..7506ea9f22c6 --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/LoggerFactoryInterface.php @@ -0,0 +1,26 @@ +buildStandardHandler($level, $file_path); + $logger->pushHandler($handler); + + $logger->pushProcessor(function ($record) { + $record['extra']['suid'] = substr(session_id(), 0, 5); + return $record; + }); // suid log + $logger->pushProcessor(new ILIASTraceProcessor(ILIASLogLevel::DEBUG)); // append trace + $logger->pushProcessor(new PsrLogMessageProcessor()); // Interpolate context variables. + + return $logger; + } + + protected function buildStandardHandler(ILIASLogLevel $level, string $file_path): Handler + { + $stream_handler = new StreamHandler( + $file_path, + $level->value, + true + ); + + $line_formatter = new ILIASLineFormatter(); + $stream_handler->setFormatter($line_formatter); + + return $stream_handler; + } + + public function nullLogger(string $name): MonologLogger + { + $logger = new MonologLogger($name); + $logger->pushHandler(new NullHandler()); + return $logger; + } +} diff --git a/components/ILIAS/Logging/src/Logger/Monolog/FactoryInterface.php b/components/ILIAS/Logging/src/Logger/Monolog/FactoryInterface.php new file mode 100755 index 000000000000..77ed9fb91aa7 --- /dev/null +++ b/components/ILIAS/Logging/src/Logger/Monolog/FactoryInterface.php @@ -0,0 +1,31 @@ + - * @ingroup ServicesLogging - */ -class ilLineFormatter extends LineFormatter +class ILIASLineFormatter extends LineFormatter { - /** - * @inheritDoc - */ + protected const string DEFAULT_FORMAT = "[%extra.suid%] [%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; + protected const string DEFAULT_DATE_FORMAT = 'Y-m-d H:i:s.u'; + + public function __construct() + { + parent::__construct( + self::DEFAULT_FORMAT, + self::DEFAULT_DATE_FORMAT, + true, + true, + false + ); + } + public function format(LogRecord $record): string { if (isset($record["extra"]["trace"])) { diff --git a/components/ILIAS/Logging/classes/extensions/class.ilTraceProcessor.php b/components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php old mode 100755 new mode 100644 similarity index 75% rename from components/ILIAS/Logging/classes/extensions/class.ilTraceProcessor.php rename to components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php index 1bfc71c6db84..588d7d5b71ca --- a/components/ILIAS/Logging/classes/extensions/class.ilTraceProcessor.php +++ b/components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php @@ -18,26 +18,16 @@ declare(strict_types=1); +namespace ILIAS\Logging\Logger\Monolog; + use Monolog\LogRecord; +use ILIAS\Logging\ILIASLogLevel; -/** - * Logging factory - * - * This class supplies an implementation for the locator. - * The locator will send its output to ist own frame, enabling more flexibility in - * the design of the desktop. - * - * @author Stefan Meyer - * @version $Id$ - * - */ -class ilTraceProcessor +class ILIASTraceProcessor { - private int $level = 0; - - public function __construct(int $a_level) - { - $this->level = $a_level; + public function __construct( + protected ILIASLogLevel $level + ) { } /** @@ -45,7 +35,7 @@ public function __construct(int $a_level) */ public function __invoke(LogRecord $record): LogRecord { - if ($record['level'] < $this->level) { + if ($record['level'] < $this->level->value) { return $record; } diff --git a/components/ILIAS/Logging/classes/Setup/class.ilLoggingSetupAgent.php b/components/ILIAS/Logging/src/Setup/Agent.php similarity index 56% rename from components/ILIAS/Logging/classes/Setup/class.ilLoggingSetupAgent.php rename to components/ILIAS/Logging/src/Setup/Agent.php index 417c53eed974..7d4d90d65459 100755 --- a/components/ILIAS/Logging/classes/Setup/class.ilLoggingSetupAgent.php +++ b/components/ILIAS/Logging/src/Setup/Agent.php @@ -18,17 +18,23 @@ declare(strict_types=1); -use ILIAS\Setup; -use ILIAS\Setup\Config; +namespace ILIAS\Logging\Setup; + +use ILIAS\Setup\Agent as AgentInterface; +use ILIAS\Setup\Agent\HasNoNamedObjective; +use ILIAS\Setup\Objective\NullObjective; +use ILIAS\Setup\ObjectiveCollection; +use ILIAS\Setup\Config as ConfigInterface; use ILIAS\Setup\Objective; use ILIAS\Setup\Metrics\Storage; use ILIAS\Refinery\Factory; use ILIAS\Refinery\Transformation; -use ILIAS\UI; +use ilDatabaseUpdateStepsExecutedObjective; +use ILIAS\Logging\Setup\Steps\DBUpdateSteps12; -class ilLoggingSetupAgent implements Setup\Agent +class Agent implements AgentInterface { - use Setup\Agent\HasNoNamedObjective; + use HasNoNamedObjective; protected Factory $refinery; @@ -37,79 +43,59 @@ public function __construct(Factory $refinery) $this->refinery = $refinery; } - /** - * @inheritdoc - */ public function hasConfig(): bool { return true; } - /** - * @inheritdoc - */ public function getArrayToConfigTransformation(): Transformation { return $this->refinery->custom()->transformation(function ($data) { - return new \ilLoggingSetupConfig( + return new Config( $data["enable"] ?? false, $data["path_to_logfile"] ?? null, - $data["errorlog_dir"] ?? null + $data["default_level"] ?? null, + $data["errorlog_dir"] ?? null, ); }); } - /** - * @inheritdoc - */ - public function getInstallObjective(?Config $config = null): Objective + public function getInstallObjective(?ConfigInterface $config = null): Objective { - return new ilLoggingConfigStoredObjective($config); + return new ConfigStoredObjective($config); } - /** - * @inheritdoc - */ - public function getUpdateObjective(?Config $config = null): Objective + public function getUpdateObjective(?ConfigInterface $config = null): Objective { - $objective = new Setup\Objective\NullObjective(); + $objective = new NullObjective(); if ($config !== null) { - $objective = new ilLoggingConfigStoredObjective($config); + $objective = new ConfigStoredObjective($config); } - return new ILIAS\Setup\ObjectiveCollection( - 'Update of Services/Logging', + return new ObjectiveCollection( + 'Update of ILIAS\Logging', false, $objective, + new DefaultLevelMigratedObjective(), new ilDatabaseUpdateStepsExecutedObjective( - new ilLoggingUpdateSteps8() + new DBUpdateSteps12() ) ); } - /** - * @inheritdoc - */ public function getBuildObjective(): Objective { - return new Setup\Objective\NullObjective(); + return new NullObjective(); } - /** - * @inheritdoc - */ public function getStatusObjective(Storage $storage): Objective { - return new Setup\ObjectiveCollection( - 'Component Logging', + return new ObjectiveCollection( + 'Component ILIAS\Logging', true, - new ilLoggingMetricsCollectedObjective($storage), - new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new ilLoggingUpdateSteps8()) + new MetricsCollectedObjective($storage) ); } - /** - * @inheritDoc - */ public function getMigrations(): array { return []; diff --git a/components/ILIAS/Logging/classes/Setup/class.ilLoggingSetupConfig.php b/components/ILIAS/Logging/src/Setup/Config.php similarity index 71% rename from components/ILIAS/Logging/classes/Setup/class.ilLoggingSetupConfig.php rename to components/ILIAS/Logging/src/Setup/Config.php index 147e54b93ba9..3cf952327d62 100755 --- a/components/ILIAS/Logging/classes/Setup/class.ilLoggingSetupConfig.php +++ b/components/ILIAS/Logging/src/Setup/Config.php @@ -18,28 +18,39 @@ declare(strict_types=1); -use ILIAS\Setup\Config; +namespace ILIAS\Logging\Setup; -class ilLoggingSetupConfig implements Config +use ILIAS\Setup\Config as ConfigInterface; +use InvalidArgumentException; +use ILIAS\Logging\ILIASLogLevel; + +class Config implements ConfigInterface { protected bool $enabled; - protected ?string $path_to_logfile; - protected ?string $path_to_errorlogfiles; + protected ?ILIASLogLevel $level; protected ?string $errorlog_dir; public function __construct( bool $enabled, ?string $path_to_logfile, + ?string $level, ?string $errorlog_dir ) { if ($enabled && !$path_to_logfile) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( "Expected a path to the logfile, if logging is enabled." ); } + $level = ILIASLogLevel::tryFromString($level); + if ($enabled && !$level) { + throw new InvalidArgumentException( + "Expected a valid default log level, if logging is enabled." + ); + } $this->enabled = $enabled; $this->path_to_logfile = $this->normalizePath($path_to_logfile); + $this->level = $level; $this->errorlog_dir = $this->normalizePath($errorlog_dir); } @@ -62,6 +73,11 @@ public function getPathToLogfile(): ?string return $this->path_to_logfile; } + public function getDefaultLevel(): ?ILIASLogLevel + { + return $this->level; + } + public function getErrorlogDir(): ?string { return $this->errorlog_dir; diff --git a/components/ILIAS/Logging/classes/Setup/class.ilLoggingConfigStoredObjective.php b/components/ILIAS/Logging/src/Setup/ConfigStoredObjective.php similarity index 82% rename from components/ILIAS/Logging/classes/Setup/class.ilLoggingConfigStoredObjective.php rename to components/ILIAS/Logging/src/Setup/ConfigStoredObjective.php index f6149424abb3..7b3470f018b9 100755 --- a/components/ILIAS/Logging/classes/Setup/class.ilLoggingConfigStoredObjective.php +++ b/components/ILIAS/Logging/src/Setup/ConfigStoredObjective.php @@ -18,12 +18,16 @@ declare(strict_types=1); +namespace ILIAS\Logging\Setup; + use ILIAS\Setup\Objective; use ILIAS\Setup\Environment; use ILIAS\Setup\Config; use ILIAS\Setup\UnachievableException; +use ilIniFilesLoadedObjective; +use ilIniFile; -class ilLoggingConfigStoredObjective implements Objective +class ConfigStoredObjective implements Objective { protected Config $config; @@ -39,7 +43,7 @@ public function getHash(): string public function getLabel(): string { - return "Fill ini with settings for Services/Logging"; + return "Fill ini with settings for ILIAS\Logging"; } public function isNotable(): bool @@ -56,6 +60,7 @@ public function getPreconditions(Environment $environment): array public function achieve(Environment $environment): Environment { + /** @var ilIniFile $ini */ $ini = $environment->getResource(Environment::RESOURCE_ILIAS_INI); $logPath = ''; @@ -68,11 +73,8 @@ public function achieve(Environment $environment): Environment $ini->setVariable("log", "enabled", $this->config->isEnabled() ? "1" : "0"); $ini->setVariable("log", "path", $logPath); $ini->setVariable("log", "file", $logFile); - $ini->setVariable( - "log", - "error_path", - $this->config->getErrorlogDir() ?? '' - ); + $ini->setVariable("log", "default_level", $this->config->getDefaultLevel()?->toString() ?? ""); + $ini->setVariable("log", "error_path", $this->config->getErrorlogDir() ?? ''); if (!$ini->write()) { throw new UnachievableException("Could not write ilias.ini.php"); @@ -81,13 +83,12 @@ public function achieve(Environment $environment): Environment return $environment; } - /** - * @inheritDoc - */ public function isApplicable(Environment $environment): bool { + /** @var ilIniFile $ini */ $ini = $environment->getResource(Environment::RESOURCE_ILIAS_INI); $enabled = $this->config->isEnabled() ? "1" : "0"; + $level = $this->config->getDefaultLevel()?->toString() ?? ""; $logPath = ''; $logFile = ''; @@ -99,6 +100,7 @@ public function isApplicable(Environment $environment): bool return $ini->readVariable("log", "path") !== $logPath || $ini->readVariable("log", "file") !== $logFile || + $ini->readVariable("log", "default_level") !== $level || $ini->readVariable("log", "error_path") !== $this->config->getErrorlogDir() || $ini->readVariable("log", "enabled") !== $enabled ; diff --git a/components/ILIAS/Logging/src/Setup/DefaultLevelMigratedObjective.php b/components/ILIAS/Logging/src/Setup/DefaultLevelMigratedObjective.php new file mode 100755 index 000000000000..162ce2738053 --- /dev/null +++ b/components/ILIAS/Logging/src/Setup/DefaultLevelMigratedObjective.php @@ -0,0 +1,92 @@ +getResource(Environment::RESOURCE_ILIAS_INI); + /** @var ilSettingsFactory $settings_factory */ + $settings_factory = $environment->getResource(Environment::RESOURCE_SETTINGS_FACTORY); + $settings = $settings_factory->settingsFor("logging"); + + if (!$ini->groupExists("log")) { + return $environment; + } + + // Default is INFO, if settings were never saved + $level = ILIASLogLevel::tryFrom((int) $settings->get("level", "")) ?? ILIASLogLevel::INFO; + $ini->setVariable("log", "default_level", $level->toString()); + + $settings->delete("level"); + /* + * It would be better to also delete the long defunct "level" field in the + * ilias.ini.php, but deleting fields is not supported by ilIniFile. + */ + + if (!$ini->write()) { + throw new UnachievableException("Could not write ilias.ini.php"); + } + + return $environment; + } + + public function isApplicable(Environment $environment): bool + { + $ini = $environment->getResource(Environment::RESOURCE_ILIAS_INI); + return !$ini->variableExists("log", "default_level"); + } +} diff --git a/components/ILIAS/Logging/classes/Setup/class.ilLoggingMetricsCollectedObjective.php b/components/ILIAS/Logging/src/Setup/MetricsCollectedObjective.php similarity index 82% rename from components/ILIAS/Logging/classes/Setup/class.ilLoggingMetricsCollectedObjective.php rename to components/ILIAS/Logging/src/Setup/MetricsCollectedObjective.php index bee5934ff019..504767c40b89 100755 --- a/components/ILIAS/Logging/classes/Setup/class.ilLoggingMetricsCollectedObjective.php +++ b/components/ILIAS/Logging/src/Setup/MetricsCollectedObjective.php @@ -18,11 +18,15 @@ declare(strict_types=1); +namespace ILIAS\Logging\Setup; + use ILIAS\Setup\Metrics\CollectedObjective; use ILIAS\Setup\Environment; use ILIAS\Setup\Metrics\Storage; +use ilIniFilesLoadedObjective; +use ilIniFile; -class ilLoggingMetricsCollectedObjective extends CollectedObjective +class MetricsCollectedObjective extends CollectedObjective { protected function getTentativePreconditions(Environment $environment): array { @@ -33,6 +37,7 @@ protected function getTentativePreconditions(Environment $environment): array protected function collectFrom(Environment $environment, Storage $storage): void { + /** @var ilIniFile $ini */ $ini = $environment->getResource(Environment::RESOURCE_ILIAS_INI); if (!$ini) { return; @@ -48,6 +53,11 @@ protected function collectFrom(Environment $environment, Storage $storage): void $ini->readVariable("log", "path") . "/" . $ini->readVariable("log", "file"), "The path to the logfile." ); + $storage->storeConfigText( + "default_level", + $ini->readVariable("log", "default_level"), + "The default log level." + ); $storage->storeConfigText( "errorlog_dir", $ini->readVariable("log", "error_path"), diff --git a/components/ILIAS/Logging/src/Setup/Steps/DBUpdateSteps12.php b/components/ILIAS/Logging/src/Setup/Steps/DBUpdateSteps12.php new file mode 100644 index 000000000000..9848feec8969 --- /dev/null +++ b/components/ILIAS/Logging/src/Setup/Steps/DBUpdateSteps12.php @@ -0,0 +1,58 @@ +db = $db; + } + + /** + * Remove default entries, all components/plugins now get a log level. + */ + public function step_1(): void + { + if ($this->db->tableExists('log_components')) { + $this->db->manipulate( + "DELETE FROM log_components WHERE log_level = 0 OR log_level IS NULL" + ); + } + } + + /** + * Remove the root logger log level. + */ + public function step_2(): void + { + if ($this->db->tableExists('log_components')) { + $this->db->manipulate( + "DELETE FROM log_components WHERE component_id = 'log_root'" + ); + } + } +} diff --git a/components/ILIAS/Logging/tests/Config/Basic/ConfigTest.php b/components/ILIAS/Logging/tests/Config/Basic/ConfigTest.php new file mode 100755 index 000000000000..4cc80fc0d0be --- /dev/null +++ b/components/ILIAS/Logging/tests/Config/Basic/ConfigTest.php @@ -0,0 +1,180 @@ +createMock(IniReaderInterface::class); + $reader + ->expects($this->never()) + ->method($this->anything()); + + $config = new Config($reader); + } + + #[TestWith(['0', false])] + #[TestWith(['', false])] + #[TestWith(['1', true])] + public function testIsLoggingEnabled(string $input, bool $expected_result): void + { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('isLoggingEnabled') + ->willReturn($input); + + $config = new Config($reader); + $actual_result = $config->isLoggingEnabled(); + + $this->assertSame($expected_result, $actual_result); + } + + public function testIsLoggingEnabledIsCached(): void + { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('isLoggingEnabled') + ->willReturn('1'); + + $config = new Config($reader); + $first_result = $config->isLoggingEnabled(); + $second_result = $config->isLoggingEnabled(); + + $this->assertSame($first_result, $second_result, 'Result should be cached and stable.'); + } + + #[TestWith(['/path/to/log', 'file.log', '/path/to/log/file.log'], 'no trailing slashes')] + #[TestWith(['/path/to/log/', 'file.log', '/path/to/log/file.log'], 'slash after path')] + #[TestWith(['/path/to/log', '/file.log', '/path/to/log/file.log'], 'slash before file')] + public function testPathToLogFile( + string $expected_path, + string $expected_file, + string $expected_result + ): void { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('logFile') + ->willReturn($expected_file); + $reader + ->expects($this->once()) + ->method('logPath') + ->willReturn($expected_path); + + $config = new Config($reader); + $actual_result = $config->pathToLogFile(); + + $this->assertSame($expected_result, $actual_result); + } + + public function testPathToLogFileIsCached(): void + { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('logFile') + ->willReturn('file.log'); + $reader + ->expects($this->once()) + ->method('logPath') + ->willReturn('/path/to/log'); + + $config = new Config($reader); + $first_result = $config->pathToLogFile(); + $second_result = $config->pathToLogFile(); + + $this->assertSame($first_result, $second_result, 'Result should be cached and stable.'); + } + + public function testPathToLogDirectory(): void + { + $expected_path = '/path/to/log/'; + + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('logPath') + ->willReturn($expected_path); + + $config = new Config($reader); + $actual_path = $config->pathToLogDirectory(); + + $this->assertSame($expected_path, $actual_path); + } + + public function testPathToLogDirectoryIsCached(): void + { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('logPath') + ->willReturn('/path/to/log/'); + + $config = new Config($reader); + $first_result = $config->pathToLogFile(); + $second_result = $config->pathToLogFile(); + + $this->assertSame($first_result, $second_result, 'Result should be cached and stable.'); + } + + #[TestWith(['250', ILIASLogLevel::NOTICE], 'from integer')] + #[TestWith(['ALERT', ILIASLogLevel::ALERT], 'from upper case string')] + #[TestWith(['warning', ILIASLogLevel::WARNING], 'from lower case string')] + #[TestWith(['Emergency', ILIASLogLevel::EMERGENCY], 'from capitalized string')] + #[TestWith(['', ILIASLogLevel::INFO], 'empty, so use fallback')] + #[TestWith(['MUJjOI', ILIASLogLevel::INFO], 'something else, so use fallback')] + public function testDefaultLevel(string $input, ILIASLogLevel $expected_level): void + { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('defaultLevel') + ->willReturn($input); + + $config = new Config($reader); + $actual_level = $config->defaultLevel(); + + $this->assertSame($expected_level, $actual_level); + } + + public function testDefaultLevelIsCached(): void + { + $reader = $this->createMock(IniReaderInterface::class); + $reader + ->expects($this->once()) + ->method('defaultLevel') + ->willReturn('WARNING'); + + $config = new Config($reader); + $first_result = $config->defaultLevel(); + $second_result = $config->defaultLevel(); + + $this->assertSame($first_result, $second_result, 'Result should be cached and stable.'); + } +} diff --git a/components/ILIAS/Logging/tests/Config/ByComponent/ConfigTest.php b/components/ILIAS/Logging/tests/Config/ByComponent/ConfigTest.php new file mode 100755 index 000000000000..4c9a999cc443 --- /dev/null +++ b/components/ILIAS/Logging/tests/Config/ByComponent/ConfigTest.php @@ -0,0 +1,129 @@ +createMock(RepositoryInterface::class); + $basic_config = $this->createMock(BasicConfigInterface::class); + + $repo + ->expects($this->never()) + ->method($this->anything()); + $basic_config + ->expects($this->never()) + ->method($this->anything()); + + $config = new Config($repo, $basic_config); + } + + public function testLevel(): void + { + $expected_component = 'comp_id'; + $expected_level = ILIASLogLevel::WARNING; + + $repo = $this->createMock(RepositoryInterface::class); + $basic_config = $this->createStub(BasicConfigInterface::class); + + $repo + ->expects($this->once()) + ->method('getAllLevelsForComponents') + ->willReturn([ + 'other_component' => ILIASLogLevel::CRITICAL, + 'comp_id' => $expected_level + ]); + + $config = new Config($repo, $basic_config); + $actual_level = $config->level($expected_component); + + $this->assertSame($expected_level, $actual_level); + } + + public function testLevelWithDefaultBecauseNoValue(): void + { + $expected_component = 'comp_id'; + $expected_level = ILIASLogLevel::EMERGENCY; + + $repo = $this->createMock(RepositoryInterface::class); + $basic_config = $this->createMock(BasicConfigInterface::class); + + $repo + ->expects($this->once()) + ->method('getAllLevelsForComponents') + ->willReturn(array_merge(['other_component' => ILIASLogLevel::CRITICAL])); + $basic_config + ->expects($this->atLeastOnce()) + ->method('defaultLevel') + ->willReturn($expected_level); + + $config = new Config($repo, $basic_config); + $actual_level = $config->level($expected_component); + + $this->assertSame( + $expected_level, + $expected_level, + 'When there is nothing set explicitly, level should fall back to the default.' + ); + } + + public function testLevelOnlyReadOutOnce(): void + { + $repo = $this->createMock(RepositoryInterface::class); + $basic_config = $this->createStub(BasicConfigInterface::class); + + $expected_component_1 = 'comp_1'; + $expected_component_2 = 'comp_2'; + $expected_level_1 = ILIASLogLevel::ALERT; + $expected_level_2 = ILIASLogLevel::CRITICAL; + + $repo + ->expects($this->once()) + ->method('getAllLevelsForComponents') + ->willReturn([ + $expected_component_1 => $expected_level_1, + $expected_component_2 => $expected_level_2 + ]); + + $config = new Config($repo, $basic_config); + $level_1 = $config->level('comp_1'); + $level_2 = $config->level('comp_2'); + $level_1_again = $config->level('comp_1'); + + + $this->assertSame( + $level_1, + $level_1_again, + 'Repeated reads on the same component should give the same level.' + ); + $this->assertNotSame( + $level_1, + $level_2, + 'Repeated reads on the different component should give the same level.' + ); + } +} diff --git a/components/ILIAS/Logging/tests/Logger/DefaultConfigLoggerFactoryTest.php b/components/ILIAS/Logging/tests/Logger/DefaultConfigLoggerFactoryTest.php new file mode 100755 index 000000000000..58053cea5349 --- /dev/null +++ b/components/ILIAS/Logging/tests/Logger/DefaultConfigLoggerFactoryTest.php @@ -0,0 +1,64 @@ +createStub(LoggerInterface::class); + $expected_level_fectcher = $this->createStub(LevelFetcherInterface::class); + + $internal_factory = $this->createMock(LazyInternalFactoryInterface::class); + $internal_factory + ->expects($this->atLeastOnce()) + ->method('getLazyGhost') + ->with($expected_component_id, $expected_level_fectcher) + ->willReturn($expected_logger); + + $basic_config = $this->createMock(BasicConfigInterface::class); + $basic_config + ->expects($this->never()) + ->method($this->anything()); + + $level_fetcher_factory = $this->createMock(LevelFetcherFactoryInterface::class); + $level_fetcher_factory + ->expects($this->atLeastOnce()) + ->method('defaultLevelFetcher') + ->with($basic_config) + ->willReturn($expected_level_fectcher); + + $factory = new DefaultConfigLoggerFactory($internal_factory, $basic_config, $level_fetcher_factory); + $actual_logger = $factory->getLazy($expected_component_id); + + $this->assertSame( + $expected_logger, + $actual_logger, + 'This should return the logger from LazyInternalFactory' + ); + } +} diff --git a/components/ILIAS/Logging/tests/Logger/LazyInternalFactoryTest.php b/components/ILIAS/Logging/tests/Logger/LazyInternalFactoryTest.php new file mode 100755 index 000000000000..28190c9b510f --- /dev/null +++ b/components/ILIAS/Logging/tests/Logger/LazyInternalFactoryTest.php @@ -0,0 +1,156 @@ +createMock(BasicConfigInterface::class); + $monolog_factory = $this->createMock(MonologFactoryInterface::class); + $level_fetcher = $this->createMock(LevelFetcherInterface::class); + + $monolog_factory + ->expects($this->never()) + ->method($this->anything()); + $basic_config + ->expects($this->never()) + ->method($this->anything()); + $level_fetcher + ->expects($this->never()) + ->method($this->anything()); + + $lazy_factory = new LazyInternalFactory($monolog_factory, $basic_config); + $logger = $lazy_factory->getLazyGhost('name', $level_fetcher); + } + + public function testGetLazyGhostIsInitializedAfterCallWithCorrectParameters(): void + { + $expected_name = 'name'; + $expected_log_level = ILIASLogLevel::EMERGENCY; + $expected_path = '/path/to/logfile'; + + $basic_config = $this->createMock(BasicConfigInterface::class); + $monolog_factory = $this->createMock(MonologFactoryInterface::class); + $level_fetcher = $this->createMock(LevelFetcherInterface::class); + + $basic_config + ->expects($this->atLeastOnce()) + ->method('isLoggingEnabled') + ->willReturn(true); + $basic_config + ->expects($this->atLeastOnce()) + ->method('pathToLogFile') + ->willReturn($expected_path); + $monolog_factory + ->expects($this->atLeastOnce()) + ->method('logger') + ->with($expected_name, $expected_log_level, $expected_path) + ->willReturn($this->createStub(MonologLogger::class)); + $level_fetcher + ->expects($this->atLeastOnce()) + ->method('fetchLevel') + ->willReturn($expected_log_level); + + $lazy_factory = new LazyInternalFactory($monolog_factory, $basic_config); + $logger = $lazy_factory->getLazyGhost($expected_name, $level_fetcher); + // actually initialize the logger + $logger->info('test'); + } + + public function testGetLazyGhostIsCached(): void + { + $basic_config = $this->createMock(BasicConfigInterface::class); + $monolog_factory = $this->createMock(MonologFactoryInterface::class); + $level_fetcher_1 = $this->createMock(LevelFetcherInterface::class); + $level_fetcher_2 = $this->createMock(LevelFetcherInterface::class); + $level_fetcher_3 = $this->createMock(LevelFetcherInterface::class); + + $monolog_factory + ->expects($this->exactly(2)) + ->method('logger') + ->willReturn($this->createStub(MonologLogger::class)); + $basic_config + ->expects($this->atLeastOnce()) + ->method('isLoggingEnabled') + ->willReturn(true); + $level_fetcher_1 + ->expects($this->atLeastOnce()) + ->method('fetchLevel') + ->willReturn(ILIASLogLevel::INFO); + $level_fetcher_2 + ->expects($this->atLeastOnce()) + ->method('fetchLevel') + ->willReturn(ILIASLogLevel::INFO); + $level_fetcher_3 + ->expects($this->never()) + ->method($this->anything()); + + $lazy_factory = new LazyInternalFactory($monolog_factory, $basic_config); + $logger_1 = $lazy_factory->getLazyGhost('name', $level_fetcher_1); + $logger_2 = $lazy_factory->getLazyGhost('another name', $level_fetcher_2); + $logger_3 = $lazy_factory->getLazyGhost('name', $level_fetcher_3); + + // actually initialize the loggers + $logger_1->info('test'); + $logger_2->info('test'); + $logger_3->info('test'); + + $this->assertSame($logger_1, $logger_3, 'There should only be one logger per component ID.'); + $this->assertNotSame($logger_1, $logger_2, 'Loggers with different component ID should be different.'); + } + + public function testGetLazyGhostWhenLoggingIsDisabled(): void + { + $expected_name = 'name'; + + $basic_config = $this->createStub(BasicConfigInterface::class); + $monolog_factory = $this->createMock(MonologFactoryInterface::class); + $level_fetcher = $this->createMock(LevelFetcherInterface::class); + + $basic_config + ->method('isLoggingEnabled') + ->willReturn(false); + $monolog_factory + ->expects($this->atLeastOnce()) + ->method('nullLogger') + ->with($expected_name) + ->willReturn($this->createStub(MonologLogger::class)); + $monolog_factory + ->expects($this->never()) + ->method('logger'); + $level_fetcher + ->expects($this->never()) + ->method($this->anything()); + + $lazy_factory = new LazyInternalFactory($monolog_factory, $basic_config); + $logger = $lazy_factory->getLazyGhost($expected_name, $level_fetcher); + // actually initialize the logger + $logger->info('test'); + } +} diff --git a/components/ILIAS/Logging/tests/Logger/LevelFetcher/LevelFetcherFactoryTest.php b/components/ILIAS/Logging/tests/Logger/LevelFetcher/LevelFetcherFactoryTest.php new file mode 100755 index 000000000000..1118c214c6a6 --- /dev/null +++ b/components/ILIAS/Logging/tests/Logger/LevelFetcher/LevelFetcherFactoryTest.php @@ -0,0 +1,87 @@ +createMock(BasicConfigInterface::class); + $basic_config + ->expects($this->atLeastOnce()) + ->method('defaultLevel') + ->willReturn($expected_level); + + $factory = new LevelFetcherFactory(); + $fetcher = $factory->defaultLevelFetcher($basic_config); + $actual_level = $fetcher->fetchLevel(); + + $this->assertSame($expected_level, $actual_level); + } + + public function testDefaultLevelFetcherIsLazy(): void + { + $basic_config = $this->createMock(BasicConfigInterface::class); + $basic_config + ->expects($this->never()) + ->method($this->anything()); + + $factory = new LevelFetcherFactory(); + $fetcher = $factory->defaultLevelFetcher($basic_config); + } + + public function testComponentLevelFetcher(): void + { + $expected_level = ILIASLogLevel::ALERT; + $expected_component = 'comp_id'; + + $by_component_config = $this->createMock(ByComponentConfigInterface::class); + $by_component_config + ->expects($this->atLeastOnce()) + ->method('level') + ->with($expected_component) + ->willReturn($expected_level); + + $factory = new LevelFetcherFactory(); + $fetcher = $factory->componentLevelFetcher($by_component_config, $expected_component); + $actual_level = $fetcher->fetchLevel(); + + $this->assertSame($expected_level, $actual_level); + } + + public function testComponentLevelFetcherIsLazy(): void + { + $by_component_config = $this->createMock(ByComponentConfigInterface::class); + $by_component_config + ->expects($this->never()) + ->method($this->anything()); + + $factory = new LevelFetcherFactory(); + $fetcher = $factory->componentLevelFetcher($by_component_config, 'comp_id'); + } +} diff --git a/components/ILIAS/Logging/tests/Logger/LoggerFactoryTest.php b/components/ILIAS/Logging/tests/Logger/LoggerFactoryTest.php new file mode 100755 index 000000000000..a4429b3aa70a --- /dev/null +++ b/components/ILIAS/Logging/tests/Logger/LoggerFactoryTest.php @@ -0,0 +1,64 @@ +createStub(LoggerInterface::class); + $expected_component = 'comp_id'; + $expected_level_fectcher = $this->createStub(LevelFetcherInterface::class); + + $internal_factory = $this->createMock(LazyInternalFactoryInterface::class); + $internal_factory + ->expects($this->atLeastOnce()) + ->method('getLazyGhost') + ->with($expected_component, $expected_level_fectcher) + ->willReturn($expected_logger); + + $config_by_component = $this->createMock(ConfigByComponentInterface::class); + $config_by_component + ->expects($this->never()) + ->method($this->anything()); + + $level_fetcher_factory = $this->createMock(LevelFetcherFactoryInterface::class); + $level_fetcher_factory + ->expects($this->atLeastOnce()) + ->method('componentLevelFetcher') + ->with($config_by_component, $expected_component) + ->willReturn($expected_level_fectcher); + + $factory = new LoggerFactory($internal_factory, $config_by_component, $level_fetcher_factory); + $actual_logger = $factory->getLazy($expected_component); + + $this->assertSame( + $expected_logger, + $actual_logger, + 'This should return the logger from LazyInternalFactory' + ); + } +} diff --git a/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php b/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php deleted file mode 100755 index 6044770181fd..000000000000 --- a/components/ILIAS/Logging/tests/ilLogComponentLevelTest.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @ingroup ServicesCopyWizard - */ -class ilLogComponentLevelTest extends TestCase -{ - protected Container $dic; - - protected function setUp(): void - { - $this->initDependencies(); - parent::setUp(); - } - - public function testLevel(): void - { - $component_level = new ilLogComponentLevel( - 'log', - ilLogLevel::CRITICAL - ); - $this->assertEquals('log', $component_level->getComponentId()); - $this->assertEquals(ilLogLevel::CRITICAL, $component_level->getLevel()); - } - - - protected function setGlobalVariable(string $name, $value): void - { - global $DIC; - - $GLOBALS[$name] = $value; - unset($DIC[$name]); - $DIC[$name] = static function (\ILIAS\DI\Container $c) use ($value) { - return $value; - }; - } - - protected function initDependencies(): void - { - $this->dic = new Container(); - $GLOBALS['DIC'] = $this->dic; - $this->setGlobalVariable('ilDB', $this->createStub(ilDBInterface::class)); - } -} diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 370ae8f87e44..097f252062b9 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -11152,28 +11152,21 @@ lng#:#lng_disable_language_detection#:#Automatische Spracherkennung deaktivieren lng#:#lng_download_deprecated#:#Veraltete Einträge herunterladen lng#:#lng_enable_language_detection#:#Automatische Spracherkennung aktivieren lng#:#lng_switch_language_detection#:#Automatische Spracherkennung umschalten -log#:#log_browser#:#Browser Konsolen-Log -log#:#log_browser_users#:#Aktivierte Benutzer für Browser-Log -log#:#log_cache_#:#Caching -log#:#log_cache_info#:#Erst wenn eine Nachricht den eingestellten Caching-Level erreicht oder überschreitet, werden innerhalb des aktuellen Server-Requests alle Nachrichten protokolliert, deren Level mindestens dem Log-Level entspricht. -log#:#log_cache_level#:#Caching-Level log#:#log_component_btn_reset#:#Einstellungen zurücksetzen log#:#log_component_col_component#:#Komponente log#:#log_component_col_level#:#Log-Level -log#:#log_component_root_desc#:#Log-Level für Nachrichten, die keiner Komponente zugeordnet sind. +log#:#log_component_unknown#:#Unbekannt (%s) log#:#log_components#:#Komponenten -log#:#log_general_settings#:#Einstellungen zur Protokollierung log#:#log_level_alert#:#ALERT log#:#log_level_critical#:#CRITICAL log#:#log_level_debug#:#DEBUG +log#:#log_level_default#:#Standard (%s) log#:#log_level_emergency#:#EMERGENCY log#:#log_level_error#:#ERROR log#:#log_level_info#:#INFO log#:#log_level_notice#:#NOTICE log#:#log_level_off#:#Deaktiviert log#:#log_level_warning#:#WARNING -log#:#log_log_level#:#Log-Level -log#:#log_memory#:#Logge Speicherbedarf logging#:#error_settings_saved#:#Einstellungen gespeichert logging#:#frm_clear_older_then#:#Dateien löschen die älter sind als logging#:#frm_clear_older_then_info#:#Bitte geben Sie den Zeitraum in Tagen ein. diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index e0b0b754d3b1..131f7dcf0886 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -11124,28 +11124,21 @@ lng#:#lng_disable_language_detection#:#Disable Language Detection lng#:#lng_download_deprecated#:#Download Deprecated List lng#:#lng_enable_language_detection#:#Enable Language Detection lng#:#lng_switch_language_detection#:#Switch Language Detection -log#:#log_browser#:#Browser Console Log -log#:#log_browser_users#:#Usernames Using Console Log -log#:#log_cache_#:#Caching -log#:#log_cache_info#:#Only if a message reaches or surpasses the caching level, all messages which have at least the log level will be added to the log for the current request. -log#:#log_cache_level#:#Caching-Level log#:#log_component_btn_reset#:#Reset Settings log#:#log_component_col_component#:#Component log#:#log_component_col_level#:#Log-Level -log#:#log_component_root_desc#:#Leg-Level for message which are not assigned to any component. +log#:#log_component_unknown#:#Unknown (%s) log#:#log_components#:#Components -log#:#log_general_settings#:#Logging Settings log#:#log_level_alert#:#ALERT log#:#log_level_critical#:#CRITICAL log#:#log_level_debug#:#DEBUG +log#:#log_level_default#:#Default (%s) log#:#log_level_emergency#:#EMERGENCY log#:#log_level_error#:#ERROR log#:#log_level_info#:#INFO log#:#log_level_notice#:#NOTICE log#:#log_level_off#:#Disabled log#:#log_level_warning#:#WARNING -log#:#log_log_level#:#Log Level -log#:#log_memory#:#Log Memory Usage logging#:#error_settings_saved#:#Settings saved logging#:#frm_clear_older_then#:#Deletes files older then logging#:#frm_clear_older_then_info#:#Please enter duration in days. From 9519acd6a0fdfa4f01d3c6d97da4e2758b86e68d Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Thu, 30 Jul 2026 14:26:18 +0200 Subject: [PATCH 146/333] fix tests --- .../ILIAS/COPage/tests/COPageTestBase.php | 6 +++++ .../tests/ilCertificateDateHelperTest.php | 1 + .../classes/public/class.ilLoggerFactory.php | 24 ++++++++----------- .../ILIAS/Logging/src/LegacyInitiator.php | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/components/ILIAS/COPage/tests/COPageTestBase.php b/components/ILIAS/COPage/tests/COPageTestBase.php index 5374c397ec61..2fb339a2675c 100755 --- a/components/ILIAS/COPage/tests/COPageTestBase.php +++ b/components/ILIAS/COPage/tests/COPageTestBase.php @@ -130,6 +130,12 @@ protected function setUp(): void $refinery_mock ); + $ini_reader_mock = $this->createStub(ilIniFile::class); + $this->setGlobalVariable( + "ilIliasIniFile", + $ini_reader_mock + ); + $this->pc_cnt = 1; } diff --git a/components/ILIAS/Certificate/tests/ilCertificateDateHelperTest.php b/components/ILIAS/Certificate/tests/ilCertificateDateHelperTest.php index d5a5e0396a95..fb967bb230ad 100644 --- a/components/ILIAS/Certificate/tests/ilCertificateDateHelperTest.php +++ b/components/ILIAS/Certificate/tests/ilCertificateDateHelperTest.php @@ -48,6 +48,7 @@ class_exists('ilDateTime'); $this->setGlobalVariable('ilLoggerFactory', $logger_factory); $this->setGlobalVariable('lng', $this->getSystemLanguageMock()); $this->setGlobalVariable('ilUser', $this->getUserMock()); + $this->setGlobalVariable('ilIliasIniFile', $this->createMock(ilIniFile::class)); $this->current_time = time(); } diff --git a/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php b/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php index afe412644d8b..849b836548af 100755 --- a/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php +++ b/components/ILIAS/Logging/classes/public/class.ilLoggerFactory.php @@ -18,8 +18,6 @@ declare(strict_types=1); -use ILIAS\Logging\Logger\LoggerFactoryInterface; -use ILIAS\Logging\Logger\DefaultConfigLoggerFactoryInterface; use ILIAS\Logging\Logger\LegacyInitiator; /** @@ -35,25 +33,21 @@ class ilLoggerFactory { private static ?ilLoggerFactory $instance = null; - private LoggerFactoryInterface $logger_factory; - private DefaultConfigLoggerFactoryInterface $default_config_logger_factory; - /** * @var array */ private array $loggers = []; - protected function __construct() - { - $initiator = LegacyInitiator::getInstance(); - $this->logger_factory = $initiator->loggerFactory(); - $this->default_config_logger_factory = $initiator->defaultConfigLoggerFactory(); + protected function __construct( + protected ilLoggingSettings $settings + ) { } public static function getInstance(): ilLoggerFactory { if (!static::$instance instanceof ilLoggerFactory) { - static::$instance = new ilLoggerFactory(); + $settings = ilLoggingDBSettings::getInstance(); + static::$instance = new ilLoggerFactory($settings); } return static::$instance; } @@ -87,7 +81,7 @@ public function initUser(string $a_login): void public function getSettings(): ilLoggingSettings { - return ilLoggingDBSettings::getInstance(); + return $this->settings; } public function getComponentLogger(string $a_component_id): ilLogger @@ -96,13 +90,15 @@ public function getComponentLogger(string $a_component_id): ilLogger return $this->loggers[$a_component_id]; } + $initiator = LegacyInitiator::getInstance(); + if ($a_component_id === 'root') { return $this->loggers['root'] = new ilComponentLogger( - $this->default_config_logger_factory->getLazy('legacy_root') + $initiator->defaultConfigLoggerFactory()->getLazy('legacy_root') ); } return $this->loggers[$a_component_id] = new ilComponentLogger( - $this->logger_factory->getLazy($a_component_id) + $initiator->loggerFactory()->getLazy($a_component_id) ); } } diff --git a/components/ILIAS/Logging/src/LegacyInitiator.php b/components/ILIAS/Logging/src/LegacyInitiator.php index 471a7ba90b3b..2348760454cd 100755 --- a/components/ILIAS/Logging/src/LegacyInitiator.php +++ b/components/ILIAS/Logging/src/LegacyInitiator.php @@ -78,7 +78,7 @@ public function componentConfigRepository(): ComponentConfigRepoInterface ); } - protected function componentConfig(): ComponentConfigInterface + public function componentConfig(): ComponentConfigInterface { return $this->component_config ??= new ComponentConfig( $this->componentConfigRepository(), From 6e5c2663ca0deed9ea7110cd37860f4d819e6c2f Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 30 Jul 2026 14:29:38 +0200 Subject: [PATCH 147/333] Test: Step-Size for Manual Scoring Should Be <1 --- .../Test/src/Scoring/Manual/class.ConsecutiveScoringGUI.php | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ILIAS/Test/src/Scoring/Manual/class.ConsecutiveScoringGUI.php b/components/ILIAS/Test/src/Scoring/Manual/class.ConsecutiveScoringGUI.php index dd35b321f8eb..b26c550c3c95 100644 --- a/components/ILIAS/Test/src/Scoring/Manual/class.ConsecutiveScoringGUI.php +++ b/components/ILIAS/Test/src/Scoring/Manual/class.ConsecutiveScoringGUI.php @@ -552,6 +552,7 @@ protected function getScoringForm(string $action, int $question_id, int $usr_act ) ) ) + ->withStepSize(0.0001) ->withAdditionalTransformation($this->refinery->kindlyTo()->float()) ->withValue($score); From ab70d88ce276e4feec91ad4c04b14423f0ea7b91 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Thu, 30 Jul 2026 15:49:22 +0200 Subject: [PATCH 148/333] Logging: appease tests which depend on the dependencies of ilLoggerFactory --- .../ILIAS/Logging/src/LegacyInitiator.php | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/Logging/src/LegacyInitiator.php b/components/ILIAS/Logging/src/LegacyInitiator.php index 2348760454cd..e6d95a3346e2 100755 --- a/components/ILIAS/Logging/src/LegacyInitiator.php +++ b/components/ILIAS/Logging/src/LegacyInitiator.php @@ -37,6 +37,7 @@ use ILIAS\Logging\Logger\LevelFetcher\LevelFetcherFactoryInterface; use ILIAS\Logging\Logger\DefaultConfigLoggerFactoryInterface; use ILIAS\Logging\Logger\DefaultConfigLoggerFactory; +use ILIAS\Logging\ILIASLogLevel; class LegacyInitiator { @@ -66,9 +67,41 @@ public static function getInstance(): self public function basicConfig(): BasicConfigInterface { - return $this->basic_config ??= new BasicConfig( - new IniReader($this->dic->iliasIni()) - ); + if (isset($this->basic_config)) { + return $this->basic_config; + } + /** + * This exists purely to appease unit tests in other components, + * which somehow depend on the dependencies of ilLoggerFactory. + */ + if ($this->dic->offsetExists('ilIliasIniFile')) { + $ini_reader = new IniReader($this->dic->iliasIni()); + } else { + $ini_reader = new class () implements BasicConfigInterface { + public function isLoggingEnabled(): bool + { + return (bool) ILIAS_LOG_ENABLED; + } + + public function pathToLogFile(): string + { + return rtrim(ILIAS_LOG_DIR, '/') . '/' . + ltrim(ILIAS_LOG_FILE, '/'); + } + + public function pathToLogDirectory(): string + { + return ILIAS_LOG_DIR; + } + + public function defaultLevel(): ILIASLogLevel + { + return ILIASLogLevel::INFO; + } + }; + } + + return $this->basic_config = new BasicConfig($ini_reader); } public function componentConfigRepository(): ComponentConfigRepoInterface From edd499450f0d195216aa1210fc2c55cbe4aecce1 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 30 Jul 2026 16:20:24 +0200 Subject: [PATCH 149/333] Test: Fix Name For Imported Users in Results --- .../classes/class.ilTestEvaluationGUI.php | 77 +++++++++++++------ .../Participants/ParticipantRepository.php | 12 ++- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestEvaluationGUI.php b/components/ILIAS/Test/classes/class.ilTestEvaluationGUI.php index 6bc1c4c6953c..96975d98d223 100755 --- a/components/ILIAS/Test/classes/class.ilTestEvaluationGUI.php +++ b/components/ILIAS/Test/classes/class.ilTestEvaluationGUI.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Test\Participants\Participant; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Results\Presentation\TitlesBuilder as ResultsTitlesBuilder; use ILIAS\Test\Presentation\PrintLayoutProvider; @@ -133,20 +134,19 @@ public function printResults(): void true ); - $selected_active_ids = explode(',', $this->testrequest->strVal('active_ids')); + $selected_participants = $this->retrieveSelectedParticipants(); $results_panel = $this->ui_factory->panel()->report( $this->lng->txt('tst_results'), array_map( - function (string $v): SubPanel { - $value = (int) $v; - $attempt_id = ilObjTest::_getResultPass($value); - $components = $this->buildAttemptComponents($value, $attempt_id, false, true); + function (int $v) use ($selected_participants): SubPanel { + $attempt_id = ilObjTest::_getResultPass($v); + $components = $this->buildAttemptComponents($v, $attempt_id, false, true); return $this->ui_factory->panel()->sub( - $this->buildResultsTitle($value, $attempt_id), + $this->buildResultsTitle($selected_participants[$v], $attempt_id), $components ); }, - $selected_active_ids + array_keys($selected_participants) ) ); @@ -165,15 +165,16 @@ public function showResults(): void { $this->setCss(); $this->ctrl->saveParameterByClass(self::class, 'active_ids'); - $selected_active_ids = explode(',', $this->testrequest->strVal('active_ids')); + + $selected_participants = $this->retrieveSelectedParticipants(); $this->addPrintResultsButtonToToolbar(); $this->addToggleBestSolutionButtonToToolbar(); - $current_active_id = (int) $selected_active_ids[0]; - if (count($selected_active_ids) > 1 + $current_active_id = array_key_first($selected_participants); + if (count($selected_participants) > 1 && ($selected_active_id = $this->testrequest->getActiveId()) > 0 - && array_search($selected_active_id, $selected_active_ids) !== false) { + && array_key_exists($selected_active_id, $selected_participants) !== false) { $current_active_id = $selected_active_id; } @@ -184,7 +185,10 @@ public function showResults(): void } $results_panel = $this->ui_factory->panel()->report( - $this->buildResultsTitle($current_active_id, $attempt_id), + $this->buildResultsTitle( + $selected_participants[$current_active_id], + $attempt_id + ), $this->buildAttemptComponents($current_active_id, $attempt_id, true, false) ); @@ -202,8 +206,8 @@ public function showResults(): void ]); } - if (count($selected_active_ids) > 1) { - $this->addParticipantSelectorToToolbar($selected_active_ids, $current_active_id); + if (count($selected_participants) > 1) { + $this->addParticipantSelectorToToolbar($selected_participants, $current_active_id); } $this->tpl->setVariable( @@ -325,7 +329,13 @@ public function outUserPassDetails(): void true ), $settings, - $this->buildResultsTitle($active_id, $pass), + $this->buildResultsTitle( + $this->participant_repository->getParticipantByActiveId( + $this->object->getTestId(), + $active_id + )->getDisplayName($this->lng), + $pass + ), false ); @@ -791,7 +801,7 @@ protected function sendPage(string $page) $this->http->close(); } - protected function buildResultsTitle(int $active_id, int $pass): string + private function buildResultsTitle(string $participant_name, int $pass): string { if ($this->object->getAnonymity()) { return sprintf( @@ -802,7 +812,7 @@ protected function buildResultsTitle(int $active_id, int $pass): string return sprintf( $this->lng->txt('tst_result_user_name_pass'), $pass + 1, - $this->participant_repository->getParticipantByActiveId($this->object->getTestId(), $active_id)->getDisplayName($this->lng) + $participant_name ); } @@ -946,29 +956,30 @@ private function addToggleBestSolutionButtonToToolbar(): void } private function addParticipantSelectorToToolbar( - array $selected_active_ids, + array $selected_participants, int $current_active_id ): void { $this->toolbar->addSeparator(); $this->toolbar->addComponent( $this->ui_factory->dropdown() ->standard( - $this->buildParticipantSelectorArray($selected_active_ids, $current_active_id) + $this->buildParticipantSelectorArray($selected_participants, $current_active_id) )->withLabel($this->lng->txt('tst_res_jump_to_participant_hint_opt')) ); } private function buildParticipantSelectorArray( - array $selected_active_ids, + array $selected_participants, int $current_active_id ): array { + $selected_active_ids = array_keys($selected_participants); $this->ctrl->setParameterByClass(self::class, 'active_ids', implode(',', $selected_active_ids)); unset($selected_active_ids[array_search($current_active_id, $selected_active_ids)]); $available_user_links = array_map( - function (int $v): StandardLink { + function (int $v) use ($selected_participants): StandardLink { $this->ctrl->setParameterByClass(self::class, 'active_id', $v); return $this->ui_factory->link()->standard( - ilObjUser::_lookupFullname($this->object->_getUserIdFromActiveId($v)), + $selected_participants[$v], $this->ctrl->getLinkTargetByClass(self::class, 'showResults') ); }, @@ -1012,4 +1023,26 @@ function (array $c, int $v): array { $this->lng->txt('select_attempt') )->withActive("{$this->lng->txt('tst_attempt')} {$selected_attempt}"); } + + /** + * @return array Array with the active_id as key and the + * display name as value + */ + private function retrieveSelectedParticipants(): array + { + $int_trafo = $this->refinery->kindlyTo()->int(); + $selected_participants = []; + foreach ($this->participant_repository->getParticipants( + $this->object->getTestId(), + [ + 'active_ids' => array_map( + fn(string $v): int => $int_trafo->transform($v), + explode(',', $this->testrequest->strVal('active_ids')) + ) + ] + ) as $participant) { + $selected_participants[$participant->getActiveId()] = $participant->getDisplayName($this->lng); + } + return $selected_participants; + } } diff --git a/components/ILIAS/Test/src/Participants/ParticipantRepository.php b/components/ILIAS/Test/src/Participants/ParticipantRepository.php index 553b99256877..5348282fd163 100755 --- a/components/ILIAS/Test/src/Participants/ParticipantRepository.php +++ b/components/ILIAS/Test/src/Participants/ParticipantRepository.php @@ -278,6 +278,15 @@ private function applyFilter( $values = array_merge($values, ["%{$filter['ip_range']}%", "%{$filter['ip_range']}%"]); } + if ($this->isFilterSet($filter, 'active_ids')) { + $where[] = $this->database->in( + 'participants.active_id', + $filter['active_ids'], + false, + \ilDBConstants::T_INTEGER + ); + } + return [$where, $types, $values]; } @@ -302,7 +311,8 @@ private function applyOrder(?Order $order): string private function isFilterSet(array $filter, string $key): bool { - return isset($filter[$key]) && trim($filter[$key]) !== ""; + return isset($filter[$key]) + && (is_array($filter[$key]) || trim($filter[$key]) !== ''); } From a1885623f2ce0228ccc04c1ecd8f41996a33549f Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 30 Jul 2026 17:21:34 +0200 Subject: [PATCH 150/333] Test: Fix Recalculation of Points If points are changed and there already is question with changed points before the one being changed, the points are not recalculated. --- components/ILIAS/Test/src/Scoring/Manual/TestScoring.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Test/src/Scoring/Manual/TestScoring.php b/components/ILIAS/Test/src/Scoring/Manual/TestScoring.php index 097c5ed5b0f9..ae33c7ea5fbc 100755 --- a/components/ILIAS/Test/src/Scoring/Manual/TestScoring.php +++ b/components/ILIAS/Test/src/Scoring/Manual/TestScoring.php @@ -133,7 +133,12 @@ private function recalculatePass( $reached_points_changed = false; foreach ($passdata->getAnsweredQuestions() as $question_data) { if ($this->getQuestionId() !== 0 || $this->getQuestionId() === $question_data['id']) { - $reached_points_changed = $reached_points_changed || $this->recalculateQuestionScore($user_id, $active_id, $pass, $question_data); + $reached_points_changed = $this->recalculateQuestionScore( + $user_id, + $active_id, + $pass, + $question_data + ) || $reached_points_changed; } } $this->updatePassResultsTable($active_id, $pass, $reached_points_changed); From b9fe77cbd58a3200faf6e25f998e77170bec5eb8 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Thu, 30 Jul 2026 17:54:46 +0200 Subject: [PATCH 151/333] Logging: fix type error --- components/ILIAS/Logging/src/LegacyInitiator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/Logging/src/LegacyInitiator.php b/components/ILIAS/Logging/src/LegacyInitiator.php index e6d95a3346e2..936b47911c35 100755 --- a/components/ILIAS/Logging/src/LegacyInitiator.php +++ b/components/ILIAS/Logging/src/LegacyInitiator.php @@ -75,9 +75,9 @@ public function basicConfig(): BasicConfigInterface * which somehow depend on the dependencies of ilLoggerFactory. */ if ($this->dic->offsetExists('ilIliasIniFile')) { - $ini_reader = new IniReader($this->dic->iliasIni()); + $basic_config = new BasicConfig(new IniReader($this->dic->iliasIni())); } else { - $ini_reader = new class () implements BasicConfigInterface { + $basic_config = new class () implements BasicConfigInterface { public function isLoggingEnabled(): bool { return (bool) ILIAS_LOG_ENABLED; @@ -101,7 +101,7 @@ public function defaultLevel(): ILIASLogLevel }; } - return $this->basic_config = new BasicConfig($ini_reader); + return $this->basic_config = $basic_config; } public function componentConfigRepository(): ComponentConfigRepoInterface From effd7c2d9706806f6fe8be26801904cc75abac10 Mon Sep 17 00:00:00 2001 From: Chris Potter Date: Mon, 3 Aug 2026 09:57:07 +0200 Subject: [PATCH 152/333] Various small editorial changes to English and German language files --- lang/ilias_de.lang | 4 ++-- lang/ilias_en.lang | 28 +++++++++++++--------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 097f252062b9..04f20d8b0edb 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -9890,7 +9890,7 @@ file#:#copyright_custom#:#Benutzerdefiniert file#:#copyright_custom_info#:#Lizenz auswählen, welche auf alle entpackten Dateien dieses Archivs angewandt wird. file#:#copyright_inherited#:#Vererbt file#:#copyright_inherited_info#:#Lizenz des Zip-Archivs auf seine entpackten Dateien anwenden.
    Lizenz des Zip-Archivs: %s. -file#:#could_not_create_file_objs#:#Beim Erstellen der Dateiobjekte ist ein Fehler aufgetreten, wenden Sie sich an die Administrierenden der Plattform. +file#:#could_not_create_file_objs#:#Beim Erstellen der Dateiobjekte ist ein Fehler aufgetreten. Bitte wenden Sie sich an die technische Betreuung dieser Installation. file#:#de_activate_icon#:#(De-)Aktivieren file#:#download_ascii_filename#:#Nur ASCII-Zeichen im Download file#:#download_ascii_filename_info#:#Die Namen heruntergeladener Dateien enthalten nur ASCII-Zeichen und können so auf allen Systemen problemlos genutzt werden. Andere Zeichen werden durch Unterstriche ersetzt. Auf Installationen, die Dateinamen mit nicht-lateinischen Schriftzeichen nutzen, muss diese Option deaktiviert werden. @@ -17334,7 +17334,7 @@ trac#:#trac_assigned#:#Ausgewählt trac#:#trac_average#:#Durchschnitt trac#:#trac_begin_at#:#Startdatum trac#:#trac_closed_expire#:#Timeout -trac#:#trac_closed_login#:#Anonym zu Login +trac#:#trac_closed_login#:#Statusänderung - Anonym zu angemeldet trac#:#trac_closed_manual#:#Logout trac#:#trac_closed_misc#:#Sonstige trac#:#trac_collection_assign#:#Für Lernfortschritts-Bestimmung auswählen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 131f7dcf0886..a9ff22bb1e53 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -2506,7 +2506,7 @@ blog#:#blog_no_keywords#:#No keywords have been entered yet. blog#:#blog_notification_activated#:#Notifications Activated blog#:#blog_notification_deactivated#:#Notifications Deactivated blog#:#blog_notification_toggle_off#:#Deactivate Notifications -blog#:#blog_notification_toggle_on#:#Activate Notification +blog#:#blog_notification_toggle_on#:#Activate Notifications blog#:#blog_number_users_notes_or_comments#:#Number of users who have attached notes or comments to this post blog#:#blog_posting#:#Blog Post blog#:#blog_posting_deleted#:#Post successfully deleted. @@ -3647,7 +3647,7 @@ common#:#auth_soap_namespace#:#Namespace common#:#auth_soap_namespace_desc#:#As defined in WSDL. Must be specified, if .NET SOAP style is used. common#:#auth_soap_port_desc#:#E.g. 8080 if the full SOAP server URI is http://auth.yourserver.com:8080/dir/server.php common#:#auth_soap_server_desc#:#E.g. auth.yourserver.com if the full SOAP server URI is http://auth.yourserver.com:8080/dir/server.php -common#:#auth_soap_settings_saved#:#SOAP authentication settings saved +common#:#auth_soap_settings_saved#:#SOAP authentication settings saved. common#:#auth_soap_uri_desc#:#Local URI, e.g. dir/server.php if the full SOAP server URI is http://auth.yourserver.com:8080/dir/server.php common#:#auth_soap_use_dotnet#:#Use .NET SOAP Style common#:#auth_soap_use_https#:#Use HTTPS @@ -4232,8 +4232,8 @@ common#:#forums_anonymized#:#Forum anonymized common#:#forums_anonymous#:#anonym common#:#forums_articles#:#Posts common#:#forums_closed#:#closed -common#:#forums_disable_forum_notification#:#Disable Notification for this Forum -common#:#forums_enable_forum_notification#:#Enable Notification for this Forum +common#:#forums_disable_forum_notification#:#Disable Notifications for This Forum +common#:#forums_enable_forum_notification#:#Enable Notifications for This Forum common#:#forums_forum_notification_enabled#:#You will be notified about new posts in this forum. common#:#forums_last_post#:#Latest Post common#:#forums_last_posting_asc#:#Last Post Ascending @@ -7904,7 +7904,7 @@ crs#:#crs_select_archive_language#:#Please select a language for the archive crs#:#crs_select_one_archive#:#Please select one archive crs#:#crs_select_starter#:#Select Start Object crs#:#crs_settings#:#Course Settings -crs#:#crs_settings_saved#:#Settings saved +crs#:#crs_settings_saved#:#Settings saved. crs#:#crs_shorten_breadcrumb#:#Breadcrumb crs#:#crs_show_all_obj#:#Expand All crs#:#crs_show_member_export#:#Participants List @@ -8087,7 +8087,7 @@ crs#:#event_title#:#Title crs#:#event_tutor_data#:#Presentation by crs#:#event_unregister#:#Unregister crs#:#event_unregistered#:#You have been unregistered. -crs#:#event_updated#:#Settings saved +crs#:#event_updated#:#Settings saved. crs#:#event_user_selection#:#Selection of Users crs#:#event_user_selection_include_filter#:#Include "%1$s" crs#:#event_user_selection_include_requests#:#Include all users on list "Join Requests" @@ -9864,7 +9864,7 @@ file#:#copyright_custom#:#Custom file#:#copyright_custom_info#:#Choose a custom copyright which will be applied to all unzipped files of this archive. file#:#copyright_inherited#:#Inherited file#:#copyright_inherited_info#:#Apply the copyright of the zip archive to its unzipped files.
    Copyright of zip archive: %s. -file#:#could_not_create_file_objs#:#An error occurred while creating your file objects. Please contact the administrators of this platform. +file#:#could_not_create_file_objs#:#An error occurred while creating your file objects. Please contact this installation's technical support. file#:#de_activate_icon#:#Activate / Deactivate file#:#download_ascii_filename#:#Allow Only ASCII Characters in Downloaded Filenames file#:#download_ascii_filename_info#:#Downloaded files should only have ASCII-characters in their filename. Deactivate to use all characters. @@ -11139,7 +11139,7 @@ log#:#log_level_info#:#INFO log#:#log_level_notice#:#NOTICE log#:#log_level_off#:#Disabled log#:#log_level_warning#:#WARNING -logging#:#error_settings_saved#:#Settings saved +logging#:#error_settings_saved#:#Settings saved. logging#:#frm_clear_older_then#:#Deletes files older then logging#:#frm_clear_older_then_info#:#Please enter duration in days. logging#:#log_error_file_cleanup_info#:#Deletes old or orphand files of error log. @@ -11969,10 +11969,8 @@ mem#:#mem_settings_tab_settings#:#General Settings mem#:#mmbr_gallery_user_actions#:#Member Gallery Actions membership#:#mem_error_preconditions#:#A registration is not possible, since minimum one setting prevents this. membership#:#mem_force_notification#:#Notification per Mail -membership#:#mem_force_notification_mode_all#:#Notification activated for all members (changeable) membership#:#mem_force_notification_mode_all_sub_blocked#:#Members can deactivate notifications membership#:#mem_force_notification_mode_blocked#:#Members will be notified automatically -membership#:#mem_force_notification_mode_custom#:#Custom membership#:#mem_force_notification_mode_self#:#Members have to manually activate notification mep#:#mep_all#:#All mep#:#mep_all_mobs#:#All Media Objects @@ -17283,7 +17281,7 @@ trac#:#trac_assigned#:#Assigned trac#:#trac_average#:#Average trac#:#trac_begin_at#:#Start date trac#:#trac_closed_expire#:#Timeout -trac#:#trac_closed_login#:#Anonymous To Login +trac#:#trac_closed_login#:#Change of status - anonymous to logged-in trac#:#trac_closed_manual#:#Logout trac#:#trac_closed_misc#:#Misc. trac#:#trac_collection_assign#:#Select for Learning Progress Determination @@ -17471,10 +17469,10 @@ trac#:#trac_session_statistics_mode_today#:#Today trac#:#trac_session_statistics_mode_week#:#Week trac#:#trac_session_statistics_mode_year#:#Year trac#:#trac_session_statistics_no_data#:#No data for this statistics could be found. -trac#:#trac_sessions_closed#:#Leavings -trac#:#trac_sessions_opened#:#Admissions +trac#:#trac_sessions_closed#:#Exits +trac#:#trac_sessions_opened#:#Entrances trac#:#trac_settings#:#Settings -trac#:#trac_settings_saved#:#Settings saved +trac#:#trac_settings_saved#:#Settings saved. trac#:#trac_short_system_load#:#Short term trac#:#trac_show_graph#:#Show Graph trac#:#trac_show_progress_block#:#Show Personal Progress Chart in ‘Content’-Tab @@ -17497,7 +17495,7 @@ trac#:#trac_title#:#Title trac#:#trac_title_description#:#Title / Description trac#:#trac_total_online#:#Total Time Online trac#:#trac_trash#:#Moved to Trash -trac#:#trac_update_edit_user#:#Settings saved +trac#:#trac_update_edit_user#:#Settings saved. trac#:#trac_updated_status#:#Saved learning progress status. trac#:#trac_user_data#:#User Data trac#:#trac_valid_request#:#Max. Time Between Requests From 0de5eddb409c32a72702e5c2b353748a22d31f83 Mon Sep 17 00:00:00 2001 From: Abraham Date: Mon, 3 Aug 2026 10:13:08 +0200 Subject: [PATCH 153/333] [Survey] fix: Check participation after period (#11844) --- .../Execution/class.ilSurveyExecutionGUI.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Survey/Execution/class.ilSurveyExecutionGUI.php b/components/ILIAS/Survey/Execution/class.ilSurveyExecutionGUI.php index 4a9ea5ed3301..68c68c54f1ea 100755 --- a/components/ILIAS/Survey/Execution/class.ilSurveyExecutionGUI.php +++ b/components/ILIAS/Survey/Execution/class.ilSurveyExecutionGUI.php @@ -149,7 +149,7 @@ public function executeCommand(): string protected function checkAuth( bool $a_may_start = false, - bool $a_ignore_status = false + bool $a_finished_run = false ): void { $rbacsystem = $this->rbacsystem; $ilUser = $this->user; @@ -162,8 +162,12 @@ protected function checkAuth( } - if (!$this->access_manager->canStartSurvey()) { - // only with read access it is possible to run the test + if ($a_finished_run && + !$this->access_manager->canRead() && + !$this->participant_manager->isExternalRater()) { + throw new ilSurveyException($this->lng->txt("cannot_read_survey")); + } + if (!$a_finished_run && !$this->access_manager->canStartSurvey()) { throw new ilSurveyException($this->lng->txt("cannot_read_survey")); } @@ -211,7 +215,11 @@ protected function checkAuth( //$_SESSION["appr_id"][$this->object->getId()] = $appr_id; - if (!$a_ignore_status) { + if ($a_finished_run) { + if (!$this->run_manager->hasFinished()) { + throw new ilSurveyException($this->lng->txt("cannot_read_survey")); + } + } else { // completed if ($this->run_manager->hasFinished()) { $this->tpl->setOnScreenMessage('failure', $this->lng->txt("already_completed_survey"), true); From 940762bb26a9b18f635fd0b67bc2be78b1147fec Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 3 Aug 2026 11:46:43 +0200 Subject: [PATCH 154/333] Logging: fix type error during setup (48150) --- components/ILIAS/Logging/src/Setup/Config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Logging/src/Setup/Config.php b/components/ILIAS/Logging/src/Setup/Config.php index 3cf952327d62..44e9a6f5f9e6 100755 --- a/components/ILIAS/Logging/src/Setup/Config.php +++ b/components/ILIAS/Logging/src/Setup/Config.php @@ -42,7 +42,7 @@ public function __construct( "Expected a path to the logfile, if logging is enabled." ); } - $level = ILIASLogLevel::tryFromString($level); + $level = $level === null ? null : ILIASLogLevel::tryFromString($level); if ($enabled && !$level) { throw new InvalidArgumentException( "Expected a valid default log level, if logging is enabled." From b9be55694102de51bd97eec1342cbde9fc10cc24 Mon Sep 17 00:00:00 2001 From: Abraham Date: Mon, 3 Aug 2026 15:19:00 +0200 Subject: [PATCH 155/333] [Survey] fix: Delete option verification within routings (#11847) --- .../Questions/class.SurveyMultipleChoiceQuestion.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMultipleChoiceQuestion.php b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMultipleChoiceQuestion.php index 7a13d54a54a6..3c86196bc892 100755 --- a/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMultipleChoiceQuestion.php +++ b/components/ILIAS/SurveyQuestionPool/Questions/class.SurveyMultipleChoiceQuestion.php @@ -459,10 +459,17 @@ public function getPreconditionValueOutput( // #18136 $category = $this->categories->getCategoryForScale((int) $value + 1); + $scale = ""; + $title = ""; + if ($category) { + $scale = $category->scale; + $title = $category->title; + } + // #17895 - see getPreconditionOptions() - return $category->scale . + return $scale . " - " . - ((strlen($category->title ?? "")) ? $category->title : $this->lng->txt('other_answer')); + ((strlen($title)) ? $title : $this->lng->txt('other_answer')); } public function getCategories(): SurveyCategories From 2582ae5bf1830d8eeedb5f5ae93bdc836edba485 Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 3 Aug 2026 15:52:00 +0200 Subject: [PATCH 156/333] Logging: properly clean up stack trace --- .../Logger/Monolog/ILIASTraceProcessor.php | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php b/components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php index 588d7d5b71ca..a5140b17c250 100644 --- a/components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php +++ b/components/ILIAS/Logging/src/Logger/Monolog/ILIASTraceProcessor.php @@ -25,14 +25,18 @@ class ILIASTraceProcessor { + protected const array SKIP_CLASS_NAMES_START_WITH = [ + "Monolog", + \ILIAS\Logging\Logger\Logger::class, + \Psr\Log\AbstractLogger::class, + "ilLogger" + ]; + public function __construct( protected ILIASLogLevel $level ) { } - /** - * @todo fix shifting calls - */ public function __invoke(LogRecord $record): LogRecord { if ($record['level'] < $this->level->value) { @@ -41,20 +45,32 @@ public function __invoke(LogRecord $record): LogRecord $trace = debug_backtrace(); - // shift current method - array_shift($trace); - - // shift internal monolog calls - array_shift($trace); - array_shift($trace); + // shift current method and first internal monolog call array_shift($trace); array_shift($trace); + $previous_line = $trace[0]['line'] ?? ''; + while (($class = $trace[0]['class'] ?? '') !== '') { + foreach (self::SKIP_CLASS_NAMES_START_WITH as $start) { + if (str_starts_with($class, $start)) { + /* + * To find the line where the logger is called, we need to stay one frame "behind", + * otherwise we'll get the line where the function that calls the logger is + * called from. + */ + $previous_line = $trace[0]['line'] ?? ''; + array_shift($trace); + continue 2; + } + } + break; + } + if (is_array($trace) && count($trace)) { $trace_info = ($trace[0]['class'] ?? '') . '::' . ($trace[0]['function'] ?? '') . ':' . - ($trace[0]['line'] ?? ''); + $previous_line; $record['extra'] = array_merge( $record['extra'], array('trace' => $trace_info) From 75efbbc209be81bce7e425f16e6bf9ef87506fd2 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 3 Aug 2026 15:56:25 +0200 Subject: [PATCH 157/333] copage: array key fix --- components/ILIAS/COPage/Link/LinkManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/COPage/Link/LinkManager.php b/components/ILIAS/COPage/Link/LinkManager.php index 45730995d62d..46daf805e495 100755 --- a/components/ILIAS/COPage/Link/LinkManager.php +++ b/components/ILIAS/COPage/Link/LinkManager.php @@ -248,7 +248,7 @@ public function moveIntLinks( ($area["Type"] ?? "") == "StructureObject") { $t = $area["Target"] ?? ""; $tid = \ilInternalLink::_extractObjIdOfTarget($t); - if ($a_from_to[$tid] > 0) { + if (($a_from_to[$tid] ?? 0) > 0) { $correction_needed = true; } } From a15bdb2ef064e4e45c51723eb8f7d50b414b6b9e Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 3 Aug 2026 16:15:16 +0200 Subject: [PATCH 158/333] copage: prevent ctrl issues in copy process --- components/ILIAS/COPage/classes/class.ilPageObjectGUI.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php b/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php index e933fc001306..e6da0672c1fd 100755 --- a/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php +++ b/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php @@ -1706,7 +1706,8 @@ public function setEditMode(): void public function setDefaultLinkXml(): void { $this->page_linker->setProfileBackUrl($this->getProfileBackUrl()); - $this->page_linker->setOffline($this->getOutputMode() == self::OFFLINE); + // prevent to run into ctrl issues in copy background process + $this->page_linker->setOffline($this->getOutputMode() == self::OFFLINE || !ilContext::supportsRedirects()); $this->setLinkXml($this->page_linker->getLinkXML($this->getPageObject()->getInternalLinks())); } @@ -1723,7 +1724,8 @@ public function getProfileBackUrl(): string return $this->profile_back_url; } if ($this->getOutputMode() === self::OFFLINE || - $this->getOutputMode() === self::PRINTING) { + $this->getOutputMode() === self::PRINTING || + !ilContext::supportsRedirects()) { return ""; } return $this->ctrl->getLinkTargetByClass(strtolower(get_class($this)), "preview"); From d64802c5102d3fbd2912a4f5141754ca9a56d4ce Mon Sep 17 00:00:00 2001 From: Tim Schmitz Date: Mon, 3 Aug 2026 16:17:14 +0200 Subject: [PATCH 159/333] Container: remove objectives-settings info blocks in course content --- .../class.ilContainerObjectiveGUI.php | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php b/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php index 311e6fad7a19..70cb1bed03b6 100755 --- a/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php +++ b/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php @@ -474,77 +474,7 @@ protected function buildObjectiveMap(): array */ protected function addItemDetails(ilObjectListGUI $a_item_list_gui, array $a_item): void { - $lng = $this->lng; - $ilCtrl = $this->ctrl; $ilUser = $this->user; - $item_ref_id = $a_item["ref_id"]; - if (is_array($this->objective_map)) { - $details = []; - if (isset($this->objective_map["material"][$item_ref_id])) { - // #12965 - foreach ($this->objective_map["material"][$item_ref_id] as $objective_id) { - $ilCtrl->setParameterByClass('ilcourseobjectivesgui', 'objective_id', $objective_id); - $url = $ilCtrl->getLinkTargetByClass(['illoeditorgui', 'ilcourseobjectivesgui'], 'edit'); - $ilCtrl->setParameterByClass('ilcourseobjectivesgui', 'objective_id', ''); - - $details[] = [ - 'desc' => $lng->txt('crs_loc_tab_materials') . ': ', - 'target' => '_top', - 'link' => $url, - 'name' => $this->objective_map["names"][$objective_id] - ]; - } - } - if (($this->objective_map["test_i"] ?? 0) == $item_ref_id) { - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 1); - $details[] = [ - 'desc' => '', - 'target' => '_top', - 'link' => $ilCtrl->getLinkTargetByClass('illoeditorgui', 'testOverview'), - 'name' => $lng->txt('crs_loc_tab_itest') - ]; - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 0); - } - if (($this->objective_map["test_q"] ?? 0) == $item_ref_id) { - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 2); - $details[] = [ - 'desc' => '', - 'target' => '_top', - 'link' => $ilCtrl->getLinkTargetByClass('illoeditorgui', 'testOverview'), - 'name' => $lng->txt('crs_loc_tab_qtest') - ]; - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 0); - } - - // #15367 - if (is_array($this->objective_map["test_ass"][$item_ref_id] ?? false)) { - foreach ($this->objective_map["test_ass"][$item_ref_id] as $type => $items) { - if ($type == ilLOSettings::TYPE_TEST_INITIAL) { - $caption = $lng->txt('crs_loc_tab_itest'); - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 1); - } else { - $caption = $lng->txt('crs_loc_tab_qtest'); - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 2); - } - foreach ($items as $objtv_title) { - $details[] = [ - 'desc' => '', - 'target' => '_top', - 'link' => $ilCtrl->getLinkTargetByClass('illoeditorgui', 'testsOverview'), - 'name' => $caption . " (" . $this->lng->txt("crs_loc_learning_objective") . ": " . $objtv_title . ")" - ]; - } - $ilCtrl->setParameterByClass('illoeditorgui', 'tt', 0); - } - } - - if (count($details)) { - $a_item_list_gui->enableItemDetailLinks(true); - $a_item_list_gui->setItemDetailLinks($details, $lng->txt('crs_loc_settings_tbl') . ': '); - } else { - $a_item_list_gui->enableItemDetailLinks(false); - } - } // order if ($this->getContainerGUI()->isActiveOrdering()) { From 34fdc2333d8af50556c48c9834cdde24882f96e8 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 3 Aug 2026 16:57:46 +0200 Subject: [PATCH 160/333] 47866: Page Editor: Text: List format options expand to the left and are not fully viewable for users --- components/ILIAS/COPage/css/content_base.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/COPage/css/content_base.css b/components/ILIAS/COPage/css/content_base.css index ff752cb79828..26afe4c29ff3 100755 --- a/components/ILIAS/COPage/css/content_base.css +++ b/components/ILIAS/COPage/css/content_base.css @@ -22,8 +22,13 @@ white-space: normal; } .ilTinyMenuSection > div.dropdown:nth-child(1) > ul.dropdown-menu { - right:auto; - left:0; + right: auto; + left: 0; +} + +/* #47866 */ +.ilTinyMenuSection > div.dropdown:nth-child(4) > ul.dropdown-menu { + right: auto; } /* see #41844 and #46127 */ @@ -252,4 +257,4 @@ button.copg-add.dropdown-toggle.btn:focus-visible { .il-copg-mob-fullscreen { height: calc(90vh - 160px); -} \ No newline at end of file +} From eb4365fb1aa1a3697c6f1fad7887bbd5d700afae Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 3 Aug 2026 17:13:17 +0200 Subject: [PATCH 161/333] 47929: Error Allowed memory size exhausted when downloading large files via action Download Selected Submission --- .../Exercise/Submission/SubmissionManager.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Exercise/Submission/SubmissionManager.php b/components/ILIAS/Exercise/Submission/SubmissionManager.php index d17a454deb79..9993180d3256 100644 --- a/components/ILIAS/Exercise/Submission/SubmissionManager.php +++ b/components/ILIAS/Exercise/Submission/SubmissionManager.php @@ -621,10 +621,24 @@ protected function copySubmissionFilesToDir( \ilFileUtils::makeDirParents($dir); $file = $dir . DIRECTORY_SEPARATOR . $targetfile; if (!is_null($stream)) { - file_put_contents( - $file, - $stream->getContents() - ); + $source_stream = $stream->detach(); + if (!is_resource($source_stream)) { + throw new \RuntimeException('Unable to read submission stream.'); + } + + $target_stream = fopen($file, 'wb'); + if ($target_stream === false) { + throw new \RuntimeException("Unable to open file for writing: $file"); + } + + try { + stream_copy_to_stream( + $source_stream, + $target_stream + ); + } finally { + fclose($target_stream); + } } // unzip blog/portfolio From de56a25165de7bf27a120d6465ea3cfc2e8767ff Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Mon, 3 Aug 2026 17:53:16 +0200 Subject: [PATCH 162/333] 47835: Using an & in a tilte of a Content-Style Class prevents page rendering --- .../COPage/classes/class.ilPageObject.php | 17 +++++++++--- .../ILIAS/COPage/tests/PageObjectTest.php | 27 +++++++++++++++++++ .../tests/class.ilUnitTestPageObject.php | 10 +++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/COPage/classes/class.ilPageObject.php b/components/ILIAS/COPage/classes/class.ilPageObject.php index 9bdd0fb89a41..2d0b0aab8f92 100755 --- a/components/ILIAS/COPage/classes/class.ilPageObject.php +++ b/components/ILIAS/COPage/classes/class.ilPageObject.php @@ -960,7 +960,7 @@ public function getLanguageVariablesXML(int $style_id = 0): string "table", "table_cell"] as $type) { $dummy_pc->getCharacteristicsOfCurrentStyle([$type]); foreach ($dummy_pc->getCharacteristics() as $char => $txt) { - $xml .= ""; + $xml .= $this->getCharacteristicLangVarXML($type, $char, $txt); } } $type = "media_cont"; @@ -968,14 +968,14 @@ public function getLanguageVariablesXML(int $style_id = 0): string $dummy_pc->setStyleId($style_id); $dummy_pc->getCharacteristicsOfCurrentStyle([$type]); foreach ($dummy_pc->getCharacteristics() as $char => $txt) { - $xml .= ""; + $xml .= $this->getCharacteristicLangVarXML($type, $char, $txt); } foreach (["text_block", "heading1", "heading2", "heading3"] as $type) { $dummy_pc = new ilPCParagraphGUI($this, null, ""); $dummy_pc->setStyleId($style_id); $dummy_pc->getCharacteristicsOfCurrentStyle([$type]); foreach ($dummy_pc->getCharacteristics() as $char => $txt) { - $xml .= ""; + $xml .= $this->getCharacteristicLangVarXML($type, $char, $txt); } } foreach ($lang_vars as $lang_var) { @@ -996,9 +996,18 @@ protected function getLangVarXML(string $var): string ); } + protected function getCharacteristicLangVarXML(string $type, string $char, string $txt): string + { + return $this->getLangVarXMLForValue( + "char_" . $type . "_" . $char, + $txt + ); + } + protected function getLangVarXMLForValue(string $var, string $val): string { - $val = str_replace('"', """, $val); + $var = htmlspecialchars($var, ENT_XML1 | ENT_QUOTES, 'UTF-8'); + $val = htmlspecialchars($val, ENT_XML1 | ENT_QUOTES, 'UTF-8'); return ""; } diff --git a/components/ILIAS/COPage/tests/PageObjectTest.php b/components/ILIAS/COPage/tests/PageObjectTest.php index 4c3471bbea03..cb1fc82df12c 100755 --- a/components/ILIAS/COPage/tests/PageObjectTest.php +++ b/components/ILIAS/COPage/tests/PageObjectTest.php @@ -114,4 +114,31 @@ public function testGeneratePCId(): void strlen($id) ); } + + public function testGetLangVarXMLForValueEscapesXMLSpecialCharacters(): void + { + $page = new ilUnitTestPageObject(0); + + $this->assertSame( + '', + $page->getLangVarXMLForValueForTesting( + 'some&', + 'one & two \' three " four < five >' + ) + ); + } + + public function testGetCharacteristicLangVarXMLForValueEscapesXMLSpecialCharacters(): void + { + $page = new ilUnitTestPageObject(0); + + $this->assertSame( + '', + $page->getCharacteristicLangVarXMLForTesting( + 'text_block', + 'My&Class', + 'one & two \' three " four < five >' + ) + ); + } } diff --git a/components/ILIAS/COPage/tests/class.ilUnitTestPageObject.php b/components/ILIAS/COPage/tests/class.ilUnitTestPageObject.php index f6d95447a5f6..1b8c21219463 100755 --- a/components/ILIAS/COPage/tests/class.ilUnitTestPageObject.php +++ b/components/ILIAS/COPage/tests/class.ilUnitTestPageObject.php @@ -40,4 +40,14 @@ public function update(bool $a_validate = true, bool $a_no_history = false): arr { return true; } + + public function getLangVarXMLForValueForTesting(string $var, string $val): string + { + return $this->getLangVarXMLForValue($var, $val); + } + + public function getCharacteristicLangVarXMLForTesting(string $type, string $char, string $txt): string + { + return $this->getCharacteristicLangVarXML($type, $char, $txt); + } } From c364175f062c1ccd12dd0e7034603a20616e9cde Mon Sep 17 00:00:00 2001 From: Christian Knof Date: Wed, 29 Jul 2026 14:46:28 +0200 Subject: [PATCH 163/333] Additional code authority for kiosk mode, language, learning sequence and maps. --- docs/development/maintenance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/maintenance.md b/docs/development/maintenance.md index 3dfabbc1512d..c84bf88ddd45 100755 --- a/docs/development/maintenance.md +++ b/docs/development/maintenance.md @@ -819,7 +819,7 @@ of ILIAS. The file contains the following fields: * **KioskMode (aka General Kiosk Mode)** * Authority to Sign off on Conceptual Changes: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) - * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332),[cknof](https://docu.ilias.de/go/usr/90890) + * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [cknof](https://docu.ilias.de/go/usr/90890), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Authority to (De-)Assign Authorities: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) * Assignee for Issues: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) @@ -832,7 +832,7 @@ of ILIAS. The file contains the following fields: * **Language** * Authority to Sign off on Conceptual Changes: [mkunkel](https://docu.ilias.de/go/usr/115) - * Authority to Sign off on Code Changes: [mkunkel](https://docu.ilias.de/go/usr/115), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [ChrisPotter](https://docu.ilias.de/go/usr/90855), [keven.clausen](https://docu.ilias.de/go/usr/100316), [cknof](https://docu.ilias.de/go/usr/90890) + * Authority to Sign off on Code Changes: [mkunkel](https://docu.ilias.de/go/usr/115), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [ChrisPotter](https://docu.ilias.de/go/usr/90855), [keven.clausen](https://docu.ilias.de/go/usr/100316), [cknof](https://docu.ilias.de/go/usr/90890), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [ChrisPotter](https://docu.ilias.de/go/usr/90855) * Authority to (De-)Assign Authorities: [mkunkel](https://docu.ilias.de/go/usr/115) * Assignee for Issues: [mkunkel](https://docu.ilias.de/go/usr/115) @@ -884,7 +884,7 @@ of ILIAS. The file contains the following fields: * **Learning Sequence** * Authority to Sign off on Conceptual Changes: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) - * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) + * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Authority to (De-)Assign Authorities: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) * Assignee for Issues: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) @@ -1004,7 +1004,7 @@ of ILIAS. The file contains the following fields: * **Maps** * Authority to Sign off on Conceptual Changes: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) - * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) + * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Authority to (De-)Assign Authorities: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Assignee for Issues: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) From 8af32b10dd023463741acab1a1c234b1749da7f0 Mon Sep 17 00:00:00 2001 From: dkippKPG Date: Fri, 31 Jul 2026 10:17:49 +0200 Subject: [PATCH 164/333] Changed entry in maintenance.md to the format: [github-handle](url-to-docu-profile) --- docs/development/maintenance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/maintenance.md b/docs/development/maintenance.md index c84bf88ddd45..d5e215924d0a 100755 --- a/docs/development/maintenance.md +++ b/docs/development/maintenance.md @@ -819,7 +819,7 @@ of ILIAS. The file contains the following fields: * **KioskMode (aka General Kiosk Mode)** * Authority to Sign off on Conceptual Changes: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) - * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [cknof](https://docu.ilias.de/go/usr/90890), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) + * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [cknof](https://docu.ilias.de/go/usr/90890), [dkippKPG](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Authority to (De-)Assign Authorities: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) * Assignee for Issues: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) @@ -832,7 +832,7 @@ of ILIAS. The file contains the following fields: * **Language** * Authority to Sign off on Conceptual Changes: [mkunkel](https://docu.ilias.de/go/usr/115) - * Authority to Sign off on Code Changes: [mkunkel](https://docu.ilias.de/go/usr/115), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [ChrisPotter](https://docu.ilias.de/go/usr/90855), [keven.clausen](https://docu.ilias.de/go/usr/100316), [cknof](https://docu.ilias.de/go/usr/90890), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) + * Authority to Sign off on Code Changes: [mkunkel](https://docu.ilias.de/go/usr/115), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [ChrisPotter](https://docu.ilias.de/go/usr/90855), [keven.clausen](https://docu.ilias.de/go/usr/100316), [cknof](https://docu.ilias.de/go/usr/90890), [dkippKPG](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [ChrisPotter](https://docu.ilias.de/go/usr/90855) * Authority to (De-)Assign Authorities: [mkunkel](https://docu.ilias.de/go/usr/115) * Assignee for Issues: [mkunkel](https://docu.ilias.de/go/usr/115) @@ -884,7 +884,7 @@ of ILIAS. The file contains the following fields: * **Learning Sequence** * Authority to Sign off on Conceptual Changes: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) - * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) + * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [dkippKPG](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Authority to (De-)Assign Authorities: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) * Assignee for Issues: [katrin.grosskopf](https://docu.ilias.de/go/usr/68340) @@ -1004,7 +1004,7 @@ of ILIAS. The file contains the following fields: * **Maps** * Authority to Sign off on Conceptual Changes: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) - * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [dkipp_kpg](https://docu.ilias.de/go/usr/120714) + * Authority to Sign off on Code Changes: [keven.clausen](https://docu.ilias.de/go/usr/100316), [katrin.grosskopf](https://docu.ilias.de/go/usr/68340), [jeanine.auerbach](https://docu.ilias.de/go/usr/101332), [dkippKPG](https://docu.ilias.de/go/usr/120714) * Authority to Curate Test Cases: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Authority to (De-)Assign Authorities: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) * Assignee for Issues: [jeanine.auerbach](https://docu.ilias.de/go/usr/101332) From bf4a2d8ff0f78f043aa1592c6b49ca9880ceaa00 Mon Sep 17 00:00:00 2001 From: Lukas Scharmer Date: Wed, 15 Jul 2026 15:16:11 +0200 Subject: [PATCH 165/333] IRSS: Change il_resource_flavour.variant to length 638 --- .../classes/Setup/DB/UpdateStepsV12.php | 40 +++++++++++++++++++ .../class.ilResourceStorageSetupAgent.php | 9 +++-- .../Flavour/Definition/FlavourDefinition.php | 2 +- .../src/Flavour/FlavourBuilder.php | 2 +- .../tests/Flavours/FlavourTest.php | 2 +- 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 components/ILIAS/ResourceStorage/classes/Setup/DB/UpdateStepsV12.php diff --git a/components/ILIAS/ResourceStorage/classes/Setup/DB/UpdateStepsV12.php b/components/ILIAS/ResourceStorage/classes/Setup/DB/UpdateStepsV12.php new file mode 100644 index 000000000000..d2b063c57e8e --- /dev/null +++ b/components/ILIAS/ResourceStorage/classes/Setup/DB/UpdateStepsV12.php @@ -0,0 +1,40 @@ +db = $db; + } + + public function step_1(): void + { + $this->db->modifyTableColumn('il_resource_flavour', 'variant', [ + 'notnull' => false, + 'length' => 638, + 'type' => \ilDBConstants::T_TEXT, + ]); + } +} diff --git a/components/ILIAS/ResourceStorage/classes/Setup/class.ilResourceStorageSetupAgent.php b/components/ILIAS/ResourceStorage/classes/Setup/class.ilResourceStorageSetupAgent.php index b29e61b845b1..87dd4233da11 100755 --- a/components/ILIAS/ResourceStorage/classes/Setup/class.ilResourceStorageSetupAgent.php +++ b/components/ILIAS/ResourceStorage/classes/Setup/class.ilResourceStorageSetupAgent.php @@ -25,6 +25,8 @@ use ILIAS\Setup\Config; use ILIAS\Setup\Objective; use ILIAS\Setup\ObjectiveCollection; +use ILIAS\ResourceStorage\Setup\DB\UpdateStepsV12; +use ILIAS\Setup\Objective\ObjectiveWithPreconditions; /** * Class ilResourceStorageSetupAgent @@ -65,9 +67,10 @@ public function getUpdateObjective(?Config $config = null): Objective new ilDatabaseUpdateStepsExecutedObjective( new ilResourceStorageDB80() ), - new ilDatabaseUpdateStepsExecutedObjective( - new ilResourceStorageDB90() - ) + new ObjectiveWithPreconditions( + new ilDatabaseUpdateStepsExecutedObjective(new UpdateStepsV12()), + new ilDatabaseUpdateStepsExecutedObjective(new ilResourceStorageDB90()), + ), ); } diff --git a/components/ILIAS/ResourceStorage/src/Flavour/Definition/FlavourDefinition.php b/components/ILIAS/ResourceStorage/src/Flavour/Definition/FlavourDefinition.php index e26ea0311a5b..335863676eda 100755 --- a/components/ILIAS/ResourceStorage/src/Flavour/Definition/FlavourDefinition.php +++ b/components/ILIAS/ResourceStorage/src/Flavour/Definition/FlavourDefinition.php @@ -51,7 +51,7 @@ public function getInternalName(): string; * such variants must be distinguishable. For example, a variant name may contain "{height}x{width}" * if these are configurable values. * - * The Variant-Name MUST be less than 768 characters long! + * The Variant-Name MUST be less than 638 characters long! */ public function getVariantName(): ?string; diff --git a/components/ILIAS/ResourceStorage/src/Flavour/FlavourBuilder.php b/components/ILIAS/ResourceStorage/src/Flavour/FlavourBuilder.php index ce6f558a7ca8..f9df49ba95ea 100755 --- a/components/ILIAS/ResourceStorage/src/Flavour/FlavourBuilder.php +++ b/components/ILIAS/ResourceStorage/src/Flavour/FlavourBuilder.php @@ -44,7 +44,7 @@ */ class FlavourBuilder { - public const VARIANT_NAME_MAX_LENGTH = 768; + public const VARIANT_NAME_MAX_LENGTH = 638; private array $current_revision_cache = []; private array $resources_cache = []; diff --git a/components/ILIAS/ResourceStorage/tests/Flavours/FlavourTest.php b/components/ILIAS/ResourceStorage/tests/Flavours/FlavourTest.php index b95a422e676b..f8a7c0859dea 100755 --- a/components/ILIAS/ResourceStorage/tests/Flavours/FlavourTest.php +++ b/components/ILIAS/ResourceStorage/tests/Flavours/FlavourTest.php @@ -85,7 +85,7 @@ public function testDefinitionVariantNameLengths(): void $flavour_definition = $this->createMock(FlavourDefinition::class); $flavour_definition->expects($this->exactly(2)) ->method('getVariantName') - ->willReturn(str_repeat('a', 768)); + ->willReturn(str_repeat('a', 638)); $flavour_builder->has( new ResourceIdentification('1'), From 2c6425c725f13a807cf93ff0b843bec2e2989e4c Mon Sep 17 00:00:00 2001 From: selyesa Date: Fri, 17 Jul 2026 18:21:54 +0200 Subject: [PATCH 166/333] Update how-to-write-a-privacy.md --- docs/development/how-to-write-a-privacy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/how-to-write-a-privacy.md b/docs/development/how-to-write-a-privacy.md index 4fb3e4f89a53..40606ca3690d 100644 --- a/docs/development/how-to-write-a-privacy.md +++ b/docs/development/how-to-write-a-privacy.md @@ -20,7 +20,7 @@ To extend or add the privacy documentation, base your Pull Request on the follow ### [Name of the component] Privacy -> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** ### General information From 1302aa5aeb5b121564042c4d1a60f87d1a95dd6d Mon Sep 17 00:00:00 2001 From: selyesa Date: Thu, 30 Jul 2026 17:21:49 +0200 Subject: [PATCH 167/333] Update how-to-write-a-privacy.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The link für the Pull Request wasn't working. --- docs/development/how-to-write-a-privacy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/how-to-write-a-privacy.md b/docs/development/how-to-write-a-privacy.md index 40606ca3690d..0b9c6cd8f2c9 100644 --- a/docs/development/how-to-write-a-privacy.md +++ b/docs/development/how-to-write-a-privacy.md @@ -20,7 +20,7 @@ To extend or add the privacy documentation, base your Pull Request on the follow ### [Name of the component] Privacy -> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** ### General information From b65df6fbd9c51600796d8482c0ece0df89c3bc7e Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Tue, 4 Aug 2026 14:36:06 +0200 Subject: [PATCH 168/333] [FEATURE] Component: add bootstrap migration guideline (#11420) * Add guideline for developers on how to migrate components to the bootstrap mechanism * Add `ilInitialisation::initILIAS()` deprecation notice * Add `Component::init()` interface description --------- Co-authored-by: Fabian Schmid --- .../docs/component-bootstrap-migration.md | 707 ++++++++++++++++++ ...lComponentBuildPluginInfoObjectiveTest.php | 4 +- .../Init/classes/class.ilInitialisation.php | 5 +- 3 files changed, 713 insertions(+), 3 deletions(-) create mode 100644 components/ILIAS/Component/docs/component-bootstrap-migration.md diff --git a/components/ILIAS/Component/docs/component-bootstrap-migration.md b/components/ILIAS/Component/docs/component-bootstrap-migration.md new file mode 100644 index 000000000000..893c33388f5e --- /dev/null +++ b/components/ILIAS/Component/docs/component-bootstrap-migration.md @@ -0,0 +1,707 @@ +# How to migrate to the component bootstrap mechanism + +This document describes how an ILIAS component can be migrated from the legacy initialisation to the new component +bootstrap mechanism. Its purpose is to aid developers and +[authorities who sign off on code changes](../../../../docs/development/maintenance.md#authorities) who are tasked with +the migration of their component to this new mechanism, or want to introduce new ones while the "component revision" +big project is still ongoing. + +The described practises and required steps for a successful migration are put in two categories: one category is for +instructions, those are patterns, steps and practises which must be followed to the best of each individuals ability; +the other category is for recommendations, which should be considered by each individual but are shaped by personal +preference and might not be suitable for all components. + +More information about the component revision and the new repository- and component-structure can be found here: + +* [Repository- and component-structure](../../../../docs/development/components-and-directories.md) +* [Component Revision (Big Project)](https://docu.ilias.de/go/wiki/wpage_7295_1357) + +## Instructions + +This chapter holds instructions which must be followed to the best of each developers ability. + +### Starting point (and bad practices) + +The current state of most ILIAS components – former modules and services – is heavily reliant on the global dependency +injection container (known as `global $DIC`), which is used in any place at any time to service-locate something from +somewhere. What we are trying to say here is that most of the components do not yet fully embrace dependency injection +(DI) like its meant to be and therefore don't use it to its full potential. While the first part of the previous +sentence may be achieved more easily, the latter is still not fulfilled by the sheer implementation of injecting +something into something else. It needs to be refined in most cases; we must not simply inject entire subsystems of +ILIAS if we are only going to use a few aspects of it. DI needs careful consideration and the migration towards the +component bootstrap mechanism is the ideal time to reconsider how your objects and facilities are managed – internally +but also externally. + +```php +class InternalThingy +{ + // this is not real DI, we still need to service-locate: + public function __construct(\ILIAS\DI\Container $dic) + { + $this->dep1 = $dic->user(); + } +} +``` + +Having said that, in addition to the above ILIAS components its facilities are also initialised inside +`ilInitialisation` which belongs to the Init component. There are currently two main practises being used to initialise +a component as a whole: + +- The entire initialisation code is implemented inside the `ilInitialisation` class directly and is invoked at the right +time given the right circumstances (context and availability of dependencies). +- The initialisation code is implemented in a dedicated class, which receives the current dependency injection container +as an argument. The classes then initialise and expose stuff by defining it as an offset inside the container. + +Both practices end up in a similar result, where facilities which are required/used by other components are exposed +inside an instance of the `ILIAS\DI\Container` which is globally available. **We frequently observe that internal +facilities are also exposed inside the container, even though other components should not use it directly.** This has +already led to many locations where some specific offset of the container is accessed directly, without the +`ILIAS\DI\Container` offering a concrete method to fetch it (or its entry point). This is not properly encapsulated and +makes other components rely on internal implementation details, which may not be known or considered during a possible +refactoring. This is another thing which is best improved during the migration of a component. + +```php +public function init(\ILIAS\DI\Container $dic): void +{ + $dic['public.thingy'] = static fn () => new PublicThingy( + $dic['internal.thingy'], + ); + // this is still public, any component can access this offset: + $dic['internal.thingy'] = static fn () => new InternalThingy( + $dic->user() + ); +} +``` + +The way the initialisation is currently orchestrated, mainly because often times it is unclear what dependency is +available at what time, we have embraced a pattern which allows the lazy-loading of objects, where instances are not +created immediately, but are wrapped inside an anonymous function / arrow function / first class function and stored in +the `ILIAS\DI\Container` instead. This way, if an actual instance of a service is requested by accessing the offset, it +triggers sort of a chain reaction where the functions are invoked recursively because dependencies are also fetched from +the same container. This pattern is still used inside the new bootstrap mechanism for the same reason, while determining +the appropriate order of dependencies during build-time (amongst some other things). + +At this point you should have a vague (or clear if we did a good job here) picture about what patterns have been used, +which mostly avoid proper DI and expose more functionality than probably necessary during the initialisation of +components. You should also know where to find most of this code, so we have established your starting point. + +### New component wiring and proper encapsulation + +The component bootstrap mechanism offers a total of +[four different ways to wire components](../../../../docs/development/components-and-directories.md#types-of-dependencies-and-integration-strategies) +with one another. Please take a look at the linked document before starting your work on the migration of a component. +In the previous chapter we have explained how some components expose more functionality to the rest of the system than +probably necessary. These new initialisation and integration strategies offer a great way to properly encapsulate your +component from the rest of the system and define precise ways of interactions. + +```php +class SomeComponent implements Component +{ + // types of dependencies and integration strategies: + public function init( + array | \ArrayAccess &$define, + array | \ArrayAccess &$implement, + array | \ArrayAccess &$use, + array | \ArrayAccess &$contribute, + array | \ArrayAccess &$seek, + array | \ArrayAccess &$provide, + array | \ArrayAccess &$pull, + array | \ArrayAccess &$internal, + ) : void { + } +} +``` + +There are also a lot of classes where an exact instance of something is exposed globally. This may make sense for some +things, but there are a lot of things which could highly benefit from refactoring into subsystems. For this we recommend +to implement the facade pattern, for which in a first step one must extract an interface from the existing class, so we +can later on swap out its parts without breaking the consumers and migrate this gradually if need be. + +We also do not or rarely use namespaces. This was mostly due to the fact that `ilCtrl` did not support this, this has +been fixed in the meantime though. So, same goes here as well, now is the best time to embrace namespaces as well. + +### What needs to be touched? + +Since there are multiple places and components which need to be touched, especially if your component happens to utilise +a lot of other components which are unmigrated yet, it makes sense to iterate over the most common ones (abstract ones +but also concrete ones) in this chapter. + +- `\ilInitialisation` and optionally your wrapper (like `Init\Dependencies\InitHttpServices`): to take a look at your +initialisation and operate it out of its old place (and improve it!). +- Your component class (`.php`): to implement the initialisation using the new dependency types and +integration strategies. +- Other's component classes: there are two scenarios in which other components need to be touched as well. Its important +we do this, because if we simply move the initialisation code to the new system and solely rely on the compatibility +layer explained in the next chapter, we will break the whole system once we remove the layer when every component is +migrated. Scenarios: + - a) the other component is unmigrated and needs a compatibility layer for the new system + - b) the component being migrated was already touched during the migration of another and it contains a compatibility + layer already. Now you can amend the usage inside the other component and drop the layer if it was the only usage. + - c) there are scalar dependencies, like an object-id (as `int`) passed to the constructor. The new system does not + support such trivial wiring, it needs to be remodeled and put behind an abstraction layer. This layer potentially + needs implementation in another component than yours. The chapter on established patterns will tell you more about it. +- Your component entry points: because there is a new way of doing things, you will need to migrate your endpoints as +well. Since a few components already migrated, its very possible that this is already done. If not, check out the entry +points chapter for guidance. + +### Setting clear boundaries! + +That's not just important in real life, but also for this project and during a migration of an ILIAS component. If you +are migrating a component to the bootstrap mechanism, you will quickly notice how you depend on A which in turn depends +on B which yet again depends on C and D, who both depend on E, F and G – all of which are not migrated. If you would +migrate such a component and migrate all facilities of different components which are needed at some point in your +component, you would probably complete half the component revision for all of us. I mean, feel free to do so, we would +all highly appreciate this ofc, but in reality this will most likely not be possible =). + +For this reason its important to set clear boundaries. What components should be migrated at the same time highly +depends on the concrete scenario. Maybe you have been contracted to migrate all of your components, so the components of +yours depending on one another can probably be migrated in one go. However, lets assume for this chapter you are +migrating exactly one component and one component only. In this scenario, you will most likely run into some kind of +dependency on another part of the system. If this part is not yet migrated, we need to introduce a compatibility layer +so you can continue with your work and make the component compatible with the unmigrated part of the system. + +This is achieved by using the proxy pattern which hides the fact a component may not be migrated yet. It mimics the +behaviour of the facility in question, while it delegates all logic to the unmigrated implementation. This is important +because during the build process of ILIAS we need some bare-minimum implementation of some facility, so it can produce +the bootstrap artifact and dependency graph correctly. How this is achieved is explained in one of the following +chapters. + +### Contributing public assets + +During the first iteration of the component revision we have moved the publicly available assets into a dedicated folder +named `public/`. This is primarily done for security reasons but it also gives us a good grasp over what assets and +access to business logic is actually provided to the client (browser). + +This directory is managed by the Component ILIAS component and is (currently) rebuilt every time the application is +built. To contribute your assets to this directory you need to follow the code snippet below. It utilises the "seek" and +"contribute" integration strategy of the component wiring, where the responsible machinery seeks for implementations of +a given interface by looking for explicit contributions of it. + +```php +class SomeComponent implements Component +{ + public function init( + // ... + array | \ArrayAccess &$contribute, + // ... + ) : void { + // contribute a different public asset like so: + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentJS($this, "js/SomeFunctionality.js"); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentCSS($this, "css/some-stylesheet.css"); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\NodeModule("@vendor/library/dist/index.js"); + } +} +``` + +Make sure your assets are in the right place; the machinery will look in `/resources/` for it. Inspect the +respective kind of asset implementation for a more detailed location. If you need to provide an endpoint (like +`ilias.php`), check out the corresponding chapter of this guide. + +### Backwards compatibility + +At this point it might have occurred to you, that we live in an entirely new and different world now, and asked +yourself how you make things available for the unmigrated part of the system. If that's not the case, don't worry, we +will explain it to you anyways. This chapter covers how we maintain backwards compatibility during the gradual +migration of ILIAS components during the progression of the "component revision" big project. + +As mentioned above, the bootstrap mechanism is something completely different than how we previously managed things. The +bootstrap mechanism creates an artifact during build-time, which contains a compiled script where all dependencies are +initialised in the appropriate order and fashion, while the types of dependencies and different integration strategies +are respected. This means, after migrating a component and removing its initialisation from `ilInitialisation`, calling +`ilInitialisation::initILIAS()` is no longer an option. + +Since we do not want to maintain a duplicate initialisation for the same component, only to provide it for migrated and +unmigrated components at the same time, we have introduced a legacy initialisation bridge. This is a structured way to +expose migrated components and its facilities inside the legacy environment. While this adds some overhead, it is the +appropriate tool for the job and exposes the true ugliness of the service locator in the first place. Here's what you +do: + +```php +// step 1: migrate your component and implement its initialisation: +class SomeComponent implements Component +{ + public function init( + array | \ArrayAccess &$define, + array | \ArrayAccess &$implement, + // ... + ) : void { + // stumble over some legacy thing which is required by unmigrated components: + $define[] = LegacyThingyInterface::class; + $implement[LegacyThingyInterface::class] = static fn() => new SomeLegacyThingy(); + } +} + +// step 2: find the \ILIAS\Init\AllModernComponents class and extend it: +class AllModernComponents implements EntryPoint +{ + // use DI to retrieve your implementation inside the bridge: + public function __construct( + protected LegacyThingyInterface $legacy_thingy, + ) { + } + + // the method you are looking for in order to 'bridge' them: + protected function populateComponentsInLegacyEnvironment(\Pimple\Container $DIC): void + { + // expose your implementation using its legacy offset: + $DIC['public.legacy_offset'] = fn() => $this->legacy_thingy; + } +} + +// step 3: create the wiring between the legacy initialisation and your component: +class Init implements Component +{ + public function init( + // ... + array | \ArrayAccess &$use, + array | \ArrayAccess &$contribute, + // ... + ) : void { + $contribute[Component\EntryPoint::class] = static fn() => + new AllModernComponents( + // inject your implementation into the bridge using DI: + $use[LegacyThingyInterface::class], + ); + } +} +``` + +Thats it. Obviously the list of components and the list of properties of the bridge above is already quite long and will +become very long in the end. But this actually shows us the unfiltered and complete list of dependencies which are used +throughout the system. + +### Combining old with new in endpoints + +Now for those of you who provide endpoints to the system, like the famous `ilias.php`, there is a new way to initialise +ILIAS and make your endpoint viable. + +There is a new interface called `\ILIAS\Component\EntryPoint`, which is used to define entry-points of the system. This +is important, because your endpoint should most likely interact with these things. While an endpoint is contributed to +the system using an `\ILIAS\Component\Resource\Endpoint` (public asset) class, your implementation is actually decoupled from all +this. It will be a 'standalone' PHP script, which needs to take care of the proper initialisation (if this is what you +need). We cannot use some fancy mechanic here, because the script is the starting point of the whole system. We cannot +inject stuff because its a simple PHP script which is responsible to kick-off the whole thing. Thats why in this chapter +we show you how to implement and contribute your endpoint and initialise ILIAS properly so the old unmigrated components +work amongst the new migrated components all the same. + +```php +class SomeComponent implements Component +{ + public function init( + // ... + array | \ArrayAccess &$contribute, + // ... + ) : void { + // contribute your endpoint to the system: + $contribute[Component\Resource\PublicAsset::class] = static fn() => + new Component\Resource\Endpoint($this, "endpoint.php"); + } +} +``` + +Make sure your endpoint is in the right place; ILIAS will look inside `/resources/` for it. Inside your +actual endpoint (the PHP script), you must implement the following lines if you want to initialise ILIAS: + +```php +// load composer's autoloader: +require_once __DIR__ . '/../vendor/composer/vendor/autoload.php'; +// include the artifact produced by the component bootstrap mechanism: +require_once __DIR__ . '/../artifacts/bootstrap_default.php'; +// enter the \ILIAS\Init\AllModernComponents entry point like so: +entry_point('ILIAS Legacy Initialisation Adapter'); + +// after this, our legacy service-locator should be ready: +/** @var $DIC \ILIAS\DI\Container */ +global $DIC; +// $DIC->ctrl()->callBaseClass(); +``` + +For someone who is interested in implementing their own entry-point, which e.g. does not fully initialise ILIAS or +performs entirely different actions, you can update the code above in a way, where `entry_point(...)` is provided with +the name returned by `ILIAS\Component\EntryPoint::getName()`. The function itself is loaded by the bootstrap artifact +and provides the way of entering one of the existing entry points of the system. + +### Newly established patterns + +The following patterns have emerged during the first migrations. The ones that need more than a sentence are described +in a dedicated sub-chapter below: + +- Backwards compatibility layer, explained by the previous chapter. +- Proxy pattern for unmigrated components, compatibility layer between migrated/unmigrated. +- Scalar dependencies put behind configuration interfaces, defined and used by the requiring system, implemented by the +providing system. +- Split read and write access to functionality on a programming level already (interfaces). + +The following are not patterns in the strict sense, but conventions you should follow nonetheless: + +- `static fn` vs `fn`: think about when to use which one, the smaller scope improves performance. +- Avoid anonymous classes. Introduce dedicated classes, even for the most trivial interface implementations, and don't +be lazy about it. There is currently one known exception where this is hard to avoid, see the caveat on the dependence +on artifacts below. +- Avoid constants. They are global state and we really don't want global state; use the configuration interfaces +described below instead. Be aware that this is the target state and not something every migration can fully achieve +today: a number of constants defined by `ilInitialisation` (`ILIAS_ABSOLUTE_PATH`, `ILIAS_DATA_DIR`, `CLIENT_ID`, …) +are currently still the only way to obtain these values during early bootstrap, before the services that would replace +them are wired. Do not introduce new ones, and replace the existing ones wherever the wiring already allows it. + +#### Proxy pattern for a compatibility layer + +Assume the following scenario for the code example below: + +- Component A is being migrated +- Component A has dependency on Component B (the B class) +- Component B is not migrated + +The procedure we currently established is the following: + +- Because we do not want to or have the capacity or authority to migrate the other component (B) we depend on, we set a +clear boundary here. We achieve this by looking into what is actually used by our component (A) and extract a new +interface from this (if there isn't one we should use instead). +- Assume the B class is not namespaced and abstracted in any way yet and lives within the `classes/` folder, we will +want to extract an interface, ideally with the required functionality only, from this class and implement it inside the +`src/` directory. The B class then implements this new interface, which allows us to create the proxy now. If there +already is a usable interface we can use, this step can be skipped ofc. +- The proxy is a bare-minimum implementation of this interface, which delegates all method calls directly to the actual +B class, which is retrieved by `global $DIC`. This works because the proxy does not require a constructor, which +makes it compatible with the build and bootstrap process without `$DIC`, while it is still functional in the web context +because its legacy implementation will be invoked at a later point which initialises the B class in this container. + + +```php +// ComponentB/classes/B.php: +class B +{ + public function functionality(): void + { + // ... + } +} + +// ComponentB/src/BInterface.php: +interface BInterface +{ + public function functionality(): void; +} + +// ComponentB/src/BLegacyProxy.php: +final class BLegacyProxy implements BInterface +{ + public function functionality(): void + { + // delegate actual call to the appropriate B class + global $DIC; + $DIC->B()->functionality(); + } +} + +// ComponentB/ComponentB.php +class ComponentB implements Component +{ + public function init( + array | \ArrayAccess &$define, + array | \ArrayAccess &$implement, + // ... + ) : void { + // define the abstraction, make it known to the system: + $define[] = BInterface::class; + // implement the abstraction using the legacy proxy, so other components can use it: + $implement[BInterface::class] = static fn() => new BLegacyProxy(); + } +} +``` + +```php +// ComponentA/ComponentA.php +class ComponentA implements Component +{ + public function init( + // ... + array | \ArrayAccess &$use, + // ... + array | \ArrayAccess &$internal, + ) : void { + // use the legacy proxy implementation here: + $internal[A::class] = static fn() => new A($use[BInterface::class]); + } +} +``` + +#### Abstraction of scalar data-types + +In some cases there will be dependencies to scalar data-types. For these cases we need an established pattern, which +will be covered in this chapter, because the new component bootstrap mechanism prefers to work with classes rather than +arbitrary offsets which hold an anonymous function returning some hardcoded value. + +While the primary target of this abstraction may be for scalar data-types, it can also +be applied to other scenarios, where a more complex object is passed along. When and when not to use this pattern may +differ across contexts, but as a rule of thumb this pattern should not be used for more complex things than data- +transfer-objects (DTO). There could be exceptions to this where some sort of builder pattern is applied to create a +service, but this should be very limited; we ought follow correctness on construction to the best of our ability. + +We currently address this problem by looking at it from a configuration perspective. In most cases, such scalar or +trivial object data-types are used during initialisation for configuring the behaviour of some implementation. A good +example for this are INI-values, which are set by system administrators. But these can also be currently hard-coded +values like the `UI\Component\Progress\AsyncRefreshInterval`, where this is still a configuration for which we currently +do not provide any interaction. + +This is a good time to think about coupling, especially because there is one caveat which needs to be considered here: +**Configuration values MUST NOT be accessed inside the constructor, ever**. The bootstrap mechanism will not be able to +build its artifact if your object depends on the implementation of something else, because the wiring does not exist at +this point. Therefore, we recommend to introduce dedicated configuration interfaces that will return exactly what is +needed by an appropriate getter – ideally without any arguments. Why is this actually a blessing in disguise? So glad +you ask, because this pattern will ultimately loosen the coupling between your component and another, by hiding the +mechanism which is ultimately used to retrieve the desired value. This will make it very easy to switch mechanism, +location or even underlying business logic in order to retrieve this value, without ever having to touch your component +again. This refactoring could even be tackled by someone else entirely, because you have just decoupled your component +so kindly. + +Let's say you have something like this inside your legacy initialisation: + +```php +// SomeComponent/src/SomeService.php: +class SomeService +{ + public function __construct( + protected readonly int $some_config_value, + ) { + } + + public function functionality(): void + { + $this->some_config_value; //... + } +} + +$some_config_value = 0; // possibly retrieved by $DIC as well though +$DIC['some_component.some_service'] = static fn($DIC) => new SomeService($some_config_value); +``` + +Then you would refactor it according to the pattern to the following structure: + +```php +// SomeComponent/src/SomeService.php: +class SomeService +{ + public function __construct( + protected SomeConfigInterface $some_config, + ) { + } + + public function functionality(): void + { + // notice how retrieval is deferred now, do not store in property please! + $this->some_config->getValue(); + } +} + +// SomeComponent/src/SomeConfigInterface.php: +interface SomeConfigInterface +{ + public function getValue(): int; +} + +// SomeComponent/SomeComponent.php: +class SomeComponent implements Component +{ + // types of dependencies and integration strategies: + public function init( + array | \ArrayAccess &$define, + // ... + array | \ArrayAccess &$use, + // ... + array | \ArrayAccess &$internal, + ) : void { + // define a new interface for your configuration and make it known to the system: + $define[] = SomeConfigInterface::class; + // use the implementation made by some other component for your config: + $internal[SomeService::class] = static fn() => new SomeService($use[SomeConfigInterface::class]); + } +} + +// OtherComponent/OtherComponent.php: +class OtherComponent implements Component +{ + // types of dependencies and integration strategies: + public function init( + // ... + array | \ArrayAccess &$implement, + // ... + ) : void { + // implement the defined interface of SomeComponent to provide its value: + $implement[SomeConfigInterface::class] = static fn() => new SomeConfig(); + } +} + +// OtherComponent/src/SomeConfig.php: +class SomeConfig implements SomeConfigInterface +{ + // ... +} +``` + +As you can see we make use of the "define", "implement" and "use" component wiring. We define an interface for the +configuration put over the scalar or trivial object data-types and use it for the initialisation. Then we search for an +appropriate component to implement this defined interface, which may be the same component ofc, and add its +implementation in the right place. + +**In many cases this will be a legacy-proxy as well, described in the previous chapter, who delegates the method call in +the web context to some method of `$DIC`.** + +#### Restricted access on a programming level + +According to the interface segregation principle, an object should only rely on methods it also really needs. In ILIAS +most of the time an entire service is simply injected with all of its functionality for free. This is a bad habit; as +already explained inside the chapter that gives an overview of the current situation, we should be more cautious when +implementing our DI. Doing proper DI can already limit the set of available methods and narrow the used +methods down to the actually used ones quite a bit. However, we noticed that many places could benefit from a +segregation of their methods that mutate stuff and methods that only return some calculated result. + +Doing so will allow us to restrict access to functionality on a programming level already. Assume we have the following +object without any abstraction: + +```php +class GodObjectThatDoesItAll +{ + public function setValueX(mixed $x): void + { + // ... + } + + public function getValueX(): mixed + { + // ... + } + // ... +} +``` + +We could introduce separate interfaces for our getters and setters here, or in a more abstract sense one for the +methods mutating state and another for the ones only reading it. The object could stay the same, we only need to +extract interfaces of the corresponding methods. The migration to the new component bootstrap mechanism is a great time +to think about this as well, especially if we need to introduce a new abstraction layer anyways, i.e. due to the need +of a legacy-proxy. + +```php +interface WriteActions +{ + public function setValueX(mixed $x): void; + // ... +} + +interface ReadActions +{ + public function getValueX(): mixed; + // ... +} + +class GodObjectThatDoesItAll implements WriteActions, ReadActions +{ + // ... +} +``` + +This also pairs well with the configuration pattern we have established as an abstraction layer put over the scalar and +trivial object data-types during initialisation. + +### Known caveats + +This chapter holds a list of known caveats which should ideally be updated anytime some new issue is discovered that +does not only concern one concrete component. These are things like missing concepts, patterns or issues with existing +ones from this document. + +- Migration of controllers: `ilCtrl` is very old-fashioned and is not yet migrated to the bootstrap mechanism itself. +This creates a problem for any component that offers functionality via GUI (the browser) and relies on the `ilCtrl` +for routing – which ofc are all. This means we need to find a viable solution for this fast, otherwise only components +formerly known as services will be able to migrate to this mechanism. This should also be a memento to the fact that we +need a more modern routing, which should be tackled by the progression of the +["static routing" big project](https://docu.ilias.de/go/wiki/wpage_8780_1357), which aims for a solution where routes +and corresponding logic are determined at build-time and stored in some kind of artifact, which should make it possible +to generate proper wiring between components and use proper DI as well. +- Context-specific logic: using the legacy initialisation it was possible to define very granularly in what context what +things should be initialised. With the new bootstrap mechanism this is a tiny bit more cumbersome, because we actually +don't differentiate between these contexts anymore. Everything is determined during build-time and different artifacts +are produced for different contexts instead. What the implications of this are is unknown at the moment. This is +probably something we need to analyse and discuss when first components that heavily rely on this mechanic are migrated. +- Dependence on artifacts: currently one needs to use `BuildArtifactObjective::PATH()` to get the path of an artifact +for its inclusion. We currently lack a facility which properly manages this stuff and can provide artifact paths / data +in a structured manner. This leads to potential anonymous classes or artifacts which cannot yet be properly injected. We +probably need to find a solution for this in early stages of this project too. +- Purged `public` directory: the current machinery which manages what assets will ultimately end up in our isolated web +root directory currently purges all files and moves contributed assets into the directory every time the application is +built. This makes it impractical for system administrators who need/want to provide additional assets to this directory, +like a `robots.txt` or if some other application potentially runs there (in a sub directory for example). We probably +want to create some sort of diff which we then compare in order to update only the changed files and basically not purge +the entire directory on every build. +- Contribute configuration screens: we currently lack a sophisticated concept or integration mechanism for components to +contribute configuration screen. A first step will be to migrate the main menu orchestrated by the GlobalScreen so these +entries can be collected inside the bootstrap mechanism and used by both ILIAS and third-party components. In the long +run we may want to improve this concept so dedicated routes/configurations/storage mechanisms can be contributed. +- Contribute translations: the Language component still relies on translation files inside the `./lang` directory. While +this mechanism still works for ILIAS components, third-party components will run into issues because they can no longer +contribute translations of their own. This will best be tackled by splitting up the translation files to their +respective components, so they can be contributed to the system using the appropriate tooling, which would also work for +third-party components. +- Refactorings initiated by single persons/institutions: there will be many use-cases where one person or institution +tackles a migration of some component or specific mechanism, where it will most likely be expected that usages inside +other components are amended. This will lead to a lot of cases where the best-practices described by this guideline will +become a huge overhead. We need to define how we should treat these cases and how the expected workload should be shared +between initiators and consumers of something. It could be that we have to establish special practises for individual +refactorings, all of which SHOULD be documented here to maintain an overview. + +### The process as a whole + +> **TODO:** this chapter has not been written yet. It is meant to describe how a migration works in terms of process, +> not in terms of actual implementation. We will provide a guide or a separate document about this in a later +> iteration. + +The questions it needs to answer: + +- who acquires funding? +- who is responsible for a concrete migration? +- who is responsible for all migrations (as an overview)? +- who is authoring the migration? +- who is reviewing the result of a migration (QA)? +- who needs to give approval and when? +- how should the result be published? + +## Recommendations + +This chapter contains recommendations which should be considered when migrating a component. These are shaped by +preference, but they still might add something valuable to your component as well. + +### Fully qualified domain names (FQDN) + +The larger a component initialisation becomes, the longer, wider and more cumbersome becomes the list of internal and +external facilities needed. The UI framework is a great example for this. In order for everybody to easily understand +and see what facilities are used in what locations, it is highly recommended to work with FQDNs only and access them +using the `::class` constant. This is great for two reasons: + +- we no longer need a `use` statement and prevent possible naming conflicts that would require aliased imports (`as`) +- we can always see directly from which exact location a facility is used and can tell whether its internal or external. + +### Consistency for discoverability + +Since it will become increasingly important to know what other ILIAS components offer what facilities, and even more +importantly what facilities are only defined but require 'external' implementation, its important to get a quick grasp +over the most important requirements each initialisation has to the system. Thats why it makes sense to streamline this +as good as possible, so this task which is repeated often, is made easy. Thats why it is recommended to follow the order +of parameters as listed by the `Component::init()` method when defining/exposing/setting its facilities into the +appropriate container to achieve its desired goal. + +## I don't understand this and/or need help + +If you feel like you don't fully understand any of the above aspects, or you do and have some constructive criticism +about it, or you simply need some help because there is an edge case or something else we haven't thought about +happening inside your component – contact us. + +Who are we you ask? While the brains behind the conceptual work and the first iteration of the component revision was +Richard Klees (from concepts and training gmbh), Thibeau Fuhrer and Fabian Schmid (from sr solutions ag) have taken over +his work and authorities of the Component ILIAS component, since he has left the ILIAS community. + +Feel free to contact us on Discord, via Email, or simply ping us on GitHub: + +- Thibeau Fuhrer +- Fabian Schmid diff --git a/components/ILIAS/Component/tests/Setup/ilComponentBuildPluginInfoObjectiveTest.php b/components/ILIAS/Component/tests/Setup/ilComponentBuildPluginInfoObjectiveTest.php index 796fc69ea884..1c5d86a2468d 100755 --- a/components/ILIAS/Component/tests/Setup/ilComponentBuildPluginInfoObjectiveTest.php +++ b/components/ILIAS/Component/tests/Setup/ilComponentBuildPluginInfoObjectiveTest.php @@ -139,14 +139,14 @@ public function testPluginsAdded(): void public function testScanDir(): void { // Use the component directory without artifacts, because this should be mostly stable. - $expected = ["Component.php", "PRIVACY.md", "README.md", "ROADMAP.md", "classes", "exceptions", "maintenance.json", "service.xml", "src", "tests"]; + $expected = ["Component.php", "PRIVACY.md", "README.md", "ROADMAP.md", "classes", "exceptions", "maintenance.json", "service.xml", "src", "tests", "docs"]; $actual = array_values( array_diff( $this->builder->_scanDir(__DIR__ . "/../.."), ["artifacts", ".DS_Store"] // .DS_Store is a macOS artifact which is not relevant for the test. ) ); - $this->assertEquals($expected, $actual); + $this->assertEqualsCanonicalizing($expected, $actual); } public function testIsDir(): void diff --git a/components/ILIAS/Init/classes/class.ilInitialisation.php b/components/ILIAS/Init/classes/class.ilInitialisation.php index 33810d466bb9..14302b56b811 100755 --- a/components/ILIAS/Init/classes/class.ilInitialisation.php +++ b/components/ILIAS/Init/classes/class.ilInitialisation.php @@ -36,6 +36,7 @@ use ILIAS\ILIASObject\Properties\AdditionalProperties\Icon\Factory as CustomIconFactory; use ILIAS\User\PublicInterface as UserPublicInterface; use ILIAS\Mail\Service\MailService; +use ILIAS\Init\AllModernComponents; // needed for slow queries, etc. if (!isset($GLOBALS['ilGlobalStartTime']) || !$GLOBALS['ilGlobalStartTime']) { @@ -1146,7 +1147,9 @@ public static function reInitUser(): void } /** - * ilias initialisation + * @deprecated since ILIAS 12; please use the {@see AllModernComponents} entry point instead. + * See `components/ILIAS/Component/docs/component-bootstrap-migration.md` for a + * more detailed description. */ public static function initILIAS(): void { From 0bf78baeedd65d1a3834fccca9fe24a7c521aa41 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Tue, 4 Aug 2026 15:00:21 +0200 Subject: [PATCH 169/333] 47920: Error Opening and ending tag mismatch when editing text blocks containing internal links to missing/deleted objects --- components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php b/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php index 0bdc6aa230c5..1a2031e09a10 100755 --- a/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php +++ b/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php @@ -1076,6 +1076,9 @@ public static function xml2output( $rtype = ($target[count($target) - 2] ?? ""); $target_type = $rtype; } + if ($target_type === "") { + $target_type = "obj"; + } $a_text = preg_replace('~~i', "[iln " . $inst_str . "$target_type=\"" . $target_id . "\"" . $tframestr . "]", $a_text); break; From f19fb025ffeadd49d419509656e1f7ebe985b62c Mon Sep 17 00:00:00 2001 From: Matheus Zych <37047867+matheuszych@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:51:56 +0200 Subject: [PATCH 170/333] [FIX] #47810 UI: update `Table\Ordering` async modal (#11577) * Fix https://mantis.ilias.de/view.php?id=47810 * Replace bootstrap modal with native `` element, ensure compatibility with `il.UI.modal.showModal()` --- .../UI/src/templates/default/Table/tpl.orderingtable.html | 6 +++--- .../UI/tests/Component/Table/OrderingRendererTest.php | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html b/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html index 568f0504355d..f79fdfcd63ba 100644 --- a/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html +++ b/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html @@ -76,16 +76,16 @@

    {TITLE}

    - +
    EOT; $this->assertEquals($this->brutallyTrimHTML($expected), $this->brutallyTrimHTML($actual)); From 3d7068b1ae91482d41150f71128279fc1b298d63 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Tue, 4 Aug 2026 17:45:37 +0200 Subject: [PATCH 171/333] 48126: Page editor whoopses on trying to save paragraph with just an empty list bullet --- .../ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php b/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php index 1a2031e09a10..d97d0541b79b 100755 --- a/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php +++ b/components/ILIAS/COPage/PC/Paragraph/class.ilPCParagraph.php @@ -1570,6 +1570,12 @@ public static function handleAjaxContent( */ public static function handleAjaxContentPost(string $text): string { + // #48126 + $text = str_replace( + ["<li/>"], + ["<li> </li>"], + $text + ); $text = str_replace( array("<ul>", "</ul>"), array("", ""), From a611d6c408c1f944cfec08d7eac5530f3e742817 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Tue, 4 Aug 2026 18:47:20 +0200 Subject: [PATCH 172/333] =?UTF-8?q?learning=20module:=2048061:=20Ausgeschn?= =?UTF-8?q?ittene=20Kapitel=20und=20Seiten=20lassen=20sich=20nicht=20in=20?= =?UTF-8?q?ein=20anderes=20Lernmodul=20einf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ILIAS/LearningModule/classes/class.ilLMObject.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/ILIAS/LearningModule/classes/class.ilLMObject.php b/components/ILIAS/LearningModule/classes/class.ilLMObject.php index 0ee5f0abad44..7aa9c4088373 100755 --- a/components/ILIAS/LearningModule/classes/class.ilLMObject.php +++ b/components/ILIAS/LearningModule/classes/class.ilLMObject.php @@ -767,6 +767,11 @@ public static function pasteTree( if ($item_lm_id != $a_target_lm->getId() && !$a_as_copy) { // @todo: check whether st is NOT in tree + // update lm object + $item->setLMId($a_target_lm->getId()); + $item->setContentObject($a_target_lm); + $item->update(); + // "move" metadata to new lm $lom_services->derive() ->fromObject($item_lm_id, $item->getId(), $item->getType()) From bdf13561cc07c77b0c7aa4959668945a5ca2a8d8 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer <47783030+lukas-heinrich@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:03:27 +0200 Subject: [PATCH 173/333] [FIX] #45665 UI: `Table\Ordering` submission on enter (#10832) * Fix https://mantis.ilias.de/view.php?id=45665 * Add event listener to numeric inputs of `Table\Ordering` that prevent form submission on 'Enter'. --- .../ILIAS/UI/resources/js/Table/dist/table.min.js | 2 +- .../UI/resources/js/Table/src/orderingtable.class.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/UI/resources/js/Table/dist/table.min.js b/components/ILIAS/UI/resources/js/Table/dist/table.min.js index 823b92627136..a1d8ba5f94ca 100644 --- a/components/ILIAS/UI/resources/js/Table/dist/table.min.js +++ b/components/ILIAS/UI/resources/js/Table/dist/table.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(e,t){"use strict";class s{#e;#t;#s;#i;#a;#o;#n;#r;constructor(e,t,s,i){if(this.#s=document.getElementById(i),null===this.#s)throw new Error(`Could not find a DataTable for id '${i}'.`);if(this.#n=this.#s.getElementsByTagName("table").item(0),null===this.#n)throw new Error("There is no in the component's HTML.");this.#i=this.#s.getElementsByClassName("c-table-data__async_modal_container").item(0),this.#a=this.#s.getElementsByClassName("c-table-data__async_message").item(0),this.#o=this.#a.getElementsByClassName("c-table-data__async_messageresponse").item(0),this.#e=e,this.#t={actionId:t,rowId:s},this.#r={},this.#s.addEventListener("keydown",(e=>this.navigateCellsWithArrowKeys(e)));const a=this.#n.getElementsByClassName("c-table-data__row-selector");for(let e=0;ethis.selectionChange()))}}registerAction(e,t,s,i){this.#r[e]={async:t,urlBuilder:s,urlTokens:i}}selectionChange(){this.#l(!this.#c())}#c(){const{urlBuilder:e}=this.#r[Object.keys(this.#r).at(0)],{urlTokens:t}=this.#r[Object.keys(this.#r).at(0)],s=t.values().next().value,i=this.collectSelectedRowIds();i.push(i[0]),e.writeParameter(s,i);try{e.getUrl().toString()}catch(e){return!1}return!0}#h(){const e=this.#s.querySelector("dialog.c-table-data__multiaction-warning");il.UI.modal.showModal(e,{},{id:e.id})}#l(e){this.#n.getElementsByClassName("c-table-data__row-selector").forEach((t=>{t.disabled=!0===e&&!t.checked}))}selectAll(e){const t=this.#n.getElementsByClassName("c-table-data__row-selector"),s=this.#n.getElementsByClassName("c-table-data__selection_all").item(0),i=this.#n.getElementsByClassName("c-table-data__selection_none").item(0);for(let s=0;s{e.checked&&t.push(e.value)})),t}doMultiAction(e){this.doAction(e,this.collectSelectedRowIds())}doSingleAction(e){const t=e.options[this.#t.rowId];this.doAction(e,[t])}doActionForAll(e){const t=e.parentNode.parentNode.parentNode,s=t.getElementsByClassName("close").item(0),i=t.getElementsByClassName("modal-body")[0].getElementsByTagName("select")[0].value;if(i in this.#r){const e=this.#t.actionId,t={options:{}};t.options[e]=i,s.click(),this.doAction(t,["ALL_OBJECTS"])}}doAction(e,t){const s=e.options[this.#t.actionId],i=this.#r[s],a=i.urlTokens.values().next().value;i.urlBuilder.writeParameter(a,t);const o=decodeURI(i.urlBuilder.getUrl().toString());i.async?this.asyncAction(o):window.location.href=o}asyncAction(e){this.#e.ajax({url:e,dataType:"html"}).done((e=>{if("SCRIPT"===this.#e(e).first().prop("tagName"))this.#e.globalEval(this.#e(e).first().text());else{let t;this.#e(e).first().hasClass("c-modal")?(this.#i.innerHTML=e,t=this.#i.firstChild):(this.#o.innerHTML=e,t=this.#a);this.#e(`
    ${e}
    `).find("[data-replace-marker='script']").each(((e,t)=>this.#e.globalEval(t.innerHTML))),il.UI.modal.showModal(t,{},{id:t.id})}}))}navigateCellsWithArrowKeys(e){if(37!==e.which&&38!==e.which&&39!==e.which&&40!==e.which)return;const t=e.target.closest("td, th"),s=t.closest("tr");let{cellIndex:i}=t,{rowIndex:a}=s;switch(e.which){case 37:i-=1;break;case 39:i+=1;break;case 38:a-=1;break;case 40:a+=1}a<0||i<0||a>=this.#n.rows.length||i>=s.cells.length||this.focusCell(t,a,i)}focusCell(e,t,s){const i=this.#n.rows[t].cells[s];i.focus(),e.setAttribute("tabindex",-1),i.setAttribute("tabindex",0)}}class i{#s;constructor(e){if(this.#s=document.getElementById(e),null===this.#s)throw new Error(`Could not find a PresentationTable for id '${e}'.`)}expandRow(e){const t=this.#s.querySelector(`#${e}`);t.classList.remove("collapsed"),t.classList.add("expanded")}collapseRow(e){const t=this.#s.querySelector(`#${e}`);t.classList.remove("expanded"),t.classList.add("collapsed")}toggleRow(e){this.#s.querySelector(`#${e}`).classList.contains("expanded")?this.collapseRow(e):this.expandRow(e)}expandAll(e){const t=this.#s.querySelectorAll(".il-table-presentation-row");e.options.expand?t.forEach((e=>this.expandRow(e.id))):t.forEach((e=>this.collapseRow(e.id)))}}class a{#s;#n;#d;#g;#w;#m;constructor(e){if(this.#s=document.getElementById(e),null===this.#s)throw new Error(`Could not find a OrderingTable for id '${e}'.`);if(this.#n=this.#s.getElementsByTagName("table").item(0),null===this.#n)throw new Error("There is no
    in the component's HTML.");this.#p(),this.#d.forEach((e=>this.#u(e)))}#p(){this.#d=Array.from(this.#n.rows),this.#d.shift(),this.#d.pop()}#u(e){e.addEventListener("dragstart",(e=>this.dragstart(e))),e.addEventListener("dragover",(e=>this.dragover(e))),e.addEventListener("dragend",(e=>this.dragend(e))),e.addEventListener("touchstart",(e=>this.touchstart(e))),e.addEventListener("touchmove",(e=>this.touchmove(e))),e.addEventListener("touchend",(e=>this.touchend(e))),e.addEventListener("touchcancel",(e=>this.touchend(e)))}dragstart(e){this.#s.classList.add("dragInProgress"),this.#g=e.target.closest("tr"),e.dataTransfer.clearData(),e.dataTransfer.setData("text/html",this.#g.outerHTML),e.dataTransfer.setData("text/plain",this.#g.textContent.replace(/[\n\r]+|[\s]{2,}/g," ").trim()),this.#w=this.#g.cloneNode(!0),this.#w.classList.add("c-table-data__row--drag-image"),this.#w.style.top="-9999px",this.#s.appendChild(this.#w),e.dataTransfer.setDragImage(this.#w,0,0),this.#m=this.#g.cloneNode(!0),this.#m.addEventListener("dragover",(e=>this.dragover(e))),this.#m.classList.add("c-table-data__row--placeholder"),Array.from(this.#m.getElementsByTagName("td")).forEach((e=>{e.innerHTML=""})),this.#g.classList.add("c-table-data__row--drag-origin")}dragover(e){if(!this.#b())return;e.preventDefault(),e.dataTransfer.effectAllowed="copyMove";const t=e.target.closest("tr");t&&t!==this.#m&&(this.#d.indexOf(t)>this.#d.indexOf(this.#g)?t.after(this.#m):t.before(this.#m))}dragend(e){e.preventDefault(),this.#w&&this.#s.contains(this.#w)&&(this.#s.removeChild(this.#w),this.#w=null),this.#m.replaceWith(this.#g),this.#g.classList.remove("c-table-data__row--drag-origin"),this.#g.classList.add("c-table-data__row--drag-settle"),this.#g.addEventListener("animationend",(()=>this.#y()),{once:!0}),this.#s.classList.remove("dragInProgress"),this.#p(),this.#R()}#y(){this.#g.classList.remove("c-table-data__row--drag-settle")}#b(){return this.#d.includes(this.#g)}touchstart(e){this.#g=e.target.closest("tr"),this.#m=this.#g.cloneNode(!0),this.#m.classList.add("c-table-data__row--placeholder"),Array.from(this.#m.getElementsByTagName("td")).forEach((e=>{e.innerHTML=""})),this.#w=this.#g.cloneNode(!0),this.#w.classList.add("c-table-data__row--touch-drag-image"),this.#s.appendChild(this.#w),this.#g.classList.add("c-table-data__row--drag-origin")}touchmove(e){e.preventDefault();const t=e.touches[0];this.#w.style.left=`${t.clientX+-50}px`,this.#w.style.top=`${t.clientY}px`;const s=document.elementFromPoint(t.clientX,t.clientY)?.closest("tr");s&&this.#d.includes(s)&&(this.#d.indexOf(s)>this.#d.indexOf(this.#g)?s.after(this.#m):s.before(this.#m)),t.clientY<100?this.#f(-8):t.clientY>window.innerHeight-100?this.#f(8):this.#v()}touchend(){this.#s.removeChild(this.#w),this.#m.replaceWith(this.#g),this.#g.classList.remove("c-table-data__row--drag-origin"),this.#p(),this.#R()}#R(){let e=10;this.#n.querySelectorAll('input[type="number"]').forEach((t=>{t.value=e,e+=10}))}#f(e){this.scrollInterval||(this.scrollInterval=setInterval((()=>{window.scrollBy(0,e)}),16))}#v(){this.scrollInterval&&(clearInterval(this.scrollInterval),this.scrollInterval=null)}}e.UI=e.UI||{},e.UI.table=e.UI.table||{},e.UI.table.data=new class{#e;#E=[];constructor(e){this.#e=e}init(e,t,i){if(void 0!==this.#E[e])throw new Error(`DataTable with id '${e}' has already been initialized.`);this.#E[e]=new s(this.#e,t,i,e)}get(e){return this.#E[e]??null}}(t),e.UI.table.presentation=new class{#E=[];init(e){if(void 0!==this.#E[e])throw new Error(`PresentationTable with input-id '${e}' has already been initialized.`);this.#E[e]=new i(e)}get(e){return this.#E[e]??null}},e.UI.table.ordering=new class{#E=[];init(e){if(void 0!==this.#E[e])throw new Error(`OrderingTable with id '${e}' has already been initialized.`);this.#E[e]=new a(e)}get(e){return this.#E[e]??null}}}(il,$); +!function(e,t){"use strict";class s{#e;#t;#s;#i;#a;#o;#n;#r;constructor(e,t,s,i){if(this.#s=document.getElementById(i),null===this.#s)throw new Error(`Could not find a DataTable for id '${i}'.`);if(this.#n=this.#s.getElementsByTagName("table").item(0),null===this.#n)throw new Error("There is no
    in the component's HTML.");this.#i=this.#s.getElementsByClassName("c-table-data__async_modal_container").item(0),this.#a=this.#s.getElementsByClassName("c-table-data__async_message").item(0),this.#o=this.#a.getElementsByClassName("c-table-data__async_messageresponse").item(0),this.#e=e,this.#t={actionId:t,rowId:s},this.#r={},this.#s.addEventListener("keydown",(e=>this.navigateCellsWithArrowKeys(e)));const a=this.#n.getElementsByClassName("c-table-data__row-selector");for(let e=0;ethis.selectionChange()))}}registerAction(e,t,s,i){this.#r[e]={async:t,urlBuilder:s,urlTokens:i}}selectionChange(){this.#l(!this.#c())}#c(){const{urlBuilder:e}=this.#r[Object.keys(this.#r).at(0)],{urlTokens:t}=this.#r[Object.keys(this.#r).at(0)],s=t.values().next().value,i=this.collectSelectedRowIds();i.push(i[0]),e.writeParameter(s,i);try{e.getUrl().toString()}catch(e){return!1}return!0}#d(){const e=this.#s.querySelector("dialog.c-table-data__multiaction-warning");il.UI.modal.showModal(e,{},{id:e.id})}#l(e){this.#n.getElementsByClassName("c-table-data__row-selector").forEach((t=>{t.disabled=!0===e&&!t.checked}))}selectAll(e){const t=this.#n.getElementsByClassName("c-table-data__row-selector"),s=this.#n.getElementsByClassName("c-table-data__selection_all").item(0),i=this.#n.getElementsByClassName("c-table-data__selection_none").item(0);for(let s=0;s{e.checked&&t.push(e.value)})),t}doMultiAction(e){this.doAction(e,this.collectSelectedRowIds())}doSingleAction(e){const t=e.options[this.#t.rowId];this.doAction(e,[t])}doActionForAll(e){const t=e.parentNode.parentNode.parentNode,s=t.getElementsByClassName("close").item(0),i=t.getElementsByClassName("modal-body")[0].getElementsByTagName("select")[0].value;if(i in this.#r){const e=this.#t.actionId,t={options:{}};t.options[e]=i,s.click(),this.doAction(t,["ALL_OBJECTS"])}}doAction(e,t){const s=e.options[this.#t.actionId],i=this.#r[s],a=i.urlTokens.values().next().value;i.urlBuilder.writeParameter(a,t);const o=decodeURI(i.urlBuilder.getUrl().toString());i.async?this.asyncAction(o):window.location.href=o}asyncAction(e){this.#e.ajax({url:e,dataType:"html"}).done((e=>{if("SCRIPT"===this.#e(e).first().prop("tagName"))this.#e.globalEval(this.#e(e).first().text());else{let t;this.#e(e).first().hasClass("c-modal")?(this.#i.innerHTML=e,t=this.#i.firstChild):(this.#o.innerHTML=e,t=this.#a);this.#e(`
    ${e}
    `).find("[data-replace-marker='script']").each(((e,t)=>this.#e.globalEval(t.innerHTML))),il.UI.modal.showModal(t,{},{id:t.id})}}))}navigateCellsWithArrowKeys(e){if(37!==e.which&&38!==e.which&&39!==e.which&&40!==e.which)return;const t=e.target.closest("td, th"),s=t.closest("tr");let{cellIndex:i}=t,{rowIndex:a}=s;switch(e.which){case 37:i-=1;break;case 39:i+=1;break;case 38:a-=1;break;case 40:a+=1}a<0||i<0||a>=this.#n.rows.length||i>=s.cells.length||this.focusCell(t,a,i)}focusCell(e,t,s){const i=this.#n.rows[t].cells[s];i.focus(),e.setAttribute("tabindex",-1),i.setAttribute("tabindex",0)}}class i{#s;constructor(e){if(this.#s=document.getElementById(e),null===this.#s)throw new Error(`Could not find a PresentationTable for id '${e}'.`)}expandRow(e){const t=this.#s.querySelector(`#${e}`);t.classList.remove("collapsed"),t.classList.add("expanded")}collapseRow(e){const t=this.#s.querySelector(`#${e}`);t.classList.remove("expanded"),t.classList.add("collapsed")}toggleRow(e){this.#s.querySelector(`#${e}`).classList.contains("expanded")?this.collapseRow(e):this.expandRow(e)}expandAll(e){const t=this.#s.querySelectorAll(".il-table-presentation-row");e.options.expand?t.forEach((e=>this.expandRow(e.id))):t.forEach((e=>this.collapseRow(e.id)))}}class a{#s;#n;#h;#g;#w;#m;constructor(e){if(this.#s=document.getElementById(e),null===this.#s)throw new Error(`Could not find a OrderingTable for id '${e}'.`);if(this.#n=this.#s.getElementsByTagName("table").item(0),null===this.#n)throw new Error("There is no
    in the component's HTML.");this.#p(),this.#h.forEach((e=>this.#u(e))),this.#y()}#y(){this.#n.querySelectorAll('input[type="number"]').forEach((e=>{e.addEventListener("keydown",(e=>{"Enter"===e.key&&e.preventDefault()}))}))}#p(){this.#h=Array.from(this.#n.rows),this.#h.shift(),this.#h.pop()}#u(e){e.addEventListener("dragstart",(e=>this.dragstart(e))),e.addEventListener("dragover",(e=>this.dragover(e))),e.addEventListener("dragend",(e=>this.dragend(e))),e.addEventListener("touchstart",(e=>this.touchstart(e))),e.addEventListener("touchmove",(e=>this.touchmove(e))),e.addEventListener("touchend",(e=>this.touchend(e))),e.addEventListener("touchcancel",(e=>this.touchend(e)))}dragstart(e){this.#s.classList.add("dragInProgress"),this.#g=e.target.closest("tr"),e.dataTransfer.clearData(),e.dataTransfer.setData("text/html",this.#g.outerHTML),e.dataTransfer.setData("text/plain",this.#g.textContent.replace(/[\n\r]+|[\s]{2,}/g," ").trim()),this.#w=this.#g.cloneNode(!0),this.#w.classList.add("c-table-data__row--drag-image"),this.#w.style.top="-9999px",this.#s.appendChild(this.#w),e.dataTransfer.setDragImage(this.#w,0,0),this.#m=this.#g.cloneNode(!0),this.#m.addEventListener("dragover",(e=>this.dragover(e))),this.#m.classList.add("c-table-data__row--placeholder"),Array.from(this.#m.getElementsByTagName("td")).forEach((e=>{e.innerHTML=""})),this.#g.classList.add("c-table-data__row--drag-origin")}dragover(e){if(!this.#b())return;e.preventDefault(),e.dataTransfer.effectAllowed="copyMove";const t=e.target.closest("tr");t&&t!==this.#m&&(this.#h.indexOf(t)>this.#h.indexOf(this.#g)?t.after(this.#m):t.before(this.#m))}dragend(e){e.preventDefault(),this.#w&&this.#s.contains(this.#w)&&(this.#s.removeChild(this.#w),this.#w=null),this.#m.replaceWith(this.#g),this.#g.classList.remove("c-table-data__row--drag-origin"),this.#g.classList.add("c-table-data__row--drag-settle"),this.#g.addEventListener("animationend",(()=>this.#R()),{once:!0}),this.#s.classList.remove("dragInProgress"),this.#p(),this.#f()}#R(){this.#g.classList.remove("c-table-data__row--drag-settle")}#b(){return this.#h.includes(this.#g)}touchstart(e){this.#g=e.target.closest("tr"),this.#m=this.#g.cloneNode(!0),this.#m.classList.add("c-table-data__row--placeholder"),Array.from(this.#m.getElementsByTagName("td")).forEach((e=>{e.innerHTML=""})),this.#w=this.#g.cloneNode(!0),this.#w.classList.add("c-table-data__row--touch-drag-image"),this.#s.appendChild(this.#w),this.#g.classList.add("c-table-data__row--drag-origin")}touchmove(e){e.preventDefault();const t=e.touches[0];this.#w.style.left=`${t.clientX+-50}px`,this.#w.style.top=`${t.clientY}px`;const s=document.elementFromPoint(t.clientX,t.clientY)?.closest("tr");s&&this.#h.includes(s)&&(this.#h.indexOf(s)>this.#h.indexOf(this.#g)?s.after(this.#m):s.before(this.#m)),t.clientY<100?this.#v(-8):t.clientY>window.innerHeight-100?this.#v(8):this.#E()}touchend(){this.#s.removeChild(this.#w),this.#m.replaceWith(this.#g),this.#g.classList.remove("c-table-data__row--drag-origin"),this.#p(),this.#f()}#f(){let e=10;this.#n.querySelectorAll('input[type="number"]').forEach((t=>{t.value=e,e+=10}))}#v(e){this.scrollInterval||(this.scrollInterval=setInterval((()=>{window.scrollBy(0,e)}),16))}#E(){this.scrollInterval&&(clearInterval(this.scrollInterval),this.scrollInterval=null)}}e.UI=e.UI||{},e.UI.table=e.UI.table||{},e.UI.table.data=new class{#e;#I=[];constructor(e){this.#e=e}init(e,t,i){if(void 0!==this.#I[e])throw new Error(`DataTable with id '${e}' has already been initialized.`);this.#I[e]=new s(this.#e,t,i,e)}get(e){return this.#I[e]??null}}(t),e.UI.table.presentation=new class{#I=[];init(e){if(void 0!==this.#I[e])throw new Error(`PresentationTable with input-id '${e}' has already been initialized.`);this.#I[e]=new i(e)}get(e){return this.#I[e]??null}},e.UI.table.ordering=new class{#I=[];init(e){if(void 0!==this.#I[e])throw new Error(`OrderingTable with id '${e}' has already been initialized.`);this.#I[e]=new a(e)}get(e){return this.#I[e]??null}}}(il,$); diff --git a/components/ILIAS/UI/resources/js/Table/src/orderingtable.class.js b/components/ILIAS/UI/resources/js/Table/src/orderingtable.class.js index 61ba0a0a7ce8..846814a0fbff 100644 --- a/components/ILIAS/UI/resources/js/Table/src/orderingtable.class.js +++ b/components/ILIAS/UI/resources/js/Table/src/orderingtable.class.js @@ -70,6 +70,17 @@ export default class OrderingTable { } this.#indexRows(); this.#rows.forEach((row) => this.#addDraglisteners(row)); + this.#initInputHandling(); + } + + #initInputHandling() { + this.#table.querySelectorAll('input[type="number"]').forEach((input) => { + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + } + }); + }); } #indexRows() { From d94726f713a28d7a2c4b18043849d8ee6e122a3f Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 16 Feb 2026 12:17:13 +0100 Subject: [PATCH 174/333] [FEATURE] UI: add new `Symbol\Glyph` variations. * Add "date", "owner", "presenter" and "location" glyphs * Update "calendar" glyph --- .../UI/resources/fonts/Iconfont/il-icons.eot | Bin 62028 -> 63236 bytes .../UI/resources/fonts/Iconfont/il-icons.json | 465 ++++++++++-------- .../UI/resources/fonts/Iconfont/il-icons.svg | 6 +- .../UI/resources/fonts/Iconfont/il-icons.ttf | Bin 61864 -> 63072 bytes .../UI/resources/fonts/Iconfont/il-icons.woff | Bin 61940 -> 63148 bytes .../UI/src/Component/Symbol/Glyph/Factory.php | 290 +++++++++-- .../UI/src/Component/Symbol/Glyph/Glyph.php | 4 + .../Component/Symbol/Glyph/Factory.php | 20 + .../Component/Symbol/Glyph/Glyph.php | 4 + .../src/examples/Symbol/Glyph/Date/date.php | 35 ++ .../Symbol/Glyph/Location/location.php | 35 ++ .../src/examples/Symbol/Glyph/Owner/owner.php | 35 ++ .../Symbol/Glyph/Presenter/presenter.php | 35 ++ .../templates/default/Symbol/tpl.glyph.html | 4 + .../Symbol/Glyph/GlyphFactoryTest.php | 6 +- .../Component/Symbol/Glyph/GlyphTest.php | 8 + lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + .../Symbol/_ui-component_glyph.scss | 21 +- templates/default/delos.css | 29 +- 20 files changed, 733 insertions(+), 266 deletions(-) create mode 100644 components/ILIAS/UI/src/examples/Symbol/Glyph/Date/date.php create mode 100644 components/ILIAS/UI/src/examples/Symbol/Glyph/Location/location.php create mode 100644 components/ILIAS/UI/src/examples/Symbol/Glyph/Owner/owner.php create mode 100644 components/ILIAS/UI/src/examples/Symbol/Glyph/Presenter/presenter.php diff --git a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.eot b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.eot index 5799dffedf804296b905de2de2c1aecb503817d8..532560f32a9a3cb5d943cfa33d5c78240f288235 100644 GIT binary patch delta 2367 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 diff --git a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.json b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.json index 5a149efa4407..83140c647041 100644 --- a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.json +++ b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.json @@ -1,1233 +1,1243 @@ { "selection": [ { - "order": 215, + "order": 222, "ligatures": "", "prevSize": 32, "name": "user-female" }, { - "order": 216, + "order": 223, "ligatures": "", "prevSize": 32, "name": "people" }, { - "order": 217, + "order": 224, "ligatures": "", "prevSize": 32, "name": "user-follow" }, { - "order": 218, + "order": 225, "ligatures": "", "prevSize": 32, "name": "user-following" }, { - "order": 219, + "order": 226, "ligatures": "", "prevSize": 32, "name": "user-unfollow" }, { - "order": 220, + "order": 227, "ligatures": "", "prevSize": 32, "name": "user" }, { - "order": 221, + "order": 228, "ligatures": "", "prevSize": 32, "name": "trophy" }, { - "order": 222, + "order": 229, "ligatures": "", "prevSize": 32, "name": "speedometer" }, { - "order": 223, + "order": 230, "ligatures": "", "prevSize": 32, "name": "social-youtube" }, { - "order": 224, + "order": 231, "ligatures": "", "prevSize": 32, "name": "social-twitter" }, { - "order": 225, + "order": 232, "ligatures": "", "prevSize": 32, "name": "social-tumblr" }, { - "order": 226, + "order": 233, "ligatures": "", "prevSize": 32, "name": "social-facebook" }, { - "order": 227, + "order": 234, "ligatures": "", "prevSize": 32, "name": "social-dropbox" }, { - "order": 228, + "order": 235, "ligatures": "", "prevSize": 32, "name": "social-dribbble" }, { - "order": 229, + "order": 236, "ligatures": "", "prevSize": 32, "name": "shield" }, { - "order": 230, + "order": 237, "ligatures": "", "prevSize": 32, "name": "screen-tablet" }, { - "order": 231, + "order": 238, "ligatures": "", "prevSize": 32, "name": "screen-smartphone" }, { - "order": 232, + "order": 239, "ligatures": "", "prevSize": 32, "name": "screen-desktop" }, { - "order": 233, + "order": 240, "ligatures": "", "prevSize": 32, "name": "plane" }, { - "order": 234, + "order": 241, "ligatures": "", "prevSize": 32, "name": "notebook" }, { - "order": 235, + "order": 242, "ligatures": "", "prevSize": 32, "name": "mustache" }, { - "order": 236, + "order": 243, "ligatures": "", "prevSize": 32, "name": "mouse" }, { - "order": 237, + "order": 244, "ligatures": "", "prevSize": 32, "name": "magnet" }, { - "order": 238, + "order": 245, "ligatures": "", "prevSize": 32, "name": "magic-wand" }, { - "order": 239, + "order": 246, "ligatures": "", "prevSize": 32, "name": "hourglass" }, { - "order": 240, + "order": 247, "ligatures": "", "prevSize": 32, "name": "graduation" }, { - "order": 241, + "order": 248, "ligatures": "", "prevSize": 32, "name": "ghost" }, { - "order": 242, + "order": 249, "ligatures": "", "prevSize": 32, "name": "game-controller" }, { - "order": 243, + "order": 250, "ligatures": "", "prevSize": 32, "name": "fire" }, { - "order": 244, + "order": 251, "ligatures": "", "prevSize": 32, "name": "eyeglass" }, { - "order": 245, + "order": 252, "ligatures": "", "prevSize": 32, "name": "envelope-open" }, { - "order": 246, + "order": 253, "ligatures": "", "prevSize": 32, "name": "envolope-letter" }, { - "order": 247, + "order": 254, "ligatures": "", "prevSize": 32, "name": "energy" }, { - "order": 248, + "order": 255, "ligatures": "", "prevSize": 32, "name": "emotsmile" }, { - "order": 249, + "order": 256, "ligatures": "", "prevSize": 32, "name": "disc" }, { - "order": 250, + "order": 257, "ligatures": "", "prevSize": 32, "name": "cursor-move" }, { - "order": 251, + "order": 258, "ligatures": "", "prevSize": 32, "name": "crop" }, { - "order": 252, + "order": 259, "ligatures": "", "prevSize": 32, "name": "credit-card" }, { - "order": 253, + "order": 260, "ligatures": "", "prevSize": 32, "name": "chemistry" }, { - "order": 254, + "order": 261, "ligatures": "", "prevSize": 32, "name": "bell" }, { - "order": 255, + "order": 262, "ligatures": "", "prevSize": 32, "name": "badge" }, { - "order": 256, + "order": 263, "ligatures": "", "prevSize": 32, "name": "anchor" }, { - "order": 257, + "order": 264, "ligatures": "", "prevSize": 32, "name": "wallet" }, { - "order": 258, + "order": 265, "ligatures": "", "prevSize": 32, "name": "vector" }, { - "order": 259, + "order": 266, "ligatures": "", "prevSize": 32, "name": "speech" }, { - "order": 260, + "order": 267, "ligatures": "", "prevSize": 32, "name": "puzzle" }, { - "order": 261, + "order": 268, "ligatures": "", "prevSize": 32, "name": "printer" }, { - "order": 262, + "order": 269, "ligatures": "", "prevSize": 32, "name": "present" }, { - "order": 263, + "order": 270, "ligatures": "", "prevSize": 32, "name": "playlist" }, { - "order": 264, + "order": 271, "ligatures": "", "prevSize": 32, "name": "pin" }, { - "order": 265, + "order": 272, "ligatures": "", "prevSize": 32, "name": "picture" }, { - "order": 266, + "order": 273, "ligatures": "", "prevSize": 32, "name": "map" }, { - "order": 267, + "order": 274, "ligatures": "", "prevSize": 32, "name": "layers" }, { - "order": 268, + "order": 275, "ligatures": "", "prevSize": 32, "name": "handbag" }, { - "order": 269, + "order": 276, "ligatures": "", "prevSize": 32, "name": "globe-alt" }, { - "order": 270, + "order": 277, "ligatures": "", "prevSize": 32, "name": "globe" }, { - "order": 271, + "order": 278, "ligatures": "", "prevSize": 32, "name": "frame" }, { - "order": 272, + "order": 279, "ligatures": "", "prevSize": 32, "name": "folder-alt" }, { - "order": 273, + "order": 280, "ligatures": "", "prevSize": 32, "name": "film" }, { - "order": 274, + "order": 281, "ligatures": "", "prevSize": 32, "name": "feed" }, { - "order": 275, + "order": 282, "ligatures": "", "prevSize": 32, "name": "earphones-alt" }, { - "order": 276, + "order": 283, "ligatures": "", "prevSize": 32, "name": "earphones" }, { - "order": 277, + "order": 284, "ligatures": "", "prevSize": 32, "name": "drop" }, { - "order": 278, + "order": 285, "ligatures": "", "prevSize": 32, "name": "drawar" }, { - "order": 279, + "order": 286, "ligatures": "", "prevSize": 32, "name": "docs" }, { - "order": 280, + "order": 287, "ligatures": "", "prevSize": 32, "name": "directions" }, { - "order": 281, + "order": 288, "ligatures": "", "prevSize": 32, "name": "direction" }, { - "order": 282, + "order": 289, "ligatures": "", "prevSize": 32, "name": "diamond" }, { - "order": 283, + "order": 290, "ligatures": "", "prevSize": 32, "name": "cup" }, { - "order": 284, + "order": 291, "ligatures": "", "prevSize": 32, "name": "compass" }, { - "order": 285, + "order": 292, "ligatures": "", "prevSize": 32, "name": "call-out" }, { - "order": 286, + "order": 293, "ligatures": "", "prevSize": 32, "name": "call-in" }, { - "order": 287, + "order": 294, "ligatures": "", "prevSize": 32, "name": "call-end" }, { - "order": 288, + "order": 295, "ligatures": "", "prevSize": 32, "name": "calculator" }, { - "order": 289, + "order": 296, "ligatures": "", "prevSize": 32, "name": "bubbles" }, { - "order": 290, + "order": 297, "ligatures": "", "prevSize": 32, "name": "briefcase" }, { - "order": 291, + "order": 298, "ligatures": "", "prevSize": 32, "name": "book-open" }, { - "order": 292, + "order": 299, "ligatures": "", "prevSize": 32, "name": "basket-loaded" }, { - "order": 293, + "order": 300, "ligatures": "", "prevSize": 32, "name": "basket" }, { - "order": 294, + "order": 301, "ligatures": "", "prevSize": 32, "name": "bag" }, { - "order": 295, + "order": 302, "ligatures": "", "prevSize": 32, "name": "action-undo" }, { - "order": 296, + "order": 303, "ligatures": "", "prevSize": 32, "name": "action-redo" }, { - "order": 297, + "order": 304, "ligatures": "", "prevSize": 32, "name": "wrench" }, { - "order": 298, + "order": 305, "ligatures": "", "prevSize": 32, "name": "umbrella" }, { - "order": 299, + "order": 306, "ligatures": "", "prevSize": 32, "name": "trash" }, { - "order": 300, + "order": 307, "ligatures": "", "prevSize": 32, "name": "tag" }, { - "order": 301, + "order": 308, "ligatures": "", "prevSize": 32, "name": "support" }, { - "order": 302, + "order": 309, "ligatures": "", "prevSize": 32, "name": "size-fullscreen" }, { - "order": 303, + "order": 310, "ligatures": "", "prevSize": 32, "name": "size-actual" }, { - "order": 304, + "order": 311, "ligatures": "", "prevSize": 32, "name": "shuffle" }, { - "order": 305, + "order": 312, "ligatures": "", "prevSize": 32, "name": "share-alt" }, { - "order": 306, + "order": 313, "ligatures": "", "prevSize": 32, "name": "share" }, { - "order": 307, + "order": 314, "ligatures": "", "prevSize": 32, "name": "launch" }, { - "order": 308, + "order": 315, "ligatures": "", "prevSize": 32, "name": "question" }, { - "order": 309, + "order": 316, "ligatures": "", "prevSize": 32, "name": "pie-chart" }, { - "order": 310, + "order": 317, "ligatures": "", "prevSize": 32, "name": "pencil" }, { - "order": 311, + "order": 318, "ligatures": "", "prevSize": 32, "name": "note" }, { - "order": 312, + "order": 319, "ligatures": "", "prevSize": 32, "name": "music-tone-alt" }, { - "order": 313, + "order": 320, "ligatures": "", "prevSize": 32, "name": "music-tone" }, { - "order": 314, + "order": 321, "ligatures": "", "prevSize": 32, "name": "microphone" }, { - "order": 315, + "order": 322, "ligatures": "", "prevSize": 32, "name": "loop" }, { - "order": 316, + "order": 323, "ligatures": "", "prevSize": 32, "name": "logout" }, { - "order": 317, + "order": 324, "ligatures": "", "prevSize": 32, "name": "login" }, { - "order": 318, + "order": 325, "ligatures": "", "prevSize": 32, "name": "list" }, { - "order": 319, + "order": 326, "ligatures": "", "prevSize": 32, "name": "like" }, { - "order": 320, + "order": 327, "ligatures": "", "prevSize": 32, "name": "home" }, { - "order": 321, + "order": 328, "ligatures": "", "prevSize": 32, "name": "grid" }, { - "order": 322, + "order": 329, "ligatures": "", "prevSize": 32, "name": "graph" }, { - "order": 323, + "order": 330, "ligatures": "", "prevSize": 32, "name": "equalizer" }, { - "order": 324, + "order": 331, "ligatures": "", "prevSize": 32, "name": "dislike" }, { - "order": 325, + "order": 332, "ligatures": "", "prevSize": 32, "name": "cursor" }, { - "order": 326, + "order": 333, "ligatures": "", "prevSize": 32, "name": "control-start" }, { - "order": 327, + "order": 334, "ligatures": "", "prevSize": 32, "name": "control-rewind" }, { - "order": 328, + "order": 335, "ligatures": "", "prevSize": 32, "name": "control-play" }, { - "order": 329, + "order": 336, "ligatures": "", "prevSize": 32, "name": "control-pause" }, { - "order": 330, + "order": 337, "ligatures": "", "prevSize": 32, "name": "control-forward" }, { - "order": 331, + "order": 338, "ligatures": "", "prevSize": 32, "name": "control-end" }, { - "order": 332, + "order": 339, "ligatures": "", "prevSize": 32, "name": "calender" }, { - "order": 333, + "order": 340, "ligatures": "", "prevSize": 32, "name": "bulb" }, { - "order": 334, + "order": 341, "ligatures": "", "prevSize": 32, "name": "chart" }, { - "order": 335, + "order": 342, "ligatures": "", "prevSize": 32, "name": "arrow-up-circle" }, { - "order": 336, + "order": 343, "ligatures": "", "prevSize": 32, "name": "arrow-right-circle" }, { - "order": 337, + "order": 344, "ligatures": "", "prevSize": 32, "name": "arrow-left-circle" }, { - "order": 338, + "order": 345, "ligatures": "", "prevSize": 32, "name": "arrow-down-circle" }, { - "order": 339, + "order": 346, "ligatures": "", "prevSize": 32, "name": "ban" }, { - "order": 340, + "order": 347, "ligatures": "", "prevSize": 32, "name": "bubble" }, { - "order": 341, + "order": 348, "ligatures": "", "prevSize": 32, "name": "camrecorder" }, { - "order": 342, + "order": 349, "ligatures": "", "prevSize": 32, "name": "camera" }, { - "order": 343, + "order": 350, "ligatures": "", "prevSize": 32, "name": "ok" }, { - "order": 344, + "order": 351, "ligatures": "", "prevSize": 32, "name": "clock" }, { - "order": 345, + "order": 352, "ligatures": "", "prevSize": 32, "name": "close" }, { - "order": 346, + "order": 353, "ligatures": "", "prevSize": 32, "name": "cloud-download" }, { - "order": 347, + "order": 354, "ligatures": "", "prevSize": 32, "name": "cloud-upload" }, { - "order": 348, + "order": 355, "ligatures": "", "prevSize": 32, "name": "doc" }, { - "order": 349, + "order": 356, "ligatures": "", "prevSize": 32, "name": "envolope" }, { - "order": 350, + "order": 357, "ligatures": "", "prevSize": 32, "name": "eye" }, { - "order": 351, + "order": 358, "ligatures": "", "prevSize": 32, "name": "flag" }, { - "order": 352, + "order": 359, "ligatures": "", "prevSize": 32, "name": "folder" }, { - "order": 353, + "order": 360, "ligatures": "", "prevSize": 32, "name": "heart" }, { - "order": 354, + "order": 361, "ligatures": "", "prevSize": 32, "name": "info" }, { - "order": 355, + "order": 362, "ligatures": "", "prevSize": 32, "name": "key" }, { - "order": 356, + "order": 363, "ligatures": "", "prevSize": 32, "name": "link" }, { - "order": 357, + "order": 364, "ligatures": "", "prevSize": 32, "name": "lock" }, { - "order": 358, + "order": 365, "ligatures": "", "prevSize": 32, "name": "lock-open" }, { - "order": 359, + "order": 366, "ligatures": "", "prevSize": 32, "name": "magnifier" }, { - "order": 360, + "order": 367, "ligatures": "", "prevSize": 32, "name": "magnifier-add" }, { - "order": 361, + "order": 368, "ligatures": "", "prevSize": 32, "name": "magnifier-remove" }, { - "order": 362, + "order": 369, "ligatures": "", "prevSize": 32, "name": "paper-clip" }, { - "order": 363, + "order": 370, "ligatures": "", "prevSize": 32, "name": "paper-plane" }, { - "order": 364, + "order": 371, "ligatures": "", "prevSize": 32, "name": "plus" }, { - "order": 365, + "order": 372, "ligatures": "", "prevSize": 32, "name": "location-pin" }, { - "order": 366, + "order": 373, "ligatures": "", "prevSize": 32, "name": "power" }, { - "order": 367, + "order": 374, "ligatures": "", "prevSize": 32, "name": "refresh" }, { - "order": 368, + "order": 375, "ligatures": "", "prevSize": 32, "name": "reload" }, { - "order": 369, + "order": 376, "ligatures": "", "prevSize": 32, "name": "settings" }, { - "order": 370, + "order": 377, "ligatures": "", "prevSize": 32, "name": "star" }, { - "order": 371, + "order": 378, "ligatures": "", "prevSize": 32, "name": "symble-female" }, { - "order": 372, + "order": 379, "ligatures": "", "prevSize": 32, "name": "symbol-male" }, { - "order": 373, + "order": 380, "ligatures": "", "prevSize": 32, "name": "target" }, { - "order": 374, + "order": 381, "ligatures": "", "prevSize": 32, "name": "volume-1" }, { - "order": 375, + "order": 382, "ligatures": "", "prevSize": 32, "name": "volume-2" }, { - "order": 376, + "order": 383, "ligatures": "", "prevSize": 32, "name": "volume-off" }, { - "order": 377, + "order": 384, "ligatures": "", "prevSize": 32, "name": "phone" }, { - "order": 378, + "order": 385, "ligatures": "", "prevSize": 32, "name": "menu" }, { - "order": 379, + "order": 386, "ligatures": "", "prevSize": 32, "name": "options-vertical" }, { - "order": 380, + "order": 387, "ligatures": "", "prevSize": 32, "name": "options" }, { - "order": 381, + "order": 388, "ligatures": "", "prevSize": 32, "name": "arrow-down" }, { - "order": 382, + "order": 389, "ligatures": "", "prevSize": 32, "name": "arrow-left" }, { - "order": 383, + "order": 390, "ligatures": "", "prevSize": 32, "name": "arrow-right" }, { - "order": 384, + "order": 391, "ligatures": "", "prevSize": 32, "name": "arrow-up" }, { - "order": 385, + "order": 392, "ligatures": "", "prevSize": 32, "name": "paypal" }, { - "order": 386, + "order": 393, "ligatures": "", "prevSize": 32, "name": "social-instagram" }, { - "order": 387, + "order": 394, "ligatures": "", "prevSize": 32, "name": "social-linkedin" }, { - "order": 388, + "order": 395, "ligatures": "", "prevSize": 32, "name": "social-pintarest" }, { - "order": 389, + "order": 396, "ligatures": "", "prevSize": 32, "name": "social-github" }, { - "order": 390, + "order": 397, "ligatures": "", "prevSize": 32, "name": "social-google" }, { - "order": 391, + "order": 398, "ligatures": "", "prevSize": 32, "name": "social-reddit" }, { - "order": 392, + "order": 399, "ligatures": "", "prevSize": 32, "name": "social-skype" }, { - "order": 393, + "order": 400, "ligatures": "", "prevSize": 32, "name": "social-behance" }, { - "order": 394, + "order": 401, "ligatures": "", "prevSize": 32, "name": "social-foursqare" }, { - "order": 395, + "order": 402, "ligatures": "", "prevSize": 32, "name": "social-soundcloud" }, { - "order": 396, + "order": 403, "ligatures": "", "prevSize": 32, "name": "social-spotify" }, { - "order": 397, + "order": 404, "ligatures": "", "prevSize": 32, "name": "social-stumbleupon" }, { - "order": 398, + "order": 405, "ligatures": "", "prevSize": 32, "name": "minus" }, { - "order": 399, + "order": 406, "ligatures": "", "prevSize": 32, "name": "organization" }, { - "order": 400, + "order": 407, "ligatures": "", "prevSize": 32, "name": "exclamation" }, { - "order": 401, + "order": 408, "ligatures": "", "prevSize": 32, "name": "lang" }, { - "order": 402, + "order": 409, "ligatures": "", "prevSize": 32, "name": "event" }, { - "order": 403, + "order": 410, "ligatures": "", "prevSize": 32, "name": "social-steam" }, { - "order": 404, + "order": 411, "name": "bulletlist", "prevSize": 32 }, { - "order": 405, + "order": 412, "prevSize": 32, "name": "numberedlist" }, { - "order": 406, + "order": 413, "name": "listindent", "prevSize": 32 }, { - "order": 407, + "order": 414, "name": "listoutdent", "prevSize": 32 }, { - "order": 408, + "order": 415, "name": "filter", "prevSize": 32 }, { - "order": 409, + "order": 416, "name": "columnselection", "prevSize": 32 }, { - "order": 410, + "order": 417, "name": "enlarge", "prevSize": 32 }, { - "order": 411, + "order": 418, "name": "preview", "prevSize": 32 }, { - "order": 412, + "order": 419, "name": "ListView", "prevSize": 32 }, { - "order": 413, + "order": 420, "name": "TileView", "prevSize": 32 }, { - "order": 414, + "order": 421, "name": "reset", "prevSize": 32 }, { - "order": 415, + "order": 422, "name": "sort", "prevSize": 32 }, { - "order": 416, + "order": 423, "name": "apply", "prevSize": 32 }, { - "order": 417, + "order": 424, "name": "drag-drop-handle", "prevSize": 32 }, { - "order": 418, + "order": 425, "name": "unselect", "prevSize": 32 }, { - "order": 419, + "order": 426, "name": "select", "prevSize": 32 }, { - "order": 420, + "order": 427, "name": "clear", "prevSize": 32 }, { - "order": 421, + "order": 428, "name": "checked", "prevSize": 32 }, { - "order": 422, + "order": 429, "name": "unchecked", "prevSize": 32 + }, + { + "order": 430, + "name": "presenter", + "prevSize": 32 + }, + { + "order": 431, + "name": "owner", + "prevSize": 32 } ], "metadata": { @@ -1236,7 +1246,7 @@ "width": 320, "height": 320 }, - "iconsHash": 1953044038 + "iconsHash": 138035486 }, "height": 1024, "prevSize": 32, @@ -2419,7 +2429,12 @@ }, { "paths": [ - "M960 95.888l-256.224 0.001v-63.776c0-17.68-14.32-32-32-32s-32 14.32-32 32v63.76h-256v-63.76c0-17.68-14.32-32-32-32s-32 14.32-32 32v63.76h-255.776c-35.344 0-64 28.656-64 64v800c0 35.344 28.656 64 64 64h896c35.344 0 64-28.656 64-64v-800c0-35.328-28.656-63.984-64-63.984zM960 959.873l-896-0.001v-800h255.776v32.24c0 17.68 14.32 32 32 32s32-14.32 32-32v-32.224h256v32.24c0 17.68 14.32 32 32 32s32-14.32 32-32v-32.24h256.224v799.984zM736 511.888h64c17.664 0 32-14.336 32-32v-64c0-17.664-14.336-32-32-32h-64c-17.664 0-32 14.336-32 32v64c0 17.664 14.336 32 32 32zM736 767.872h64c17.664 0 32-14.32 32-32v-64c0-17.664-14.336-32-32-32h-64c-17.664 0-32 14.336-32 32v64c0 17.696 14.336 32 32 32zM544 639.872h-64c-17.664 0-32 14.336-32 32v64c0 17.68 14.336 32 32 32h64c17.664 0 32-14.32 32-32v-64c0-17.648-14.336-32-32-32zM544 383.888h-64c-17.664 0-32 14.336-32 32v64c0 17.664 14.336 32 32 32h64c17.664 0 32-14.336 32-32v-64c0-17.68-14.336-32-32-32zM288 383.888h-64c-17.664 0-32 14.336-32 32v64c0 17.664 14.336 32 32 32h64c17.664 0 32-14.336 32-32v-64c0-17.68-14.336-32-32-32zM288 639.872h-64c-17.664 0-32 14.336-32 32v64c0 17.68 14.336 32 32 32h64c17.664 0 32-14.32 32-32v-64c0-17.648-14.336-32-32-32z" + "M155.187-2.125c-0.037 0-0.081 0-0.125 0-17.629 0-31.929 14.256-32 31.868v101.132h-43c-43.804 0-80.062 36.009-80.062 79.812v163.25c0 0.037 0 0.081 0 0.125s0 0.088 0 0.132v-0.007 568.562c0 43.804 36.259 80.062 80.062 80.062h863.875c43.804 0 80.062-36.259 80.062-80.062v-568.562c0-0.037 0-0.081 0-0.125s0-0.088 0-0.132v0.007-163.25c0-43.804-36.259-79.812-80.062-79.812h-42.5v-99c-0.071-17.597-14.335-31.84-31.934-31.875h-0.003c-0.037 0-0.081 0-0.125 0-17.629 0-31.929 14.256-32 31.868v99.007h-650.312v-101.125c-0.071-17.576-14.299-31.804-31.868-31.875h-0.007zM80.062 194.875h863.875c9.216 0 16.062 6.597 16.062 15.812v131.375h-896v-131.375c0-9.216 6.847-15.812 16.062-15.812zM64 406.062h896v536.687c0 9.216-6.847 16.063-16.062 16.063h-863.875c-9.216 0-16.062-6.847-16.062-16.063v-536.687zM277.812 449.312c-17.646 0.035-31.938 14.349-31.938 32 0 0.044 0 0.088 0 0.132v-0.007 75.312h-86.187c-0.037 0-0.081 0-0.125 0-17.651 0-31.964 14.291-32 31.934v0.003c0 0.037 0 0.081 0 0.125 0 17.673 14.327 32 32 32 0.044 0 0.088 0 0.132 0h86.18v126.375h-86.187c-0.037 0-0.081 0-0.125 0-17.629 0-31.929 14.256-32 31.869v0.007c0 0.037 0 0.081 0 0.125 0 17.673 14.327 32 32 32 0.044 0 0.088 0 0.132 0h86.18v71.125c0.071 17.597 14.335 31.84 31.934 31.875h0.003c0.037 0 0.081 0 0.125 0 17.629 0 31.929-14.256 32-31.868v-71.132h167.687v71.125c0.071 17.619 14.371 31.875 32 31.875 0.044 0 0.088 0 0.132 0h-0.007c17.576-0.071 31.804-14.299 31.875-31.868v-71.132h172.5v71.125c0.071 17.619 14.371 31.875 32 31.875 0.044 0 0.088 0 0.132 0h-0.007c17.576-0.071 31.804-14.299 31.875-31.868v-71.132h86.187c0.037 0 0.081 0 0.125 0 17.673 0 32-14.327 32-32 0-0.044 0-0.088 0-0.132v0.007c-0.071-17.619-14.371-31.875-32-31.875-0.044 0-0.088 0-0.132 0h-86.18v-126.375h86.187c0.037 0 0.081 0 0.125 0 17.673 0 32-14.327 32-32 0-0.044 0-0.088 0-0.132v0.007c-0.035-17.646-14.349-31.938-32-31.938-0.044 0-0.088 0-0.132 0h-86.18v-75.312c0-0.037 0-0.081 0-0.125 0-17.629-14.256-31.929-31.868-32h-0.007c-0.037 0-0.081 0-0.125 0-17.673 0-32 14.327-32 32 0 0.044 0 0.088 0 0.132v-0.007 75.312h-172.5v-75.312c0-0.037 0-0.081 0-0.125 0-17.629-14.256-31.929-31.868-32h-0.007c-0.037 0-0.081 0-0.125 0-17.673 0-32 14.327-32 32 0 0.044 0 0.088 0 0.132v-0.007 75.312h-167.687v-75.312c0-0.037 0-0.081 0-0.125 0-17.673-14.327-32-32-32-0.044 0-0.088 0-0.132 0h0.007zM309.937 620.812h167.687v126.375h-167.687v-126.375zM541.625 620.812h172.5v126.375h-172.5v-126.375z" + ], + "attrs": [ + { + "opacity": 0.96 + } ], "tags": [ "calender" @@ -3119,7 +3134,12 @@ }, { "paths": [ - "M676 862c-16 0-28-13-28-29v-142c0-16 12-28 28-28h142c16 0 29 12 29 28v142c0 16-13 29-29 29h-142zM818 691h-142v142h142v-142zM960 96c35 0 64 29 64 64v800c0 35-29 64-64 64h-896c-35 0-64-29-64-64v-800c0-35 29-64 64-64h256v-64c0-18 14-32 32-32s32 14 32 32v64h256v-64c0-18 14-32 32-32s32 14 32 32v64h256zM64 960h896v-800h-256v32c0 18-14 32-32 32s-32-14-32-32v-32h-256v32c0 18-14 32-32 32s-32-14-32-32v-32h-256v800z" + "M155.187-2.125c-0.037 0-0.081 0-0.125 0-17.629 0-31.929 14.256-32 31.868v101.132h-43c-43.804 0-80.062 36.009-80.062 79.812v163.25c0 0.037 0 0.081 0 0.125s0 0.088 0 0.132v-0.007 568.562c0 43.804 36.259 80.062 80.062 80.062h863.875c43.804 0 80.062-36.259 80.062-80.062v-568.562c0-0.037 0-0.081 0-0.125s0-0.088 0-0.132v0.007-163.25c0-43.804-36.259-79.812-80.062-79.812h-42.5v-99c-0.071-17.597-14.335-31.84-31.934-31.875h-0.003c-0.037 0-0.081 0-0.125 0-17.629 0-31.929 14.256-32 31.868v99.007h-650.312v-101.125c-0.071-17.576-14.299-31.804-31.868-31.875h-0.007zM80.062 194.875h863.875c9.216 0 16.062 6.597 16.062 15.812v131.375h-896v-131.375c0-9.216 6.847-15.812 16.062-15.812zM64 406.062h896v536.687c0 9.216-6.847 16.063-16.062 16.063h-863.875c-9.216 0-16.062-6.847-16.062-16.063v-536.687zM221.812 465.812c-0.036 0-0.079 0-0.122 0-17.63 0-31.93 14.255-32.003 31.868v150.007c0 0.036 0 0.079 0 0.122 0 17.675 14.328 32.003 32.003 32.003 0.043 0 0.086 0 0.129 0h152.618c17.62-0.073 31.875-14.373 31.875-32.003 0-0.043 0-0.086 0-0.129v0.007-150c-0.073-17.575-14.3-31.802-31.868-31.875h-152.632z" + ], + "attrs": [ + { + "opacity": 0.96 + } ], "tags": [ "event" @@ -3363,6 +3383,26 @@ ], "defaultCode": 59668, "grid": 0 + }, + { + "paths": [ + "M857.875 142.438c-0.283-0.008-0.615-0.013-0.949-0.013-7.96 0-15.316 2.584-21.277 6.958l0.1-0.070-120.25 87.375c-9.059 6.619-14.877 17.205-14.877 29.151 0 7.954 2.58 15.305 6.948 21.263l-0.071-0.101c6.617 9.083 17.217 14.918 29.18 14.918 7.941 0 15.281-2.571 21.234-6.926l-0.101 0.071 120.25-87.438c9.059-6.619 14.877-17.205 14.877-29.151 0-7.954-2.579-15.305-6.948-21.263l0.070 0.101c-6.431-8.809-16.607-14.553-28.138-14.874l-0.050-0.001zM392.375 153.313c-79.224 2.093-140.484 39.963-176.25 92.562s-48.406 117.918-43 180c7.736 88.837 62.195 139.371 112.25 170.375-2.691 4.45-3.526 11.816-8.313 15.062-9.427 6.394-25.096 11.224-42 15.875-34.358 9.453-96.801 9.707-146 59.063-30.025 30.12-63.894 71.968-82.187 178.812-0.327 1.824-0.515 3.923-0.515 6.065 0 17.768 12.872 32.529 29.8 35.466l0.215 0.031c1.82 0.326 3.916 0.513 6.055 0.513 17.789 0 32.565-12.904 35.477-29.862l0.030-0.214c16.71-97.596 33.48-111.326 62.125-140.062 26.661-26.746 65.533-27.005 114.125-40.375 16.23-4.466 40.175-10.058 63.313-25.75s43.636-44.74 47.437-82.187c0.12-1.103 0.188-2.381 0.188-3.676 0-14.419-8.476-26.858-20.718-32.606l-0.22-0.093c-39.498-17.944-92.741-58.069-99.25-132.813-4.156-47.729 6.455-97.457 30.75-133.187s59.939-59.388 118.562-60.938c54.465-1.439 92.722 18.13 120 50.313s42.445 78.504 41.313 129.188c-1.92 85.908-10.438 123.165-87.125 182.375-8.602 6.646-14.088 16.964-14.088 28.564 0 13.087 6.983 24.543 17.426 30.846l0.162 0.090c69.821 41.712 136.525 49.51 175.063 69.687 3.14 1.644 15.231 19.645 24.312 51.75s16.787 73.375 31 112.437c5.172 13.948 18.364 23.709 33.835 23.709 4.457 0 8.724-0.81 12.663-2.29l-0.248 0.082c13.916-5.188 23.648-18.364 23.648-33.813 0-4.395-0.787-8.605-2.229-12.499l0.081 0.249c-11.314-31.095-19.105-70.75-29.5-107.5s-21.603-75.64-60.25-95.875c-43.872-22.971-88.278-34.628-131.813-52.187 51.923-57.491 77.445-118.787 79.125-193.937 1.476-66.048-18.12-129.725-58.562-177.437s-102.63-77.769-176.688-75.812zM985.25 337.813c-0.128-0.002-0.279-0.003-0.431-0.003-1.5 0-2.979 0.092-4.431 0.27l0.174-0.017-205.5 24.312c-17.938 2.228-31.683 17.378-31.683 35.738 0 1.48 0.089 2.939 0.263 4.372l-0.017-0.173c2.142 18.024 17.334 31.868 35.76 31.868 1.472 0 2.923-0.088 4.349-0.26l-0.172 0.017 205.5-24.312c17.938-2.228 31.683-17.378 31.683-35.738 0-1.48-0.089-2.939-0.263-4.372l0.017 0.173c-2.116-17.855-17.037-31.608-35.223-31.875l-0.027-0zM745.188 484.375c-11.585 0.271-21.784 5.973-28.18 14.651l-0.070 0.099c-4.337 5.874-6.942 13.259-6.942 21.251 0 11.892 5.766 22.437 14.656 28.992l0.099 0.070 120 87.875c5.874 4.337 13.259 6.942 21.251 6.942 11.892 0 22.437-5.766 28.992-14.656l0.070-0.099c4.337-5.874 6.942-13.259 6.942-21.251 0-11.892-5.766-22.437-14.656-28.992l-0.099-0.070-120-87.875c-5.876-4.34-13.262-6.947-21.257-6.947-0.283 0-0.566 0.003-0.847 0.010l0.042-0.001z" + ], + "tags": [ + "glyph_presenter" + ], + "defaultCode": 59662, + "grid": 0 + }, + { + "paths": [ + "M342.947 0.080c-70.426 1.861-124.831 35.679-156.625 82.437s-43.056 104.75-38.25 159.937c6.871 78.902 55.223 123.798 99.688 151.375-2.39 3.943-3.065 10.619-7.312 13.5-8.38 5.684-22.098 10.053-37.125 14.187-30.542 8.403-86.203 8.563-129.938 52.438-26.69 26.775-56.676 63.959-72.937 158.938-0.284 1.602-0.447 3.446-0.447 5.328 0 15.778 11.418 28.888 26.442 31.519l0.192 0.028c1.599 0.283 3.439 0.445 5.318 0.445 15.799 0 28.924-11.45 31.53-26.504l0.027-0.191c14.854-86.757 29.786-98.892 55.25-124.438 23.7-23.776 58.18-24.052 101.375-35.938 14.428-3.97 35.62-8.925 56.187-22.875s38.933-39.711 42.313-73c0.109-0.993 0.172-2.144 0.172-3.309 0-12.816-7.533-23.872-18.413-28.983l-0.196-0.083c-35.112-15.952-82.589-51.682-88.375-118.125-3.695-42.428 5.84-86.55 27.437-118.312s53.199-52.998 105.313-54.375c48.416-1.279 82.564 16.267 106.812 44.875s37.82 69.758 36.813 114.812c-1.707 76.368-9.267 109.428-77.438 162.063-7.684 5.906-12.587 15.101-12.587 25.441 0 15.224 10.63 27.965 24.873 31.206l0.215 0.041c57.197 12.836 85.062 22.871 109.625 30.938 26.548 8.718 40.89 18.79 47.688 28.375 5.872 8.197 15.366 13.475 26.093 13.475 6.949 0 13.381-2.215 18.628-5.978l-0.096 0.066c8.214-5.87 13.505-15.374 13.505-26.114 0-6.967-2.227-13.415-6.007-18.669l0.065 0.095c-18.294-25.796-46.629-40.959-80.062-51.938-13.531-4.443-44.018-12.38-67.563-19.125 43.312-50.023 65.671-103.384 67.125-168.437 1.312-58.713-16.299-115.149-52.25-157.562s-91.229-69.302-157.062-67.562zM428.197 584.517c-87.963 0-159.438 72.802-159.438 161.312s71.475 161.312 159.438 161.312c77.127 0 141.602-55.978 156.313-129.437h240.812v57.875c-0 0.037-0 0.081-0 0.125 0 17.629 14.256 31.929 31.868 32l0.007 0c0.037 0 0.081 0 0.125 0 17.673 0 32-14.327 32-32 0-0.044-0-0.088-0-0.132l0 0.007v-57.875h70.625v105.125c0.071 17.576 14.299 31.804 31.868 31.875l0.007 0c0.037 0 0.081 0 0.125 0 17.629 0 31.929-14.256 32-31.868l0-0.007v-137c0-0.037 0-0.081 0-0.125 0-17.673-14.327-32-32-32-0.044 0-0.088 0-0.132 0l0.007-0h-407.375c-14.802-73.343-79.206-129.187-156.25-129.187zM428.197 648.267c52.787 0 95.587 43.026 95.688 97.375-0 0.028-0 0.061-0 0.094s0 0.066 0 0.099l-0-0.005c0 54.44-42.838 97.312-95.688 97.312s-95.437-42.872-95.437-97.312c0-54.44 42.588-97.562 95.437-97.562z" + ], + "tags": [ + "glyph_owner" + ], + "defaultCode": 59663, + "grid": 0 } ], "colorThemes": [], @@ -3390,8 +3430,7 @@ "useClassSelector": true, "color": 0, "bgColor": 16777215, - "classSelector": ".icon", - "name": "icomoon" + "classSelector": ".icon" }, "historySize": 50, "showCodes": true, diff --git a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.svg b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.svg index 1a6ce8888a27..99f0dd8b7880 100644 --- a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.svg +++ b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.svg @@ -124,7 +124,7 @@ - + @@ -194,7 +194,7 @@ - + @@ -210,6 +210,8 @@ + + diff --git a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.ttf b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.ttf index 7de5f9df5c51cb6a8fcb87c8f4e5bf4caeb83f75..e80272ac083c7ededc2ec75fce8cf896cee7915c 100644 GIT binary patch delta 2345 zcmcIkdu&rx7(eHp`)qq}+k0;xd)nKUt#fl*+daB69&U`yF`yf|%`q5rPNt4g7%sz@ z)rcZKVt^5YF(DdICs{CNM$tqwO^k*_vncY1CI*E}L`5`8d@$#CZnudL6aB;X-tT^_kDK@Rq9*)hCEnLP(G_>=bjtv%gR|5Ob^KY|V)+tNLp zL}&4B2>o;r{Cxx6TQYi|YLtEl#c^=ho!mOO0}(KxOUw5i-?bzGoCE$;g^};{v(p*V zzOVJ!nb}$D1R6ArK!Z#To;~AZGoR0V#`88r;EDV3%MYe_gk~pMMBYJhREHAiGDet1 zNQ>dHpU=SoN`Q~yD(uwVQ6o}O?wpg$%kl~_kU-d9YC0RHAQ%yM4hFs=Di}_k4;cQu za;L}7yJ!+uz|8D|jA6)^f+c#D&QyVoxRKhu=7E|j;eA@x3XPHygKVO-P?fQ)%?xSsDzZtCF5fQ+4g%!zGlFzV z)>;)+Ra(>KMJJ2yT~-qGaV665g9dvq73>abDLBWs%6 z(<^K5O2V#Pg7opsSArx7Oh%z5H_^N!G;clKu^y-<&rzxclcF}!1WgNcd}L$@v zF2?y`PQhUH08byCr~cu1+J7mAh`FbFAvCXosw&(N>w=I}dXF$SzI*~R*VC*ZXrCE7qVWN4>ANw{Z*KBVBX5^lV3-73`E5f)$;y<+${C(B^llS;T6| zxsfAwO^p>5EY@8vCOgiH7O`w&4&&ej#lmV$ zCOoXfa3T@xi#$yU%wkz5PWh%A+`7fWT5PuKvoV9MPEKA{6{ye3G#H%i^`>g`mNynb zT8o3b&SPgXz23#Sv|v`X5uSLeva-wVs!J}~7>jfUg9Z3?FQY4vNKqG<{aEBV3oW*t zCBT|>w-cD9b0c*UZAUMnHvw7DnZLs(MVpmv;*9nYa28zv4KSr;^N_m&H#8*OUe`Zk zW&to7%sTi4X=a$abkwHDsW}HHUAh*|4D|-e$Wip4_tu~P0N~J z*7mJEn`lUU)ZExS)#7eBu+F#c*v(Jg5^TM<^>kZD+o$cx_AhVU(}6lZT)%St$#W(-y2PLUb^e^yBqF4-tX^!WlPaOXyD}5 zqgy{sipiGb1IZKH?%OuK{jTj728#w;22bp`bkFpVWvF^+)6nBXAHk_Gyr=LcB-0Zj OW^~^V+61}y9R3pvw#6?1 delta 1065 zcmZ8gTTGH+7=Ay0qFi7CznEwn2c?T#TL`pH&XuWo3auq3mUsdagoHJ^Xs}ggjgjTn zvbAQ;xmI47xaM8dYHO~m)>Umq#yqXiNWCbG-k*b%7yt7;&wKjb@BO~t|BZU|mFg2a zmu7G001*7ZZqVDTxTd4-!*z?nbg@14Q1~Y5y8v@b#x{NHu7{)702X@DuQsA#^J^CR z7ksF5jb^)}BrP(BIsiavOqLe?)!kKH06`_zcx=`?tT0WQ0T%rjFYeNtv)TsAQ-1?2 zp(Z_6i_H!cQuxt(Yw~iY78xJ=p&$hai2V;|vS!5d+Y)QJ!gUzHjU+0iTqb4j_=iZ@89Z88AW^7Q zJaqCTJV^#>h58Ul5*`&bDCN5dlT8Z+8}!a-Ra$s}IGIePoLV=;v}A+c=%;)U5OBGI z1ZL;uWm7H&!!8o_B+y^P6$rThgEuKH>-5jhb(##PnqtNnVs*brE*DYa?7Z+GB^EQK z;~4f6UveL&uS7Xlk`n0OQ$}?9J{&zFGgL!_}2-EUIbglB;!TINy;!MJ)Ps>@OT`L$HTN9b}>kRpT-r$ zaLq-zN+VH20-G^Bp7_L*@&&Wv#}>))Q2ex_7{*7yPh&=ah)-(vVFl9c6{o2L7fGw7QC3dJ3ZL2ypy9kkIlw{6TeZD-pR Rb_bpY{~~zreX#tJ`~@LM73%;1 diff --git a/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.woff b/components/ILIAS/UI/resources/fonts/Iconfont/il-icons.woff index ad247bb355d54ece30fa76328a36f971ad6be96f..d7b9d34ca70d43afb6248ca6dd88faa309a96bfb 100644 GIT binary patch delta 2395 zcmcImYitx%6ux)vJa>0ycV}iFeX_H=v|C!+?rtC3Z2@heEu}zPgw|3@ORZEXrBJNJ zm@W||zGB20qcJfWP|-9drp5<}T1kwCsA&-Sfy982iVzha5TcZN?rd9m#6*9%opbKF z=bqQx^UXH`;s;d!3@VhjMtmfJ=a_Sdtw#jIQmRFgE#hdZs|toA`3=) zC_P^%{n@{DUnB#mIN^<;j#u-w*oq)buoHAkaxPi9mx)4W2&j zV^d#EeaZ7Nc;K%~++ASooBEVyVaoOy1zobH$z9#D_K76y-YrO9O?@v&lE9?o zYH~e|J51x&(2O;}HF<_pEgUIoJq=Jl$47^UhbbZeDbwSmlZ>N0RF2xwSjHnKni8|k z@B2R*Z{E8JBD0O*=uCS|_?Q>G>0jOOi}YomGueN+unJXqmfi)hv5&r4Bqspfa0OFv z1%C7b`VgIhZ>P`?*bu^kf&puuKqiJFOT(p$D7>@7`8YewDHu*Ynb>0rAk#}_E}Rj zHR`iw+8thEky7I8&2Lr2fDr#IrWH_GXWkX+5W0t_! zt(757l_VlrEmA6SkYqW!S7a?* zR#P~yX`XLBo_e>vs&YeJX`Gf2znk($6}7Z(aam4IES8f~hQ|IpdkUwQr=0$DgT3M+ zvcfch-{fD5ow1Z$qD zwbkya{bSj1U8HVuxxT!=-e3P%{W~ivS4^yIT6uX@&#E&GwGBra>l!DT+)dA{_N{*N z_Gi}w*WSPOWOHlt@s|FU@9ub{6}5i3u6o_kwvBC1wjFGnT(7P_xk21;v{UW!bsfHQ zqWjlPeLYIg!TP%|-Sh3{+Rg9v`g`BrlGhjNJG%9at;hSt{-*v1`wwq>VB6&Od$#{H skT=jYaCpb1dnX4igB61vgHH?|fw#gy$8ZOd=^G-3{@XOR>cT7dFUd~QY5)KL delta 1115 zcmZ8gOH5Ny5S`1bRbz`S?@^>+>__T?#+bI$_(Q^{KmjR86tpTufhvU-+W1+RcAa#-VgLFZtBr)@p>@y%zwU_*=%#W0LTQ!>*6M9c3fyeZW3U6p4$3- z(Hr(g+bMwAajY>?%f2^w#f~Jv7ZJu=sjYn>xM6SgoJVd40ArzMN{&rCoF}nv&WCe8 zq;`3`uA;5gb{-d=*J7iBw~wTu&DPq8TsanXQoC(l|J&ts<7U5)_e8Jnuf@U?kN^Wo zcrZoy-c_DLa-56FAdo~Atsgh*=e4e7h&#iPbT z0$@U*f?ZGwb#NLkz*V@7t15F08nsHe`9DV`OqFsny;5h)kz!J+lqyp()*1Ja6-UbO zgh7){Xg4QgSH;`-SvlDrVxye~4fBs!j+Lw8)p;fq16C|%6PS^emBIKZMty|OBrqJP zDq%^GV$z=lhiMgNJM^-R4VfO{q$T>A4LKw$zpaHmwb$`IK~qR zY{2q(<0F642keS#BG=#{Qj)1wh;e;VlBqt3073mkt~mJM>wA*7wY;l>tLQth^}tx=?#jm&xuw4l|}&u0mILdv5#v4sFL!$DEsUce{t&A3Z{kj=pAmYXIZH I*~l~U7v;h&1ONa4 diff --git a/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php b/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php index d145c1a27433..523e03cc266b 100755 --- a/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php +++ b/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php @@ -31,7 +31,8 @@ interface Factory * purpose: > * The Settings Glyph symbolizes opening a dropdown that allows to edit settings of the displayed block. * composition: > - * The Settings Glyph uses the glyphicon-cog. + * The Settings Glyph uses the CSS class glyphicon-cog. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking triggers the opening of a settings Dropdown. * @@ -55,6 +56,8 @@ public function settings(): Glyph; * some neighbouring Container Collection, such as the content of a Dropdown or an Accordion currently shown. * composition: > * The Collapse Glyph is composed of a triangle pointing to the bottom indicating that content is currently shown. + * It uses the CSS class glyphicon-triangle-bottom. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking hides the display of some Container Collection. * rivals: @@ -82,6 +85,8 @@ public function collapse(): Glyph; * some neighbouring Container Collection, such as the content of a Dropdown or an Accordion currently shown. * composition: > * The Expand Glyph is composed of a triangle pointing to the right indicating that content is currently collapsed. + * It uses the CSS class glyphicon-triangle-up. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking displays some Container Collection. * rivals: @@ -108,7 +113,8 @@ public function expand(): Glyph; * The Add Glyph serves as a replacement for the respective textual * button in very crowded screens. It allows adding a new item. * composition: > - * The Add Glyph uses the glyphicon-plus-sign. + * The Add Glyph uses the CSS class glyphicon-plus-sign. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking adds a new input to a form or an event to the calendar. * @@ -143,7 +149,8 @@ public function add(): Glyph; * The Remove Glyph serves as a replacement for the respective textual * button in very crowded screens. It allows removing an item. * composition: > - * The Remove Glyph uses the glyphicon-minus-sign. + * The Remove Glyph uses the CSS class glyphicon-minus-sign. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking deletes an existing input from a form. * @@ -176,8 +183,10 @@ public function remove(): Glyph; * The Up Glyph allows for manually arranging rows in tables embedded in forms. * It allows moving an item up. * composition: > - * The Up Glyph uses the glyphicon-circle-arrow-up. The Up Glyph - * can be combined with the Add/Remove Glyph. + * The Up Glyph uses the CSS class glyphicon-circle-arrow-up. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). + * The Up Glyph can be combined with the Add/Remove Glyph. * effect: > * When placed in a Button or Link, clicking moves an item up. * @@ -212,7 +221,9 @@ public function up(): Glyph; * The Down Glyph allows for manually arranging rows in tables embedded in forms. * It allows moving an item down. * composition: > - * The Down Glyph uses the glyphicon-circle-arrow-down. The Down Glyph + * The Down Glyph uses the CSS class glyphicon-circle-arrow-down. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). The Down Glyph * can be combined with the Add/Remove Glyph. * effect: > * When placed in a Button or Link, clicking moves an item down. @@ -247,7 +258,9 @@ public function down(): Glyph; * purpose: > * The Back Glyph indicates a possible change of the view. The view change leads back to some previous view. * composition: > - * The chevron-left glyphicon is used. + * The Back Glyph uses the CSS class glyphicon-chevron-left. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * The click on a Back Glyph leads back to a previous view. * @@ -277,7 +290,9 @@ public function back(): Glyph; * purpose: > * The Next Glyph indicates a possible change of the view. The view change leads back to some previous view. * composition: > - * The chevron-right glyphicon is used. + * The Next Glyph uses the css-class glyphicon-chevron-right. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * The click on a Next Glyph opens a new view. * context: @@ -308,6 +323,8 @@ public function next(): Glyph; * Only one Glyph is shown at a time. When placed in a Button or Link, clicking reverses the sorting direction. * composition: > * The Sort Ascending Glyph uses glyphicon-arrow-up. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking reverses the direction of ordering in a table. * @@ -327,7 +344,9 @@ public function sortAscending(): Glyph; * The Sorting Glyphs indicate the current sorting direction of a column in a table as ascending (up) or descending (down). * Only one Glyph is shown at a time. When placed in a Button or Link, clicking reverses the sorting direction. * composition: > - * The Sort Descending Glyph uses glyphicon-arrow-descending. + * The Sort Descending Glyph uses the CSS class glyphicon-arrow-descending. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking reverses the direction of ordering in a table. * @@ -346,7 +365,8 @@ public function sortDescending(): Glyph; * purpose: > * The Briefcase Glyph symbolizes some ongoing work that is done. It was introduced for the background tasks. * composition: > - * The Briefcase Glyph uses glyphicon-briefcase. + * The Briefcase Glyph uses the CSS class glyphicon-briefcase. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * A click on the Briefcase Glyph opens a popup that shows the background tasks. * @@ -366,7 +386,8 @@ public function briefcase(): Glyph; * The User Glyph symbolizes the “Who is online?” Popover in the Top Navigation. * The User Glyph indicates the number of pending contact requests and users online via the the Novelty Counter and Status Counter respectively. * composition: > - * The User Glyph uses the glyphicon-user. + * The User Glyph uses the CSS class glyphicon-user. + * The glyph's design is rendered using the il-icons font which was based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking opens the “Who is online?” Popover. * @@ -386,7 +407,8 @@ public function user(): Glyph; * purpose: > * The Mail Glyph provides a shortcut to the mail service. The Mail Glyph indicates the number of new mails received. * composition: > - * The Mail Glyph uses the glyphicon-envelope. + * The Mail Glyph uses the CSS class glyphicon-envelope. + * The glyph's design is rendered using the il-icons font which was based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking transfers the user to the full-screen mail service. * rivals: @@ -411,6 +433,8 @@ public function mail(): Glyph; * composition: > * If used to toggle the notifications at an individual object, the Notification Glyph uses link-color to * indicate inactivity and the brand-warning color to indicate activity. + * It uses the CSS class glyphicon-bell. + * The glyph's design is rendered using the il-icons font which was based on the simplelineicons font. * * rules: * accessibility: @@ -427,7 +451,9 @@ public function notification(): Glyph; * purpose: > * The Tag Glyph is used to indicate the possibility of adding tags to an object. * composition: > - * The Tag Glyph uses the glyphicon-tag. + * The Tag Glyph uses the CSS class glyphicon-tag. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking opens the Round Trip Modal to add new Tags. * @@ -449,7 +475,9 @@ public function tag(): Glyph; * purpose: > * The Note Glyph is used to indicate the possibility of adding notes to an object. * composition: > - * The Note Glyph uses the glyphicon-pushpin. + * The Note Glyph uses the CSS class glyphicon-pushpin. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking opens the Round Trip Modal to add new notes. * @@ -471,7 +499,9 @@ public function note(): Glyph; * purpose: > * The Comment Glyph is used to indicate the possibility of adding comments to an object. * composition: > - * The Comment Glyph uses the glyphicon-comment. + * The Comment Glyph uses the CSS class glyphicon-comment. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking opens the Round Trip Modal to add new comments. * @@ -494,6 +524,7 @@ public function comment(): Glyph; * The Like Glyph symbolizes a user approving an item, e.g. a posting. * composition: > * The Like Glyph uses the "thumbs up" unicode emoji U+1F44D, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The glyph rendered is from the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Like Glyph acts as a toggle: A first click adds a Like to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Like away, which is also reflected in colour and counter. * @@ -519,6 +550,7 @@ public function like(): Glyph; * The Love Glyph symbolizes a user adoring an item, e.g. a posting. * composition: > * The Love Glyph uses the "red heart" unicode emoji U+2764, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The glyph rendered is from the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Love Glyph acts as a toggle: A first click adds a Love to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Love away, which is also reflected in colour and counter. * @@ -544,6 +576,7 @@ public function love(): Glyph; * The Dislike Glyph symbolizes a user disapproving an item, e.g. a posting. * composition: > * The Dislike Glyph uses the "thumbs down" unicode emoji U+1F44E, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The glyph rendered is from the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Dislike Glyph acts as a toggle: A first click adds a Dislike to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Dislike away, which is also reflected in colour and counter. * @@ -569,6 +602,7 @@ public function dislike(): Glyph; * The Laugh Glyph symbolizes a user finding an item hilarious, e.g. a posting. * composition: > * The Laugh Glyph uses the "grinning face with smiling eyes" unicode emoji U+1F604, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The glyph rendered is from the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Laugh Glyph acts as a toggle: A first click adds a Laugh to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Laugh away, which is also reflected in colour and counter. * @@ -594,6 +628,7 @@ public function laugh(): Glyph; * The Astounded Glyph symbolizes a user finding an item surprising, e.g. a posting. * composition: > * The Astounded Glyph uses the "face with open mouth" unicode emoji U+1F62E, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The design rendered is from the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Astounded Glyph acts as a toggle: A first click adds an Astounded to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Astounded away, which is also reflected in colour and counter. * @@ -619,6 +654,7 @@ public function astounded(): Glyph; * The Sad Glyph symbolizes a user finding an item disconcerting, e.g. a posting. * composition: > * The Sad Glyph uses the "sad but relieved face" unicode emoji U+1F625, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The design presented is through the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Sad Glyph acts as a toggle: A first click adds a Sad to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Sad away, which is also reflected in colour and counter. * @@ -644,6 +680,7 @@ public function sad(): Glyph; * The Angry Glyph symbolizes a user finding an item outraging, e.g. a posting. * composition: > * The Angry Glyph uses the "angry face" unicode emoji U+1F620, see https://unicode.org/emoji/charts/full-emoji-list.html. + * The design presented is through the Open Sans Emoji font. * effect: > * When placed in a Button or Link, the Angry Glyph acts as a toggle: A first click adds an Angry to the respective item, which is reflected in the colour of the Glyph and in the counter. A second click takes the Angry away, which is also reflected in colour and counter. * @@ -669,7 +706,9 @@ public function angry(): Glyph; * The Eye Closed Glyph is used to toggle the revelation-mode of password fields. * With the Eye Closed Glyph shown, the field is currently unmasked. * composition: > - * The Eye Closed Glyph uses the glyphicon-eye-close. + * The Eye Closed Glyph uses the CSS class glyphicon-eye-close. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When clicked, the password-field is masked, thus hiding the input. * @@ -695,7 +734,9 @@ public function eyeclosed(): Glyph; * The Eye Open Glyph is used to toggle the revelation-mode of password fields. * With the Eye Open Glyph shown, the field is currently masked. * composition: > - * The Eye Open Glyph uses the glyphicon-eye-open. + * The Eye Open Glyph uses the CSS class glyphicon-eye-open. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When clicked, the password-field is unmasked, thus revealing the input. * @@ -720,7 +761,8 @@ public function eyeopen(): Glyph; * purpose: > * The Attachment Glyph indicates that a file is attached or can be attached to an object or entity. * composition: > - * The Attachment Glyph uses the glyphicon-paperclip. + * The Attachment Glyph uses the CSS class glyphicon-paperclip. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking executes an action which delivers these attachments to the actor OR initiates a process to add new attachments. * context: @@ -744,7 +786,8 @@ public function attachment(): Glyph; * The Reset Glyph is used to indicate the possibilty of resetting changes made by the user * within a control back to a previous state. * composition: > - * The Reset Glyph uses the glyphicon-repeat. + * The Reset Glyph uses the CSS class glyphicon-repeat. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking reloads the related control immediately and goes back to state * before the user changes. @@ -777,7 +820,8 @@ public function reset(): Glyph; * The Apply Glyph is used to indicate the possibilty of applying changes which the user has made * within a control, i.e. a filter. * composition: > - * The Apply Glyph uses the glyphicon-ok. + * The Apply Glyph uses the CSS class glyphicon-ok. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking reloads the page immediately with the updated content reflected in the control. In case of * a filter, it means that the entries in a table change in accordance with the filter values set by the user. @@ -809,7 +853,8 @@ public function apply(): Glyph; * purpose: > * The Search Glyph is used to trigger a search dialog. * composition: > - * The Search Glyph uses the glyphicon-search. + * The Search Glyph uses the CSS class glyphicon-search. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking opens a search dialog. * Since the context for the Search Glyph primarily is the Meta Bar, @@ -833,7 +878,8 @@ public function search(): Glyph; * purpose: > * The Help Glyph opens a context-sensitive help screen. * composition: > - * The Help Glyph uses the glyphicon-question-sign. + * The Help Glyph uses the CSS class glyphicon-question-sign. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When clicked, the user is provided with explanations or * instructions for the usage of the current context. @@ -858,7 +904,8 @@ public function help(): Glyph; * purpose: > * The Calendar glyph is used to symbolize date-related actions or alerts. * composition: > - * The Calendar Glyph uses the glyphicon-calendar. + * The Calendar Glyph uses the CSS class glyphicon-calendar. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking usually opens a date-picker. * @@ -878,9 +925,11 @@ public function calendar(): Glyph; * --- * description: * purpose: > - * The Time Glyph is used to symbolize time-related actions or alerts. + * The Time Glyph is used to symbolize time-related actions or alerts. * composition: > - * The Time Glyph uses the glyphicon-time. + * The Time Glyph uses the CSS class glyphicon-time. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking usually opens a time-picker. * @@ -903,7 +952,8 @@ public function time(): Glyph; * The Close Glyph is used to symbolize an action that closes something * or leaves a previously initiated context. * composition: > - * The Close Glyph uses the glyphicon-remove. + * The Close Glyph uses the CSS class glyphicon-remove. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking closes an overlay or changes the view. * @@ -925,12 +975,13 @@ public function close(): Glyph; * The More glyph offers viewing the rest of the shortened set of * entries so that the entire set becomes visible. * composition: > - * The More Glyph uses the glyphicon-option-horizontal. + * The More Glyph uses the CSS class glyphicon-option-horizontal. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking shows the rest of the set of entries. * rivals: * Disclosure Glyph: > - * The Disclosure Glyph hides the complete set of entries, wherear the + * The Disclosure Glyph hides the complete set of entries, whereas the * More Glyph only hides parts of it. * Mini Action Dropdown: > * The Dropdown in the ListGUI without text is used to offer a @@ -981,7 +1032,8 @@ public function more(): Glyph; * are too long to be presented fully or would be overwhelming. * The Disclosure Glyphs offers viewing the entirety of the hidden set of entries. * composition: > - * The Disclosure Glyph uses the glyphicon-option-vertical. + * The Disclosure Glyph uses the CSS class glyphicon-option-vertical. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking shows the entire set of entries. * rivals: @@ -1035,7 +1087,8 @@ public function disclosure(): Glyph; * The Language Glyph is used to indicate the option to switch languages * by some shorthand workflow without navigating to the personal settings. * composition: > - * The Language Glyph uses the glyphicon-lang from the il-icons set. + * The Language Glyph uses the CSS class glyphicon-lang. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When clicked, the user is shown a set of active languages to choose from. * rivals: @@ -1065,7 +1118,8 @@ public function language(): Glyph; * The Login Glyph is used to trigger the login interaction. * It is displayed in the Meta Bar of the user is not yet logged in. * composition: > - * The Login Glyph uses the login glyph from the il-icons font. + * The Login Glyph uses the CSS class glyphicon-login. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking triggers the interaction to authenticate and login. * rivals: @@ -1094,7 +1148,8 @@ public function login(): Glyph; * The Logout Glyph is used to trigger the logout interaction. * It is displayed in the Slate triggered by clicking on the User Avatar in the Meta Bar. * composition: > - * The Logout Glyph uses the logout glyph from the il-icons font. + * The Logout Glyph uses the CSS class glyphicon-logout. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking triggers the interaction to logout. * rivals: @@ -1121,7 +1176,8 @@ public function logout(): Glyph; * The Bullet List Glyph is used to indicate the possibility to group related content together * and organize vertically, when you don’t need to convey a specific order for list items. * composition: > - * The Bullet List Glyph uses the glyphicon-listbullet. + * The Bullet List Glyph uses the CSS class glyphicon-listbullet. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking groups a list of entries with bullet points. * rivals: @@ -1147,7 +1203,8 @@ public function bulletlist(): Glyph; * and organize vertically, where you need to convey a priority, hierarchy, * or sequence between list items. * composition: > - * The Numbered List Glyph uses the glyphicon-listnumbered. + * The Numbered List Glyph uses the CSS class glyphicon-listnumbered. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking groups a list of entries with enumeration number. * rivals: @@ -1175,7 +1232,8 @@ public function numberedlist(): Glyph; * It leads to an increased indentation and thus gives the impression of a * subordinate level. * composition: > - * The Indent List Glyph uses the glyphicon-listindent. + * The Indent List Glyph uses the CSS class glyphicon-listindent. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking indents the content to the next subordinate level of the list. * rivals: @@ -1200,7 +1258,8 @@ public function listindent(): Glyph; * The Outdent Glyph is used to define the gradation of a structured list. * It leads to a decreased indentation and thus gives the impression of a superordinate level. * composition: > - * The Outdent List Glyph uses the glyphicon-listoutdent. + * The Outdent List Glyph uses the CSS class glyphicon-listoutdent. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking outdents the content to the next superordinate level of the list. * rivals: @@ -1224,7 +1283,8 @@ public function listoutdent(): Glyph; * purpose: > * The Filter Glyph is used to trigger a filter action. * composition: > - * The Filter Glyph uses the glyphicon-filter. + * The Filter Glyph uses the CSS class glyphicon-filter. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking filters a list of entries. * rivals: @@ -1253,6 +1313,7 @@ public function filter(): Glyph; * indicated by a left-triangle than by a down-triangle. * composition: > * The Collapse Horizontal Glyph is composed of a triangle pointing to the left. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When placed in a Button or Link, clicking hides the display of some Container Collection. * It might simultaneously trigger the display of another Container Collection. @@ -1282,6 +1343,8 @@ public function collapseHorizontal(): Glyph; * transforms some text from or into a heading. * composition: > * The Heading Glyph is composed of the letter H. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking may insert or transform some text into a heading. * rivals: @@ -1309,6 +1372,8 @@ public function header(): Glyph; * transforms some text from or into cursive one. * composition: > * The Italic Glyph is composed of the letter I. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking may insert or transform some text into cursive one. * rivals: @@ -1336,6 +1401,8 @@ public function italic(): Glyph; * transforms some text from or into bold one. * composition: > * The Bold Glyph is composed of the letter B. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking may insert or transform some text into bold one. * rivals: @@ -1364,6 +1431,9 @@ public function bold(): Glyph; * composition: > * The Link Glyph is composed out of two linked chain-pieces that ilustrate the official * URL symbol. + * It uses the CSS class glyphicon-link. + * The glyph's design is rendered using the Glyphicons-Halflings font which + * originated from Bootstrap 3 (deprecated symbol source). * effect: > * When placed in a Button or Link, clicking may insert or transform some text into a link. * rivals: @@ -1390,7 +1460,8 @@ public function link(): Glyph; * The Launch Glyph indicates a process to start, e.g. subscribing to a * Course or triggering a SCORM Module. * composition: > - * The Launch Glyph uses the glyphicon plane. + * The Launch Glyph uses the CSS-class glyphicon-plane. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking will immediately start or continue the process; this * may manifest as a Modal to open or the redirection to the appropriate Page. @@ -1414,7 +1485,8 @@ public function launch(): Glyph; * purpose: > * The Enlarge glyph indicates the possibility of enlarging the content to see more details or to improve the display. * composition: > - * The Enlarge Glyph uses the glyphicon-enlarge. + * The Enlarge Glyph uses the CSS class glyphicon-enlarge. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking triggers an interaction that displays an enlarged version of the content just seen. * This can be a modal with an enlarged display of an image. @@ -1440,7 +1512,8 @@ public function enlarge(): Glyph; * The List View Glyph displays data stacked on top of each other in a list. * The glyph is suitable for views that are read from top to bottom and where the focus is on text. * composition: > - * The List View Glyph uses the glyphicon-ListView. + * The List View Glyph uses the CSS class glyphicon-listView. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When placed in a Button or Link, clicking displays the collection of data as a list. * rivals: @@ -1464,7 +1537,8 @@ public function listView(): Glyph; * The Preview Glyph indicates the possibility to display a preview or a short preview of a * content before the user performs a final action. * composition: > - * The Preview Glyph uses the glyphicon-preview. + * The Preview Glyph uses the CSS class glyphicon-preview. + * The glyph's design is rendered using the il-icons font and is based on the simplelineicons font. * effect: > * When a user clicks on the "Preview" icon, a preview of the content is displayed without a permanent * change or a larger display. This can be a modal with several pages of a file preview. @@ -1488,7 +1562,8 @@ public function preview(): Glyph; * purpose: > * The Sort Glyph indicates the possibility of changing the order of elements within a list, table or other structured data. * composition: > - * The Sort Glyph uses the glyphicon-sort. + * The Sort Glyph uses the CSS class glyphicon-sort. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When a user clicks on the "Sort" icon, all possible sorting options are displayed. * The elements will be reordered based on a specific criterion, such as alphabet, date or size. @@ -1511,7 +1586,8 @@ public function sort(): Glyph; * purpose: > * The Column Selection Glyph shows the option of displaying or hiding columns in a table. * composition: > - * The Column Selection Glyph uses the glyphicon-columnselection. + * The Column Selection Glyph uses the CSS class glyphicon-columnselection. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * If a user clicks on the Colum Selection symbol, an overview is displayed showing which columns are * already visible and which are hidden. @@ -1533,7 +1609,8 @@ public function columnSelection(): Glyph; * The Tile View Glyph displays data in cells arrayed in vertical and horizontal layouts. * The glyph works well for collections that are read from side-to-side and where images are the main focus. * composition: > - * The Tile View Glyph uses the glyphicon-TileView. + * The Tile View Glyph uses the CSS class glyphicon-TileView. + * The glyph's design is rendered using the il-icons font and was created by the community. * effect: > * When you click on the glyph, the displayed data is shown in a grid view. * rivals: @@ -1562,6 +1639,8 @@ public function tileView(): Glyph; * The glyph works best when there is a background or border indicating the dimension of the element that is * draggable. * composition: > + * The Drag Handle Glyph uses the CSS class glyphicon-dragHandle. + * The glyph's design is rendered using the il-icons font and was created by the community. * The cells of the Ordering Table use this glyph. * effect: > * When you click and hold on the glyph, the item it is on can be dragged and dropped. @@ -1569,7 +1648,7 @@ public function tileView(): Glyph; * No glyph: > * In some instances the design and context of an element might already sufficiently indicate that it can * be dragged. However, if an element could be confused with a non-draggable counterpart or is draggable - * only some of the time, you SHOULD use the glyph to indicate when it is draggable or otherwise change the + * only some of the time, you SHOULD use the glyph to indicate when it is draggable. or otherwise change the * appearance to communicate the drag and drop functionality. * context: * - The Drag Glyph communicates the drag and drop feature on the Ordering Table cells. @@ -1631,4 +1710,125 @@ public function checked(): Glyph; * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph */ public function unchecked(): Glyph; + + + /** + * --- + * description: + * purpose: > + * The presenter glyph represents the individual or organization hosting or offering + * a piece of content. On a button, it indicates that the name of one or more + * presenters can be edited, or it can set a corresponding role for a user. + * As a link it may lead to the list of all content featuring this presenter. + * composition: > + * The presenter glyph uses the CSS class glyphicon-presenter. + * The glyph's design is rendered using the il-icons font and was created by the community. + * effect: > + * As part of a button, it opens an input to define the user who is presenting some content. + * It can also be used to apply the presenter role to a user or filter for a specific presenter. + * rivals: + * User: > + * If the referenced entity is not clearly a person or institution tasked with presenting, + * hosting or creating content, you might want to use the more general user glyph instead. + * Owner: > + * Consider choosing the owner glyph to represent entities and roles managing an object + * instead of being the creators or face of a piece of content + * context: + * - The presenter glyph MAY appear with other object actions or metadata properties. + * - It also MAY be one of multiple options for the role of a user. + * rules: + * accessibility: + * 1: > + * The aria-label MUST be 'Presenter'. + * --- + * @param string|null $action + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function presenter(): Glyph; + + /** + * --- + * description: + * purpose: > + * The owner glyph represents the individual responsible for managing a piece of content. + * On a button, it indicates that the name of one or more owners can be edited, or it can set + * a corresponding role for a user. + * composition: > + * The owner glyph uses the CSS class glyphicon-owner. + * The glyph's design is rendered using the il-icons font and was created by the community. + * effect: > + * As part of a button, it opens an input to define the owner of some content. + * It can also be used to set the owner of an object or other access roles. + * rivals: + * User: > + * If the referenced entity is not clearly a person or institution managing or being in some + * other way responsible for it, you might want to use the more general user glyph instead. + * context: + * - The owner glyph MAY appear with other object actions or metadata properties. + * - It also may be one of multiple options for the role of a user. + * rules: + * accessibility: + * 1: > + * The aria-label MUST be 'Owner'. + * --- + * @param string|null $action + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function owner(): Glyph; + + /** + * --- + * description: + * purpose: > + * The date glyph indicates a single specific date e.g. on a button opening a date picker + * or switching a view to the current date. + * composition: > + * The date glyph uses the CSS class glyphicon-date. + * The glyph's design is rendered using the il-icons font and was created by the community. + * effect: > + * In a form, it opens a date picker. In a calendar view it switches to the current day. + * rivals: + * Calendar: > + * There is a calendar glyph more suitable for symbolizing a calendar view. + * context: + * - The date glyph may be part of a date picker + * - As part of some View Controls, it switches to the current date. + * rules: + * usage: + * 1: > + * The date glyph MUST always refer to a single date, not a range. + * accessibility: + * 1: > + * The aria-label MUST be 'Date'. + * --- + * @param string|null $action + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function date(): Glyph; + + /** + * --- + * description: + * purpose: > + * The location glyph indicates postal addresses, specific buildings, rooms, or geo-coordinates. + * composition: > + * The location glyph uses the CSS class glyphicon-owner. + * The glyph's design is rendered using the il-icons font and was created by the community. + * effect: > + * As part of a button, it opens a dropdown of preset locations or modal with a searchable map. + * It can also be used in filters or meta-data properties to filter for a specific location. + * rivals: + * Mail: > + * Use the mail glyph when referring to digital addresses in connection to the Mail Service + * context: + * - It may appear in combination with maps, location booking and similar services + * rules: + * accessibility: + * 1: > + * The aria-label MUST be 'Location'. + * --- + * @param string|null $action + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function location(): Glyph; } diff --git a/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php b/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php index 88192e3fc116..0bdce5618dd9 100755 --- a/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php +++ b/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php @@ -86,6 +86,10 @@ interface Glyph extends Symbol public const DRAG_HANDLE = "dragHandle"; public const CHECKED = "checked"; public const UNCHECKED = "unchecked"; + public const PRESENTER = "presenter"; + public const OWNER = "owner"; + public const DATE = "date"; + public const LOCATION = "location"; /** * Override the default label text with a more specific one diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php index 151e0b0bbb94..e62b547f2ca9 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php @@ -328,4 +328,24 @@ public function unchecked(): G\Glyph { return new Glyph(G\Glyph::UNCHECKED, $this->language->txt("unchecked")); } + + public function presenter(): G\Glyph + { + return new Glyph(G\Glyph::PRESENTER, "presenter"); + } + + public function owner(): G\Glyph + { + return new Glyph(G\Glyph::OWNER, "owner"); + } + + public function date(): G\Glyph + { + return new Glyph(G\Glyph::DATE, "date"); + } + + public function location(): G\Glyph + { + return new Glyph(G\Glyph::LOCATION, "location"); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php index 2f5836091f39..9f60a4740936 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php @@ -91,6 +91,10 @@ class Glyph implements C\Symbol\Glyph\Glyph self::DRAG_HANDLE, self::CHECKED, self::UNCHECKED, + self::PRESENTER, + self::OWNER, + self::DATE, + self::LOCATION, ]; private string $type; diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/Date/date.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/Date/date.php new file mode 100644 index 000000000000..4066e2aca433 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/Date/date.php @@ -0,0 +1,35 @@ + + * Example for rendering a Date Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function date() +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->date(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Active" => $glyph, + "Highlighted" => $glyph->withHighlight() + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/Location/location.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/Location/location.php new file mode 100644 index 000000000000..0030d719d998 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/Location/location.php @@ -0,0 +1,35 @@ + + * Example for rendering a Owner Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function location() +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->location(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Active" => $glyph, + "Highlighted" => $glyph->withHighlight() + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/Owner/owner.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/Owner/owner.php new file mode 100644 index 000000000000..c5ec941b39db --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/Owner/owner.php @@ -0,0 +1,35 @@ + + * Example for rendering a Owner Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function owner() +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->owner(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Active" => $glyph, + "Highlighted" => $glyph->withHighlight() + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/Presenter/presenter.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/Presenter/presenter.php new file mode 100644 index 000000000000..6ef0e37a74db --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/Presenter/presenter.php @@ -0,0 +1,35 @@ + + * Example for rendering a Presenter Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function presenter() +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->presenter(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Active" => $glyph, + "Highlighted" => $glyph->withHighlight() + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html b/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html index 324913696fd8..dfed5bffc441 100755 --- a/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html +++ b/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html @@ -59,6 +59,10 @@ glyphicon-dragHandle glyphicon-checked glyphicon-unchecked + glyphicon-owner + glyphicon-presenter + glyphicon-date + glyphicon-location " aria-hidden="true"> diff --git a/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphFactoryTest.php b/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphFactoryTest.php index f6f0e0c35f62..e29354ef62c7 100755 --- a/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphFactoryTest.php +++ b/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphFactoryTest.php @@ -39,7 +39,11 @@ class GlyphFactoryTest extends AbstractFactoryTestCase "close" => ["context" => false], "settings" => ["context" => false], "sort" => ["context" => false], - "listView" => ["context" => false] + "listView" => ["context" => false], + "presenter" => ["context" => false], + "owner" => ["context" => false], + "date" => ["context" => false], + "location" => ["context" => false], ]; public static string $factory_title = 'ILIAS\\UI\\Component\\Symbol\\Glyph\\Factory'; diff --git a/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php b/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php index e99081b51963..9db8f97ec4ba 100755 --- a/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php +++ b/components/ILIAS/UI/tests/Component/Symbol/Glyph/GlyphTest.php @@ -108,6 +108,10 @@ public function getCounterFactory(): C\Factory G\Glyph::DRAG_HANDLE => "glyphicon glyphicon-dragHandle", G\Glyph::CHECKED => "glyphicon glyphicon-checked", G\Glyph::UNCHECKED => "glyphicon glyphicon-unchecked", + G\Glyph::PRESENTER => "glyphicon glyphicon-presenter", + G\Glyph::OWNER => "glyphicon glyphicon-owner", + G\Glyph::DATE => "glyphicon glyphicon-date", + G\Glyph::LOCATION => "glyphicon glyphicon-location", ); public static array $aria_labels = array( @@ -171,6 +175,10 @@ public function getCounterFactory(): C\Factory G\Glyph::DRAG_HANDLE => "drag_handle", G\Glyph::CHECKED => "checked", G\Glyph::UNCHECKED => "unchecked", + G\Glyph::PRESENTER => "presenter", + G\Glyph::OWNER => "owner", + G\Glyph::DATE => "date", + G\Glyph::LOCATION => "location", ); #[\PHPUnit\Framework\Attributes\DataProvider('getGlyphTypeProvider')] diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 04f20d8b0edb..6e98728b27c4 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -5382,6 +5382,7 @@ common#:#preconditions_optional_hint#:#Bitte erfüllen Sie mindestens %s common#:#predefined_template#:#Vordefinierte Rollenvorlage common#:#preferences#:#Benutzerdefinierte Einstellungen common#:#presentation_table_more#:#Mehr anzeigen +common#:#presenter#:#Präsentiert von common#:#preview#:#Vorschau common#:#preview_create#:#Vorschau erstellen common#:#preview_delete#:#Vorschau löschen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index a9ff22bb1e53..c5b130c336b0 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -5383,6 +5383,7 @@ common#:#preconditions_optional_hint#:#You have to fulfill %s of the foll common#:#predefined_template#:#Predefined role template common#:#preferences#:#Preferences common#:#presentation_table_more#:#Show More +common#:#presenter#:#Presenter common#:#preview#:#Preview common#:#preview_create#:#Create Preview common#:#preview_delete#:#Delete Preview diff --git a/templates/default/070-components/UI-framework/Symbol/_ui-component_glyph.scss b/templates/default/070-components/UI-framework/Symbol/_ui-component_glyph.scss index 241b2fe03d78..43acfa46c87b 100644 --- a/templates/default/070-components/UI-framework/Symbol/_ui-component_glyph.scss +++ b/templates/default/070-components/UI-framework/Symbol/_ui-component_glyph.scss @@ -154,7 +154,6 @@ $icon-font-svg-id: "glyphicons_halflingsregular" !default; .glyphicon-eye-close { &:before { content: "\e106"; } } .glyphicon-warning-sign { &:before { content: "\e107"; } } .glyphicon-plane { &:before { content: "\e108"; } } - .glyphicon-calendar { &:before { content: "\e109"; } } .glyphicon-random { &:before { content: "\e110"; } } .glyphicon-comment { &:before { content: "\e111"; } } .glyphicon-magnet { &:before { content: "\e112"; } } @@ -548,3 +547,23 @@ $icon-font-svg-id: "glyphicons_halflingsregular" !default; font-family: il-icons; content: "\e914"; } +.glyphicon-presenter:before { + font-family: il-icons; + content: "\e90e"; +} +.glyphicon-owner:before { + font-family: il-icons; + content: "\e90f"; +} +.glyphicon-date:before { + font-family: il-icons; + content: "\e619"; +} +.glyphicon-calendar:before { + font-family: il-icons; + content: "\e075"; +} +.glyphicon-location:before { + font-family: il-icons; + content: "\e096"; +} diff --git a/templates/default/delos.css b/templates/default/delos.css index 9a0cef832343..3e4ee45dc825 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -11329,10 +11329,6 @@ div.alert ul { content: "\e108"; } -.glyphicon-calendar:before { - content: "\e109"; -} - .glyphicon-random:before { content: "\e110"; } @@ -12206,6 +12202,31 @@ div.alert ul { content: "\e914"; } +.glyphicon-presenter:before { + font-family: il-icons; + content: "\e90e"; +} + +.glyphicon-owner:before { + font-family: il-icons; + content: "\e90f"; +} + +.glyphicon-date:before { + font-family: il-icons; + content: "\e619"; +} + +.glyphicon-calendar:before { + font-family: il-icons; + content: "\e075"; +} + +.glyphicon-location:before { + font-family: il-icons; + content: "\e096"; +} + .il-avatar { height: 45px; width: 45px; From f6f369bda8fc8d653c5855628ca61f3b848a5bde Mon Sep 17 00:00:00 2001 From: Michael Jansen Date: Wed, 5 Aug 2026 11:12:05 +0200 Subject: [PATCH 175/333] [FIX] #47745 UI: stop `Dropdown` close on 'Enter' (#11517) * Fix https://mantis.ilias.de/view.php?id=47745 --- components/ILIAS/UI/resources/js/Dropdown/dist/dropdown.js | 2 +- components/ILIAS/UI/resources/js/Dropdown/src/Dropdown.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/UI/resources/js/Dropdown/dist/dropdown.js b/components/ILIAS/UI/resources/js/Dropdown/dist/dropdown.js index 7deb8976abeb..da1c5d9c02a9 100644 --- a/components/ILIAS/UI/resources/js/Dropdown/dist/dropdown.js +++ b/components/ILIAS/UI/resources/js/Dropdown/dist/dropdown.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(t){"use strict";class e{#t;#e;#i;#n;constructor(t){if(this.#e=t,this.#t=t.ownerDocument,this.#i=this.#e.querySelector(":scope > button"),null===this.#i)throw new Error("Dropdown: Expected exactly one button in dropdown element.",this.#e);if(this.#n=this.#e.querySelector(".dropdown-menu"),null===this.#n)throw new Error("Dropdown: Expected exactly a dropdown element.",this.#e);this.#i.addEventListener("click",this.#s)}#o=t=>{27===t.key&&this.hide()};#s=t=>{t.stopPropagation(),this.show()};#d=()=>{this.hide()};#h=t=>{this.#e.contains(t.relatedTarget)||this.hide()};#l=()=>{const t=this.#t.documentElement.clientWidth;this.#i.getBoundingClientRect().left+this.#n.getBoundingClientRect().width>t?(this.#n.classList.remove("dropdown-menu__right"),this.#n.classList.add("dropdown-menu__left")):(this.#n.classList.remove("dropdown-menu__left"),this.#n.classList.add("dropdown-menu__right"))};show(){il.UI.dropdown.opened?.hide(),il.UI.dropdown.opened=this,this.#n.style.display="block",this.#l(),this.#i.setAttribute("aria-expanded","true"),this.#t.addEventListener("keydown",this.#o),this.#t.addEventListener("click",this.#d),this.#e.addEventListener("focusout",this.#h),this.#i.removeEventListener("click",this.#s)}hide(){this.#n.style.display="none",this.#i.setAttribute("aria-expanded","false"),this.#t.removeEventListener("keydown",this.#o),this.#t.removeEventListener("click",this.#d),this.#e.removeEventListener("focusout",this.#h),this.#i.addEventListener("click",this.#s)}}t.UI=t.UI||{},t.UI.dropdown={},t.UI.dropdown.opened=null,t.UI.dropdown.init=function(t){return new e(t)}}(il); +!function(t){"use strict";class e{#t;#e;#i;#n;constructor(t){if(this.#e=t,this.#t=t.ownerDocument,this.#i=this.#e.querySelector(":scope > button"),null===this.#i)throw new Error("Dropdown: Expected exactly one button in dropdown element.",this.#e);if(this.#n=this.#e.querySelector(".dropdown-menu"),null===this.#n)throw new Error("Dropdown: Expected exactly a dropdown element.",this.#e);this.#i.addEventListener("click",this.#s)}#o=t=>{"Escape"===t.key&&this.hide()};#s=t=>{t.stopPropagation(),this.show()};#d=()=>{this.hide()};#h=t=>{this.#e.contains(t.relatedTarget)||this.hide()};#l=()=>{const t=this.#t.documentElement.clientWidth;this.#i.getBoundingClientRect().left+this.#n.getBoundingClientRect().width>t?(this.#n.classList.remove("dropdown-menu__right"),this.#n.classList.add("dropdown-menu__left")):(this.#n.classList.remove("dropdown-menu__left"),this.#n.classList.add("dropdown-menu__right"))};show(){il.UI.dropdown.opened?.hide(),il.UI.dropdown.opened=this,this.#n.style.display="block",this.#l(),this.#i.setAttribute("aria-expanded","true"),this.#t.addEventListener("keydown",this.#o),this.#t.addEventListener("click",this.#d),this.#e.addEventListener("focusout",this.#h),this.#i.removeEventListener("click",this.#s)}hide(){this.#n.style.display="none",this.#i.setAttribute("aria-expanded","false"),this.#t.removeEventListener("keydown",this.#o),this.#t.removeEventListener("click",this.#d),this.#e.removeEventListener("focusout",this.#h),this.#i.addEventListener("click",this.#s)}}t.UI=t.UI||{},t.UI.dropdown={},t.UI.dropdown.opened=null,t.UI.dropdown.init=function(t){return new e(t)}}(il); diff --git a/components/ILIAS/UI/resources/js/Dropdown/src/Dropdown.js b/components/ILIAS/UI/resources/js/Dropdown/src/Dropdown.js index b87a1a10f158..249b3bca0ac3 100644 --- a/components/ILIAS/UI/resources/js/Dropdown/src/Dropdown.js +++ b/components/ILIAS/UI/resources/js/Dropdown/src/Dropdown.js @@ -58,7 +58,7 @@ export default class Dropdown { * @type {function(KeyboardEvent)} */ #hideOnEscape = (/** @param {KeyboardEvent} event */ event) => { - if (event.key === 27) { // ESCAPE + if (event.key === 'Escape') { this.hide(); } }; From 502d13c7cf6c990a0cd595da706c1f4b464c2966 Mon Sep 17 00:00:00 2001 From: selyesa Date: Tue, 7 Jul 2026 12:16:18 +0200 Subject: [PATCH 176/333] Create PRIVACY.md --- components/ILIAS/Badge/PRIVACY.md | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 components/ILIAS/Badge/PRIVACY.md diff --git a/components/ILIAS/Badge/PRIVACY.md b/components/ILIAS/Badge/PRIVACY.md new file mode 100644 index 000000000000..9528ee298b33 --- /dev/null +++ b/components/ILIAS/Badge/PRIVACY.md @@ -0,0 +1,66 @@ +# Badge Privacy + +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** + + +## General Information + +The Badge Service allows the awarding of digital badges to users for various achievements. Badges can be awarded automatically (triggered by events like course completion) or manually by persons with appropriate permissions. Users can view and manage their received badges in their personal profile. + +The badge backpack export feature (Mozilla Open Badges) is currently disabled. + +## Integrated Services + +The Badge component employs the following services (please consult their respective PRIVACY.md files): + +- **[User](../User/PRIVACY.md)**: The Badge service stores references to user accounts and triggers badge evaluation based on user events. +- **[Tracking](../Tracking/PRIVACY.md)**: Badge awards can be triggered by learning progress status changes. +- **[Mail](../Mail/PRIVACY.md)**: Email notifications are sent to users when badges are awarded. +- **[AccessControl](../AccessControl/PRIVACY.md)**: Permissions control who can manage badges and view badge assignments. +- **[Notification](../Notification/PRIVACY.md)**: On-screen notifications are sent when badges are awarded. + +## Data being stored + +- **Badge Assignment**: When a badge is assigned to a user, the following data is stored in the database table `badge_user_badge`: + - The user ID of the badge recipient + - The timestamp when the badge was awarded + - The user ID of the person who manually awarded the badge, if applicable + - A position value for ordering badges in the user's profile display + +- **User Preferences**: The following user preferences are stored via the user preference system: + - `badge_last_checked`: Timestamp of when the user last viewed their badges + - `badge_mozilla_bp`: Optional email address for badge backpack integration + +## Data being presented + +- **Personal Badge View**: Each user can view their own badges in their personal profile. This includes badge title, description, image, award date, and the name of the awarding object. + +- **Badge Management View**: Persons with "write" permission on a repository object (course, group, etc.) can access the badge management interface. This view presents: + - Names (firstname, lastname) of users who have been awarded badges + - Login names of badge recipients + - The date when badges were issued + - The badge type and title + +- **Administration View**: Persons with "visible,read" permission on the Badge Administration can view all object badges and their assignments across the system. This includes: + - List of users with badge assignments, showing name, login, badge type, title, and issue date + - The parent object where the badge was awarded + +- **Badge Award Information**: When viewing a badge in the tile view, the name of the awarding object is displayed to the badge recipient. + +## Data being deleted + +- **User Deletion**: When a user account is deleted from the system, all badge assignments for that user are automatically deleted. + +- **Badge Deletion**: When a badge definition is deleted, all assignments of that badge to users are automatically deleted. + +- **Manual Removal**: Persons with "write" permission on a repository object can manually remove badge assignments from individual users via the "Users" tab in badge management. + +- **Object Deletion**: When a repository object containing badges is deleted from trash, the badge definitions and their assignments are deleted. Note: the Badge service does not implement `deleteByParentId` cleanup automatically; badge definitions may remain in the database referencing deleted objects. + +## Data being exported + +- **No Export of Assignments**: There is no built-in export functionality for badge assignment data. + +- **Backpack Export (Disabled)**: The Open Badges backpack export feature is currently disabled. When enabled, it generates static JSON files according to the Open Badges specification containing badge class information and user-specific assertion files. + +- **XML Export**: Badge definitions can be exported as part of repository object exports, but this does not include user assignment data or other personal information. From f3b1d794fd4db900785421e8ff7ba362f3082eb4 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Fri, 17 Jul 2026 22:01:49 +0200 Subject: [PATCH 177/333] [Improvement] Badge: Privacy.md --- components/ILIAS/Badge/PRIVACY.md | 128 ++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 25 deletions(-) diff --git a/components/ILIAS/Badge/PRIVACY.md b/components/ILIAS/Badge/PRIVACY.md index 9528ee298b33..b87c21ab24f6 100644 --- a/components/ILIAS/Badge/PRIVACY.md +++ b/components/ILIAS/Badge/PRIVACY.md @@ -1,66 +1,144 @@ # Badge Privacy -> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information via [Pull Request](docs/development/contributing.md#pull-request-to-the-repositories).** - +This documentation does not guarantee completeness or correctness. Please report any +missing or incorrect information using the [ILIAS issue tracker](https://mantis.ilias.de) +or contribute a fix via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories). ## General Information -The Badge Service allows the awarding of digital badges to users for various achievements. Badges can be awarded automatically (triggered by events like course completion) or manually by persons with appropriate permissions. Users can view and manage their received badges in their personal profile. +The Badge service allows the awarding of digital badges to users for various achievements. +Badges can be awarded automatically (triggered by events like course completion) or manually by persons with +appropriate permissions. Users can view and manage their received badges under +**Achievements > Badges**. + +Badge instances are typically managed on **courses** and **groups** (when the badges +feature is activated for the object). Other components can provide badge types via the +badge provider API (e.g. profile badges from the User service). + +Events such as trigger automatic awards: -The badge backpack export feature (Mozilla Open Badges) is currently disabled. +- **Learning progress completion** of a course (`Tracking/updateStatus`) +- **Profile updates** for profile badges (`User/afterUpdate`) + +Badges are not awarded retrospectively when settings change; only future actions are +evaluated. + +The Open Badges backpack **export** from ILIAS is no longer supported and has been disabled. +Mozilla has shut down the Open Badges backpack service, and ILIAS has removed the export functionality +(see [Mantis #20124](https://mantis.ilias.de/view.php?id=20124)). ## Integrated Services The Badge component employs the following services (please consult their respective PRIVACY.md files): -- **[User](../User/PRIVACY.md)**: The Badge service stores references to user accounts and triggers badge evaluation based on user events. -- **[Tracking](../Tracking/PRIVACY.md)**: Badge awards can be triggered by learning progress status changes. -- **[Mail](../Mail/PRIVACY.md)**: Email notifications are sent to users when badges are awarded. -- **[AccessControl](../AccessControl/PRIVACY.md)**: Permissions control who can manage badges and view badge assignments. -- **[Notification](../Notification/PRIVACY.md)**: On-screen notifications are sent when badges are awarded. +- **[User](../User/PRIVACY.md)**: Stores user references for assignments and triggers + badge evaluation on profile updates. +- **[Tracking](../Tracking/PRIVACY.md)**: Triggers badge awards on learning progress + status changes. +- **[Mail](../Mail/PRIVACY.md)**: Sends email notifications when badges are awarded. + The Mail service is used as a transport channel; badge titles and links are passed + through as message content. +- **[Notifications](../Notifications/PRIVACY.md)**: Sends on-screen notifications when + badges are awarded. +- **[AccessControl](../AccessControl/PRIVACY.md)**: Controls who can manage badges and + view badge assignments. +- **[LearningHistory](../LearningHistory/PRIVACY.md)**: Presents awarded badges in the + user's learning history timeline. +- **[ResourceStorage](../ResourceStorage/PRIVACY.md)**: Stores badge images referenced + by badge definitions. +- **GlobalScreen**: Displays badge notification counters in the main bar. ## Data being stored -- **Badge Assignment**: When a badge is assigned to a user, the following data is stored in the database table `badge_user_badge`: +- **Badge Definition**: Badge instances are stored in the database table `badge_badge`. + This includes title, description, criteria, configuration, and references to the + parent object and badge image. These definitions do not contain personal data by + themselves but may reference user-configured content in their text fields. + +- **Badge Assignment**: When a badge is assigned to a user, the following data is + stored in the database table `badge_user_badge`: - The user ID of the badge recipient - The timestamp when the badge was awarded - - The user ID of the person who manually awarded the badge, if applicable - - A position value for ordering badges in the user's profile display + - The user ID of the person who manually awarded the badge (only for manual awards; + not displayed as a person's name in the user interface) + - A position value (`pos`) controlling visibility and ordering on the user's profile + (`null` = not shown on the public profile) -- **User Preferences**: The following user preferences are stored via the user preference system: +- **User Preferences**: The following user preferences are stored via the user + preference system: - `badge_last_checked`: Timestamp of when the user last viewed their badges - `badge_mozilla_bp`: Optional email address for badge backpack integration +- **Static Open Badges Files**: Legacy static JSON files for Open Badges publishing may + exist under the webspace directory `pub_badges/`. These are removed when + assignments or badge definitions are deleted. + ## Data being presented -- **Personal Badge View**: Each user can view their own badges in their personal profile. This includes badge title, description, image, award date, and the name of the awarding object. +- **Personal Badge View**: Each user can view their own badges under + **Achievements > Badges**. This includes badge title, description, image, award date, + and the name of the awarding object (parent repository object). + +- **Public User Profile**: Users can choose which badges are visible on their public + profile (controlled via the `pos` field). Other users who can access the profile see + badge title, image, and awarding object. Only badges with a positive position value + are shown. -- **Badge Management View**: Persons with "write" permission on a repository object (course, group, etc.) can access the badge management interface. This view presents: +- **Badge Management View**: Persons with **write** permission on a course or group can + access the badge management interface. This view presents: - Names (firstname, lastname) of users who have been awarded badges - Login names of badge recipients - The date when badges were issued - The badge type and title + - The parent object where the badge was awarded (in container context) -- **Administration View**: Persons with "visible,read" permission on the Badge Administration can view all object badges and their assignments across the system. This includes: - - List of users with badge assignments, showing name, login, badge type, title, and issue date +- **Administration View**: Persons with **read** permission on the Badge + Administration can view all object badges and their assignments across the system. + This includes: + - List of users with badge assignments, showing name, login, badge type, title, and + issue date - The parent object where the badge was awarded -- **Badge Award Information**: When viewing a badge in the tile view, the name of the awarding object is displayed to the badge recipient. +- **Badge Award Information**: When viewing a badge in the tile view, the name of the + awarding object is displayed to the badge recipient. The label "awarded by" refers to + the parent object, not to the user who performed a manual award. + +- **Notifications**: When badges are awarded, users receive: + - An **email** containing badge titles and a link to their badge profile + - An **on-screen notification** with badge titles and a link + +- **Learning History**: Awarded badges appear in the user's learning history with badge + title, context object, and timestamp. ## Data being deleted -- **User Deletion**: When a user account is deleted from the system, all badge assignments for that user are automatically deleted. +- **User Deletion**: When a user account is deleted, all badge assignments for that + user are automatically deleted (`ilBadgeAssignment::deleteByUserId`). -- **Badge Deletion**: When a badge definition is deleted, all assignments of that badge to users are automatically deleted. +- **Badge Deletion**: When a badge definition is deleted, all assignments of that + badge to users and associated static files are automatically deleted. -- **Manual Removal**: Persons with "write" permission on a repository object can manually remove badge assignments from individual users via the "Users" tab in badge management. +- **Manual Removal**: Persons with **write** permission on a repository object can + manually remove badge assignments from individual users via the "Users" tab in badge + management. -- **Object Deletion**: When a repository object containing badges is deleted from trash, the badge definitions and their assignments are deleted. Note: the Badge service does not implement `deleteByParentId` cleanup automatically; badge definitions may remain in the database referencing deleted objects. +- **Object Deletion**: When a repository object (e.g. course, group) is permanently + deleted, badge definitions in `badge_badge` and their assignments are **not** + automatically cleaned up. The method `ilBadgeAssignment::deleteByParentId` exists + but is not hooked into the object deletion workflow. Orphaned badge definitions may + remain in the database referencing deleted objects. ## Data being exported -- **No Export of Assignments**: There is no built-in export functionality for badge assignment data. +- **No Export of Assignments**: There is no built-in export functionality for badge + assignment data. + +- **Backpack Export (Disabled)**: Exporting badges to an external Open Badges backpack + is currently disabled. -- **Backpack Export (Disabled)**: The Open Badges backpack export feature is currently disabled. When enabled, it generates static JSON files according to the Open Badges specification containing badge class information and user-specific assertion files. +- **Backpack Import / Display**: Code for displaying badges from an external Mozilla + Open Badges backpack (using the email stored in `badge_mozilla_bp`) still exists, but + this feature is not actively maintained. -- **XML Export**: Badge definitions can be exported as part of repository object exports, but this does not include user assignment data or other personal information. +- **Object Copy**: When a course is copied, badge definitions are cloned to the new + object. User assignments are not copied. From 792f89b45d0b8772a991b77e73950570750dbae1 Mon Sep 17 00:00:00 2001 From: selyesa Date: Wed, 5 Aug 2026 11:16:11 +0200 Subject: [PATCH 178/333] Update PRIVACY.md for Badge Disclaimer changed. --- components/ILIAS/Badge/PRIVACY.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/Badge/PRIVACY.md b/components/ILIAS/Badge/PRIVACY.md index b87c21ab24f6..658d7fae796b 100644 --- a/components/ILIAS/Badge/PRIVACY.md +++ b/components/ILIAS/Badge/PRIVACY.md @@ -1,8 +1,7 @@ # Badge Privacy -This documentation does not guarantee completeness or correctness. Please report any -missing or incorrect information using the [ILIAS issue tracker](https://mantis.ilias.de) -or contribute a fix via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories). +> **Disclaimer: This documentation does not guarantee completeness or accuracy. Please report any missing or incorrect information by submitting a [Pull Request](https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/docs/development/contributing.md#pull-request-to-the-repositories) or, if you prefer, via the [ILIAS bug tracker](https://mantis.ilias.de). When using the bug tracker, please select the corresponding component in the **Category** field.** + ## General Information From 4c51ed69211ab378711104278c0fc7fc9e34c3c5 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Wed, 5 Aug 2026 11:06:04 +0200 Subject: [PATCH 179/333] [FIX] Database: replace deprecated `PDO::ATTR_USE_BUFFERED_QUERY` Use `PDO\Mysql::ATTR_USE_BUFFERED_QUERY` instead, see https://www.php.net/manual/en/migration85.deprecated.php --- components/ILIAS/Database/classes/PDO/ilDBPdo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Database/classes/PDO/ilDBPdo.php b/components/ILIAS/Database/classes/PDO/ilDBPdo.php index 276f9f8da593..108dfaeb1194 100755 --- a/components/ILIAS/Database/classes/PDO/ilDBPdo.php +++ b/components/ILIAS/Database/classes/PDO/ilDBPdo.php @@ -102,7 +102,7 @@ protected function getAttributes(): array { return [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + PDO\Mysql::ATTR_USE_BUFFERED_QUERY => true, PDO::ATTR_TIMEOUT => 300 * 60, ]; } From aed700baec70d308efe8655570747d28d783a475 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Wed, 5 Aug 2026 14:34:56 +0200 Subject: [PATCH 180/333] [FIX] #46107 UI: update `MainControls\Footer` section sizes (#10372) * Fixes https://mantis.ilias.de/view.php?id=46107 * Update section 1 and 5 from 4 column grid to 2 column grid * Add section grid size CSS modifier --- .../Component/MainControls/Renderer.php | 31 +++++++++++++++---- .../default/MainControls/tpl.footer.html | 2 +- .../Component/MainControls/FooterTest.php | 12 +++---- .../MainControls/_ui-component_footer.scss | 5 +-- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php index 9c88d35df7f0..f2f517fb157a 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php @@ -44,6 +44,9 @@ class Renderer extends AbstractComponentRenderer public const BLOCK_MAINBAR_TOOLS = 'tool_trigger_item'; public const BLOCK_METABAR_ENTRIES = 'meta_element'; + protected const FOOTER_SECTION_SIZE_MEDIUM = 'md'; + protected const FOOTER_SECTION_SIZE_SMALL = 'sm'; + private array $trigger_signals = []; /** @@ -445,7 +448,12 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ $template->setCurrentBlock('with_additional_item'); $template->setVariable('ITEM_CONTENT', $this->permanentLink((string) $permanent_url, $default_renderer)); $template->parseCurrentBlock(); - $this->parseFooterSection($template, 'permanent-link', $this->txt('footer_permanent_link')); + $this->parseFooterSection( + $template, + 'permanent-link', + $this->txt('footer_permanent_link'), + self::FOOTER_SECTION_SIZE_MEDIUM, + ); } // maybe render section 2 (link groups): @@ -462,6 +470,7 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ 'link-groups', $this->txt('footer_link_groups'), $link_groups, + self::FOOTER_SECTION_SIZE_SMALL, ); } @@ -475,6 +484,7 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ 'links', $this->txt('footer_links'), $links, + self::FOOTER_SECTION_SIZE_SMALL, ); } @@ -498,7 +508,8 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ $default_renderer, 'icons', $this->txt('footer_icons'), - $icons + $icons, + self::FOOTER_SECTION_SIZE_SMALL, ); } @@ -512,6 +523,7 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ 'texts', $this->txt('footer_texts'), $texts, + self::FOOTER_SECTION_SIZE_MEDIUM, ); } @@ -522,13 +534,15 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ /** * @param array $section_items (use as [$content, $title] = ) + * @param string $section_size (self::FOOTER_SECTION_SIZE_MEDIUM|self::FOOTER_SECTION_SIZE_SMALL) */ protected function parseAdditionalFooterSectionItems( Template $template, RendererInterface $default_renderer, string $section_type, string $section_label, - array $section_items = [], + array $section_items, + string $section_size, ): void { foreach ($section_items as [$content, $title]) { $template->setCurrentBlock('with_additional_item'); @@ -544,18 +558,20 @@ protected function parseAdditionalFooterSectionItems( $template->parseCurrentBlock(); } - $this->parseFooterSection($template, $section_type, $section_label); + $this->parseFooterSection($template, $section_type, $section_label, $section_size); } /** * @param array $section_icons + * @param string $section_size (self::FOOTER_SECTION_SIZE_MEDIUM|self::FOOTER_SECTION_SIZE_SMALL) */ protected function parseAdditionalFooterSectionIcons( Template $template, RendererInterface $default_renderer, string $section_type, string $section_label, - array $section_icons = [], + array $section_icons, + string $section_size, ): void { foreach ($section_icons as $icon) { $template->setCurrentBlock('with_additional_icon'); @@ -563,17 +579,20 @@ protected function parseAdditionalFooterSectionIcons( $template->parseCurrentBlock(); } - $this->parseFooterSection($template, $section_type, $section_label); + $this->parseFooterSection($template, $section_type, $section_label, $section_size); } + /** @param string $section_size (self::FOOTER_SECTION_SIZE_MEDIUM|self::FOOTER_SECTION_SIZE_SMALL) */ protected function parseFooterSection( Template $template, string $section_type, string $section_label, + string $section_size, ): void { $template->setCurrentBlock('with_additional_section'); $template->setVariable('SECTION_TYPE', $section_type); $template->setVariable('SECTION_LABEL', $section_label); + $template->setVariable('SECTION_SIZE', $section_size); $template->parseCurrentBlock(); } diff --git a/components/ILIAS/UI/src/templates/default/MainControls/tpl.footer.html b/components/ILIAS/UI/src/templates/default/MainControls/tpl.footer.html index d1b5aecd5b98..7f4b0aa41b62 100755 --- a/components/ILIAS/UI/src/templates/default/MainControls/tpl.footer.html +++ b/components/ILIAS/UI/src/templates/default/MainControls/tpl.footer.html @@ -1,6 +1,6 @@
    -
    @@ -193,7 +193,7 @@ public function testRenderWithAdditionalIcon(): void $expected_html = << -