N°2060 - Migrate error page to the Symfony framework

This commit is contained in:
Molkobain
2020-01-30 13:49:42 +01:00
parent c6325dce8e
commit f990a83453
6 changed files with 217 additions and 139 deletions

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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
*

View File

@@ -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 %}

View File

@@ -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',

View File

@@ -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',