diff --git a/components/ILIAS/Init/classes/ErrorHandling/Http/ErrorPageResponder.php b/components/ILIAS/Init/classes/ErrorHandling/Http/ErrorPageResponder.php new file mode 100644 index 000000000000..4d5cc9ebc380 --- /dev/null +++ b/components/ILIAS/Init/classes/ErrorHandling/Http/ErrorPageResponder.php @@ -0,0 +1,93 @@ +global_screen->tool()->context()->claim()->external(); + $this->language->loadLanguageModule('error'); + + $message_box = $this->ui->factory()->messageBox()->failure($error_message); + + if ($back_target !== null) { + $ui_button = $this->ui->factory()->button()->standard( + $back_target->getLabel(), + ilUtil::secureUrl((string) $back_target->getURL()) + ); + $message_box = $message_box->withButtons([$ui_button]); + } + + $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() + ->withStatus($status_code) + ->withHeader(ResponseHeader::CONTENT_TYPE, 'text/html') + ); + + $this->ui->mainTemplate()->setContent($local_tpl->get()); + $this->ui->mainTemplate()->printToStdout(); + + $this->http->close(); + } +} diff --git a/components/ILIAS/Init/classes/ErrorHandling/Http/PlainTextFallbackResponder.php b/components/ILIAS/Init/classes/ErrorHandling/Http/PlainTextFallbackResponder.php new file mode 100644 index 000000000000..06a100015ed3 --- /dev/null +++ b/components/ILIAS/Init/classes/ErrorHandling/Http/PlainTextFallbackResponder.php @@ -0,0 +1,96 @@ +getInt(1, 9999); + $timestamp = (new DateTimeImmutable()) + ->setTimezone(new DateTimeZone('UTC')) + ->format('Y-m-d\TH:i:s\Z'); + + echo "Internal Server Error\n"; + echo "Incident: $incident_id\n"; + echo "Timestamp: $timestamp\n"; + + 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"; + } + + 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() + )); + + exit(1); + } +} diff --git a/components/ILIAS/Init/classes/ErrorHandling/README.md b/components/ILIAS/Init/classes/ErrorHandling/README.md new file mode 100644 index 000000000000..607adbdb3f9a --- /dev/null +++ b/components/ILIAS/Init/classes/ErrorHandling/README.md @@ -0,0 +1,33 @@ +# Error Responders + +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. + +- **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. + +## Consumer responsibility + +**The consumer MUST implement a try-catch block.** Both responders must be invoked 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). + +Example: + +```php +try { + entry_point('ILIAS Legacy Initialisation Adapter'); + global $DIC; + (new ErrorPageResponder( + $DIC->globalScreen(), + $DIC->language(), + $DIC->ui(), + $DIC->http() + ))->respond($message, 500, $back_target); +} catch (Throwable $e) { + (new PlainTextFallbackResponder())->respond($e); +} +``` diff --git a/components/ILIAS/Init/resources/error.php b/components/ILIAS/Init/resources/error.php index a5d031a0dd93..3e124b6b5e27 100644 --- a/components/ILIAS/Init/resources/error.php +++ b/components/ILIAS/Init/resources/error.php @@ -20,71 +20,42 @@ namespace ILIAS\Init; +use Throwable; +use ILIAS\HTTP\StatusCode; +use ILIAS\Data\Factory as DataFactory; +use ILIAS\Init\ErrorHandling\Http\ErrorPageResponder; +use ILIAS\Init\ErrorHandling\Http\PlainTextFallbackResponder; + try { require_once '../vendor/composer/vendor/autoload.php'; \ilInitialisation::initILIAS(); - $DIC->globalScreen()->tool()->context()->claim()->external(); - - $lng->loadLanguageModule('error'); - $txt = $lng->txt('error_back_to_repository'); - - $local_tpl = new \ilGlobalTemplate('tpl.error.html', true, true); - $local_tpl->setCurrentBlock('ErrorLink'); - $local_tpl->setVariable('TXT_LINK', $txt); - $local_tpl->setVariable('LINK', \ilUtil::secureUrl(ILIAS_HTTP_PATH . '/ilias.php?baseClass=ilRepositoryGUI')); - $local_tpl->parseCurrentBlock(); + /** @var \ILIAS\DI\Container $DIC */ + global $DIC; \ilSession::clear('referer'); \ilSession::clear('message'); - $DIC->http()->saveResponse( - $DIC->http() - ->response() - ->withStatus(500) - ->withHeader(\ILIAS\HTTP\Response\ResponseHeader::CONTENT_TYPE, 'text/html') - ); - - $tpl->setContent($local_tpl->get()); - $tpl->printToStdout(); + $DIC->language()->loadLanguageModule('error'); - $DIC->http()->close(); -} catch (\Throwable $e) { - if (\defined('DEVMODE') && DEVMODE) { - throw $e; - } + $message = \ilSession::get('failure') ?? $DIC->language()->txt('http_500_internal_server_error'); - /* - * Since we are already in the `error.php` and an unexpected error occurred, we should not rely on the $DIC or any - * other components here and use "Vanilla PHP" instead to handle the error. - */ - if (!headers_sent()) { - http_response_code(500); - header('Content-Type: text/plain; charset=UTF-8'); - } - - $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'); - - echo "Internal Server Error\n"; - echo "Incident: $incident_id\n"; - echo "Timestamp: $timestamp\n"; - 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"; - } - - 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() - )); + $df = new DataFactory(); + $back_target = $df->link( + $DIC->language()->txt('error_back_to_repository'), + $df->uri(ILIAS_HTTP_PATH . '/ilias.php?baseClass=ilRepositoryGUI') + ); - exit(1); + (new ErrorPageResponder( + $DIC->globalScreen(), + $DIC->language(), + $DIC->ui(), + $DIC->http() + ))->respond( + $message, + StatusCode::HTTP_INTERNAL_SERVER_ERROR, + $back_target + ); +} catch (Throwable $e) { + (new PlainTextFallbackResponder())->respond($e); } diff --git a/components/ILIAS/Init/resources/ilias.php b/components/ILIAS/Init/resources/ilias.php index a356d442502d..cc79af0f874f 100644 --- a/components/ILIAS/Init/resources/ilias.php +++ b/components/ILIAS/Init/resources/ilias.php @@ -18,6 +18,10 @@ declare(strict_types=1); +use ILIAS\HTTP\StatusCode; +use ILIAS\Data\Factory as DataFactory; +use ILIAS\Init\ErrorHandling\Http\ErrorPageResponder; + if (!file_exists('../ilias.ini.php')) { die('The ILIAS setup is not completed. Please run the setup routine.'); } @@ -26,7 +30,7 @@ ilInitialisation::initILIAS(); -/** @var $DIC \ILIAS\DI\Container */ +/** @var \ILIAS\DI\Container $DIC */ global $DIC; try { @@ -36,14 +40,26 @@ throw $e; } - if (!str_contains($e->getMessage(), 'not given a baseclass') && - !str_contains($e->getMessage(), 'not a baseclass')) { - throw new RuntimeException(sprintf('ilCtrl could not dispatch request: %s', $e->getMessage()), 0, $e); - } - $DIC->logger()->root()->error($e->getMessage()); $DIC->logger()->root()->error($e->getTraceAsString()); - $DIC->ctrl()->redirectToURL(ilUtil::_getHttpPath()); + + $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') + ); + + (new ErrorPageResponder( + $DIC->globalScreen(), + $DIC->language(), + $DIC->ui(), + $DIC->http() + ))->respond( + $DIC->language()->txt('http_404_not_found'), + StatusCode::HTTP_NOT_FOUND, + $back_target + ); } $DIC['ilBench']->save(); diff --git a/components/ILIAS/UICore/classes/Path/class.ilCtrlExistingPath.php b/components/ILIAS/UICore/classes/Path/class.ilCtrlExistingPath.php index ded890d6c295..f9566e9c18b0 100755 --- a/components/ILIAS/UICore/classes/Path/class.ilCtrlExistingPath.php +++ b/components/ILIAS/UICore/classes/Path/class.ilCtrlExistingPath.php @@ -1,8 +1,24 @@ Extended GPL, see docs/LICENSE */ +use ILIAS\UICore\Exceptions\ilCtrlPathException; /** * Class ilCtrlExistingPath @@ -42,7 +58,7 @@ protected function ensureValidCidPath(): void $child_class = $this->structure->getClassNameByCid($child_cid); $allowed_children = $this->structure->getChildrenByCid($parent_cid) ?? []; if (null === $child_class || !in_array($child_class, $allowed_children, true)) { - throw new RuntimeException('ilCtrl: invalid ' . ilCtrlInterface::PARAM_CID_PATH . ' parameter requested.'); + throw new ilCtrlPathException('ilCtrl: invalid ' . ilCtrlInterface::PARAM_CID_PATH . ' parameter requested.'); } } } diff --git a/components/ILIAS/UICore/exceptions/ilCtrlPathException.php b/components/ILIAS/UICore/exceptions/ilCtrlPathException.php new file mode 100644 index 000000000000..10a5c4d60259 --- /dev/null +++ b/components/ILIAS/UICore/exceptions/ilCtrlPathException.php @@ -0,0 +1,31 @@ + - -

- {TXT_LINK} -

- + +{MESSAGE_BOX} + \ No newline at end of file