8000 [Debug] Support showing exceptions as plain text · symfony/symfony@a1782bf · GitHub
[go: up one dir, main page]

Skip to content

Commit a1782bf

Browse files
committed
[Debug] Support showing exceptions as plain text
1 parent f52a35c commit a1782bf

File tree

8 files changed

+706
-214
lines changed

8 files changed

+706
-214
lines changed

src/Symfony/Component/Debug/Debug.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace Symfony\Component\Debug;
1313

14+
use Symfony\Component\Debug\Formatter\FormatterInterface;
15+
use Symfony\Component\Debug\Formatter\HtmlFormatter;
16+
use Symfony\Component\Debug\Formatter\TextFormatter;
17+
1418
/**
1519
* Registers all the debug tools.
1620
*
@@ -44,7 +48,16 @@ public static function enable($errorReportingLevel = E_ALL, $displayErrors = tru
4448

4549
if ('cli' !== PHP_SAPI) {
4650
ini_set('display_errors', 0);
47-
ExceptionHandler::register();
51+
52+
// Default to HTML, unless requested with a text-only browser or with XMLHttpRequest.
53+
if (!(isset($_SERVER['HTTP_ACCEPT']) && !preg_match('@(^|,\s*)text/html\s*(,|;|$)@', $_SERVER['HTTP_ACCEPT'])
54+
|| isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')) {
55+
$formatter = new HtmlFormatter();
56+
} else {
57+
$formatter = new TextFormatter();
58+
}
59+
60+
ExceptionHandler::register(true, null, null, $formatter);
4861
} elseif ($displayErrors && (!ini_get('log_errors') || ini_get('error_log'))) {
4962
// CLI - display errors only if they're not already logged to STDERR
5063
ini_set('display_errors', 1);

src/Symfony/Component/Debug/ExceptionHandler.php

Lines changed: 77 additions & 209 deletions
Large diffs are not rendered by default.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Debug\Formatter;
13+
14+
use Symfony\Component\Debug\Exception\FlattenException;
15+
16+
interface FormatterInterface
17+
{
18+
/**
19+
* Sets the charset used by exception messages.
20+
*
21+
* @param string $charset The charset used by exception messages.
22+
*/
23+
public function setCharset($string);
24+
25+
/**
26+
* Gets the MIME type of the content returned by this formatter.
27+
*
28+
* @return string A MIME-type, possibly with a charset parameter.
29+
*/
30+
public function getContentType();
31+
32+
/**
33+
* Gets the formatted exception.
34+
*
35+
* @param FlattenException The exception to format.
36+
* @param bool Whether to output detailed debug information.
37+
*
38+
* @return string The formatted exception.
39+
*/
40+
public function getContent(FlattenException $exception, $debug);
41+
}

src/Symfony/Component/Debug/Formatter/HtmlFormatter.php

