");
$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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