diff --git a/components/ILIAS/Init/classes/ErrorHandling/README.md b/components/ILIAS/Init/classes/ErrorHandling/README.md index cef25f90620d..887ea6642fbe 100644 --- a/components/ILIAS/Init/classes/ErrorHandling/README.md +++ b/components/ILIAS/Init/classes/ErrorHandling/README.md @@ -4,16 +4,16 @@ This package provides responders for rendering HTTP error pages in ILIAS. ## When to use which responder -- **ErrorPageResponder** (`Http\ErrorPageResponder`): Use when the DI container and all ILIAS services (UI, language, HTTP, etc.) are available. Renders a full ILIAS page with a UI-Framework MessageBox and optional back button. Use for expected errors (e.g. routing failures, access denied) that should be shown as a proper HTML page. +- **ErrorPageResponder** (`Http\ErrorPageResponder`): Full ILIAS page: UI MessageBox when the fourth argument is `UIServices`, or `tpl.error.html` block `plain_html_fallback` when it is `ilGlobalTemplateInterface` (no `ui.factory` / `ui.renderer`). Constructor: `global_screen`, `language`, `http`, `shell`. On any `Throwable` from bootstrap or `respond()`, use `PlainTextFallbackResponder` (see `ilias.php` / `error.php`). -- **PlainTextFallbackResponder** (`Http\PlainTextFallbackResponder`): Use when the DI container or other infrastructure is *not* available — for instance in the catch block of `error.php` when the bootstrap itself has failed. Sends a minimal plain-text response with `Content-Type: text/plain; charset=UTF-8` and logs the exception via `error_log`. This responder always works because it uses only PHP built-ins. The HTTP status code defaults to 500; pass a different code (e.g. 502) when the failure context is known. +- **PlainTextFallbackResponder** (`Http\PlainTextFallbackResponder`): Use when the DI container or other infrastructure is *not* available — for instance in the catch block of `error.php` when the bootstrap itself has failed. Sends a minimal plain-text response with `Content-Type: text/plain; charset=UTF-8` and logs the exception via `error_log`. The HTTP status code defaults to 500; pass a different code when the failure context is known. ## Consumer responsibility -**The consumer MUST implement a try-catch block.** Both responders must be invoked explicitly: +**The consumer MUST implement a try-catch block.** Call `respond()` explicitly: -1. Wrap the main logic (bootstrap, routing, etc.) in a `try` block. -2. In the `catch` block, call either `ErrorPageResponder::respond()` (if DIC is available) or `PlainTextFallbackResponder::respond()` (if DIC is not available). +1. Wrap bootstrap (and `ErrorPageResponder::respond()` when applicable) in one `try` block. +2. In `catch (Throwable)`, call `PlainTextFallbackResponder` (optionally pass a user-facing message if you set it before the failure). Example: @@ -21,13 +21,13 @@ Example: try { entry_point('ILIAS Legacy Initialisation Adapter'); global $DIC; - new ErrorPageResponder( - $DIC->globalScreen(), + (new ErrorPageResponder( + $DIC->offsetExists('global_screen') ? $DIC->globalScreen() : null, $DIC->language(), - $DIC->ui(), - $DIC->http() - )->respond($message, 500, $back_target); -} catch (Throwable $e) { - new PlainTextFallbackResponder()->respond($e); + $DIC->http(), + $DIC->ui() + ))->respond($message, 500, $back_target); +} catch (Throwable $t) { + (new PlainTextFallbackResponder())->respond($t); } ``` diff --git a/components/ILIAS/Init/resources/error.php b/components/ILIAS/Init/resources/error.php index 55764207b53f..ccbd9acad2ce 100644 --- a/components/ILIAS/Init/resources/error.php +++ b/components/ILIAS/Init/resources/error.php @@ -26,6 +26,7 @@ use ILIAS\Init\ErrorHandling\Http\ErrorPageResponder; use ILIAS\Init\ErrorHandling\Http\PlainTextFallbackResponder; +$message = null; try { require_once '../vendor/composer/vendor/autoload.php'; @@ -49,15 +50,19 @@ ); new ErrorPageResponder( - $DIC->globalScreen(), + $DIC->offsetExists('global_screen') ? $DIC->globalScreen() : null, $DIC->language(), - $DIC->ui(), - $DIC->http() + $DIC->http(), + $DIC->ui() )->respond( $message, StatusCode::HTTP_INTERNAL_SERVER_ERROR, $back_target ); } catch (Throwable $e) { - new PlainTextFallbackResponder()->respond($e); + new PlainTextFallbackResponder()->respond( + $e, + StatusCode::HTTP_INTERNAL_SERVER_ERROR, + $message + ); } diff --git a/components/ILIAS/Init/resources/ilias.php b/components/ILIAS/Init/resources/ilias.php index bf42ab16c6cd..f57945a4c7ab 100644 --- a/components/ILIAS/Init/resources/ilias.php +++ b/components/ILIAS/Init/resources/ilias.php @@ -18,9 +18,10 @@ declare(strict_types=1); -use ILIAS\HTTP\StatusCode; use ILIAS\Data\Factory as DataFactory; +use ILIAS\HTTP\StatusCode; use ILIAS\Init\ErrorHandling\Http\ErrorPageResponder; +use ILIAS\Init\ErrorHandling\Http\PlainTextFallbackResponder; if (!file_exists('../ilias.ini.php')) { die('The ILIAS setup is not completed. Please run the setup routine.'); @@ -28,39 +29,82 @@ require_once '../vendor/composer/vendor/autoload.php'; require_once __DIR__ . '/../artifacts/bootstrap_default.php'; -entry_point('ILIAS Legacy Initialisation Adapter'); - -/** @var \ILIAS\DI\Container $DIC */ -global $DIC; try { + entry_point('ILIAS Legacy Initialisation Adapter'); + global $DIC; $DIC->ctrl()->callBaseClass(); } catch (ilCtrlException $e) { + global $DIC; + if (defined('DEVMODE') && DEVMODE) { throw $e; } - $DIC->logger()->root()->error($e->getMessage()); - $DIC->logger()->root()->error($e->getTraceAsString()); + if ($DIC->offsetExists('ilLoggerFactory')) { + $DIC->logger()->root()->error($e->getMessage()); + $DIC->logger()->root()->error($e->getTraceAsString()); + } - $DIC->language()->loadLanguageModule('error'); - $df = new DataFactory(); - $back_target = $df->link( - $DIC->language()->txt('error_back_to_repository'), - $df->uri(ILIAS_HTTP_PATH . '/ilias.php?baseClass=ilRepositoryGUI') - ); + $repository_href = defined('ILIAS_HTTP_PATH') + ? ILIAS_HTTP_PATH . '/ilias.php?baseClass=ilRepositoryGUI' + : '/ilias.php?baseClass=ilRepositoryGUI'; - new ErrorPageResponder( - $DIC->globalScreen(), - $DIC->language(), - $DIC->ui(), - $DIC->http() - )->respond( - $DIC->language()->txt('http_404_not_found'), - StatusCode::HTTP_NOT_FOUND, - $back_target - ); + $public_message = null; + if ($DIC->offsetExists('lng')) { + $DIC->language()->loadLanguageModule('error'); + $public_message = $DIC->language()->txt('http_404_not_found'); + } + + $can_html_error_page = $DIC->offsetExists('lng') + && $DIC->offsetExists('http') + && $DIC->offsetExists('tpl') + && $DIC->offsetExists('ui.factory') + && $DIC->offsetExists('ui.renderer'); + + $can_plain_html_error_page = $DIC->offsetExists('lng') + && $DIC->offsetExists('http') + && $DIC->offsetExists('tpl') + && ( + !$DIC->offsetExists('ui.factory') + || !$DIC->offsetExists('ui.renderer') + ); + + try { + if ($can_html_error_page || $can_plain_html_error_page) { + $df = $DIC->offsetExists(\ILIAS\Data\Factory::class) + ? $DIC[\ILIAS\Data\Factory::class] + : new DataFactory(); + $back_target = $df->link( + $DIC->language()->txt('error_back_to_repository'), + $df->uri($repository_href) + ); + $shell = $can_html_error_page + ? $DIC->ui() + : $DIC['tpl']; + new ErrorPageResponder( + $DIC->offsetExists('global_screen') ? $DIC->globalScreen() : null, + $DIC->language(), + $DIC->http(), + $shell + )->respond( + $public_message ?? '', + StatusCode::HTTP_NOT_FOUND, + $back_target + ); + } + new PlainTextFallbackResponder()->respond($e, StatusCode::HTTP_NOT_FOUND, $public_message); + } catch (Throwable $t) { + new PlainTextFallbackResponder()->respond( + $t, + StatusCode::HTTP_NOT_FOUND, + $public_message + ); + } } +/** @var \ILIAS\DI\Container $DIC */ +global $DIC; + $DIC['ilBench']->save(); $DIC['http']?->close(); diff --git a/components/ILIAS/Init/src/ErrorHandling/Http/ErrorPageResponder.php b/components/ILIAS/Init/src/ErrorHandling/Http/ErrorPageResponder.php index f80efb4f37f7..e063e8d22dfd 100644 --- a/components/ILIAS/Init/src/ErrorHandling/Http/ErrorPageResponder.php +++ b/components/ILIAS/Init/src/ErrorHandling/Http/ErrorPageResponder.php @@ -24,6 +24,7 @@ use ilLanguage; use ILIAS\Data\Link; use ilGlobalTemplate; +use ilGlobalTemplateInterface; use ILIAS\DI\UIServices; use ILIAS\HTTP\Response\ResponseHeader; use ILIAS\HTTP\Services as HTTPServices; @@ -33,6 +34,11 @@ * Responder that renders a full ILIAS error page (UI-Framework MessageBox) * and sends it with the appropriate HTTP status code. * + * Pass {@see UIServices} as {@code $shell} for a MessageBox. If {@code ui.factory} + * / {@code ui.renderer} are not available, pass {@see ilGlobalTemplateInterface} + * (e.g. {@code $DIC['tpl']}) — {@code tpl.error.html} is then filled via + * {@code plain_html_fallback} (simple alert + link). + * * Use this when the DI container and all ILIAS services are available. * The consumer MUST wrap the main logic in a try-catch and call * {@see respond()} in the catch block for expected errors (e.g., routing @@ -41,14 +47,21 @@ * * The error message is rendered via MessageBox::failure(). If a back target * (Data\Link) is provided, it is embedded into the MessageBox via withButtons(). + * + * {@see GlobalScreenServices} may be null when ilCtrl fails during + * {@see ilInitialisation::initILIAS()} before GlobalScreen is registered; the + * external context claim is skipped in that case. */ -class ErrorPageResponder +readonly class ErrorPageResponder { + /** + * @param UIServices|ilGlobalTemplateInterface $shell {@see UIServices} or main page template without UI stack. + */ public function __construct( - private readonly GlobalScreenServices $global_screen, - private readonly ilLanguage $language, - private readonly UIServices $ui, - private readonly HTTPServices $http + private ?GlobalScreenServices $global_screen, + private ilLanguage $language, + private HTTPServices $http, + private UIServices|ilGlobalTemplateInterface $shell, ) { } @@ -56,28 +69,53 @@ public function respond( string $error_message, int $status_code, ?Link $back_target = null - ): void { - $this->global_screen->tool()->context()->claim()->external(); + ): never { + $this->global_screen?->tool()->context()->claim()->external(); + $this->language->loadLanguageModule('error'); - $message_box = $this->ui->factory()->messageBox()->failure($error_message); + $local_tpl = new ilGlobalTemplate('tpl.error.html', true, true); + + if ($this->shell instanceof UIServices) { + $message_box = $this->shell->factory()->messageBox()->failure($error_message); + + if ($back_target !== null) { + $message_box = $message_box->withButtons([ + $this->shell->factory()->button()->standard( + $back_target->getLabel(), + ilUtil::secureUrl((string) $back_target->getURL()) + ), + ]); + } - if ($back_target !== null) { - $ui_button = $this->ui->factory()->button()->standard( - $back_target->getLabel(), - ilUtil::secureUrl((string) $back_target->getURL()) + $local_tpl->setCurrentBlock('msg_box'); + $local_tpl->setVariable( + 'MESSAGE_BOX', + $this->shell->renderer()->render($message_box) ); - $message_box = $message_box->withButtons([$ui_button]); + $local_tpl->parseCurrentBlock(); + } else { + $local_tpl->setCurrentBlock('plain_html_fallback'); + $local_tpl->setVariable( + 'ERROR_MESSAGE', + htmlspecialchars($error_message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') + ); + if ($back_target !== null) { + $local_tpl->setVariable( + 'LINK_HREF', + ilUtil::secureUrl((string) $back_target->getURL()) + ); + $local_tpl->setVariable( + 'LINK_TEXT', + htmlspecialchars($back_target->getLabel(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') + ); + } else { + $local_tpl->setVariable('LINK_HREF', ''); + $local_tpl->setVariable('LINK_TEXT', ''); + } + $local_tpl->parseCurrentBlock(); } - $local_tpl = new ilGlobalTemplate('tpl.error.html', true, true); - $local_tpl->setCurrentBlock('msg_box'); - $local_tpl->setVariable( - 'MESSAGE_BOX', - $this->ui->renderer()->render($message_box) - ); - $local_tpl->parseCurrentBlock(); - $this->http->saveResponse( $this->http ->response() @@ -85,9 +123,17 @@ public function respond( ->withHeader(ResponseHeader::CONTENT_TYPE, 'text/html') ); - $this->ui->mainTemplate()->setContent($local_tpl->get()); - $this->ui->mainTemplate()->printToStdout(); + $main = $this->mainShellTemplate(); + $main->setContent($local_tpl->get()); + $main->printToStdout(); $this->http->close(); } + + private function mainShellTemplate(): ilGlobalTemplateInterface + { + return $this->shell instanceof UIServices + ? $this->shell->mainTemplate() + : $this->shell; + } } diff --git a/components/ILIAS/Init/src/ErrorHandling/Http/PlainTextFallbackResponder.php b/components/ILIAS/Init/src/ErrorHandling/Http/PlainTextFallbackResponder.php index 0a05876c432f..c0f7536c8a15 100644 --- a/components/ILIAS/Init/src/ErrorHandling/Http/PlainTextFallbackResponder.php +++ b/components/ILIAS/Init/src/ErrorHandling/Http/PlainTextFallbackResponder.php @@ -51,11 +51,15 @@ class PlainTextFallbackResponder * The status code defaults to 500 (Internal Server Error). The caller may pass * a different code when the failure context is known. * - * @param int $status_code HTTP status code (default: 500). + * @param int $status_code HTTP status code (default: 500). + * @param string|null $public_message If set, shown instead of the exception message (details stay in error_log). * @throws Throwable in DEVMODE */ - public function respond(Throwable $e, int $status_code = StatusCode::HTTP_INTERNAL_SERVER_ERROR): never - { + public function respond( + Throwable $e, + int $status_code = StatusCode::HTTP_INTERNAL_SERVER_ERROR, + ?string $public_message = null + ): never { if (defined('DEVMODE') && DEVMODE) { throw $e; } @@ -65,8 +69,8 @@ public function respond(Throwable $e, int $status_code = StatusCode::HTTP_INTERN header('Content-Type: text/plain; charset=UTF-8'); } - $incident_id = session_id() . '_' . (new \Random\Randomizer())->getInt(1, 9999); - $timestamp = (new DateTimeImmutable()) + $incident_id = session_id() . '_' . new \Random\Randomizer()->getInt(1, 9999); + $timestamp = new DateTimeImmutable() ->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s\Z'); @@ -77,7 +81,8 @@ public function respond(Throwable $e, int $status_code = StatusCode::HTTP_INTERN if ($e instanceof PDOException) { echo "Message: A database error occurred. Please contact the system administrator with the incident id.\n"; } else { - echo "Message: {$e->getMessage()}\n"; + $display = $public_message ?? $e->getMessage(); + echo "Message: {$display}\n"; } error_log(sprintf( diff --git a/templates/default/tpl.error.html b/templates/default/tpl.error.html index 219e2d866d32..3e7a0e506c06 100755 --- a/templates/default/tpl.error.html +++ b/templates/default/tpl.error.html @@ -1,3 +1,9 @@ {MESSAGE_BOX} - \ No newline at end of file + + +
+ {LINK_TEXT} +
+