From d6ec16d9efa65087d89c365c2ca220ec57c6562b Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 09:27:40 +0200 Subject: [PATCH 01/12] [FEATURE] Init/ErrorHandling: Add `ErrorIncident` domain model --- .../ErrorHandling/Incident/ErrorIncident.php | 38 +++++++++++++++++ .../Incident/ErrorIncidentFactory.php | 29 +++++++++++++ .../Incident/ErrorIncidentId.php | 41 +++++++++++++++++++ .../Incident/ErrorIncidentRegistry.php | 33 +++++++++++++++ .../InMemoryErrorIncidentRegistry.php | 41 +++++++++++++++++++ .../SessionPrefixedErrorIncidentFactory.php | 40 ++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncident.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentFactory.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentId.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentRegistry.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Incident/InMemoryErrorIncidentRegistry.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Incident/SessionPrefixedErrorIncidentFactory.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncident.php b/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncident.php new file mode 100644 index 000000000000..0088adc823c3 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncident.php @@ -0,0 +1,38 @@ +id; + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentFactory.php b/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentFactory.php new file mode 100644 index 000000000000..66e0220aac6d --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentFactory.php @@ -0,0 +1,29 @@ +value; + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentRegistry.php b/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentRegistry.php new file mode 100644 index 000000000000..41fb19438355 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Incident/ErrorIncidentRegistry.php @@ -0,0 +1,33 @@ +current = $incident; + } + + public function current(): ?ErrorIncident + { + return $this->current; + } + + public function clear(): void + { + $this->current = null; + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Incident/SessionPrefixedErrorIncidentFactory.php b/components/ILIAS/Init/src/ErrorHandling/Incident/SessionPrefixedErrorIncidentFactory.php new file mode 100644 index 000000000000..e1d15d77cf3f --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Incident/SessionPrefixedErrorIncidentFactory.php @@ -0,0 +1,40 @@ +randomizer->getInt(1, 9999); + + return new ErrorIncident(new ErrorIncidentId($session_prefix . '_' . $error_number)); + } +} From 8fe7042690e756d594c13c8d7895f2c3d83ea797 Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 09:38:38 +0200 Subject: [PATCH 02/12] [FEATURE] Init/ErrorHandling: Add notification formatter --- .../Notification/ErrorIncidentUserMessage.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Notification/ErrorIncidentUserMessage.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Notification/ErrorIncidentUserMessage.php b/components/ILIAS/Init/src/ErrorHandling/Notification/ErrorIncidentUserMessage.php new file mode 100644 index 000000000000..a2355c1011bf --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Notification/ErrorIncidentUserMessage.php @@ -0,0 +1,67 @@ +identifier()->value(); + + if ($language !== null) { + $language->loadLanguageModule('logging'); + $message = \sprintf($language->txt('log_error_message'), $identifier); + + $mail = $this->error_settings->mail(); + if ($mail !== '') { + $message .= ' ' . \sprintf( + $language->txt('log_error_message_send_mail'), + $mail, + $identifier, + $mail + ); + } + + return $message; + } + + $message = 'Sorry, an error occured. A logfile has been created which can be identified via the code "' + . $identifier . '"'; + + $mail = $this->error_settings->mail(); + if ($mail !== '') { + $message .= ' ' . 'Please send a mail to ' . $mail . ''; + } + + return $message; + } +} From 6fb6567eb687b4b804bcb37cc4a2c39029b2032f Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 09:40:26 +0200 Subject: [PATCH 03/12] [FEATURE] Init/ErrorHandling: Add `ReportErrorIncident` use case and application ports --- .../Application/DevmodeState.php | 32 ++++++++++ .../Application/ErrorIncidentReporting.php | 32 ++++++++++ .../Application/ErrorLogDirectory.php | 29 +++++++++ .../Application/ErrorLogFileStorage.php | 39 ++++++++++++ .../ProductionOnlyErrorIncidentReporting.php | 46 ++++++++++++++ .../Application/ReportErrorIncident.php | 61 +++++++++++++++++++ 6 files changed, 239 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/DevmodeState.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/ErrorIncidentReporting.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/ErrorLogDirectory.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/ErrorLogFileStorage.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/ProductionOnlyErrorIncidentReporting.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/ReportErrorIncident.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Application/DevmodeState.php b/components/ILIAS/Init/src/ErrorHandling/Application/DevmodeState.php new file mode 100644 index 000000000000..42a3891a8da3 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Application/DevmodeState.php @@ -0,0 +1,32 @@ + $sensitive_parameter_names + */ + public function write( + Inspector $inspector, + string $directory, + string $file_name, + array $sensitive_parameter_names + ): void; +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Application/ProductionOnlyErrorIncidentReporting.php b/components/ILIAS/Init/src/ErrorHandling/Application/ProductionOnlyErrorIncidentReporting.php new file mode 100644 index 000000000000..20776fb33210 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Application/ProductionOnlyErrorIncidentReporting.php @@ -0,0 +1,46 @@ +devmode_state->isActive()) { + return null; + } + + return $this->reporting->report($inspector); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Application/ReportErrorIncident.php b/components/ILIAS/Init/src/ErrorHandling/Application/ReportErrorIncident.php new file mode 100644 index 000000000000..270af6daa88e --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Application/ReportErrorIncident.php @@ -0,0 +1,61 @@ + */ + private readonly array $sensitive_parameter_names + ) { + } + + public function report(Inspector $inspector): ?ErrorIncident + { + $directory = $this->log_directory->path(); + if ($directory === '') { + return null; + } + + $incident = $this->incident_factory->create(session_id()); + $this->log_file_storage->write( + $inspector, + $directory, + $incident->identifier()->value(), + $this->sensitive_parameter_names + ); + $this->incident_registry->record($incident); + + return $incident; + } +} From 3aedc87ceeeeda002924d5e348f555ba8983849a Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 10:26:00 +0200 Subject: [PATCH 04/12] [FEATURE] Init/ErrorHandling: Add new interface to mask `ilErrorHandling` in `DelegatingHandler` --- .../ContextErrorHandlerProvider.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Application/ContextErrorHandlerProvider.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Application/ContextErrorHandlerProvider.php b/components/ILIAS/Init/src/ErrorHandling/Application/ContextErrorHandlerProvider.php new file mode 100644 index 000000000000..4a9c36464949 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Application/ContextErrorHandlerProvider.php @@ -0,0 +1,31 @@ + Date: Tue, 9 Jun 2026 13:49:17 +0200 Subject: [PATCH 05/12] [FEATURE] Init/ErrorHandling: Add DEVMODE state implementation --- .../Environment/RuntimeDevmodeState.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Infrastructure/Environment/RuntimeDevmodeState.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Environment/RuntimeDevmodeState.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Environment/RuntimeDevmodeState.php new file mode 100644 index 000000000000..b6860fb8fd4a --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Environment/RuntimeDevmodeState.php @@ -0,0 +1,37 @@ + Date: Tue, 9 Jun 2026 09:46:50 +0200 Subject: [PATCH 06/12] [FEATURE] Init/ErrorHandling: Add "Whoops" handler for unified error logging --- .../Whoops/RecordErrorIncidentHandler.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/RecordErrorIncidentHandler.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/RecordErrorIncidentHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/RecordErrorIncidentHandler.php new file mode 100644 index 000000000000..93db5c4d366f --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/RecordErrorIncidentHandler.php @@ -0,0 +1,42 @@ +error_incident_reporting->report($this->getInspector()); + + return null; + } +} From 8fd3a4216d2d72254bc4c26eb46356c033612a91 Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 09:50:00 +0200 Subject: [PATCH 07/12] [FEATURE] Init/ErrorHandling: Add outbound logging adapters for error incident reporting --- .../LoggingErrorFileStorageAdapter.php | 38 +++++++++++++++++++ .../Logging/LoggingErrorLogDirectory.php | 31 +++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorFileStorageAdapter.php create mode 100644 components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorLogDirectory.php diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorFileStorageAdapter.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorFileStorageAdapter.php new file mode 100644 index 000000000000..bf2ba44f315c --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorFileStorageAdapter.php @@ -0,0 +1,38 @@ +withExclusionList($sensitive_parameter_names); + $writer->write(); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorLogDirectory.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorLogDirectory.php new file mode 100644 index 000000000000..30fab2c48bdf --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LoggingErrorLogDirectory.php @@ -0,0 +1,31 @@ +folder(); + } +} From 3a10c964c3905c36fddc2a090278d104d3433b1a Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 10:13:13 +0200 Subject: [PATCH 08/12] [FEATURE] Init/ErrorHandling: Move error handlers to `src` --- .../Whoops/DelegatingHandler.php} | 17 +++++----- .../Whoops/PlainTextHandler.php} | 31 +++++++------------ .../Whoops/SoapExceptionHandler.php} | 21 +++++++++---- .../Infrastructure/Whoops/TestingHandler.php} | 5 +-- 4 files changed, 40 insertions(+), 34 deletions(-) rename components/ILIAS/Init/{classes/ErrorHandling/class.ilDelegatingHandler.php => src/ErrorHandling/Infrastructure/Whoops/DelegatingHandler.php} (89%) rename components/ILIAS/Init/{classes/ErrorHandling/class.ilPlainTextHandler.php => src/ErrorHandling/Infrastructure/Whoops/PlainTextHandler.php} (87%) rename components/ILIAS/Init/{classes/ErrorHandling/class.ilSoapExceptionHandler.php => src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php} (81%) rename components/ILIAS/Init/{classes/ErrorHandling/class.ilTestingHandler.php => src/ErrorHandling/Infrastructure/Whoops/TestingHandler.php} (89%) diff --git a/components/ILIAS/Init/classes/ErrorHandling/class.ilDelegatingHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/DelegatingHandler.php similarity index 89% rename from components/ILIAS/Init/classes/ErrorHandling/class.ilDelegatingHandler.php rename to components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/DelegatingHandler.php index 221a908a9030..44884b1cc182 100755 --- a/components/ILIAS/Init/classes/ErrorHandling/class.ilDelegatingHandler.php +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/DelegatingHandler.php @@ -18,6 +18,9 @@ declare(strict_types=1); +namespace ILIAS\Init\ErrorHandling\Infrastructure\Whoops; + +use ILIAS\Init\ErrorHandling\Application\ContextErrorHandlerProvider; use Whoops\Handler\Handler; use Whoops\Handler\HandlerInterface; @@ -30,9 +33,8 @@ * workaround. * This class is not ment to be extended, as the definition of error handlers should be handled in one place * in ilErrorHandling, so this class acts rather dump and asks ilErrorHandling for a handler. - * @author Richard Klees */ -final class ilDelegatingHandler extends Handler +final class DelegatingHandler extends Handler { private ?HandlerInterface $current_handler = null; @@ -40,7 +42,7 @@ final class ilDelegatingHandler extends Handler * @param list $sensitive_data */ public function __construct( - private readonly ilErrorHandling $error_handling, + private readonly ContextErrorHandlerProvider $error_handling, private readonly array $sensitive_data = [] ) { } @@ -48,15 +50,15 @@ public function __construct( private function hideSensitiveData(array $key_value_pairs): array { foreach ($key_value_pairs as $key => &$value) { - if (is_array($value)) { + if (\is_array($value)) { $value = $this->hideSensitiveData($value); } - if (is_string($value) && in_array($key, $this->sensitive_data, true)) { + if (\is_string($value) && \in_array($key, $this->sensitive_data, true)) { $value = 'REMOVED FOR SECURITY'; } - if ($key === 'PHPSESSID' && is_string($value)) { + if ($key === 'PHPSESSID' && \is_string($value)) { $value = substr($value, 0, 5) . ' (SHORTENED FOR SECURITY)'; } @@ -85,7 +87,7 @@ private function hideSensitiveData(array $key_value_pairs): array */ public function handle(): ?int { - if (defined('IL_INITIAL_WD')) { + if (\defined('IL_INITIAL_WD')) { chdir(IL_INITIAL_WD); } @@ -103,6 +105,7 @@ public function handle(): ?int $this->current_handler->setRun($this->getRun()); $this->current_handler->setException($this->getException()); $this->current_handler->setInspector($this->getInspector()); + return $this->current_handler->handle(); } diff --git a/components/ILIAS/Init/classes/ErrorHandling/class.ilPlainTextHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/PlainTextHandler.php similarity index 87% rename from components/ILIAS/Init/classes/ErrorHandling/class.ilPlainTextHandler.php rename to components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/PlainTextHandler.php index ef66bf13c729..bfc6e0f1997c 100755 --- a/components/ILIAS/Init/classes/ErrorHandling/class.ilPlainTextHandler.php +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/PlainTextHandler.php @@ -18,16 +18,19 @@ declare(strict_types=1); +namespace ILIAS\Init\ErrorHandling\Infrastructure\Whoops; + +use Throwable; use Whoops\Exception\Formatter; +use Whoops\Handler\PlainTextHandler as WhoopsPlainTextHandler; /** * A Whoops error handler that prints the same content as the PrettyPageHandler but as plain text. * This is used for better coexistence with xdebug, see #16627. - * @author Richard Klees */ -class ilPlainTextHandler extends \Whoops\Handler\PlainTextHandler +class PlainTextHandler extends WhoopsPlainTextHandler { - protected const KEY_SPACE = 25; + protected const int KEY_SPACE = 25; /** @var list */ private array $exclusion_list = []; @@ -39,6 +42,7 @@ public function withExclusionList(array $exclusion_list): self { $clone = clone $this; $clone->exclusion_list = $exclusion_list; + return $clone; } @@ -54,18 +58,15 @@ public function generateResponse(): string protected function getSimpleExceptionOutput(Throwable $exception): string { - return sprintf( + return \sprintf( '%s: %s in file %s on line %d', - get_class($exception), + $exception::class, $exception->getMessage(), $exception->getFile(), $exception->getLine() ); } - /** - * Get a short info about the exception. - */ protected function getPlainTextExceptionOutput(bool $with_previous = true): string { $message = Formatter::formatExceptionPlain($this->getInspector()); @@ -82,20 +83,15 @@ protected function getPlainTextExceptionOutput(bool $with_previous = true): stri return $message; } - /** - * Get the header for the page. - */ protected function tablesContent(): string { $ret = ''; foreach ($this->tables() as $title => $content) { $ret .= "\n\n-- $title --\n\n"; - if (count($content) > 0) { + if ($content !== []) { foreach ($content as $key => $value) { $key = str_pad((string) $key, self::KEY_SPACE); - // indent multiline values, first print_r, split in lines, - // indent all but first line, then implode again. $first = true; $indentation = str_pad('', self::KEY_SPACE); $value = implode( @@ -106,6 +102,7 @@ static function ($line) use (&$first, $indentation): string { $first = false; return $line; } + return $indentation . $line; }, explode("\n", print_r($value, true)) @@ -122,9 +119,6 @@ static function ($line) use (&$first, $indentation): string { return $this->stripNullBytes($ret); } - /** - * Get the tables that should be rendered. - */ protected function tables(): array { $post = $_POST; @@ -170,8 +164,7 @@ private function hideSensitiveData(array $super_global): array */ private function shortenPHPSessionId(array $server): array { - $cookie_content = $server['HTTP_COOKIE']; - $cookie_content = explode(';', $cookie_content); + $cookie_content = explode(';', $server['HTTP_COOKIE']); foreach ($cookie_content as $key => $content) { $content_array = explode('=', $content); diff --git a/components/ILIAS/Init/classes/ErrorHandling/class.ilSoapExceptionHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php similarity index 81% rename from components/ILIAS/Init/classes/ErrorHandling/class.ilSoapExceptionHandler.php rename to components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php index 6efb7c69662e..f5fb1265ed07 100644 --- a/components/ILIAS/Init/classes/ErrorHandling/class.ilSoapExceptionHandler.php +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php @@ -18,15 +18,24 @@ declare(strict_types=1); -class ilSoapExceptionHandler extends \Whoops\Handler\Handler +namespace ILIAS\Init\ErrorHandling\Infrastructure\Whoops; + +use Throwable; +use Whoops\Exception\Formatter; +use Whoops\Handler\Handler; + +/** + * Whoops handler that renders SOAP fault responses for SOAP POST requests. + */ +final class SoapExceptionHandler extends Handler { private function buildFaultString(): string { - if (!defined('DEVMODE') || DEVMODE !== 1) { + if (!\defined('DEVMODE') || DEVMODE !== 1) { return htmlspecialchars($this->getInspector()->getException()->getMessage()); } - $fault_string = \Whoops\Exception\Formatter::formatExceptionPlain($this->getInspector()); + $fault_string = Formatter::formatExceptionPlain($this->getInspector()); $exception = $this->getInspector()->getException(); $previous = $exception->getPrevious(); while ($previous) { @@ -39,9 +48,9 @@ private function buildFaultString(): string private function getSimpleExceptionOutput(Throwable $exception): string { - return sprintf( + return \sprintf( '%s: %s in file %s on line %d', - get_class($exception), + $exception::class, $exception->getMessage(), $exception->getFile(), $exception->getLine() @@ -52,7 +61,7 @@ public function handle(): ?int { echo $this->toXml(); - return \Whoops\Handler\Handler::QUIT; + return Handler::QUIT; } private function toXml(): string diff --git a/components/ILIAS/Init/classes/ErrorHandling/class.ilTestingHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/TestingHandler.php similarity index 89% rename from components/ILIAS/Init/classes/ErrorHandling/class.ilTestingHandler.php rename to components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/TestingHandler.php index 9fed81e5416a..89f5bc4d47f1 100755 --- a/components/ILIAS/Init/classes/ErrorHandling/class.ilTestingHandler.php +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/TestingHandler.php @@ -18,13 +18,14 @@ declare(strict_types=1); +namespace ILIAS\Init\ErrorHandling\Infrastructure\Whoops; + /** * A Whoops error handler for testing. * This yields the same output as the plain text handler, but prints a nice message to the tester on top of * the page. - * @author Richard Klees */ -class ilTestingHandler extends ilPlainTextHandler +final class TestingHandler extends PlainTextHandler { public function generateResponse(): string { From 707c04d0549ffb80fda3997fafa7b4c00744312d Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 12:39:55 +0200 Subject: [PATCH 09/12] [FEATURE] Init/ErrorHandling: Provide incident id in SOAP-based handler --- .../Whoops/SoapExceptionHandler.php | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php index f5fb1265ed07..f4ccf3e838c5 100644 --- a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php @@ -20,6 +20,8 @@ namespace ILIAS\Init\ErrorHandling\Infrastructure\Whoops; +use ILIAS\Init\ErrorHandling\Application\DevmodeState; +use ILIAS\Init\ErrorHandling\Incident\ErrorIncidentRegistry; use Throwable; use Whoops\Exception\Formatter; use Whoops\Handler\Handler; @@ -29,18 +31,30 @@ */ final class SoapExceptionHandler extends Handler { + public function __construct( + private readonly ErrorIncidentRegistry $incident_registry, + private readonly DevmodeState $devmode_state + ) { + } + private function buildFaultString(): string { - if (!\defined('DEVMODE') || DEVMODE !== 1) { - return htmlspecialchars($this->getInspector()->getException()->getMessage()); + $incident = $this->incident_registry->current(); + + if ($this->devmode_state->isActive()) { + $fault_string = Formatter::formatExceptionPlain($this->getInspector()); + $exception = $this->getInspector()->getException(); + $previous = $exception->getPrevious(); + while ($previous) { + $fault_string .= "\n\nCaused by\n" . $this->getSimpleExceptionOutput($previous); + $previous = $previous->getPrevious(); + } + } else { + $fault_string = $this->getInspector()->getException()->getMessage(); } - $fault_string = Formatter::formatExceptionPlain($this->getInspector()); - $exception = $this->getInspector()->getException(); - $previous = $exception->getPrevious(); - while ($previous) { - $fault_string .= "\n\nCaused by\n" . $this->getSimpleExceptionOutput($previous); - $previous = $previous->getPrevious(); + if ($incident !== null) { + $fault_string .= "\n\n (incident code: " . $incident->identifier()->value() . ')'; } return htmlspecialchars($fault_string); From 1888f60a77a08cfcf4bb8d9250d7ecb1d33dffb2 Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 11:11:57 +0200 Subject: [PATCH 10/12] [FEATURE] Init/ErrorHandling: Unify error logging --- .../Init/classes/class.ilErrorHandling.php | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/components/ILIAS/Init/classes/class.ilErrorHandling.php b/components/ILIAS/Init/classes/class.ilErrorHandling.php index 7676978ba4c1..33807ca5b0bd 100755 --- a/components/ILIAS/Init/classes/class.ilErrorHandling.php +++ b/components/ILIAS/Init/classes/class.ilErrorHandling.php @@ -18,6 +18,9 @@ declare(strict_types=1); +use ILIAS\Init\ErrorHandling\Infrastructure\Whoops as ErrorHandlers; +use ILIAS\Init\ErrorHandling\Infrastructure\Logging as ErrorLogging; +use ILIAS\Init\ErrorHandling; use Whoops\Run; use Whoops\RunInterface; use Whoops\Handler\PrettyPageHandler; @@ -34,7 +37,7 @@ * @todo when an error occured and clicking the back button to return to previous page the referer-var in session is deleted -> server error * @todo This class is a candidate for a singleton. initHandlers could only be called once per process anyways, as it checks for static $handlers_registered. */ -class ilErrorHandling +class ilErrorHandling implements ErrorHandling\Application\ContextErrorHandlerProvider { /** @var list */ private const array SENSTIVE_PARAMETER_NAMES = [ @@ -55,6 +58,8 @@ class ilErrorHandling protected ?RunInterface $whoops; protected string $message; + protected ErrorHandling\Incident\ErrorIncidentRegistry $error_incident_registry; + protected ErrorHandling\Application\DevmodeState $devmode_state; /** Error level 1: exit application immedietly */ public int $FATAL = 1; /** Error level 2: show warning page */ @@ -67,6 +72,8 @@ public function __construct() $this->FATAL = 1; $this->WARNING = 2; $this->MESSAGE = 3; + $this->error_incident_registry = new ErrorHandling\Incident\InMemoryErrorIncidentRegistry(); + $this->devmode_state = new ErrorHandling\Infrastructure\Environment\RuntimeDevmodeState(); $this->initWhoopsHandlers(); @@ -89,10 +96,26 @@ protected function initWhoopsHandlers(): void $runtime = $this->getRuntime(); $this->whoops = $this->getWhoops(); - $this->whoops->pushHandler(new ilDelegatingHandler($this, self::SENSTIVE_PARAMETER_NAMES)); + $this->whoops->pushHandler( + new ErrorHandlers\DelegatingHandler($this, self::SENSTIVE_PARAMETER_NAMES) + ); if ($runtime->shouldLogErrors()) { $this->whoops->pushHandler($this->loggingHandler()); } + $this->whoops->pushHandler( + new ErrorHandlers\RecordErrorIncidentHandler( + new ErrorHandling\Application\ProductionOnlyErrorIncidentReporting( + new ErrorHandling\Application\ReportErrorIncident( + new ErrorLogging\LoggingErrorLogDirectory(), + new ErrorLogging\LoggingErrorFileStorageAdapter(), + new ErrorHandling\Incident\SessionPrefixedErrorIncidentFactory(), + $this->error_incident_registry, + self::SENSTIVE_PARAMETER_NAMES + ), + $this->devmode_state + ) + ) + ); $this->whoops->register(); self::$whoops_handlers_registered = true; @@ -106,7 +129,10 @@ public function getHandler(): HandlerInterface { if (ilContext::getType() === ilContext::CONTEXT_SOAP && strcasecmp($_SERVER['REQUEST_METHOD'] ?? '', 'post') === 0) { - return new ilSoapExceptionHandler(); + return new ErrorHandlers\SoapExceptionHandler( + $this->error_incident_registry, + $this->devmode_state + ); } // TODO: There might be more specific execution contexts (WebDAV, REST, etc.) that need specific error handling. @@ -222,7 +248,7 @@ protected function getWhoops(): RunInterface protected function isDevmodeActive(): bool { - return defined('DEVMODE') && (int) DEVMODE === 1; + return $this->devmode_state->isActive(); } protected function defaultHandler(): HandlerInterface @@ -230,40 +256,18 @@ protected function defaultHandler(): HandlerInterface return new CallbackHandler(function ($exception, Inspector $inspector, Run $run) { global $DIC; - $logger = ilLoggingErrorSettings::getInstance(); - $message = 'Sorry, an error occured.'; if ($DIC->isDependencyAvailable('language')) { $DIC->language()->loadLanguageModule('logging'); $message = $DIC->language()->txt('error_sry_error'); } - if (!empty($logger->folder())) { - $session_id = substr(session_id(), 0, 5); - $r = new \Random\Randomizer(); - $err_num = $r->getInt(1, 9999); - $file_name = $session_id . '_' . $err_num; - - $lwriter = new ilLoggingErrorFileStorage($inspector, $logger->folder(), $file_name); - $lwriter = $lwriter->withExclusionList(self::SENSTIVE_PARAMETER_NAMES); - $lwriter->write(); - - if ($DIC->isDependencyAvailable('language')) { - $message = sprintf($DIC->language()->txt('log_error_message'), $file_name); - if ($logger->mail()) { - $message .= ' ' . sprintf( - $DIC->language()->txt('log_error_message_send_mail'), - $logger->mail(), - $file_name, - $logger->mail() - ); - } - } else { - $message = 'Sorry, an error occured. A logfile has been created which can be identified via the code "' . $file_name . '"'; - if ($logger->mail()) { - $message .= ' ' . 'Please send a mail to ' . $logger->mail() . ''; - } - } + $incident = $this->error_incident_registry->current(); + if ($incident !== null) { + $language = $DIC->isDependencyAvailable('language') ? $DIC->language() : null; + $message = new ErrorHandling\Notification\ErrorIncidentUserMessage( + ilLoggingErrorSettings::getInstance() + )->format($incident, $language); } if ($DIC->isDependencyAvailable('ui') && isset($DIC['tpl']) && $DIC->isDependencyAvailable('ctrl')) { @@ -283,10 +287,12 @@ protected function devmodeHandler(): HandlerInterface switch (ERROR_HANDLER) { case 'TESTING': - return (new ilTestingHandler())->withExclusionList(self::SENSTIVE_PARAMETER_NAMES); + return new ErrorHandlers\TestingHandler() + ->withExclusionList(self::SENSTIVE_PARAMETER_NAMES); case 'PLAIN_TEXT': - return (new ilPlainTextHandler())->withExclusionList(self::SENSTIVE_PARAMETER_NAMES); + return new ErrorHandlers\PlainTextHandler() + ->withExclusionList(self::SENSTIVE_PARAMETER_NAMES); case 'PRETTY_PAGE': // fallthrough From 4d4412b1770a2a24ef73ad3d03fecc93d57bdeae Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 11:07:35 +0200 Subject: [PATCH 11/12] [FEATURE] Init/ErrorHandling: Add error handling tests --- .../ErrorHandling/ErrorIncidentIdTest.php | 40 ++++++ .../tests/ErrorHandling/ErrorIncidentTest.php | 35 ++++++ .../ErrorIncidentUserMessageTest.php | 59 +++++++++ .../InMemoryErrorIncidentRegistryTest.php | 54 ++++++++ .../LoggingErrorFileStorageAdapterTest.php | 62 ++++++++++ ...oductionOnlyErrorIncidentReportingTest.php | 92 ++++++++++++++ .../RecordErrorIncidentHandlerTest.php | 39 ++++++ .../ErrorHandling/ReportErrorIncidentTest.php | 116 ++++++++++++++++++ ...essionPrefixedErrorIncidentFactoryTest.php | 45 +++++++ .../SoapExceptionHandlerTest.php | 88 +++++++++++++ 10 files changed, 630 insertions(+) create mode 100644 components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentIdTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentUserMessageTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/InMemoryErrorIncidentRegistryTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/LoggingErrorFileStorageAdapterTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/ProductionOnlyErrorIncidentReportingTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/RecordErrorIncidentHandlerTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/ReportErrorIncidentTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/SessionPrefixedErrorIncidentFactoryTest.php create mode 100644 components/ILIAS/Init/tests/ErrorHandling/SoapExceptionHandlerTest.php diff --git a/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentIdTest.php b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentIdTest.php new file mode 100644 index 000000000000..0096d2990622 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentIdTest.php @@ -0,0 +1,40 @@ +value()); + } + + public function testRejectsEmptyValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Error incident identifier must not be empty.'); + + new ErrorIncidentId(''); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentTest.php b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentTest.php new file mode 100644 index 000000000000..78b56e5c1249 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentTest.php @@ -0,0 +1,35 @@ +identifier()); + self::assertSame('abc_1234', $incident->identifier()->value()); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentUserMessageTest.php b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentUserMessageTest.php new file mode 100644 index 000000000000..134c7df9a2be --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentUserMessageTest.php @@ -0,0 +1,59 @@ +createMock(ilLoggingErrorSettings::class); + $settings->method('mail')->willReturn('admin@example.org'); + + $message_formatter = new ErrorIncidentUserMessage($settings); + $message = $message_formatter->format(new ErrorIncident(new ErrorIncidentId('abc_12')), null); + + self::assertStringContainsString('abc_12', $message); + self::assertStringContainsString('admin@example.org', $message); + } + + public function testFormatsLocalizedMessageWithLanguage(): void + { + $settings = $this->createMock(ilLoggingErrorSettings::class); + $settings->method('mail')->willReturn(''); + + $language = $this->createMock(ilLanguage::class); + $language->expects($this->once())->method('loadLanguageModule')->with('logging'); + $language->method('txt')->willReturnCallback( + static fn(string $key): string => match ($key) { + 'log_error_message' => 'Logged error %s', + default => $key, + } + ); + + $message_formatter = new ErrorIncidentUserMessage($settings); + $message = $message_formatter->format(new ErrorIncident(new ErrorIncidentId('abc_12')), $language); + + self::assertSame('Logged error abc_12', $message); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/InMemoryErrorIncidentRegistryTest.php b/components/ILIAS/Init/tests/ErrorHandling/InMemoryErrorIncidentRegistryTest.php new file mode 100644 index 000000000000..48c9b9370169 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/InMemoryErrorIncidentRegistryTest.php @@ -0,0 +1,54 @@ +current()); + } + + public function testRecordsAndReturnsCurrentIncident(): void + { + $registry = new InMemoryErrorIncidentRegistry(); + $incident = new ErrorIncident(new ErrorIncidentId('abc_99')); + + $registry->record($incident); + + self::assertSame($incident, $registry->current()); + } + + public function testClearRemovesCurrentIncident(): void + { + $registry = new InMemoryErrorIncidentRegistry(); + $registry->record(new ErrorIncident(new ErrorIncidentId('abc_99'))); + + $registry->clear(); + + self::assertNull($registry->current()); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/LoggingErrorFileStorageAdapterTest.php b/components/ILIAS/Init/tests/ErrorHandling/LoggingErrorFileStorageAdapterTest.php new file mode 100644 index 000000000000..09332741b8c1 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/LoggingErrorFileStorageAdapterTest.php @@ -0,0 +1,62 @@ +skipIfVfsStreamNotAvailable(); + + vfsStream::setup(); + vfsStream::create([ + 'errors' => [], + ]); + + $log_directory = vfsStream::url('root/errors'); + $log_file = vfsStream::url('root/errors/abcde_42.log'); + $inspector = new Inspector(new RuntimeException('adapter test')); + + $adapter = new LoggingErrorFileStorageAdapter(); + $adapter->write( + $inspector, + $log_directory, + 'abcde_42', + ['password'] + ); + + self::assertFileExists($log_file); + self::assertStringContainsString('adapter test', (string) file_get_contents($log_file)); + } + + private function skipIfVfsStreamNotAvailable(): void + { + if (!class_exists(vfsStreamWrapper::class)) { + self::markTestSkipped( + 'vfsStream (https://github.com/bovigo/vfsStream) is required for virtual filesystem tests.' + ); + } + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/ProductionOnlyErrorIncidentReportingTest.php b/components/ILIAS/Init/tests/ErrorHandling/ProductionOnlyErrorIncidentReportingTest.php new file mode 100644 index 000000000000..afe48058eef2 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ProductionOnlyErrorIncidentReportingTest.php @@ -0,0 +1,92 @@ +createMock(ErrorIncidentReporting::class); + $inner->expects($this->once()) + ->method('report') + ->with($inspector) + ->willReturn($incident); + + $reporting = new ProductionOnlyErrorIncidentReporting($inner, $this->devmodeState(false)); + + $result = $reporting->report($inspector); + + self::assertSame($incident, $result); + } + + public function testSkipsReportingWhenDevmodeIsActive(): void + { + $inner = $this->createMock(ErrorIncidentReporting::class); + $inner->expects($this->never())->method('report'); + + $reporting = new ProductionOnlyErrorIncidentReporting($inner, $this->devmodeState(true)); + + $result = $reporting->report(new Inspector(new RuntimeException('test'))); + + self::assertNull($result); + } + + public function testEvaluatesDevmodeLazilyOnEveryReport(): void + { + $inspector = new Inspector(new RuntimeException('test')); + $incident = new ErrorIncident(new ErrorIncidentId('abc_12')); + + $inner = $this->createStub(ErrorIncidentReporting::class); + $inner->method('report')->willReturn($incident); + + $devmode = $this->devmodeState(true); + $reporting = new ProductionOnlyErrorIncidentReporting($inner, $devmode); + + self::assertNull($reporting->report($inspector)); + + $devmode->is_active = false; + + self::assertSame($incident, $reporting->report($inspector)); + } + + private function devmodeState(bool $is_active): DevmodeState + { + return new class ($is_active) implements DevmodeState { + public function __construct(public bool $is_active) + { + } + + public function isActive(): bool + { + return $this->is_active; + } + }; + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/RecordErrorIncidentHandlerTest.php b/components/ILIAS/Init/tests/ErrorHandling/RecordErrorIncidentHandlerTest.php new file mode 100644 index 000000000000..0b3ca8aefbfe --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/RecordErrorIncidentHandlerTest.php @@ -0,0 +1,39 @@ +createMock(ErrorIncidentReporting::class); + $inspector = new Inspector(new RuntimeException('test')); + $reporting->expects($this->once())->method('report')->with($inspector)->willReturn(null); + + $handler = new RecordErrorIncidentHandler($reporting); + $handler->setInspector($inspector); + + self::assertNull($handler->handle()); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/ReportErrorIncidentTest.php b/components/ILIAS/Init/tests/ErrorHandling/ReportErrorIncidentTest.php new file mode 100644 index 000000000000..d9fe44baa31b --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ReportErrorIncidentTest.php @@ -0,0 +1,116 @@ +createMock(ErrorLogFileStorage::class); + $storage->expects($this->never())->method('write'); + + $registry = new InMemoryErrorIncidentRegistry(); + $report = new ReportErrorIncident( + new readonly class () implements ErrorLogDirectory { + public function path(): string + { + return ''; + } + }, + $storage, + new SessionPrefixedErrorIncidentFactory(), + $registry, + ['password'] + ); + + $result = $report->report(new Inspector(new RuntimeException('test'))); + + self::assertNull($result); + self::assertNull($registry->current()); + } + + public function testWritesLogFileAndRecordsIncident(): void + { + $this->skipIfVfsStreamNotAvailable(); + + vfsStream::setup(); + vfsStream::create([ + 'errors' => [], + ]); + + $log_directory = vfsStream::url('root/errors'); + $log_file = vfsStream::url('root/errors/abcde_42.log'); + $inspector = new Inspector(new RuntimeException('test')); + $incident = new ErrorIncident(new ErrorIncidentId('abcde_42')); + + $incident_factory = $this->createMock(ErrorIncidentFactory::class); + $incident_factory->expects($this->once()) + ->method('create') + ->willReturn($incident); + + $registry = new InMemoryErrorIncidentRegistry(); + $report = new ReportErrorIncident( + new readonly class ($log_directory) implements ErrorLogDirectory { + public function __construct( + private string $path + ) { + } + + public function path(): string + { + return $this->path; + } + }, + new LoggingErrorFileStorageAdapter(), + $incident_factory, + $registry, + ['password'] + ); + + $result = $report->report($inspector); + + self::assertSame($incident, $result); + self::assertSame($incident, $registry->current()); + self::assertFileExists($log_file); + self::assertStringContainsString('test', (string) file_get_contents($log_file)); + } + + private function skipIfVfsStreamNotAvailable(): void + { + if (!class_exists(vfsStreamWrapper::class)) { + self::markTestSkipped( + 'vfsStream (https://github.com/bovigo/vfsStream) is required for virtual filesystem tests.' + ); + } + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/SessionPrefixedErrorIncidentFactoryTest.php b/components/ILIAS/Init/tests/ErrorHandling/SessionPrefixedErrorIncidentFactoryTest.php new file mode 100644 index 000000000000..dce430a0a33f --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/SessionPrefixedErrorIncidentFactoryTest.php @@ -0,0 +1,45 @@ +create('abcdef0123456789'); + + self::assertSame('abcde_9997', $incident->identifier()->value()); + } + + public function testCreatesIdentifierWhenSessionIdIsEmpty(): void + { + $engine = new \Random\Engine\Mt19937(99); + $factory = new SessionPrefixedErrorIncidentFactory(new \Random\Randomizer($engine)); + + $incident = $factory->create(''); + + self::assertSame('_3172', $incident->identifier()->value()); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/SoapExceptionHandlerTest.php b/components/ILIAS/Init/tests/ErrorHandling/SoapExceptionHandlerTest.php new file mode 100644 index 000000000000..69b344d819b9 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/SoapExceptionHandlerTest.php @@ -0,0 +1,88 @@ +record(new ErrorIncident(new ErrorIncidentId('abc_12'))); + + $handler = new SoapExceptionHandler($registry, $this->devmodeState(false)); + $handler->setInspector(new Inspector(new RuntimeException('internal soap failure'))); + + ob_start(); + $handler->handle(); + $output = (string) ob_get_clean(); + + self::assertStringContainsString('internal soap failure', $output); + self::assertStringContainsString('abc_12', $output); + } + + public function testFallsBackToExceptionMessageWithoutIncident(): void + { + $handler = new SoapExceptionHandler(new InMemoryErrorIncidentRegistry(), $this->devmodeState(false)); + $handler->setInspector(new Inspector(new RuntimeException('internal soap failure'))); + + ob_start(); + $handler->handle(); + $output = (string) ob_get_clean(); + + self::assertStringContainsString('internal soap failure', $output); + } + + public function testAppendsIncidentReferenceInDevmodeFaultString(): void + { + $registry = new InMemoryErrorIncidentRegistry(); + $registry->record(new ErrorIncident(new ErrorIncidentId('abc_12'))); + + $handler = new SoapExceptionHandler($registry, $this->devmodeState(true)); + $handler->setInspector(new Inspector(new RuntimeException('internal soap failure'))); + + ob_start(); + $handler->handle(); + $output = (string) ob_get_clean(); + + self::assertStringContainsString('internal soap failure', $output); + self::assertStringContainsString('abc_12', $output); + } + + private function devmodeState(bool $is_active): DevmodeState + { + return new class ($is_active) implements DevmodeState { + public function __construct(private bool $is_active) + { + } + + public function isActive(): bool + { + return $this->is_active; + } + }; + } +} From 4020add60a922bdc2f1c6d07a9c5a4f3076c34c1 Mon Sep 17 00:00:00 2001 From: mjansen Date: Tue, 9 Jun 2026 11:23:47 +0200 Subject: [PATCH 12/12] [FEATURE] Init/ErrorHandling: Update ROADMAP.md and README.md --- .../{classes => src}/ErrorHandling/README.md | 31 +++++++++++++++++-- .../ILIAS/Init/src/ErrorHandling/ROADMAP.md | 29 +++++++---------- 2 files changed, 39 insertions(+), 21 deletions(-) rename components/ILIAS/Init/{classes => src}/ErrorHandling/README.md (54%) diff --git a/components/ILIAS/Init/classes/ErrorHandling/README.md b/components/ILIAS/Init/src/ErrorHandling/README.md similarity index 54% rename from components/ILIAS/Init/classes/ErrorHandling/README.md rename to components/ILIAS/Init/src/ErrorHandling/README.md index cef25f90620d..e4d4d7b0eca7 100644 --- a/components/ILIAS/Init/classes/ErrorHandling/README.md +++ b/components/ILIAS/Init/src/ErrorHandling/README.md @@ -1,8 +1,33 @@ -# Error Responders +# Error Handling -This package provides responders for rendering HTTP error pages in ILIAS. +This package covers HTTP error responses and exception logging for ILIAS. -## When to use which responder +## Error incidents and log files + +If a dedicated error log folder is configured, uncaught exceptions are written +to a file in that folder. The user-facing error message references the same +identifier as the file name (for example `abcde_1234`), so reports can be matched +to log files on disk. + +The identifier is represented as an `ErrorIncident` and kept for the current +request in an `ErrorIncidentRegistry`. That way the handler writing the log file +and the handler building the response message share one value. + +`ReportErrorIncident` performs the actual reporting. It is invoked from +`RecordErrorIncidentHandler`, which is registered in the Whoops chain before the +response handlers run. Implementation code is under `Init/src/ErrorHandling/` +(`Incident`, `Application`, `Notification`, `Infrastructure`). + +## Whoops handler chain + +`ilErrorHandling` registers handlers in reverse order (the last pushed handler +runs first): + +1. `RecordErrorIncidentHandler` — writes the dedicated log file when configured +2. `loggingHandler()` — application log and `error_log()` where enabled +3. `DelegatingHandler` — selects the response handler (production, SOAP, devmode, …) + +## When to use which HTTP 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. diff --git a/components/ILIAS/Init/src/ErrorHandling/ROADMAP.md b/components/ILIAS/Init/src/ErrorHandling/ROADMAP.md index 3770ffb90e59..47178e21b1e3 100644 --- a/components/ILIAS/Init/src/ErrorHandling/ROADMAP.md +++ b/components/ILIAS/Init/src/ErrorHandling/ROADMAP.md @@ -49,24 +49,17 @@ In almost all cases this redirect is unnecessary: ### Unified log file reporting for all handlers -**Current behaviour** - -Only the **default handler** (production) writes exceptions to a dedicated log -file (via `ilLoggingErrorFileStorage`) when configured. Other handlers (e.g., -SOAP, testing, devmode handlers) do not write to that log file. +**Done** (ILIAS 12). -**Goal** +Previously only the production default handler wrote exceptions to the dedicated +log file via `ilLoggingErrorFileStorage`. SOAP, testing, and devmode handlers did +not. -- Make the ability to report an exception to the dedicated log file available to - **all** Whoops handlers (default, SOAP, testing, devmode, etc.), not only the - default handler. -- Ensure a consistent reporting path: whenever an exception is handled and - logging is enabled, it can be written to the configured log file regardless - of which handler rendered the response. - -**Outcome** +Log file writing now happens in `RecordErrorIncidentHandler`, which runs for every +handled exception before the response handler is chosen. It calls +`ReportErrorIncident` and stores the incident in `ErrorIncidentRegistry`. The +production handler reads that value when it builds the message for the user, so +the code in the UI matches the log file name. -- Administrators and developers get a single, consistent log of reported exceptions - across all entry points and handler types. -- Easier auditing and debugging when errors occur in SOAP, tests, or other - contexts that today do not use the dedicated log file. +Details are documented in `README.md` and implemented under +`Init/src/ErrorHandling/`.