diff --git a/components/ILIAS/Init/classes/ErrorHandling/README.md b/components/ILIAS/Init/classes/ErrorHandling/README.md index cef25f90620d..17de2bef43ab 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` blocks `plain_html_fallback` / optional `plain_html_back_link` 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 45cf471b2a4e..e6a62f46d40b 100644 --- a/components/ILIAS/Init/resources/ilias.php +++ b/components/ILIAS/Init/resources/ilias.php @@ -18,8 +18,8 @@ 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; @@ -38,39 +38,76 @@ $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'; - try { - 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') ); - } catch (Throwable) { + + 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( - $e, + $t, StatusCode::HTTP_NOT_FOUND, - $DIC->language()->txt('http_404_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..f58d41b0229d 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} uses {@code plain_html_fallback} + * for the message and, if a {@see Link} is passed, {@code plain_html_back_link} for the anchor. + * * 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,27 +69,55 @@ 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 ($back_target !== null) { - $ui_button = $this->ui->factory()->button()->standard( - $back_target->getLabel(), - ilUtil::secureUrl((string) $back_target->getURL()) + 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()) + ), + ]); + } + + $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(); + $content_html = $local_tpl->get(); + } else { + $local_tpl->setCurrentBlock('plain_html_fallback'); + $local_tpl->setVariable( + 'ERROR_MESSAGE', + htmlspecialchars($error_message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') + ); + $local_tpl->parseCurrentBlock(); + $content_html = $local_tpl->get('plain_html_fallback'); - $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(); + if ($back_target !== null) { + $local_tpl->setCurrentBlock('plain_html_back_link'); + $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') + ); + $local_tpl->parseCurrentBlock(); + $content_html .= $local_tpl->get('plain_html_back_link'); + } + } $this->http->saveResponse( $this->http @@ -85,9 +126,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($content_html); + $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 4bcdcd96c7df..cce57e1edc2c 100644 --- a/components/ILIAS/Init/src/ErrorHandling/Http/PlainTextFallbackResponder.php +++ b/components/ILIAS/Init/src/ErrorHandling/Http/PlainTextFallbackResponder.php @@ -51,15 +51,16 @@ 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, - ?string $status_message = null + ?string $public_message = null ): never { - if (\defined('DEVMODE') && DEVMODE) { + if (defined('DEVMODE') && DEVMODE) { throw $e; } @@ -69,8 +70,8 @@ public function respond( } $session_prefix = session_id() !== '' ? session_id() : 'no-session'; - $incident_id = $session_prefix . '_' . (new \Random\Randomizer())->getInt(1, 9999); - $timestamp = (new DateTimeImmutable()) + $incident_id = $session_prefix . '_' . new \Random\Randomizer()->getInt(1, 9999); + $timestamp = new DateTimeImmutable() ->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s\Z'); @@ -81,20 +82,22 @@ public function respond( 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( - "[%s] INCIDENT %s — Uncaught %s: %s in %s:%d\nStack trace:\n%s\n", - $timestamp, - $incident_id, - \get_class($e), - $e->getMessage(), - $e->getFile(), - $e->getLine(), - $e->getTraceAsString() - )); + "[%s] INCIDENT %s — Uncaught %s: %s in %s:%d\nStack trace:\n%s\n", + $timestamp, + $incident_id, + \get_class($e), + $e->getMessage(), + $e->getFile(), + $e->getLine(), + $e->getTraceAsString() + ) + ); exit(1); } diff --git a/templates/default/tpl.error.html b/templates/default/tpl.error.html index 219e2d866d32..04e25606c9a6 100755 --- a/templates/default/tpl.error.html +++ b/templates/default/tpl.error.html @@ -1,3 +1,11 @@ {MESSAGE_BOX} - \ No newline at end of file + + + + + +

+ {LINK_TEXT} +

+