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
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

namespace ILIAS\Init\ErrorHandling\Http;

use ilUtil;
use ilLanguage;
use ILIAS\Data\Link;
use ilGlobalTemplate;
use ILIAS\DI\UIServices;
use ILIAS\HTTP\Response\ResponseHeader;
use ILIAS\HTTP\Services as HTTPServices;
use ILIAS\GlobalScreen\Services as GlobalScreenServices;

/**
* Responder that renders a full ILIAS error page (UI-Framework MessageBox)
* and sends it with the appropriate HTTP status code.
*
* 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
* failures). For unexpected errors during bootstrap, use
* {@see \ILIAS\Init\ErrorHandling\Http\PlainTextFallbackResponder} instead.
*
* The error message is rendered via MessageBox::failure(). If a back target
* (Data\Link) is provided, it is embedded into the MessageBox via withButtons().
*/
class ErrorPageResponder
{
public function __construct(
private readonly GlobalScreenServices $global_screen,
private readonly ilLanguage $language,
private readonly UIServices $ui,
private readonly HTTPServices $http
) {
}

public function respond(
string $error_message,
int $status_code,
?Link $back_target = null
): void {
$this->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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

namespace ILIAS\Init\ErrorHandling\Http;

use PDOException;
use Throwable;
use DateTimeZone;
use DateTimeImmutable;
use ILIAS\HTTP\StatusCode;

/**
* Responder that sends a minimal plain-text error response without relying on
* any ILIAS service (no DIC, no UI framework, no templates).
*
* Use this as a last-resort fallback 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.
*
* The consumer MUST wrap the bootstrap / main logic in a try-catch and call
* {@see respond()} in the catch block. In DEVMODE the exception is re-thrown
* so that Whoops / the developer can inspect the full stack trace.
*
* This responder always works: it uses only PHP built-ins (headers, echo,
* error_log, exit). Prefer {@see \ILIAS\Init\ErrorHandling\Http\ErrorPageResponder}
* when the DIC is available, as it renders a proper ILIAS page with the
* UI framework.
*/
class PlainTextFallbackResponder
{
/**
* Send a minimal plain-text error response and terminate the process.
*
* 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).
* @throws Throwable in DEVMODE
*/
public function respond(Throwable $e, int $status_code = StatusCode::HTTP_INTERNAL_SERVER_ERROR): never
{
if (defined('DEVMODE') && DEVMODE) {
throw $e;
}

if (!headers_sent()) {
http_response_code($status_code);
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()
));

exit(1);
}
}
33 changes: 33 additions & 0 deletions components/ILIAS/Init/classes/ErrorHandling/README.md
Original file line number Diff line number Diff line change
@@ -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);
}
```
83 changes: 27 additions & 56 deletions components/ILIAS/Init/resources/error.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
30 changes: 23 additions & 7 deletions components/ILIAS/Init/resources/ilias.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand All @@ -26,7 +30,7 @@

ilInitialisation::initILIAS();

/** @var $DIC \ILIAS\DI\Container */
/** @var \ILIAS\DI\Container $DIC */
global $DIC;

try {
Expand All @@ -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();
Expand Down
Loading