Lines changed: 280 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Debug\Formatter;
13+
14+
use Symfony\Component\Debug\Exception\FlattenException;
15+
16+
/**
17+
* TextFormatter formats an exception as a plain-text string.
18+
*/
19+
class TextFormatter implements FormatterInterface
20+
{
21+
protected $charset = 'UTF-8';
22+
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function setCharset($charset)
27+
{
28+
$this->charset = $charset;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function getContentType()
35+
{
36+
return 'text/plain; charset='.$this->charset;
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function getContent(FlattenException $exception, $debug)
43+
{
44+
$content = '';
45+
46+
switch ($exception->getStatusCode()) {
47+
case 404:
48+
$title = 'Sorry, the page you are looking for could not be found.';
49+
break;
50+
default:
51+
$title = 'Whoops, looks like something went wrong.';
52+
}
53+
54+
if ($debug) {
55+
try {
56+
$count = count($exception->getAllPrevious());
57+
$total = $count + 1;
58+
foreach ($exception->toArray() as $position => $e) {
59+
$ind = $count - $position + 1;
60+
$class = $this->formatClass($e['class']);
61+
$message = $this->sanitizeString($e['message']);
62+
$path = $this->formatPath($e['trace'][0]['file'], $e['trace'][0]['line']);
63+
$trace = $this->formatTrace($e['trace']);
64+
$content .= "$ind/$total: $class\n $message\n\n$trace\n\n";
65+
}
66+
} catch (\Exception $e) {
67+
// something nasty happened and we cannot throw an exception anymore
68+
$title = sprintf('Exception thrown when handling an exception (%s: %s)', get_class($e), $this->sanitizeString($e->getMessage()));
69+
}
70+
}
71+
72+
return "$title\n\n$content";
73+
}
74+
75+
protected function formatTrace(array $trace)
76+
{
77+
$content = '';
78+
foreach ($trace as $trace) {
79+
$line = '';
80+
if ($trace['function']) {
81+
$line .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args']));
82+
}
83+
if (isset($trace['file']) && isset($trace['line'])) {
84+
if ($line) {
85+
$line .= ' ';
86+
}
87+
$line .= $this->formatPath($trace['file'], $trace['line']);
88+
}
89+
$content .= " $line\n";
90+
}
91+
92+
return $content;
93+
}
94+
95+
protected function formatClass($class)
96+
{
97+
return $class;
98+
}
99+
100+
protected function formatPath($path, $line)
101+
{
102+
$path = $this->sanitizeString($path);
103+
104+
return sprintf('in %s:%u', $path, $line);
105+
}
106+
107+
/**
108+
* Formats an array as a string.
109+
*
110+
* @param array $args The argument array
111+
*
112+
* @return string
113+
*/
114+
protected function formatArgs(array $args)
115+
{
116+
$result = array();
117+
foreach ($args as $key => $item) {
118+
if ('object' === $item[0]) {
119+
$formattedValue = sprintf('object(%s)', $this->formatClass($item[1]));
120+
} elseif ('array' === $item[0]) {
121+
$formattedValue = sprintf('array(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
122+
} elseif ('string' === $item[0]) {
123+
$formattedValue = sprintf("'%s'", $this->sanitizeString($item[1]));
124+
} elseif ('null' === $item[0]) {
125+
$formattedValue = 'null';
126+
} elseif ('boolean' === $item[0]) {
127+
$formattedValue = ''.strtolower(var_export($item[1], true)).'';
128+
} elseif ('resource' === $item[0]) {
129+
$formattedValue = 'resource';
130+
} else {
131+
$formattedValue = str_replace("\n", '', var_export($this->sanitizeString((string) $item[1]), true));
132+
}
133+
134+
$result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->sanitizeString($key), $formattedValue);
135+
}
136+
137+
return implode(', ', $result);
138+
}
139+
140+
/**
141+
* Removes control characters from a string.
142+
*/
143+
protected function sanitizeString($str)
144+
{
145+
return preg_replace('@[\x00-\x1F]+@', ' ', $str);
146+
}
147+
}

src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Debug\ExceptionHandler;
1616
use Symfony\Component\Debug\Exception\OutOfMemoryException;
17+
use Symfony\Component\Debug\Formatter\TextFormatter;
1718
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
1819
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
1920

@@ -54,7 +55,20 @@ public function testDebug()
5455

5556
public function testStatusCode()
5657
{
57-
$handler = new ExceptionHandler(false, 'iso8859-1');
58+
$handler = new ExceptionHandler(false, 'iso-8859-1');
59+
60+
ob_start();
61+
$handler->sendPhpResponse(new \RuntimeException('Foo'));
62+
$response = ob_get_clean();
63+
64+
$this->assertContains('Whoops, looks like something went wrong.', $response);
65+
66+
$expectedHeaders = array(
67+
array('HTTP/1.0 500', true, null),
68+
array('Content-Type: text/html; charset=iso-8859-1', true, null),
69+
);
70+
71+
$this->assertSame($expectedHeaders, testHeader());
5872

5973
ob_start();
6074
$handler->sendPhpResponse(new NotFoundHttpException('Foo'));
@@ -64,15 +78,43 @@ public function testStatusCode()
6478

6579
$expectedHeaders = array(
6680
array('HTTP/1.0 404', true, null),
67-
array('Content-Type: text/html; charset=iso8859-1', true, null),
81+
array('Content-Type: text/html; charset=iso-8859-1', true, null),
6882
);
6983

7084
$this->assertSame($expectedHeaders, testHeader());
7185
}
7286

87+
public function testContentType()
88+
{
89+
$handler = new ExceptionHandler(false, 'iso-8859-1');
90+
91+
ob_start();
92+
$handler->sendPhpResponse(new \RuntimeException('Foo'));
93+
$response = ob_get_clean();
94+
95+
$this->assertContains(array('Content-Type: text/html; charset=iso-8859-1', true, null), testHeader());
96+
97+
$handler->getFormatter()->setCharset('ISO-8859-1');
98+
99+
ob_start();
100+
$handler->sendPhpResponse(new \RuntimeException('Foo'));
101+
$response = ob_get_clean();
102+
103+
$this->assertContains(array('Content-Type: text/html; charset=ISO-8859-1', true, null), testHeader());
104+
105+
$handler = new ExceptionHandler(false, 'iso-8859-15');
106+
$handler->setFormatter(new TextFormatter());
107+
108+
ob_start();
109+
$handler->sendPhpResponse(new \RuntimeException('Foo'));
110+
$response = ob_get_clean();
111+
112+
$this->assertContains(array('Content-Type: text/plain; charset=iso-8859-15', true, null), testHeader());
113+
}
114+
73115
public function testHeaders()
74116
{
75-
$handler = new ExceptionHandler(false, 'iso8859-1');
117+
$handler = new ExceptionHandler(false, 'iso-8859-1');
76118

77119
ob_start();
78120
$handler->sendPhpResponse(new MethodNotAllowedHttpException(array('POST')));
@@ -81,7 +123,7 @@ public function testHeaders()
81123
$expectedHeaders = array(
82124
array('HTTP/1.0 405', true, null),
83125
array('Allow: POST', false, null),
84-
array('Content-Type: text/html; charset=iso8859-1', true, null),
126+
array('Content-Type: text/html; charset=iso-8859-1', true, null),
85127
);
86128

87129
$this->assertSame($expectedHeaders, testHeader());
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Debug\Tests\Formatter;
13+
14+
use Symfony\Component\Debug\Exception\FlattenException;
15+
use Symfony\Component\Debug\Formatter\HtmlFormatter;
16+
17+
class HtmlFormatterTest extends \PHPUnit\Framework\TestCase
18+
{
19+
private function getException($a1, $a2, $a3, $a4, $a5, $a6, $a7)
20+
{
21+
return FlattenException::create(new \Exception('foo'));
22+
}
23+
24+
public function testTrace()
25+
{
26+
$formatter = new HtmlFormatter();
27+
28+
$line = __LINE__ + 1;
29+
$exception = $this->getException(null, 1, 1.0, true, 'foo"<bar', array(1, 'b<' => 2), new \stdClass());
30+
31+
$content = $formatter->getContent($exception, true);
32+
33+
$this->assertContains('<td>at <span class="trace-class"><abbr title="Symfony\Component\Debug\Tests\Formatter\HtmlFormatterTest">HtmlFormatterTest</abbr></span><span class="trace-type">-></span><span class="trace-method">getException</span>', $content);
34+
$this->assertContains("<em>null</em>, 1, 1.0, <em>true</em>, 'foo&quot;&lt;bar', <em>array</em>(1, 'b&lt;' => 2), <em>object</em>(<abbr title=\"stdClass\">stdClass</abbr>)", $content);
35+
$this->assertRegExp('@in <a[^>]+><strong>HtmlFormatterTest.php</strong> \(line '.$line.'\)</a>@', $content);
36+
37+
$content = $formatter->getContent($exception, false);
38+
39+
$this->assertNotContains('HtmlFormatterTest', $content);
40+
}
41+
42+
public function testNestedExceptions()
43+
{
44+
$formatter = new HtmlFormatter();
45+
46+
$exception = FlattenException::create(new \RuntimeException('Foo', 0, new \RuntimeException('Bar')));
47+
$content = $formatter->getContent($exception, true);
48+
49+
$this->assertStringMatchesFormat('%A<p class="break-long-words trace-message">Foo</p>%A<p class="break-long-words trace-message">Bar</p>%A', $content);
50+
}
51+
}

0 commit comments

Comments
 (0)
0