diff --git a/components/ILIAS/Init/classes/class.ilErrorHandling.php b/components/ILIAS/Init/classes/class.ilErrorHandling.php index 7676978ba4c1..9e5db386407f 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\LazySettings(), + new ErrorLogging\FileWriter(new ErrorHandling\Logging\FileHandler(), new ErrorLogging\ContentProcessor()), + 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,17 @@ 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; + $settings = $DIC->isDependencyAvailable('clientIni') ? new ErrorHandling\Notification\Settings($DIC->clientIni()) : null; + $message = new ErrorHandling\Notification\ErrorIncidentUserMessage()->format($incident, $language, $settings); } if ($DIC->isDependencyAvailable('ui') && isset($DIC['tpl']) && $DIC->isDependencyAvailable('ctrl')) { @@ -283,10 +286,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 diff --git a/components/ILIAS/Init/service.xml b/components/ILIAS/Init/service.xml index 8274f30dd083..f01bf137e772 100755 --- a/components/ILIAS/Init/service.xml +++ b/components/ILIAS/Init/service.xml @@ -4,5 +4,8 @@ + + + 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 @@ + $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; + } +} 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)); + } +} 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 @@ + */ - private array $exclusion_list = []; - - public function __construct(Inspector $inspector, string $file_path, string $file_name) - { - $this->inspector = $inspector; - $this->file_path = $file_path; - $this->file_name = $file_name; - } + private const int KEY_SPACE = 25; /** - * @param list $exclusion_list + * @param list $sensitive_fields */ - public function withExclusionList(array $exclusion_list): self - { - $clone = clone $this; - $clone->exclusion_list = $exclusion_list; - return $clone; - } - - private function stripNullBytes(string $ret): string - { - return str_replace("\0", '', $ret); - } - - protected function createDir(): void - { - if (!is_dir($this->file_path)) { - ilFileUtils::makeDirParents($this->file_path); - } - } - - protected function content(): string - { - return $this->pageHeader() - . $this->exceptionContent() - . $this->tablesContent(); - } - - public function write(): void - { - $this->createDir(); - - $file_name = $this->file_path . '/' . $this->file_name . self::FILE_FORMAT; - $stream = fopen($file_name, 'wb+'); - fwrite($stream, $this->content()); - fclose($stream); - chmod($file_name, 0755); - } - - protected function pageHeader(): string - { - return ''; + public function collectAndFormatContent( + Inspector $inspector, + array $sensitive_fields + ): string { + return $this->formatExceptionContent($inspector) + . $this->formatTables($this->tablesFromSuperGlobals($sensitive_fields)); } - protected function exceptionContent(): string + private function formatExceptionContent(Inspector $inspector): string { - $message = Formatter::formatExceptionPlain($this->inspector); + $message = Formatter::formatExceptionPlain($inspector); - $exception = $this->inspector->getException(); + $exception = $inspector->getException(); $previous = $exception->getPrevious(); while ($previous) { $message .= "\n\nCaused by\n" . sprintf( @@ -104,10 +58,13 @@ protected function exceptionContent(): string return $message; } - protected function tablesContent(): string + /** + * @param array> $tables + */ + private function formatTables(array $tables): string { $ret = ''; - foreach ($this->tables() as $title => $content) { + foreach ($tables as $title => $content) { $ret .= "\n\n-- $title --\n\n"; if (count($content) > 0) { foreach ($content as $key => $value) { @@ -135,33 +92,40 @@ protected function tablesContent(): string return $this->stripNullBytes($ret); } - protected function tables(): array + /** + * @param list $sensitive_fields + * @return array> + */ + private function tablesFromSuperGlobals(array $sensitive_fields): array { - $post = $_POST; - $server = $_SERVER; + $post = (array) $_POST; + $server = (array) $_SERVER; - $post = $this->hideSensitiveData($post); - $server = $this->hideSensitiveData($server); + $post = $this->hideSensitiveData($post, $sensitive_fields); + $server = $this->hideSensitiveData($server, $sensitive_fields); $server = $this->shortenPHPSessionId($server); return [ - 'GET Data' => $_GET, + 'GET Data' => (array) $_GET, 'POST Data' => $post, - 'Files' => $_FILES, - 'Cookies' => $_COOKIE, - 'Session' => $_SESSION ?? [], + 'Files' => (array) $_FILES, + 'Cookies' => (array) $_COOKIE, + 'Session' => (array) ($_SESSION ?? []), 'Server/Request Data' => $server, - 'Environment Variables' => $_ENV + 'Environment Variables' => (array) $_ENV ]; } /** * @param array $super_global + * @param list $sensitive_fields * @return array */ - private function hideSensitiveData(array $super_global): array - { - foreach ($this->exclusion_list as $parameter) { + private function hideSensitiveData( + array $super_global, + array $sensitive_fields + ): array { + foreach ($sensitive_fields as $parameter) { if (isset($super_global[$parameter])) { $super_global[$parameter] = 'REMOVED FOR SECURITY'; } @@ -174,6 +138,10 @@ private function hideSensitiveData(array $super_global): array return $super_global; } + /** + * @param array $server + * @return array + */ private function shortenPHPSessionId(array $server): array { if (!isset($server['HTTP_COOKIE'])) { @@ -194,4 +162,9 @@ private function shortenPHPSessionId(array $server): array return $server; } + + private function stripNullBytes(string $ret): string + { + return str_replace("\0", '', $ret); + } } diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/FileWriter.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/FileWriter.php new file mode 100755 index 000000000000..0636ef995e4b --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/FileWriter.php @@ -0,0 +1,50 @@ + $sensitive_parameter_names + */ + public function write( + Inspector $inspector, + string $directory, + string $file_name, + array $sensitive_parameter_names + ): void { + $this->file_handler->createFile( + $directory, + $file_name, + $this->content_processor->collectAndFormatContent($inspector, $sensitive_parameter_names), + ); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LazySettings.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LazySettings.php new file mode 100755 index 000000000000..ef5f9d8a0114 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Logging/LazySettings.php @@ -0,0 +1,52 @@ +fetchSettings()?->directory() ?? ''; + } + + private function fetchSettings(): ?Settings + { + global $DIC; + + + if ($DIC->offsetExists('ilIliasIniFile')) { + return new Settings($DIC->iliasIni()); + } + if ($DIC->offsetExists('ini')) { + return new Settings($DIC['ini']); + } + return null; + } +} 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/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; + } +} diff --git a/components/ILIAS/Init/classes/ErrorHandling/class.ilSoapExceptionHandler.php b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php similarity index 55% rename from components/ILIAS/Init/classes/ErrorHandling/class.ilSoapExceptionHandler.php rename to components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php index 6efb7c69662e..f4ccf3e838c5 100644 --- a/components/ILIAS/Init/classes/ErrorHandling/class.ilSoapExceptionHandler.php +++ b/components/ILIAS/Init/src/ErrorHandling/Infrastructure/Whoops/SoapExceptionHandler.php @@ -18,20 +18,43 @@ declare(strict_types=1); -class ilSoapExceptionHandler extends \Whoops\Handler\Handler +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; + +/** + * Whoops handler that renders SOAP fault responses for SOAP POST requests. + */ +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 = \Whoops\Exception\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); @@ -39,9 +62,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 +75,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 { diff --git a/components/ILIAS/Init/src/ErrorHandling/Logging/CleanUp/CleanUpOldErrorLogsJob.php b/components/ILIAS/Init/src/ErrorHandling/Logging/CleanUp/CleanUpOldErrorLogsJob.php new file mode 100755 index 000000000000..932190ed0024 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Logging/CleanUp/CleanUpOldErrorLogsJob.php @@ -0,0 +1,142 @@ +lng = $DIC->language(); + $this->lng->loadLanguageModule("logging"); + + $this->settings = new Settings( + new ilSetting('log') + ); + $this->error_log_settings = new ErrorLogSettings($DIC->iliasIni()); + $this->file_handler = new FileHandler(); + } + + public function getId(): string + { + return "log_error_file_cleanup"; + } + + public function getTitle(): string + { + return $this->lng->txt("log_error_file_cleanup_title"); + } + + public function getDescription(): string + { + return $this->lng->txt("log_error_file_cleanup_info"); + } + + public function getDefaultScheduleType(): JobScheduleType + { + return JobScheduleType::IN_DAYS; + } + + public function getDefaultScheduleValue(): int + { + return 10; + } + + public function hasAutoActivation(): bool + { + return false; + } + + public function hasFlexibleSchedule(): bool + { + return true; + } + + public function hasCustomSettings(): bool + { + return true; + } + + public function run(): JobResult + { + $result = new JobResult(); + + $error_log_directory = $this->error_log_settings->directory(); + if (!$this->file_handler->doesDirectoryExist($error_log_directory)) { + $result->setStatus(JobResult::STATUS_OK); + $result->setMessage($this->lng->txt("log_error_path_not_configured_or_wrong")); + return $result; + } + + $deleted_files = $this->file_handler->deleteFilesOlderThan( + $error_log_directory, + $this->settings->deletionCutoffInDays() + ); + + if ($deleted_files > 0) { + $result->setStatus(JobResult::STATUS_OK); + } else { + $result->setStatus(JobResult::STATUS_NO_ACTION); + } + return $result; + } + + public function usesLegacyForms(): bool + { + return false; + } + + public function getCustomConfigurationInput( + UIFactory $ui_factory, + Refinery $factory, + ilLanguage $lng + ): FormInput { + $lng->loadLanguageModule("logging"); + + return $ui_factory->input()->field() + ->numeric($this->lng->txt('frm_clear_older_then'), $this->lng->txt('frm_clear_older_then_info')) + ->withAdditionalTransformation($factory->int()->isGreaterThanOrEqual(1)) + ->withValue($this->settings->deletionCutoffInDays()); + } + + public function saveCustomConfiguration(mixed $form_data): void + { + $this->settings->saveDeletionCutoff((int) $form_data); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Logging/CleanUp/Settings.php b/components/ILIAS/Init/src/ErrorHandling/Logging/CleanUp/Settings.php new file mode 100755 index 000000000000..98132374fa23 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Logging/CleanUp/Settings.php @@ -0,0 +1,50 @@ +deletion_cutoff_in_days ??= + (int) $this->storage->get('clear_older_then', (string) self::DEFAULT_CUTOFF); + } + + public function saveDeletionCutoff(int $days): void + { + if ($days < 1) { + $days = 1; + } + $this->deletion_cutoff_in_days = $days; + $this->storage->set('clear_older_then', (string) $days); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Logging/FileHandler.php b/components/ILIAS/Init/src/ErrorHandling/Logging/FileHandler.php new file mode 100755 index 000000000000..fc0ee9c1293f --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Logging/FileHandler.php @@ -0,0 +1,97 @@ +createDirectoryIfNecessary($directory); + + $file_name = rtrim($directory, '/') . '/' . $file_name . self::FILE_FORMAT; + $stream = fopen($file_name, 'wb+'); + fwrite($stream, $content); + fclose($stream); + chmod($file_name, 0755); + } + + public function deleteFilesOlderThan( + string $directory, + int $cutoff_in_days + ): int { + $files = $this->readFilesInDirectory($directory); + $delete_date = new DateTimeImmutable(); + $delete_date = $delete_date->sub(new DateInterval('P' . $cutoff_in_days . 'D')); + + $count = 0; + foreach ($files as $file) { + $file_path = rtrim($directory, '/') . '/' . $file; + $file_date = date('Y-m-d', filemtime($file_path)); + + if ($file_date <= $delete_date->format('Y-m-d')) { + $this->deleteFile($file_path); + $count++; + } + } + return $count; + } + + private function createDirectoryIfNecessary(string $directory): void + { + if (!$this->doesDirectoryExist($directory)) { + ilFileUtils::makeDirParents($directory); + } + } + + private function deleteFile(string $file_path): void + { + unlink($file_path); + } + + private function readFilesInDirectory(string $directory): array + { + $ret = []; + + $folder = dir($directory); + while ($file_name = $folder->read()) { + if (filetype(rtrim($directory, '/') . '/' . $file_name) != 'dir') { + $ret[] = $file_name; + } + } + $folder->close(); + + return $ret; + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Logging/Settings.php b/components/ILIAS/Init/src/ErrorHandling/Logging/Settings.php new file mode 100755 index 000000000000..396215c3c66f --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Logging/Settings.php @@ -0,0 +1,36 @@ +ilias_ini->readVariable('log', 'error_path'), '/'); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Logging/SettingsInterface.php b/components/ILIAS/Init/src/ErrorHandling/Logging/SettingsInterface.php new file mode 100755 index 000000000000..722f61759583 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Logging/SettingsInterface.php @@ -0,0 +1,26 @@ +identifier()->value(); + + $mail = ''; + if ($notification_settings !== null) { + $mail = $notification_settings->errorRecipient(); + } + + if ($language !== null) { + $language->loadLanguageModule('logging'); + $message = \sprintf($language->txt('log_error_message'), $identifier); + + 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 . '"'; + + if ($mail !== '') { + $message .= ' ' . 'Please send a mail to ' . $mail . ''; + } + + return $message; + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Notification/Settings.php b/components/ILIAS/Init/src/ErrorHandling/Notification/Settings.php new file mode 100755 index 000000000000..9df913ad31e1 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Notification/Settings.php @@ -0,0 +1,46 @@ +error_recipient ??= + $this->client_ini->readVariable('log', 'error_recipient'); + } + + public function saveErrorRecipient(string $recipient): void + { + $this->client_ini->addGroup('log'); + $this->client_ini->setVariable('log', 'error_recipient', trim($recipient)); + $this->client_ini->write(); + } +} diff --git a/components/ILIAS/Init/src/ErrorHandling/Notification/SettingsInterface.php b/components/ILIAS/Init/src/ErrorHandling/Notification/SettingsInterface.php new file mode 100755 index 000000000000..5afe89bba616 --- /dev/null +++ b/components/ILIAS/Init/src/ErrorHandling/Notification/SettingsInterface.php @@ -0,0 +1,30 @@ +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..84d65c27e018 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ErrorIncidentUserMessageTest.php @@ -0,0 +1,57 @@ +createMock(Settings::class); + $settings->method('errorRecipient')->willReturn('admin@example.org'); + + $message_formatter = new ErrorIncidentUserMessage(); + $message = $message_formatter->format(new ErrorIncident(new ErrorIncidentId('abc_12')), null, $settings); + + self::assertStringContainsString('abc_12', $message); + self::assertStringContainsString('admin@example.org', $message); + } + + public function testFormatsLocalizedMessageWithLanguageWithoutMail(): void + { + $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(); + $message = $message_formatter->format(new ErrorIncident(new ErrorIncidentId('abc_12')), $language, null); + + self::assertSame('Logged error abc_12', $message); + } +} diff --git a/components/ILIAS/Init/tests/ErrorHandling/ErrorLoggingFileWriterTest.php b/components/ILIAS/Init/tests/ErrorHandling/ErrorLoggingFileWriterTest.php new file mode 100644 index 000000000000..0563b5e7bd05 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ErrorLoggingFileWriterTest.php @@ -0,0 +1,64 @@ +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('writer test')); + + $writer = new FileWriter(new FileHandler(), new ContentProcessor()); + $writer->write( + $inspector, + $log_directory, + 'abcde_42', + ['password'] + ); + + self::assertFileExists($log_file); + self::assertStringContainsString('writer 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/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/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..78c19f82b739 --- /dev/null +++ b/components/ILIAS/Init/tests/ErrorHandling/ReportErrorIncidentTest.php @@ -0,0 +1,118 @@ +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 FileWriter(new FileHandler(), new ContentProcessor()), + $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; + } + }; + } +} diff --git a/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php b/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php index fc91f1e100ff..86ae0b302f48 100755 --- a/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php +++ b/components/ILIAS/Logging/classes/class.ilObjLoggingSettingsGUI.php @@ -17,9 +17,14 @@ *********************************************************************/ declare(strict_types=1); + use ILIAS\DI\Container; use ILIAS\Refinery\Factory as Refinery; use ILIAS\HTTP\Services as Services; +use ILIAS\Init\ErrorHandling\Logging\Settings as ErrorLogSettings; +use ILIAS\Init\ErrorHandling\Logging\SettingsInterface as ErrorLogSettingsInterface; +use ILIAS\Init\ErrorHandling\Notification\Settings as ErrorNotificationSettings; +use ILIAS\Init\ErrorHandling\Notification\SettingsInterface as ErrorNotificationSettingsInterface; /** * @@ -31,21 +36,17 @@ */ class ilObjLoggingSettingsGUI extends ilObjectGUI { - protected const SECTION_SETTINGS = 'settings'; - protected const SUB_SECTION_MAIN = 'log_general_settings'; - protected const SUB_SECTION_COMPONENTS = 'log_components'; - protected const SUB_SECTION_ERROR = 'log_error_settings'; + protected const string SECTION_SETTINGS = 'settings'; + protected const string SUB_SECTION_MAIN = 'log_general_settings'; + protected const string SUB_SECTION_COMPONENTS = 'log_components'; + protected const string SUB_SECTION_ERROR = 'log_error_settings'; protected ilLoggingDBSettings $log_settings; protected ilLogger $log; - protected ilLoggingErrorSettings $error_settings; + protected ErrorLogSettingsInterface $error_log_settings; + protected ErrorNotificationSettingsInterface $error_notification_settings; protected Refinery $refinery; - /** - * - * @param mixed $a_data - * @param boolean $a_prepare_output - */ public function __construct($a_data, int $a_id, bool $a_call_by_reference, bool $a_prepare_output = true) { global $DIC; @@ -56,11 +57,13 @@ public function __construct($a_data, int $a_id, bool $a_call_by_reference, bool $this->lng = $DIC->language(); $this->initSettings(); - $this->initErrorSettings(); $this->lng->loadLanguageModule('logging'); $this->lng->loadLanguageModule('log'); $this->log = ilLoggerFactory::getLogger('log'); + $this->error_log_settings = new ErrorLogSettings($DIC->iliasIni()); + $this->error_notification_settings = new ErrorNotificationSettings($DIC->clientIni()); + $this->refinery = $DIC->refinery(); } @@ -312,8 +315,7 @@ protected function updateErrorSettings(): void $this->checkPermission('write'); $form = $this->initFormErrorSettings(); if ($form->checkInput()) { - $this->getErrorSettings()->setMail($form->getInput('error_mail')); - $this->getErrorSettings()->update(); + $this->error_notification_settings->saveErrorRecipient($form->getInput('error_mail')); $this->tpl->setOnScreenMessage('success', $this->lng->txt('error_settings_saved'), true); $this->ctrl->redirect($this, 'errorSettings'); @@ -334,22 +336,12 @@ protected function initFormErrorSettings(): ilPropertyFormGUI } $folder = new ilNonEditableValueGUI($this->lng->txt('log_error_folder'), 'error_folder'); - $folder->setValue($this->getErrorSettings()->folder()); + $folder->setValue($this->error_log_settings->directory()); $form->addItem($folder); $mail = new ilTextInputGUI($this->lng->txt('log_error_mail'), 'error_mail'); - $mail->setValue($this->getErrorSettings()->mail()); + $mail->setValue($this->error_notification_settings->errorRecipient()); $form->addItem($mail); return $form; } - - protected function initErrorSettings(): void - { - $this->error_settings = ilLoggingErrorSettings::getInstance(); - } - - protected function getErrorSettings(): ilLoggingErrorSettings - { - return $this->error_settings; - } } diff --git a/components/ILIAS/Logging/classes/error/class.ilLoggerCronCleanErrorFiles.php b/components/ILIAS/Logging/classes/error/class.ilLoggerCronCleanErrorFiles.php deleted file mode 100755 index 748470e98756..000000000000 --- a/components/ILIAS/Logging/classes/error/class.ilLoggerCronCleanErrorFiles.php +++ /dev/null @@ -1,163 +0,0 @@ -lng = $DIC->language(); - $this->lng->loadLanguageModule("logging"); - $this->settings = new ilSetting('log'); - $this->error_settings = ilLoggingErrorSettings::getInstance(); - } - - public function getId(): string - { - return "log_error_file_cleanup"; - } - - public function getTitle(): string - { - return $this->lng->txt("log_error_file_cleanup_title"); - } - - public function getDescription(): string - { - return $this->lng->txt("log_error_file_cleanup_info"); - } - - public function getDefaultScheduleType(): JobScheduleType - { - return JobScheduleType::IN_DAYS; - } - - public function getDefaultScheduleValue(): int - { - return 10; - } - - public function hasAutoActivation(): bool - { - return false; - } - - public function hasFlexibleSchedule(): bool - { - return true; - } - - public function hasCustomSettings(): bool - { - return true; - } - - public function run(): JobResult - { - $result = new JobResult(); - $folder = $this->error_settings->folder(); - if (!is_dir($folder)) { - $result->setStatus(JobResult::STATUS_OK); - $result->setMessage($this->lng->txt("log_error_path_not_configured_or_wrong")); - return $result; - } - - $offset = $this->settings->get('clear_older_then', ''); - if ($offset) { - $offset = (int) $offset; - } else { - $offset = self::DEFAULT_VALUE_OLDER_THAN; - } - - $files = $this->readLogDir($folder); - $delete_date = new ilDateTime(date("Y-m-d"), IL_CAL_DATE); - $delete_date->increment(ilDateTime::DAY, (-1 * $offset)); - - foreach ($files as $file) { - $file_date = date("Y-m-d", filemtime($this->error_settings->folder() . "/" . $file)); - - if ($file_date <= $delete_date->get(IL_CAL_DATE)) { - $this->deleteFile($this->error_settings->folder() . "/" . $file); - } - } - - $result->setStatus(JobResult::STATUS_OK); - return $result; - } - - protected function readLogDir(string $path): array - { - $ret = []; - - $folder = dir($path); - while ($file_name = $folder->read()) { - if (filetype($path . "/" . $file_name) != "dir") { - $ret[] = $file_name; - } - } - $folder->close(); - - return $ret; - } - - protected function deleteFile(string $path): void - { - unlink($path); - } - - public function addCustomSettingsToForm(ilPropertyFormGUI $a_form): void - { - $offset = $this->settings->get('clear_older_then', ''); - if (!$offset) { - $offset = (string) self::DEFAULT_VALUE_OLDER_THAN; - } - - $clear_older_then = new ilNumberInputGUI($this->lng->txt('frm_clear_older_then'), 'clear_older_then'); - $clear_older_then->allowDecimals(false); - $clear_older_then->setMinValue(1, true); - $clear_older_then->setValue($offset); - $clear_older_then->setInfo($this->lng->txt('frm_clear_older_then_info')); - - $a_form->addItem($clear_older_then); - } - - public function saveCustomSettings(ilPropertyFormGUI $a_form): bool - { - $threshold = $a_form->getInput('clear_older_then'); - if ((string) $threshold === '') { - $this->settings->delete('clear_older_then'); - } else { - $this->settings->set('clear_older_then', (string) ((int) $a_form->getInput('clear_older_then'))); - } - - return true; - } -} diff --git a/components/ILIAS/Logging/classes/error/class.ilLoggingErrorSettings.php b/components/ILIAS/Logging/classes/error/class.ilLoggingErrorSettings.php deleted file mode 100755 index ecebb7c466e8..000000000000 --- a/components/ILIAS/Logging/classes/error/class.ilLoggingErrorSettings.php +++ /dev/null @@ -1,95 +0,0 @@ - - */ -class ilLoggingErrorSettings -{ - protected string $folder = ''; - protected string $mail = ''; - protected ?ilIniFile $ilias_ini = null; - protected ?ilIniFile $gClientIniFile = null; - - protected function __construct() - { - global $DIC; - - if ($DIC->offsetExists('ilIliasIniFile')) { - $this->ilias_ini = $DIC->iliasIni(); - } elseif ($DIC->offsetExists('ini')) { - $this->ilias_ini = $DIC['ini']; - } - if ($DIC->offsetExists('ilClientIniFile')) { - $this->gClientIniFile = $DIC->clientIni(); - } - $this->read(); - } - - public static function getInstance(): ilLoggingErrorSettings - { - return new ilLoggingErrorSettings(); - } - - protected function setFolder(string $folder): void - { - $this->folder = $folder; - } - - public function setMail(string $mail): void - { - $this->mail = $mail; - } - - public function folder(): string - { - return $this->folder; - } - - public function mail(): string - { - return $this->mail; - } - - /** - * reads the values from ilias.ini.php - */ - protected function read(): void - { - if ($this->ilias_ini instanceof ilIniFile) { - $this->setFolder((string) $this->ilias_ini->readVariable("log", "error_path")); - } - if ($this->gClientIniFile instanceof \ilIniFile) { - $this->setMail((string) $this->gClientIniFile->readVariable("log", "error_recipient")); - } - } - - /** - * writes mail recipient into client.ini.php - */ - public function update(): void - { - if ($this->gClientIniFile instanceof \ilIniFile) { - $this->gClientIniFile->addGroup("log"); - $this->gClientIniFile->setVariable("log", "error_recipient", trim($this->mail())); - $this->gClientIniFile->write(); - } - } -} diff --git a/components/ILIAS/Logging/service.xml b/components/ILIAS/Logging/service.xml index 13cdbbe73f12..31e109a29e12 100755 --- a/components/ILIAS/Logging/service.xml +++ b/components/ILIAS/Logging/service.xml @@ -9,8 +9,5 @@ adm - - -