2021-04-08 08:08:59 +00:00
< ? 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\ErrorRenderer ;
use Psr\Log\LoggerInterface ;
use Symfony\Component\ErrorHandler\Exception\FlattenException ;
use Symfony\Component\HttpFoundation\RequestStack ;
use Symfony\Component\HttpFoundation\Response ;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter ;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface ;
/**
* @ author Yonel Ceruto < yonelceruto @ gmail . com >
*/
class HtmlErrorRenderer implements ErrorRendererInterface
{
private const GHOST_ADDONS = [
'02-14' => self :: GHOST_HEART ,
'02-29' => self :: GHOST_PLUS ,
'10-18' => self :: GHOST_GIFT ,
];
private const GHOST_GIFT = ' M124 . 005340576171 88 , 5.3606138080358505 C124 . 40059661865234 , 4.644828304648399 125.1237564086914 , 3.712414965033531 123.88127899169922 , 3.487462028861046 C123 . 53517150878906 , 3.3097832053899765 123.18894958496094 , 2.9953975528478622 122.8432846069336 , 3.345616325736046 C122 . 07421112060547 , 3.649444565176964 121.40750122070312 , 4.074306473135948 122.2164306640625 , 4.869479164481163 C122 . 57514953613281 , 5.3830065578222275 122.90142822265625 , 6.503447040915489 123.3077621459961 , 6.626829609274864 C123 . 55027770996094 , 6.210384353995323 123.7774658203125 , 5.785196766257286 124.00534057617188 , 5.3606138080358505 zM122 . 30630493164062 , 7.336987480521202 C121 . 60028076171875 , 6.076864704489708 121.03211975097656 , 4.72498320043087 120.16796875 , 3.562500938773155 C119 . 11695098876953 , 2.44033907353878 117.04605865478516 , 2.940566048026085 116.57544708251953 , 4.387995228171349 C115 . 95028686523438 , 5.819030746817589 117.2991714477539 , 7.527640804648399 118.826171875 , 7.348545059561729 C119 . 98493194580078 , 7.367936596274376 121.15027618408203 , 7.420116886496544 122.30630493164062 , 7.336987480521202 zM128 . 1732177734375 , 7.379541382193565 C129 . 67486572265625 , 7.17823551595211 130.53842163085938 , 5.287807449698448 129.68344116210938 , 4.032590612769127 C128 . 92578125 , 2.693056806921959 126.74605560302734 , 2.6463639587163925 125.98509216308594 , 4.007616028189659 C125 . 32617950439453 , 5.108129009604454 124.75428009033203 , 6.258124336600304 124.14962768554688 , 7.388818249106407 C125 . 48638916015625 , 7.465229496359825 126.8357162475586 , 7.447416767477989 128.1732177734375 , 7.379541382193565 zM130 . 6601104736328 , 8.991325363516808 C131 . 17202758789062 , 8.540884003043175 133.1543731689453 , 8.009847149252892 131.65304565429688 , 7.582054600119591 C131 . 2811279296875 , 7.476506695151329 130.84751892089844 , 6.99234913289547 130.5132598876953 , 7.124847874045372 C129 . 78744506835938 , 8.02728746831417 128.67140197753906 , 8.55669592320919 127.50616455078125 , 8.501235947012901 C127 . 27806091308594 , 8.576229080557823 126.11459350585938 , 8.38720129430294 126.428955078125 , 8.601900085806847 C127 . 25099182128906 , 9.070617660880089 128.0523223876953 , 9.579657539725304 128.902587890625 , 9.995706543326378 C129 . 49813842773438 , 9.678531631827354 130.0761260986328 , 9.329126343131065 130.6601104736328 , 8.991325363516808 zM118 . 96446990966797 , 9.246344551444054 C119 . 4022445678711 , 8.991325363516808 119.84001922607422 , 8.736305221915245 120.27779388427734 , 8.481284126639366 C118 . 93965911865234 , 8.414779648184776 117.40827941894531 , 8.607666000723839 116.39698791503906 , 7.531384453177452 C116 . 11186981201172 , 7.212117180228233 115.83845520019531 , 6.846597656607628 115.44329071044922 , 7.248530372977257 C114 . 96995544433594 , 7.574637398123741 113.5140609741211 , 7.908811077475548 114.63501739501953 , 8.306883797049522 C115 . 61112976074219 , 8.883499130606651 116.58037567138672 , 9.474181160330772 117.58061218261719 , 10.008124336600304 C118 . 05723571777344 , 9.784612640738487 118.50651550292969 , 9.5052699893713 118.96446990966797 , 9.246344551444054 zM125 . 38018035888672 , 12.091858848929405 C125 . 9474868774414 , 11.636047348380089 127.32159423828125 , 11.201767906546593 127.36749267578125 , 10.712632164359093 C126 . 08487701416016 , 9.974547371268272 124.83960723876953 , 9.152772888541222 123.49772644042969 , 8.528907760977745 C123 . 035 94207763672 , 8.353693947196007 122.66152954101562 , 8.623294815421104 122.28982543945312 , 8.857431396842003 C121 . 19065856933594 , 9.51122473180294 120.06505584716797 , 10.12446115911007 119.00167083740234 , 10.835315689444542 C120 . 39238739013672 , 11.69529627263546 121.79983520507812 , 12.529837593436241 123.22095489501953 , 13.338589653372765 C123 . 94580841064453 , 12.932025894522667 124.66128540039062 , 12.508862480521202 125.38018035888672 , 12.091858848929405 zM131 . 07164001464 844 , 13.514615997672081 C131 . 66018676757812 , 13.143282875418663 132.2487335205078 , 12.771927818655968 132.8372802734375 , 12.400571808218956 C132 . 8324737548828 , 11.156818374991417 132.8523406982422 , 9.912529930472374 132.81829833984375 , 8.669195160269737 C131 . 63046264648438 , 9.332009300589561 130.45948791503906 , 10.027913078665733 129.30828857421875 , 10.752535805106163 C129 . 18237304
private const GHOST_HEART = 'M125.91386369681868,8.305165958366445 C128.95033202169043,-0.40540639102854037 140.8469835342744,8.305165958366445 125.91386369681868,19.504526138305664 C110.98208663272044,8.305165958366445 122.87795231771452,-0.40540639102854037 125.91386369681868,8.305165958366445 z' ;
private const GHOST_PLUS = 'M111.36824226379395,8.969108581542969 L118.69175148010254,8.969108581542969 L118.69175148010254,1.6455793380737305 L126.20429420471191,1.6455793380737305 L126.20429420471191,8.969108581542969 L133.52781105041504,8.969108581542969 L133.52781105041504,16.481630325317383 L126.20429420471191,16.481630325317383 L126.20429420471191,23.805158615112305 L118.69175148010254,23.805158615112305 L118.69175148010254,16.481630325317383 L111.36824226379395,16.481630325317383 z' ;
private $debug ;
private $charset ;
private $fileLinkFormat ;
private $projectDir ;
private $outputBuffer ;
private $logger ;
/**
* @ param bool | callable $debug The debugging mode as a boolean or a callable that should return it
* @ param string | FileLinkFormatter | null $fileLinkFormat
* @ param bool | callable $outputBuffer The output buffer as a string or a callable that should return it
*/
public function __construct ( $debug = false , string $charset = null , $fileLinkFormat = null , string $projectDir = null , $outputBuffer = '' , LoggerInterface $logger = null )
{
if ( ! \is_bool ( $debug ) && ! \is_callable ( $debug )) {
throw new \TypeError ( sprintf ( 'Argument 1 passed to "%s()" must be a boolean or a callable, "%s" given.' , __METHOD__ , \is_object ( $debug ) ? \get_class ( $debug ) : \gettype ( $debug )));
}
if ( ! \is_string ( $outputBuffer ) && ! \is_callable ( $outputBuffer )) {
throw new \TypeError ( sprintf ( 'Argument 5 passed to "%s()" must be a string or a callable, "%s" given.' , __METHOD__ , \is_object ( $outputBuffer ) ? \get_class ( $outputBuffer ) : \gettype ( $outputBuffer )));
}
$this -> debug = $debug ;
2022-12-14 15:55:13 +00:00
$this -> charset = $charset ? : ( \ini_get ( 'default_charset' ) ? : 'UTF-8' );
$this -> fileLinkFormat = $fileLinkFormat ? : ( \ini_get ( 'xdebug.file_link_format' ) ? : get_cfg_var ( 'xdebug.file_link_format' ));
2021-04-08 08:08:59 +00:00
$this -> projectDir = $projectDir ;
$this -> outputBuffer = $outputBuffer ;
$this -> logger = $logger ;
}
/**
* { @ inheritdoc }
*/
public function render ( \Throwable $exception ) : FlattenException
{
$exception = FlattenException :: createFromThrowable ( $exception , null , [
'Content-Type' => 'text/html; charset=' . $this -> charset ,
]);
return $exception -> setAsString ( $this -> renderException ( $exception ));
}
/**
* Gets the HTML content associated with the given exception .
*/
public function getBody ( FlattenException $exception ) : string
{
return $this -> renderException ( $exception , 'views/exception.html.php' );
}
/**
* Gets the stylesheet associated with the given exception .
*/
public function getStylesheet () : string
{
if ( ! $this -> debug ) {
return $this -> include ( 'assets/css/error.css' );
}
return $this -> include ( 'assets/css/exception.css' );
}
public static function isDebug ( RequestStack $requestStack , bool $debug ) : \Closure
{
return static function () use ( $requestStack , $debug ) : bool {
if ( ! $request = $requestStack -> getCurrentRequest ()) {
return $debug ;
}
return $debug && $request -> attributes -> getBoolean ( 'showException' , true );
};
}
public static function getAndCleanOutputBuffer ( RequestStack $requestStack ) : \Closure
{
return static function () use ( $requestStack ) : string {
if ( ! $request = $requestStack -> getCurrentRequest ()) {
return '' ;
}
$startObLevel = $request -> headers -> get ( 'X-Php-Ob-Level' , - 1 );
if ( ob_get_level () <= $startObLevel ) {
return '' ;
}
Response :: closeOutputBuffers ( $startObLevel + 1 , true );
return ob_get_clean ();
};
}
private function renderException ( FlattenException $exception , string $debugTemplate = 'views/exception_full.html.php' ) : string
{
$debug = \is_bool ( $this -> debug ) ? $this -> debug : ( $this -> debug )( $exception );
$statusText = $this -> escape ( $exception -> getStatusText ());
$statusCode = $this -> escape ( $exception -> getStatusCode ());
if ( ! $debug ) {
return $this -> include ( 'views/error.html.php' , [
'statusText' => $statusText ,
'statusCode' => $statusCode ,
]);
}
$exceptionMessage = $this -> escape ( $exception -> getMessage ());
return $this -> include ( $debugTemplate , [
'exception' => $exception ,
'exceptionMessage' => $exceptionMessage ,
'statusText' => $statusText ,
'statusCode' => $statusCode ,
'logger' => $this -> logger instanceof DebugLoggerInterface ? $this -> logger : null ,
'currentContent' => \is_string ( $this -> outputBuffer ) ? $this -> outputBuffer : ( $this -> outputBuffer )(),
]);
}
/**
* Formats an array as a string .
*/
private function formatArgs ( array $args ) : string
{
$result = [];
foreach ( $args as $key => $item ) {
if ( 'object' === $item [ 0 ]) {
$formattedValue = sprintf ( '<em>object</em>(%s)' , $this -> abbrClass ( $item [ 1 ]));
} elseif ( 'array' === $item [ 0 ]) {
$formattedValue = sprintf ( '<em>array</em>(%s)' , \is_array ( $item [ 1 ]) ? $this -> formatArgs ( $item [ 1 ]) : $item [ 1 ]);
} elseif ( 'null' === $item [ 0 ]) {
$formattedValue = '<em>null</em>' ;
} elseif ( 'boolean' === $item [ 0 ]) {
$formattedValue = '<em>' . strtolower ( var_export ( $item [ 1 ], true )) . '</em>' ;
} elseif ( 'resource' === $item [ 0 ]) {
$formattedValue = '<em>resource</em>' ;
} else {
$formattedValue = str_replace ( " \n " , '' , $this -> escape ( var_export ( $item [ 1 ], true )));
}
$result [] = \is_int ( $key ) ? $formattedValue : sprintf ( " '%s' => %s " , $this -> escape ( $key ), $formattedValue );
}
return implode ( ', ' , $result );
}
private function formatArgsAsText ( array $args )
{
return strip_tags ( $this -> formatArgs ( $args ));
}
private function escape ( string $string ) : string
{
return htmlspecialchars ( $string , \ENT_COMPAT | \ENT_SUBSTITUTE , $this -> charset );
}
private function abbrClass ( string $class ) : string
{
$parts = explode ( '\\' , $class );
$short = array_pop ( $parts );
return sprintf ( '<abbr title="%s">%s</abbr>' , $class , $short );
}
private function getFileRelative ( string $file ) : ? string
{
$file = str_replace ( '\\' , '/' , $file );
if ( null !== $this -> projectDir && 0 === strpos ( $file , $this -> projectDir )) {
return ltrim ( substr ( $file , \strlen ( $this -> projectDir )), '/' );
}
return null ;
}
/**
* Returns the link for a given file / line pair .
*
* @ return string | false A link or false
*/
private function getFileLink ( string $file , int $line )
{
if ( $fmt = $this -> fileLinkFormat ) {
return \is_string ( $fmt ) ? strtr ( $fmt , [ '%f' => $file , '%l' => $line ]) : $fmt -> format ( $file , $line );
}
return false ;
}
/**
* Formats a file path .
*
* @ param string $file An absolute file path
* @ param int $line The line number
* @ param string $text Use this text for the link rather than the file path
*/
private function formatFile ( string $file , int $line , string $text = null ) : string
{
$file = trim ( $file );
if ( null === $text ) {
$text = $file ;
if ( null !== $rel = $this -> getFileRelative ( $text )) {
$rel = explode ( '/' , $rel , 2 );
$text = sprintf ( '<abbr title="%s%2$s">%s</abbr>%s' , $this -> projectDir , $rel [ 0 ], '/' . ( $rel [ 1 ] ? ? '' ));
}
}
if ( 0 < $line ) {
$text .= ' at line ' . $line ;
}
if ( false !== $link = $this -> getFileLink ( $file , $line )) {
return sprintf ( '<a href="%s" title="Click to open this file" class="file_link">%s</a>' , $this -> escape ( $link ), $text );
}
return $text ;
}
/**
* Returns an excerpt of a code file around the given line number .
*
* @ param string $file A file path
* @ param int $line The selected line number
* @ param int $srcContext The number of displayed lines around or - 1 for the whole file
*
* @ return string An HTML string
*/
private function fileExcerpt ( string $file , int $line , int $srcContext = 3 ) : string
{
if ( is_file ( $file ) && is_readable ( $file )) {
// highlight_file could throw warnings
// see https://bugs.php.net/25725
$code = @ highlight_file ( $file , true );
// remove main code/span tags
$code = preg_replace ( '#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s' , '\\1' , $code );
// split multiline spans
$code = preg_replace_callback ( '#<span ([^>]++)>((?:[^<]*+<br \/>)++[^<]*+)</span>#' , function ( $m ) {
return " <span $m[1] > " . str_replace ( '<br />' , " </span><br /><span $m[1] > " , $m [ 2 ]) . '</span>' ;
}, $code );
$content = explode ( '<br />' , $code );
$lines = [];
if ( 0 > $srcContext ) {
$srcContext = \count ( $content );
}
for ( $i = max ( $line - $srcContext , 1 ), $max = min ( $line + $srcContext , \count ( $content )); $i <= $max ; ++ $i ) {
$lines [] = '<li' . ( $i == $line ? ' class="selected"' : '' ) . '><a class="anchor" name="line' . $i . '"></a><code>' . $this -> fixCodeMarkup ( $content [ $i - 1 ]) . '</code></li>' ;
}
return '<ol start="' . max ( $line - $srcContext , 1 ) . '">' . implode ( " \n " , $lines ) . '</ol>' ;
}
return '' ;
}
private function fixCodeMarkup ( string $line )
{
// </span> ending tag from previous line
$opening = strpos ( $line , '<span' );
$closing = strpos ( $line , '</span>' );
if ( false !== $closing && ( false === $opening || $closing < $opening )) {
$line = substr_replace ( $line , '' , $closing , 7 );
}
// missing </span> tag at the end of line
$opening = strpos ( $line , '<span' );
$closing = strpos ( $line , '</span>' );
if ( false !== $opening && ( false === $closing || $closing > $opening )) {
$line .= '</span>' ;
}
return trim ( $line );
}
private function formatFileFromText ( string $text )
{
return preg_replace_callback ( '/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s' , function ( $match ) {
return 'in ' . $this -> formatFile ( $match [ 2 ], $match [ 3 ]);
}, $text );
}
private function formatLogMessage ( string $message , array $context )
{
if ( $context && false !== strpos ( $message , '{' )) {
$replacements = [];
foreach ( $context as $key => $val ) {
2022-12-14 15:55:13 +00:00
if ( \is_scalar ( $val )) {
2021-04-08 08:08:59 +00:00
$replacements [ '{' . $key . '}' ] = $val ;
}
}
if ( $replacements ) {
$message = strtr ( $message , $replacements );
}
}
return $this -> escape ( $message );
}
private function addElementToGhost () : string
{
if ( ! isset ( self :: GHOST_ADDONS [ date ( 'm-d' )])) {
return '' ;
}
return '<path d="' . self :: GHOST_ADDONS [ date ( 'm-d' )] . '" fill="#fff" fill-opacity="0.6"></path>' ;
}
private function include ( string $name , array $context = []) : string
{
extract ( $context , \EXTR_SKIP );
ob_start ();
include __DIR__ . '/../Resources/' . $name ;
return trim ( ob_get_clean ());
}
}