[ErrorHandler] Decouple from ErrorRenderer component
This commit is contained in:
parent
a7852c0da8
commit
8f13fc013d
@ -15,7 +15,7 @@ use Symfony\Component\Debug\Exception\FlattenException;
|
||||
use Symfony\Component\Debug\Exception\OutOfMemoryException;
|
||||
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
|
||||
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionHandler::class, \Symfony\Component\ErrorHandler\ExceptionHandler::class), E_USER_DEPRECATED);
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionHandler::class, \Symfony\Component\ErrorHandler\ErrorHandler::class), E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* ExceptionHandler converts an exception to a Response object.
|
||||
@ -31,7 +31,7 @@ use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
|
||||
*
|
||||
* @final since Symfony 4.3
|
||||
*
|
||||
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ExceptionHandler instead.
|
||||
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ErrorHandler instead.
|
||||
*/
|
||||
class ExceptionHandler
|
||||
{
|
||||
|
@ -44,7 +44,6 @@ class Debug
|
||||
|
||||
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
|
||||
ini_set('display_errors', 0);
|
||||
ExceptionHandler::register();
|
||||
} elseif ($displayErrors && (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log'))) {
|
||||
// CLI - display errors only if they're not already logged to STDERR
|
||||
ini_set('display_errors', 1);
|
||||
|
@ -21,7 +21,6 @@ use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHand
|
||||
use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface;
|
||||
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
|
||||
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
|
||||
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
|
||||
|
||||
/**
|
||||
* A generic ErrorHandler for the PHP engine.
|
||||
@ -414,7 +413,7 @@ class ErrorHandler
|
||||
}
|
||||
|
||||
if (false !== strpos($message, "class@anonymous\0")) {
|
||||
$logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage();
|
||||
$logMessage = $this->parseAnonymousClass($message);
|
||||
} else {
|
||||
$logMessage = $this->levels[$type].': '.$message;
|
||||
}
|
||||
@ -539,7 +538,7 @@ class ErrorHandler
|
||||
|
||||
if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
|
||||
if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) {
|
||||
$message = (new FlattenException())->setMessage($message)->getMessage();
|
||||
$message = $this->parseAnonymousClass($message);
|
||||
}
|
||||
if ($exception instanceof FatalErrorException) {
|
||||
if ($exception instanceof FatalThrowableError) {
|
||||
@ -712,4 +711,15 @@ class ErrorHandler
|
||||
|
||||
return $lightTrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the error message by removing the anonymous class notation
|
||||
* and using the parent class instead if possible.
|
||||
*/
|
||||
private function parseAnonymousClass(string $message): string
|
||||
{
|
||||
return preg_replace_callback('/class@anonymous\x00.*?\.php0x?[0-9a-fA-F]++/', static function ($m) {
|
||||
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
|
||||
}, $message);
|
||||
}
|
||||
}
|
||||
|
@ -1,187 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\ErrorHandler;
|
||||
|
||||
use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException;
|
||||
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
|
||||
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
|
||||
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
|
||||
|
||||
/**
|
||||
* ExceptionHandler converts an exception to a Response object.
|
||||
*
|
||||
* It is mostly useful in debug mode to replace the default PHP/XDebug
|
||||
* output with something prettier and more useful.
|
||||
*
|
||||
* As this class is mainly used during Kernel boot, where nothing is yet
|
||||
* available, the Response content is always HTML.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @final since Symfony 4.3
|
||||
*/
|
||||
class ExceptionHandler
|
||||
{
|
||||
private $debug;
|
||||
private $charset;
|
||||
private $handler;
|
||||
private $caughtBuffer;
|
||||
private $caughtLength;
|
||||
private $fileLinkFormat;
|
||||
private $htmlErrorRenderer;
|
||||
|
||||
public function __construct(bool $debug = true, string $charset = null, $fileLinkFormat = null)
|
||||
{
|
||||
$this->debug = $debug;
|
||||
$this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8';
|
||||
$this->fileLinkFormat = $fileLinkFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the exception handler.
|
||||
*
|
||||
* @param bool $debug Enable/disable debug mode, where the stack trace is displayed
|
||||
* @param string|null $charset The charset used by exception messages
|
||||
* @param string|null $fileLinkFormat The IDE link template
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function register($debug = true, $charset = null, $fileLinkFormat = null)
|
||||
{
|
||||
$handler = new static($debug, $charset, $fileLinkFormat);
|
||||
|
||||
$prev = set_exception_handler([$handler, 'handle']);
|
||||
if (\is_array($prev) && $prev[0] instanceof ErrorHandler) {
|
||||
restore_exception_handler();
|
||||
$prev[0]->setExceptionHandler([$handler, 'handle']);
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a user exception handler.
|
||||
*
|
||||
* @param callable $handler An handler that will be called on Exception
|
||||
*
|
||||
* @return callable|null The previous exception handler if any
|
||||
*/
|
||||
public function setHandler(callable $handler = null)
|
||||
{
|
||||
$old = $this->handler;
|
||||
$this->handler = $handler;
|
||||
|
||||
return $old;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the format for links to source files.
|
||||
*
|
||||
* @param string|FileLinkFormatter $fileLinkFormat The format for links to source files
|
||||
*
|
||||
* @return string The previous file link format
|
||||
*/
|
||||
public function setFileLinkFormat($fileLinkFormat)
|
||||
{
|
||||
$old = $this->fileLinkFormat;
|
||||
$this->fileLinkFormat = $fileLinkFormat;
|
||||
|
||||
return $old;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a response for the given Exception.
|
||||
*
|
||||
* To be as fail-safe as possible, the exception is first handled
|
||||
* by our simple exception handler, then by the user exception handler.
|
||||
* The latter takes precedence and any output from the former is cancelled,
|
||||
* if and only if nothing bad happens in this handling path.
|
||||
*/
|
||||
public function handle(\Exception $exception)
|
||||
{
|
||||
if (null === $this->handler || $exception instanceof OutOfMemoryException) {
|
||||
$this->sendPhpResponse($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$caughtLength = $this->caughtLength = 0;
|
||||
|
||||
ob_start(function ($buffer) {
|
||||
$this->caughtBuffer = $buffer;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
$this->sendPhpResponse($exception);
|
||||
while (null === $this->caughtBuffer && ob_end_flush()) {
|
||||
// Empty loop, everything is in the condition
|
||||
}
|
||||
if (isset($this->caughtBuffer[0])) {
|
||||
ob_start(function ($buffer) {
|
||||
if ($this->caughtLength) {
|
||||
// use substr_replace() instead of substr() for mbstring overloading resistance
|
||||
$cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength);
|
||||
if (isset($cleanBuffer[0])) {
|
||||
$buffer = $cleanBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
});
|
||||
|
||||
echo $this->caughtBuffer;
|
||||
$caughtLength = ob_get_length();
|
||||
}
|
||||
$this->caughtBuffer = null;
|
||||
|
||||
try {
|
||||
($this->handler)($exception);
|
||||
$this->caughtLength = $caughtLength;
|
||||
} catch (\Exception $e) {
|
||||
if (!$caughtLength) {
|
||||
// All handlers failed. Let PHP handle that now.
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the error associated with the given Exception as a plain PHP response.
|
||||
*
|
||||
* This method uses plain PHP functions like header() and echo to output
|
||||
* the response.
|
||||
*
|
||||
* @param \Throwable|FlattenException $exception A \Throwable or FlattenException instance
|
||||
*/
|
||||
public function sendPhpResponse($exception)
|
||||
{
|
||||
if ($exception instanceof \Throwable) {
|
||||
$exception = FlattenException::createFromThrowable($exception);
|
||||
}
|
||||
|
||||
if (!headers_sent()) {
|
||||
header(sprintf('HTTP/1.0 %s', $exception->getStatusCode()));
|
||||
foreach ($exception->getHeaders() as $name => $value) {
|
||||
header($name.': '.$value, false);
|
||||
}
|
||||
header('Content-Type: text/html; charset='.$this->charset);
|
||||
}
|
||||
|
||||
if (null === $this->htmlErrorRenderer) {
|
||||
$this->htmlErrorRenderer = new HtmlErrorRenderer($this->debug, $this->charset, $this->fileLinkFormat);
|
||||
}
|
||||
|
||||
echo $this->htmlErrorRenderer->render($exception);
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\ErrorHandler\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException;
|
||||
use Symfony\Component\ErrorHandler\ExceptionHandler;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
require_once __DIR__.'/HeaderMock.php';
|
||||
|
||||
class ExceptionHandlerTest extends TestCase
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
testHeader();
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
testHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
public function testDebug()
|
||||
{
|
||||
$handler = new ExceptionHandler(false);
|
||||
|
||||
ob_start();
|
||||
$handler->sendPhpResponse(new \RuntimeException('Foo'));
|
||||
$response = ob_get_clean();
|
||||
|
||||
$this->assertContains('The server returned a "500 Internal Server Error".', $response);
|
||||
$this->assertNotContains('<div class="trace trace-as-html">', $response);
|
||||
|
||||
$handler = new ExceptionHandler(true);
|
||||
|
||||
ob_start();
|
||||
$handler->sendPhpResponse(new \RuntimeException('Foo'));
|
||||
$response = ob_get_clean();
|
||||
|
||||
$this->assertContains('<h1 class="break-long-words exception-message">Foo</h1>', $response);
|
||||
$this->assertContains('<div class="trace trace-as-html" id="trace-box-1">', $response);
|
||||
|
||||
// taken from https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)
|
||||
$htmlWithXss = '<body onload=alert(\'test1\')> <b onmouseover=alert(\'Wufff!\')>click me!</b> <img src="jAvascript:alert(\'test2\')"> <meta http-equiv="refresh" content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg">';
|
||||
ob_start();
|
||||
$handler->sendPhpResponse(new \RuntimeException($htmlWithXss));
|
||||
$response = ob_get_clean();
|
||||
|
||||
$this->assertContains(sprintf('<h1 class="break-long-words exception-message long">%s</h1>', htmlspecialchars($htmlWithXss, ENT_COMPAT | ENT_SUBSTITUTE, 'UTF-8')), $response);
|
||||
}
|
||||
|
||||
public function testStatusCode()
|
||||
{
|
||||
$handler = new ExceptionHandler(false, 'iso8859-1');
|
||||
|
||||
ob_start();
|
||||
$handler->sendPhpResponse(new NotFoundHttpException('Foo'));
|
||||
$response = ob_get_clean();
|
||||
|
||||
$this->assertContains('The server returned a "404 Not Found".', $response);
|
||||
|
||||
$expectedHeaders = [
|
||||
['HTTP/1.0 404', true, null],
|
||||
['Content-Type: text/html; charset=iso8859-1', true, null],
|
||||
];
|
||||
|
||||
$this->assertSame($expectedHeaders, testHeader());
|
||||
}
|
||||
|
||||
public function testHeaders()
|
||||
{
|
||||
$handler = new ExceptionHandler(false, 'iso8859-1');
|
||||
|
||||
ob_start();
|
||||
$handler->sendPhpResponse(new MethodNotAllowedHttpException(['POST']));
|
||||
$response = ob_get_clean();
|
||||
|
||||
$expectedHeaders = [
|
||||
['HTTP/1.0 405', true, null],
|
||||
['Allow: POST', false, null],
|
||||
['Content-Type: text/html; charset=iso8859-1', true, null],
|
||||
];
|
||||
|
||||
$this->assertSame($expectedHeaders, testHeader());
|
||||
}
|
||||
|
||||
public function testNestedExceptions()
|
||||
{
|
||||
$handler = new ExceptionHandler(true);
|
||||
ob_start();
|
||||
$handler->sendPhpResponse(new \RuntimeException('Foo', 0, new \RuntimeException('Bar')));
|
||||
$response = ob_get_clean();
|
||||
|
||||
$this->assertStringMatchesFormat('%A<h1 class="break-long-words exception-message">Foo</h1>%A<p class="break-long-words trace-message">Bar</p>%A', $response);
|
||||
}
|
||||
|
||||
public function testHandle()
|
||||
{
|
||||
$exception = new \Exception('foo');
|
||||
|
||||
$handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock();
|
||||
$handler
|
||||
->expects($this->exactly(2))
|
||||
->method('sendPhpResponse');
|
||||
|
||||
$handler->handle($exception);
|
||||
|
||||
$handler->setHandler(function ($e) use ($exception) {
|
||||
$this->assertSame($exception, $e);
|
||||
});
|
||||
|
||||
$handler->handle($exception);
|
||||
}
|
||||
|
||||
public function testHandleOutOfMemoryException()
|
||||
{
|
||||
$exception = new OutOfMemoryException('foo', 0, E_ERROR, __FILE__, __LINE__);
|
||||
|
||||
$handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock();
|
||||
$handler
|
||||
->expects($this->once())
|
||||
->method('sendPhpResponse');
|
||||
|
||||
$handler->setHandler(function ($e) {
|
||||
$this->fail('OutOfMemoryException should bypass the handler');
|
||||
});
|
||||
|
||||
$handler->handle($exception);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\ErrorHandler\Tests;
|
||||
|
||||
use Symfony\Component\ErrorHandler\ExceptionHandler;
|
||||
|
||||
class MockExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
public $e;
|
||||
|
||||
public function handle(\Exception $e)
|
||||
{
|
||||
$this->e = $e;
|
||||
}
|
||||
}
|
@ -17,8 +17,7 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"psr/log": "~1.0",
|
||||
"symfony/error-renderer": "^4.4|^5.0"
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/http-kernel": "<3.4"
|
||||
|
@ -16,7 +16,6 @@ use Symfony\Component\Console\ConsoleEvents;
|
||||
use Symfony\Component\Console\Event\ConsoleEvent;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\ErrorHandler\ErrorHandler;
|
||||
use Symfony\Component\ErrorHandler\ExceptionHandler;
|
||||
use Symfony\Component\ErrorRenderer\ErrorRenderer;
|
||||
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
|
||||
use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException;
|
||||
@ -138,19 +137,7 @@ class DebugHandlersListener implements EventSubscriberInterface
|
||||
}
|
||||
if ($this->exceptionHandler) {
|
||||
if ($handler instanceof ErrorHandler) {
|
||||
$h = $handler->setExceptionHandler('var_dump');
|
||||
if (\is_array($h) && $h[0] instanceof ExceptionHandler) {
|
||||
$handler->setExceptionHandler($h);
|
||||
$handler = $h[0];
|
||||
} else {
|
||||
$handler->setExceptionHandler($this->exceptionHandler);
|
||||
}
|
||||
}
|
||||
if ($handler instanceof ExceptionHandler) {
|
||||
$handler->setHandler($this->exceptionHandler);
|
||||
if (null !== $this->fileLinkFormat) {
|
||||
$handler->setFileLinkFormat($this->fileLinkFormat);
|
||||
}
|
||||
$handler->setExceptionHandler($this->exceptionHandler);
|
||||
}
|
||||
$this->exceptionHandler = null;
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\ErrorHandler\ErrorHandler;
|
||||
use Symfony\Component\ErrorHandler\ExceptionHandler;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
@ -38,9 +37,7 @@ class DebugHandlersListenerTest extends TestCase
|
||||
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
|
||||
$userHandler = function () {};
|
||||
$listener = new DebugHandlersListener($userHandler, $logger);
|
||||
$xHandler = new ExceptionHandler();
|
||||
$eHandler = new ErrorHandler();
|
||||
$eHandler->setExceptionHandler([$xHandler, 'handle']);
|
||||
|
||||
$exception = null;
|
||||
set_error_handler([$eHandler, 'handleError']);
|
||||
@ -56,7 +53,7 @@ class DebugHandlersListenerTest extends TestCase
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->assertSame($userHandler, $xHandler->setHandler('var_dump'));
|
||||
$this->assertSame($userHandler, $eHandler->setExceptionHandler('var_dump'));
|
||||
|
||||
$loggers = $eHandler->setLoggers([]);
|
||||
|
||||
|
Reference in New Issue
Block a user