From 9c782c41dfdfefce740045b3e53bce92b3793000 Mon Sep 17 00:00:00 2001 From: Benjamin DALSASS Date: Fri, 5 Jun 2026 17:07:02 +0200 Subject: [PATCH] log exception --- application/exceptions/CoreException.php | 4 +- application/startup.inc.php | 4 + core/log.class.inc.php | 16 ++- lib/composer/autoload_classmap.php | 2 + lib/composer/autoload_static.php | 2 + pages/UI.php | 38 +----- .../Helper/ExceptionHandlerHelper.php | 110 ++++++++++++++++++ sources/Exception/ItopException.php | 28 +++++ 8 files changed, 167 insertions(+), 37 deletions(-) create mode 100644 sources/Application/Helper/ExceptionHandlerHelper.php create mode 100644 sources/Exception/ItopException.php diff --git a/application/exceptions/CoreException.php b/application/exceptions/CoreException.php index eba5616f3a..5e26173b87 100644 --- a/application/exceptions/CoreException.php +++ b/application/exceptions/CoreException.php @@ -5,7 +5,9 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ -class CoreException extends Exception +use Combodo\iTop\Exception\ItopException; + +class CoreException extends ItopException { protected $m_sIssue; protected $m_sImpact; diff --git a/application/startup.inc.php b/application/startup.inc.php index 73460293b5..f5dfd4bd6f 100644 --- a/application/startup.inc.php +++ b/application/startup.inc.php @@ -16,6 +16,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see +use Combodo\iTop\Application\Helper\ExceptionHandlerHelper; use Combodo\iTop\Application\Helper\Session; require_once(APPROOT.'core/cmdbobject.class.inc.php'); @@ -63,6 +64,9 @@ register_shutdown_function(function () { } } }); + +set_exception_handler([ExceptionHandlerHelper::class, 'HandleException']); + $oKPI = new ExecutionKPI(); Session::Start(); $oKPI->ComputeAndReport("Session Start"); diff --git a/core/log.class.inc.php b/core/log.class.inc.php index deb4349751..234e909cf4 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -614,6 +614,12 @@ class LogChannels * @since 3.2.0 */ public const SECURITY = 'Security'; + + /** + * @var string + * @since 3.3.0 + */ + public const EXCEPTION = 'Exception'; } abstract class LogAPI @@ -708,8 +714,14 @@ abstract class LogAPI private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array { - $aContext['Error Message'] = $oException->getMessage(); - $aContext['Stack Trace'] = $oException->getTraceAsString(); + $aContext['exception'] = get_class($oException); + $aContext['message'] = $oException->getMessage(); + $aContext['stack'] = $oException->getTraceAsString(); + + if(method_exists($oException, 'GetContext')) { + $aContext = array_merge($aContext, $oException->GetContext()); + } + return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext]; } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 50b73aabc9..49bbb7ea6a 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -132,6 +132,7 @@ return array( 'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/Application/Branding.php', 'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php', 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php', + 'Combodo\\iTop\\Application\\Helper\\ExceptionHandlerHelper' => $baseDir . '/sources/Application/Helper/ExceptionHandlerHelper.php', 'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php', 'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php', 'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php', @@ -444,6 +445,7 @@ return array( 'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => $baseDir . '/sources/Dependencies/NPM/iTopNPM.php', 'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php', + 'Combodo\\iTop\\Exception\\ItopException' => $baseDir . '/sources/Exception/ItopException.php', 'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => $baseDir . '/sources/Form/Field/AbstractSimpleField.php', 'Combodo\\iTop\\Form\\Field\\BlobField' => $baseDir . '/sources/Form/Field/BlobField.php', 'Combodo\\iTop\\Form\\Field\\CaseLogField' => $baseDir . '/sources/Form/Field/CaseLogField.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 2c315e4f0e..9d1ba0d725 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -533,6 +533,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/Application/Branding.php', 'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php', 'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php', + 'Combodo\\iTop\\Application\\Helper\\ExceptionHandlerHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExceptionHandlerHelper.php', 'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php', 'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php', 'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php', @@ -845,6 +846,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => __DIR__ . '/../..' . '/sources/Dependencies/NPM/iTopNPM.php', 'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', + 'Combodo\\iTop\\Exception\\ItopException' => __DIR__ . '/../..' . '/sources/Exception/ItopException.php', 'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => __DIR__ . '/../..' . '/sources/Form/Field/AbstractSimpleField.php', 'Combodo\\iTop\\Form\\Field\\BlobField' => __DIR__ . '/../..' . '/sources/Form/Field/BlobField.php', 'Combodo\\iTop\\Form\\Field\\CaseLogField' => __DIR__ . '/../..' . '/sources/Form/Field/CaseLogField.php', diff --git a/pages/UI.php b/pages/UI.php index 2761915084..a2c949b313 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -19,12 +19,12 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory; use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory; use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock; use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory; -use Combodo\iTop\Application\WebPage\ErrorPage; use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\Application\WelcomePopup\WelcomePopupService; use Combodo\iTop\Controller\Base\Layout\ObjectController; use Combodo\iTop\Controller\WelcomePopupController; +use Combodo\iTop\Exception\ItopException; use Combodo\iTop\Service\Router\Router; /** @@ -1303,39 +1303,9 @@ try { $oKPI->ComputeAndReport('Compute page'); $oP->output(); } catch (Exception $e) { - $oErrorPage = new ErrorPage(Dict::S('UI:PageTitle:FatalError')); - if ($e instanceof SecurityException) { - $oErrorPage->add("

".Dict::S('UI:SystemIntrusion')."

\n"); - } else { - $oErrorPage->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); - } - $sErrorDetails = ($e instanceof CoreException) ? $e->getHtmlDesc() : $e->getMessage(); - $oErrorPage->error(Dict::Format('UI:Error_Details', utils::EscapeHtml($sErrorDetails)), $e); - $oErrorPage->output(); - - $sErrorStackTrace = ($e instanceof CoreException) ? $e->getFullStackTraceAsString() : $e->getTraceAsString(); - if (MetaModel::IsLogEnabledIssue()) { - if (MetaModel::IsValidClass('EventIssue')) { - try { - $oLog = new EventIssue(); - - $oLog->Set('message', $e->getMessage()); - $oLog->Set('userinfo', ''); - $sIssue = ($e instanceof CoreException) ? $e->GetIssue() : 'PHP Exception'; - $oLog->Set('issue', $sIssue); - $oLog->Set('impact', 'Page could not be displayed'); - $oLog->Set('callstack', $sErrorStackTrace); - $aData = ($e instanceof CoreException) ? $e->getContextData() : []; - $oLog->Set('data', $aData); - $oLog->DBInsertNoReload(); - } catch (Exception $e) { - IssueLog::Exception("Failed to log issue into the DB", $e); - } - } - } - - $sOperationToLog = $operation ?? 'N/A'; - IssueLog::Debug('UI.php operation='.$sOperationToLog.', error='.$e->getMessage()."\n".$sErrorStackTrace, LogChannels::CONSOLE); + throw new ItopException("Unable to handle UI operation", previous: $e, aContext: [ + 'operation' => ($operation ?? 'N/A'), + ]); } class UI diff --git a/sources/Application/Helper/ExceptionHandlerHelper.php b/sources/Application/Helper/ExceptionHandlerHelper.php new file mode 100644 index 0000000000..af83f241ce --- /dev/null +++ b/sources/Application/Helper/ExceptionHandlerHelper.php @@ -0,0 +1,110 @@ + 'json', + 'application/xml' => 'xml', + 'text/html' => 'html', + 'text/plain' => 'text', + ]; + + public static function HandleException(Throwable $oException) + { + ob_end_clean(); + + // Log the exception + IssueLog::Exception('Fatal error', $oException); + + $mime = self::NegotiateMimeType(); + + if ($mime === null) { + http_response_code(406); + header('Content-Type: application/json; charset=utf-8'); + header('Vary: Accept'); + echo json_encode(['error' => 'Not Acceptable'], JSON_UNESCAPED_UNICODE); + return; + } + + http_response_code(500); + header("Content-Type: {$mime}; charset=utf-8"); + header('Vary: Accept'); + + switch (self::$aSupportedMimeTypes[$mime]) { + case 'json': +// echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + break; + + case 'xml': + // Version simple: sérialisation minimale +// $xml = new SimpleXMLElement(''); +// array_walk_recursive((array)$data, function ($value, $key) use ($xml) { +// $xml->addChild((string)$key, htmlspecialchars((string)$value, ENT_QUOTES | ENT_XML1, 'UTF-8')); +// }); +// echo $xml->asXML(); + break; + + case 'html': + // Create error page + $oErrorPage = new ErrorPage('Fatal error'); + $oErrorPage->error('We are sorry, an unexpected error has occurred. Please try again later.

', $oException); + $oErrorPage->output(); + break; + + case 'text': + echo 'Fatal error'; + break; + + } + } + + public static function NegotiateMimeType(): ?string + { + $supportedMimes = array_keys(self::$aSupportedMimeTypes); + $acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '*/*'; + + if (trim($acceptHeader) === '' || $acceptHeader === '*/*') { + return in_array('application/json', $supportedMimes, true) ? 'application/json' : $supportedMimes[0]; + } + + $accepted = []; + foreach (explode(',', $acceptHeader) as $part) { + $part = trim($part); + $q = 1.0; + if (str_contains($part, ';')) { + [$type, $params] = array_map('trim', explode(';', $part, 2)); + if (preg_match('/q=([0-9.]+)/', $params, $m)) { + $q = (float)$m[1]; + } + } else { + $type = $part; + } + $accepted[] = ['type' => $type, 'q' => $q]; + } + + usort($accepted, fn($a, $b) => $b['q'] <=> $a['q']); + + foreach ($accepted as $a) { + foreach ($supportedMimes as $mime) { + if ($a['type'] === $mime || $a['type'] === '*/*') { + return $mime; + } + // Ex: application/* match application/json + if (str_ends_with($a['type'], '/*')) { + $prefix = explode('/', $a['type'])[0].'/'; + if (str_starts_with($mime, $prefix)) { + return $mime; + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/sources/Exception/ItopException.php b/sources/Exception/ItopException.php new file mode 100644 index 0000000000..763a6e3795 --- /dev/null +++ b/sources/Exception/ItopException.php @@ -0,0 +1,28 @@ +aContext = $aContext; + } + + public function getContext(): array + { + return $this->aContext; + } + + +} \ No newline at end of file