mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°2060 - Migrate error page to the Symfony framework
This commit is contained in:
@@ -76,6 +76,10 @@ services:
|
||||
tags: [{ name: 'kernel.event_listener', event: 'kernel.request', priority: 300 }]
|
||||
Combodo\iTop\Portal\EventListener\CssFromSassCompiler:
|
||||
tags: [{ name: 'kernel.event_listener', event: 'kernel.request', priority: 200 }]
|
||||
Combodo\iTop\Portal\EventListener\ExceptionListener:
|
||||
tags: [{ name: 'kernel.event_listener', event: 'kernel.exception', priority: 500 }]
|
||||
calls:
|
||||
- [setContainer, ['@service_container']]
|
||||
|
||||
# Add more service definitions when explicit configuration is needed
|
||||
# Please note that last definitions always *replace* previous ones
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2020 Combodo SARL
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
|
||||
namespace Combodo\iTop\Portal\EventListener;
|
||||
|
||||
|
||||
use Dict;
|
||||
use IssueLog;
|
||||
use Symfony\Component\Debug\Exception\FlattenException;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
/**
|
||||
* Class ExceptionListener
|
||||
*
|
||||
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
|
||||
* @package Combodo\iTop\Portal\EventListener
|
||||
* @since 2.7.0
|
||||
*/
|
||||
class ExceptionListener implements ContainerAwareInterface
|
||||
{
|
||||
/** @var \Symfony\Component\DependencyInjection\ContainerInterface $container */
|
||||
private $oContainer;
|
||||
|
||||
/**
|
||||
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $oEvent
|
||||
*
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function onKernelException(GetResponseForExceptionEvent $oEvent)
|
||||
{
|
||||
// Get the exception object from the received event
|
||||
$oException = $oEvent->getException();
|
||||
|
||||
// Prepare / format exception data
|
||||
$sErrorMessage = $oException->getMessage();
|
||||
// - For none HTTP exception, status code will be a generic 500
|
||||
$iStatusCode = ($oException instanceof HttpExceptionInterface) ? $oException->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR;
|
||||
switch ($iStatusCode)
|
||||
{
|
||||
case 404:
|
||||
$sErrorTitle = Dict::S('Error:HTTP:404');
|
||||
break;
|
||||
default:
|
||||
$sErrorTitle = Dict::S('Error:HTTP:500');
|
||||
break;
|
||||
}
|
||||
|
||||
// Prepare flatten exception
|
||||
$oFlattenException = ($_SERVER['APP_DEBUG'] == 1) ? FlattenException::create($oException) : null;
|
||||
// Remove APPROOT from file paths if in production (SF context)
|
||||
if (!is_null($oFlattenException) && ($_SERVER['APP_ENV'] === 'prod'))
|
||||
{
|
||||
$oFlattenException->setFile($this->removeAppRootFromPath($oFlattenException->getFile()));
|
||||
|
||||
$aTrace = $oFlattenException->getTrace();
|
||||
foreach ($aTrace as $iIdx => $aEntry)
|
||||
{
|
||||
$aTrace[$iIdx]['file'] = $this->removeAppRootFromPath($aEntry['file']);
|
||||
}
|
||||
$oFlattenException->setTrace($aTrace, $oFlattenException->getFile(), $oFlattenException->getLine());
|
||||
}
|
||||
|
||||
// Log exception in iTop log
|
||||
IssueLog::Error($sErrorTitle.': '.$sErrorMessage);
|
||||
|
||||
// Prepare data for template
|
||||
$aData = array(
|
||||
'exception' => $oFlattenException,
|
||||
'code' => $iStatusCode,
|
||||
'error_title' => $sErrorTitle,
|
||||
'error_message' => $sErrorMessage,
|
||||
);
|
||||
|
||||
// Generate the response
|
||||
if ($oEvent->getRequest()->isXmlHttpRequest())
|
||||
{
|
||||
$oResponse = new JsonResponse($aData);
|
||||
}
|
||||
else
|
||||
{
|
||||
$oResponse = new Response();
|
||||
$oResponse->setContent($this->oContainer->get('twig')->render('itop-portal-base/portal/templates/errors/layout.html.twig',
|
||||
$aData));
|
||||
}
|
||||
$oResponse->setStatusCode($iStatusCode);
|
||||
|
||||
// HttpExceptionInterface is a special type of exception that holds status code and header details
|
||||
if ($oException instanceof HttpExceptionInterface)
|
||||
{
|
||||
$oResponse->headers->replace($oException->getHeaders());
|
||||
}
|
||||
|
||||
// Send the modified response object to the event
|
||||
$oEvent->setResponse($oResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a path by replacing '\' with '/'
|
||||
*
|
||||
* @param string $sInputPath
|
||||
*
|
||||
* @return string|string[]
|
||||
*/
|
||||
protected function normalizePath($sInputPath)
|
||||
{
|
||||
return str_replace('\\', '/', $sInputPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove iTop's APPROOT path from the $sInputPath. Used to avoid "full path disclosure" vulnerabilities.
|
||||
*
|
||||
* @param string $sInputPath
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function removeAppRootFromPath($sInputPath)
|
||||
{
|
||||
$sNormalizedAppRoot = $this->normalizePath(APPROOT);
|
||||
$sNormalizedInputPath = $this->normalizePath($sInputPath);
|
||||
return str_replace($sNormalizedAppRoot, '', $sNormalizedInputPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setContainer(ContainerInterface $oContainer = null)
|
||||
{
|
||||
$this->oContainer = $oContainer;
|
||||
}
|
||||
}
|
||||
@@ -89,121 +89,6 @@ class ApplicationHelper
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an exception handler that will intercept controllers exceptions and display them in a nice template.
|
||||
* Note : It is only active when $oApp['debug'] is false
|
||||
*
|
||||
* @param Application $oApp
|
||||
*
|
||||
* @todo
|
||||
*/
|
||||
public static function RegisterExceptionHandler(Application $oApp)
|
||||
{
|
||||
// Intercepting fatal errors and exceptions
|
||||
ErrorHandler::register();
|
||||
ExceptionHandler::register(($oApp['debug'] === true));
|
||||
|
||||
// Intercepting manually aborted request
|
||||
if (1 || !$oApp['debug'])
|
||||
{
|
||||
$oApp->error(function (Exception $oException /*, Request $oRequest*/) use ($oApp) {
|
||||
$iErrorCode = ($oException instanceof HttpException) ? $oException->getStatusCode() : 500;
|
||||
|
||||
$aData = array(
|
||||
'exception' => $oException,
|
||||
'code' => $iErrorCode,
|
||||
'error_title' => '',
|
||||
'error_message' => $oException->getMessage(),
|
||||
);
|
||||
|
||||
switch ($iErrorCode)
|
||||
{
|
||||
case 404:
|
||||
$aData['error_title'] = Dict::S('Error:HTTP:404');
|
||||
break;
|
||||
default:
|
||||
$aData['error_title'] = Dict::S('Error:HTTP:500');
|
||||
break;
|
||||
}
|
||||
|
||||
IssueLog::Error($aData['error_title'].' : '.$aData['error_message']);
|
||||
|
||||
if ($oApp['request_stack']->getCurrentRequest()->isXmlHttpRequest())
|
||||
{
|
||||
$oResponse = $oApp->json($aData, $iErrorCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Preparing debug trace
|
||||
$aSteps = array();
|
||||
foreach ($oException->getTrace() as $aStep)
|
||||
{
|
||||
// - Default file name
|
||||
if (!isset($aStep['file']))
|
||||
{
|
||||
$aStep['file'] = '';
|
||||
}
|
||||
$aFileParts = explode('\\', $aStep['file']);
|
||||
// - Default line number
|
||||
if (!isset($aStep['line']))
|
||||
{
|
||||
$aStep['line'] = 'unknown';
|
||||
}
|
||||
// - Default class name
|
||||
if (isset($aStep['class']) && isset($aStep['function']) && isset($aStep['type']))
|
||||
{
|
||||
$aClassParts = explode('\\', $aStep['class']);
|
||||
$sClassName = $aClassParts[count($aClassParts) - 1];
|
||||
$sClassFQ = $aStep['class'];
|
||||
|
||||
$aArgsAsString = array();
|
||||
foreach ($aStep['args'] as $arg)
|
||||
{
|
||||
if (is_array($arg))
|
||||
{
|
||||
$aArgsAsString[] = 'array(...)';
|
||||
}
|
||||
elseif (is_object($arg))
|
||||
{
|
||||
$aArgsAsString[] = 'object('.get_class($arg).')';
|
||||
}
|
||||
else
|
||||
{
|
||||
$aArgsAsString[] = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
$sFunctionCall = $sClassName.$aStep['type'].$aStep['function'].'('.implode(', ',
|
||||
$aArgsAsString).')';
|
||||
}
|
||||
else
|
||||
{
|
||||
$sClassName = null;
|
||||
$sClassFQ = null;
|
||||
$sFunctionCall = null;
|
||||
}
|
||||
|
||||
$aSteps[] = array(
|
||||
'file_fq' => $aStep['file'],
|
||||
'file_name' => $aFileParts[count($aFileParts) - 1],
|
||||
'line' => $aStep['line'],
|
||||
'class_name' => $sClassName,
|
||||
'class_fq' => $sClassFQ,
|
||||
'function_call' => $sFunctionCall,
|
||||
);
|
||||
}
|
||||
|
||||
$aData['debug_trace_steps'] = $aSteps;
|
||||
|
||||
$oResponse = $oApp['twig']->render('itop-portal-base/portal/templates/errors/layout.html.twig',
|
||||
$aData);
|
||||
}
|
||||
|
||||
return $oResponse;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the brick's security from the OQL queries to profiles arrays
|
||||
*
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
{# Base error layout #}
|
||||
{% extends 'itop-portal-base/portal/templates/layout.html.twig' %}
|
||||
|
||||
{% block pNavigationWrapper %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pMainWrapper %}
|
||||
{% block pStyleinline %}
|
||||
<style>
|
||||
.well {
|
||||
margin: 50px auto;
|
||||
@@ -24,18 +21,34 @@
|
||||
p a.btn {
|
||||
margin: 0 5px;
|
||||
}
|
||||
h1 .ion {
|
||||
vertical-align: -5%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
abbr[title]{
|
||||
border-bottom: none;
|
||||
}
|
||||
.traces.list_exception{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
{# Stack trace is only displayed in debug #}
|
||||
{% if app['kernel'].debug == true %}
|
||||
code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
{# Include SF style for the stack trace #}
|
||||
{{ include('@Twig/exception.css.twig') }}
|
||||
|
||||
{# In production (SF context, not iTop), we hide some element as the code will not be displayed #}
|
||||
{% if app['kernel'].environment == 'prod' %}
|
||||
.trace-line-header > .icon{
|
||||
display: none !important;
|
||||
}
|
||||
.trace-code{
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pNavigationWrapper %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pMainWrapper %}
|
||||
<div class="container">
|
||||
<div class="well">
|
||||
<h1><div class="ion ion-alert-circled"></div> {{ error_title }}</h1>
|
||||
@@ -44,21 +57,40 @@
|
||||
<p>
|
||||
<a class="btn btn-default" href="#" onclick="history.back(); return false;"><span class="fas fa-arrow-left"></span> {{ 'Page:GoPreviousPage'|dict_s }}</a>
|
||||
<a class="btn btn-default" href=""><span class="fas fa-redo"></span> {{ 'Page:ReloadPage'|dict_s }}</a>
|
||||
<a class="btn btn-default" href="{{ app.url_generator.generate('p_home') }}"><span class="fas fa-home"></span> {{ 'Page:GoPortalHome'|dict_s }}</a>
|
||||
<a class="btn btn-default" href="{{ app['url_generator'].generate('p_home') }}"><span class="fas fa-home"></span> {{ 'Page:GoPortalHome'|dict_s }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if app['kernel'].debug == true %}
|
||||
<div class="well">
|
||||
<ol class="traces list_exception">
|
||||
{% for aStep in debug_trace_steps %}
|
||||
<li>
|
||||
{% if aStep.function_call is not null %}at <abbr title="{{ aStep.class_fq }}">{{ aStep.function_call }}</abbr>{% endif %}
|
||||
in <a title="{{ aStep.file_fq }}">{{ aStep.file_name }}</a> line {{ aStep.line }}
|
||||
</li>
|
||||
<div class="exceptions-container">
|
||||
{# Note: The following is copied by the '@Twig/Exception/exception.html.twig' #}
|
||||
{% set exception_as_array = exception.toarray %}
|
||||
{% set _exceptions_with_user_code = [] %}
|
||||
{% for i, e in exception_as_array %}
|
||||
{% for trace in e.trace %}
|
||||
{% if (trace.file is not empty) and ('/vendor/' not in trace.file) and ('/var/cache/' not in trace.file) and not loop.last %}
|
||||
{% set _exceptions_with_user_code = _exceptions_with_user_code|merge([i]) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endfor %}
|
||||
<h3 class="tab-title">
|
||||
{% if exception_as_array|length > 1 %}
|
||||
Exceptions <span class="badge">{{ exception_as_array|length }}</span>
|
||||
{% else %}
|
||||
Exception
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<div class="tab-content">
|
||||
{% for i, e in exception_as_array %}
|
||||
{{ include('@Twig/Exception/traces.html.twig', { exception: e, index: loop.index, expand: i in _exceptions_with_user_code or (_exceptions_with_user_code is empty and loop.first) }, with_context = false) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pPageLiveScripts %}
|
||||
{{ include('@Twig/base_js.html.twig') }}
|
||||
{% endblock %}
|
||||
@@ -51,6 +51,7 @@ return array(
|
||||
'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetPluginPropertyClass' => $baseDir . '/src/EventListener/ApplicationContextSetPluginPropertyClass.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetUrlMakerClass' => $baseDir . '/src/EventListener/ApplicationContextSetUrlMakerClass.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\CssFromSassCompiler' => $baseDir . '/src/EventListener/CssFromSassCompiler.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\ExceptionListener' => $baseDir . '/src/EventListener/ExceptionListener.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\UserProvider' => $baseDir . '/src/EventListener/UserProvider.php',
|
||||
'Combodo\\iTop\\Portal\\Form\\ObjectFormManager' => $baseDir . '/src/Form/ObjectFormManager.php',
|
||||
'Combodo\\iTop\\Portal\\Form\\PasswordFormManager' => $baseDir . '/src/Form/PasswordFormManager.php',
|
||||
|
||||
@@ -71,6 +71,7 @@ class ComposerStaticInitdf408f3f8ea034d298269cdf7647358b
|
||||
'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetPluginPropertyClass' => __DIR__ . '/../..' . '/src/EventListener/ApplicationContextSetPluginPropertyClass.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\ApplicationContextSetUrlMakerClass' => __DIR__ . '/../..' . '/src/EventListener/ApplicationContextSetUrlMakerClass.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\CssFromSassCompiler' => __DIR__ . '/../..' . '/src/EventListener/CssFromSassCompiler.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\ExceptionListener' => __DIR__ . '/../..' . '/src/EventListener/ExceptionListener.php',
|
||||
'Combodo\\iTop\\Portal\\EventListener\\UserProvider' => __DIR__ . '/../..' . '/src/EventListener/UserProvider.php',
|
||||
'Combodo\\iTop\\Portal\\Form\\ObjectFormManager' => __DIR__ . '/../..' . '/src/Form/ObjectFormManager.php',
|
||||
'Combodo\\iTop\\Portal\\Form\\PasswordFormManager' => __DIR__ . '/../..' . '/src/Form/PasswordFormManager.php',
|
||||
|
||||
Reference in New Issue
Block a user