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