Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions components/ILIAS/Init/classes/ErrorHandling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ 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:

```php
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);
}
```
13 changes: 9 additions & 4 deletions components/ILIAS/Init/resources/error.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
);
}
90 changes: 67 additions & 23 deletions components/ILIAS/Init/resources/ilias.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,93 @@

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.');
}

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();
92 changes: 69 additions & 23 deletions components/ILIAS/Init/src/ErrorHandling/Http/ErrorPageResponder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -41,53 +47,93 @@
*
* 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,
) {
}

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', '');
Comment on lines +113 to +114
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not render an a element with empty attributes/innerHTML in this case. IMO we should not render this element at all here.

}
$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()
->withStatus($status_code)
->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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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');

Expand All @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion templates/default/tpl.error.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
<!-- BEGIN msg_box -->
{MESSAGE_BOX}
<!-- END msg_box -->
<!-- END msg_box -->
<!-- BEGIN plain_html_fallback -->
<div class="alert alert-danger" role="alert">{ERROR_MESSAGE}</div>
<p>
<a class="btn btn-default" href="{LINK_HREF}">{LINK_TEXT}</a>
</p>
<!-- END plain_html_fallback -->