Commit 4f66a53b authored by catch's avatar catch

Issue #2157691 by damiankloip: Move some helper methods in errors.inc to an Error utility class.

parent 77456421
......@@ -11,6 +11,7 @@
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Utility\Title;
use Drupal\Core\Utility\Error;
use Symfony\Component\ClassLoader\ApcClassLoader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
......@@ -1456,7 +1457,7 @@ function request_uri($omit_query_string = FALSE) {
* A link to associate with the message.
*
* @see watchdog()
* @see _drupal_decode_exception()
* @see \Drupal\Core\Utility\Error::decodeException()
*/
function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) {
......@@ -1464,7 +1465,7 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
if (empty($message)) {
// The exception message is run through
// \Drupal\Component\Utility\String::checkPlain() by
// _drupal_decode_exception().
// \Drupal\Core\Utility\Error:decodeException().
$message = '%type: !message in %function (line %line of %file).';
}
// $variables must be an array so that we can add the exception information.
......@@ -1472,8 +1473,7 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
$variables = array();
}
require_once __DIR__ . '/errors.inc';
$variables += _drupal_decode_exception($exception);
$variables += Error::decodeException($exception);
watchdog($type, $message, $variables, $severity, $link);
}
......@@ -1934,15 +1934,15 @@ function _drupal_exception_handler($exception) {
try {
// Log the message to the watchdog and return an error page to the user.
_drupal_log_error(_drupal_decode_exception($exception), TRUE);
_drupal_log_error(Error::decodeException($exception), TRUE);
}
catch (Exception $exception2) {
// Another uncaught exception was thrown while handling the first one.
// If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
if (error_displayable()) {
print '<h1>Additional uncaught exception thrown while handling exception.</h1>';
print '<h2>Original</h2><p>' . _drupal_render_exception_safe($exception) . '</p>';
print '<h2>Additional</h2><p>' . _drupal_render_exception_safe($exception2) . '</p><hr />';
print '<h2>Original</h2><p>' . Error::renderExceptionSafe($exception) . '</p>';
print '<h2>Additional</h2><p>' . Error::renderExceptionSafe($exception2) . '</p><hr />';
}
}
}
......@@ -2977,7 +2977,7 @@ function _drupal_shutdown_function() {
require_once __DIR__ . '/errors.inc';
if (error_displayable()) {
print '<h1>Uncaught exception thrown in shutdown function.</h1>';
print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />';
}
error_log($exception);
}
......
......@@ -5,6 +5,7 @@
* Functions for error handling.
*/
use Drupal\Core\Utility\Error;
use Drupal\Component\Utility\String;
use Symfony\Component\HttpFoundation\Response;
......@@ -58,7 +59,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
$types = drupal_error_levels();
list($severity_msg, $severity_level) = $types[$error_level];
$backtrace = debug_backtrace();
$caller = _drupal_get_last_caller($backtrace);
$caller = Error::getLastCaller($backtrace);
if (!function_exists('filter_xss_admin')) {
require_once __DIR__ . '/common.inc';
......@@ -79,69 +80,6 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
}
}
/**
* Decodes an exception and retrieves the correct caller.
*
* @param $exception
* The exception object that was thrown.
*
* @return
* An error in the format expected by _drupal_log_error().
*/
function _drupal_decode_exception($exception) {
$message = $exception->getMessage();
$backtrace = $exception->getTrace();
// Add the line throwing the exception to the backtrace.
array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile()));
// For PDOException errors, we try to return the initial caller,
// skipping internal functions of the database layer.
if ($exception instanceof PDOException) {
// The first element in the stack is the call, the second element gives us the caller.
// We skip calls that occurred in one of the classes of the database layer
// or in one of its global functions.
$db_functions = array('db_query', 'db_query_range');
while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
in_array($caller['function'], $db_functions))) {
// We remove that call.
array_shift($backtrace);
}
if (isset($exception->query_string, $exception->args)) {
$message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
}
}
$caller = _drupal_get_last_caller($backtrace);
return array(
'%type' => get_class($exception),
// The standard PHP exception handler considers that the exception message
// is plain-text. We mimick this behavior here.
'!message' => String::checkPlain($message),
'%function' => $caller['function'],
'%file' => $caller['file'],
'%line' => $caller['line'],
'severity_level' => WATCHDOG_ERROR,
'backtrace' => $backtrace,
);
}
/**
* Renders an exception error message without further exceptions.
*
* @param $exception
* The exception object that was thrown.
*
* @return
* An error message.
*/
function _drupal_render_exception_safe($exception) {
$decode = _drupal_decode_exception($exception);
unset($decode['backtrace']);
return String::checkPlain(strtr('%type: !message in %function (line %line of %file).', $decode));
}
/**
* Determines whether an error should be displayed.
*
......@@ -312,43 +250,6 @@ function _drupal_get_error_level() {
}
}
/**
* Gets the last caller from a backtrace.
*
* @param $backtrace
* A standard PHP backtrace. Passed by reference.
*
* @return
* An associative array with keys 'file', 'line' and 'function'.
*/
function _drupal_get_last_caller(&$backtrace) {
// Errors that occur inside PHP internal functions do not generate
// information about file and line. Ignore black listed functions.
$blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
while (($backtrace && !isset($backtrace[0]['line'])) ||
(isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
array_shift($backtrace);
}
// The first trace is the call itself.
// It gives us the line and the file of the last call.
$call = $backtrace[0];
// The second call give us the function where the call originated.
if (isset($backtrace[1])) {
if (isset($backtrace[1]['class'])) {
$call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
}
else {
$call['function'] = $backtrace[1]['function'] . '()';
}
}
else {
$call['function'] = 'main()';
}
return $call;
}
/**
* Formats a backtrace into a plain-text string.
*
......@@ -359,31 +260,9 @@ function _drupal_get_last_caller(&$backtrace) {
*
* @return string
* A plain-text line-wrapped string ready to be put inside <pre>.
*
* @deprecated Use \Drupal\Core\Utility\Error::formatBacktrace() instead.
*/
function format_backtrace(array $backtrace) {
$return = '';
foreach ($backtrace as $trace) {
$call = array('function' => '', 'args' => array());
if (isset($trace['class'])) {
$call['function'] = $trace['class'] . $trace['type'] . $trace['function'];
}
elseif (isset($trace['function'])) {
$call['function'] = $trace['function'];
}
else {
$call['function'] = 'main';
}
if (isset($trace['args'])) {
foreach ($trace['args'] as $arg) {
if (is_scalar($arg)) {
$call['args'][] = is_string($arg) ? '\'' . filter_xss($arg) . '\'' : $arg;
}
else {
$call['args'][] = ucfirst(gettype($arg));
}
}
}
$return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n";
}
return $return;
return Error::formatBacktrace($backtrace);
}
......@@ -17,8 +17,8 @@
*/
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Utility\Error;
/**
* Session handler assigned by session_set_save_handler().
......@@ -228,7 +228,7 @@ function _drupal_session_write($sid, $value) {
// uncaught exception being thrown.
if (error_displayable()) {
print '<h1>Uncaught exception thrown in session handler.</h1>';
print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />';
}
return FALSE;
}
......
......@@ -14,6 +14,7 @@
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Utility\Error;
use Drupal\Component\Uuid\Uuid;
use Drupal\Component\Utility\NestedArray;
use Symfony\Component\HttpFoundation\Request;
......@@ -815,12 +816,11 @@ function update_do_one($module, $number, $dependency_map, &$context) {
catch (Exception $e) {
watchdog_exception('update', $e);
require_once __DIR__ . '/errors.inc';
$variables = _drupal_decode_exception($e);
$variables = Error::decodeException($e);
unset($variables['backtrace']);
// The exception message is run through
// \Drupal\Component\Utility\String::checkPlain() by
// _drupal_decode_exception().
// \Drupal\Core\Utility\Error::decodeException().
$ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
}
}
......
......@@ -18,6 +18,7 @@
use Drupal\Component\Utility\String;
use Symfony\Component\Debug\Exception\FlattenException;
use Drupal\Core\ContentNegotiation;
use Drupal\Core\Utility\Error;
/**
* This controller handles HTTP errors generated by the routing system.
......@@ -413,7 +414,8 @@ protected function decodeException(FlattenException $exception) {
array_shift($backtrace);
}
}
$caller = $this->getLastCaller($backtrace);
$caller = Error::getLastCaller($backtrace);
return array(
'%type' => $exception->getClass(),
......@@ -427,46 +429,4 @@ protected function decodeException(FlattenException $exception) {
);
}
/**
* Gets the last caller from a backtrace.
*
* The last caller is not necessarily the first item in the backtrace. Rather,
* it is the first item in the backtrace that is a PHP userspace function,
* and not one of our debug functions.
*
* @param $backtrace
* A standard PHP backtrace.
*
* @return array
* An associative array with keys 'file', 'line' and 'function'.
*/
protected function getLastCaller($backtrace) {
// Ignore black listed error handling functions.
$blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
// Errors that occur inside PHP internal functions do not generate
// information about file and line.
while (($backtrace && !isset($backtrace[0]['line'])) ||
(isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
array_shift($backtrace);
}
// The first trace is the call itself.
// It gives us the line and the file of the last call.
$call = $backtrace[0];
// The second call give us the function where the call originated.
if (isset($backtrace[1])) {
if (isset($backtrace[1]['class'])) {
$call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
}
else {
$call['function'] = $backtrace[1]['function'] . '()';
}
}
else {
$call['function'] = 'main()';
}
return $call;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Utility\Error.
*/
namespace Drupal\Core\Utility;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Xss;
/**
* Drupal error utility class.
*/
class Error {
/**
* The error severity level.
*
* @var int
*/
const ERROR = 3;
/**
* An array of blacklisted functions.
*
* @var array
*/
protected static $blacklistFunctions = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
/**
* Decodes an exception and retrieves the correct caller.
*
* @param \Exception $exception
* The exception object that was thrown.
*
* @return array
* An error in the format expected by _drupal_log_error().
*/
public static function decodeException(\Exception $exception) {
$message = $exception->getMessage();
$backtrace = $exception->getTrace();
// Add the line throwing the exception to the backtrace.
array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile()));
// For PDOException errors, we try to return the initial caller,
// skipping internal functions of the database layer.
if ($exception instanceof \PDOException) {
// The first element in the stack is the call, the second element gives us
// the caller. We skip calls that occurred in one of the classes of the
// database layer or in one of its global functions.
$db_functions = array('db_query', 'db_query_range');
while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
in_array($caller['function'], $db_functions))) {
// We remove that call.
array_shift($backtrace);
}
if (isset($exception->query_string, $exception->args)) {
$message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
}
}
$caller = static::getLastCaller($backtrace);
return array(
'%type' => get_class($exception),
// The standard PHP exception handler considers that the exception message
// is plain-text. We mimic this behavior here.
'!message' => String::checkPlain($message),
'%function' => $caller['function'],
'%file' => $caller['file'],
'%line' => $caller['line'],
'severity_level' => static::ERROR,
'backtrace' => $backtrace,
);
}
/**
* Renders an exception error message without further exceptions.
*
* @param \Exception $exception
* The exception object that was thrown.
*
* @return string
* An error message.
*/
public static function renderExceptionSafe(\Exception $exception) {
$decode = static::decodeException($exception);
unset($decode['backtrace']);
return String::format('%type: !message in %function (line %line of %file).', $decode);
}
/**
* Gets the last caller from a backtrace.
*
* @param array $backtrace
* A standard PHP backtrace. Passed by reference.
*
* @return array
* An associative array with keys 'file', 'line' and 'function'.
*/
public static function getLastCaller(array &$backtrace) {
// Errors that occur inside PHP internal functions do not generate
// information about file and line. Ignore black listed functions.
while (($backtrace && !isset($backtrace[0]['line'])) ||
(isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], static::$blacklistFunctions))) {
array_shift($backtrace);
}
// The first trace is the call itself.
// It gives us the line and the file of the last call.
$call = $backtrace[0];
// The second call gives us the function where the call originated.
if (isset($backtrace[1])) {
if (isset($backtrace[1]['class'])) {
$call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
}
else {
$call['function'] = $backtrace[1]['function'] . '()';
}
}
else {
$call['function'] = 'main()';
}
return $call;
}
/**
* Formats a backtrace into a plain-text string.
*
* The calls show values for scalar arguments and type names for complex ones.
*
* @param array $backtrace
* A standard PHP backtrace.
*
* @return string
* A plain-text line-wrapped string ready to be put inside <pre>.
*/
public static function formatBacktrace(array $backtrace) {
$return = '';
foreach ($backtrace as $trace) {
$call = array('function' => '', 'args' => array());
if (isset($trace['class'])) {
$call['function'] = $trace['class'] . $trace['type'] . $trace['function'];
}
elseif (isset($trace['function'])) {
$call['function'] = $trace['function'];
}
else {
$call['function'] = 'main';
}
if (isset($trace['args'])) {
foreach ($trace['args'] as $arg) {
if (is_scalar($arg)) {
$call['args'][] = is_string($arg) ? '\'' . Xss::filter($arg) . '\'' : $arg;
}
else {
$call['args'][] = ucfirst(gettype($arg));
}
}
}
$return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n";
}
return $return;
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\migrate;
use Drupal\Core\Utility\Error;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
......@@ -539,7 +540,7 @@ protected function timeExceeded() {
* in contexts where this doesn't make sense.
*/
public function handleException($exception, $save = TRUE) {
$result = _drupal_decode_exception($exception);
$result = Error::decodeException($exception);
$message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')';
if ($save) {
$this->saveMessage($message);
......
......@@ -18,6 +18,7 @@
use Drupal\Core\DrupalKernel;
use Drupal\Core\Language\Language;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\Core\Utility\Error;
use Symfony\Component\HttpFoundation\Request;
/**
......@@ -397,7 +398,7 @@ protected function getAssertionCall() {
array_shift($backtrace);
}
return _drupal_get_last_caller($backtrace);
return Error::getLastCaller($backtrace);
}
/**
......@@ -1177,10 +1178,10 @@ public function errorHandler($severity, $message, $file = NULL, $line = NULL) {
if ($severity !== E_USER_NOTICE) {
$verbose_backtrace = $backtrace;
array_shift($verbose_backtrace);
$message .= '<pre class="backtrace">' . format_backtrace($verbose_backtrace) . '</pre>';
$message .= '<pre class="backtrace">' . Error::formatBacktrace($verbose_backtrace) . '</pre>';
}
$this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace));
$this->error($message, $error_map[$severity], Error::getLastCaller($backtrace));
}
return TRUE;
}
......@@ -1199,14 +1200,15 @@ protected function exceptionHandler($exception) {
'line' => $exception->getLine(),
'file' => $exception->getFile(),
));
// The exception message is run through check_plain()
// by _drupal_decode_exception().
$decoded_exception = _drupal_decode_exception($exception);
// The exception message is run through
// \Drupal\Component\Utility\checkPlain() by
// \Drupal\Core\Utility\decodeException().
$decoded_exception = Error::decodeException($exception);
unset($decoded_exception['backtrace']);
$message = format_string('%type: !message in %function (line %line of %file). <pre class="backtrace">!backtrace</pre>', $decoded_exception + array(
'!backtrace' => format_backtrace($verbose_backtrace),
'!backtrace' => Error::formatBacktrace($verbose_backtrace),
));
$this->error($message, 'Uncaught exception', _drupal_get_last_caller($backtrace));
$this->error($message, 'Uncaught exception', Error::getLastCaller($backtrace));
}
/**
......
......@@ -39,8 +39,8 @@ function testShutdownFunctions() {
$this->assertText(t('First shutdown function, arg1 : @arg1, arg2: @arg2', array('@arg1' => $arg1, '@arg2' => $arg2)));
$this->assertText(t('Second shutdown function, arg1 : @arg1, arg2: @arg2', array('@arg1' => $arg1, '@arg2' => $arg2)));
// Make sure exceptions displayed through _drupal_render_exception_safe()
// are correctly escaped.
$this->assertRaw('Drupal is &amp;lt;blink&amp;gt;awesome&amp;lt;/blink&amp;gt;.');
// Make sure exceptions displayed through
// \Drupal\Core\Utility\Error::renderExceptionSafe() are correctly escaped.
$this->assertRaw('Drupal is &lt;blink&gt;awesome&lt;/blink&gt;.');
}
}
......@@ -157,7 +157,7 @@ function _system_test_second_shutdown_function($arg1, $arg2) {
// Throw an exception with an HTML tag. Since this is called in a shutdown
// function, it will not bubble up to the default exception handler but will
// be caught in _drupal_shutdown_function() and be displayed through
// _drupal_render_exception_safe().
// \Drupal\Core\Utility\Error::renderExceptionSafe().
throw new Exception('Drupal is <blink>awesome</blink>.');
}
......
<
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Utility\ErrorTest.
*/
namespace Drupal\Tests\Core\Utility;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Utility\Error;
/**
* Tests the Error class.
*
* @group Drupal
*
* @see \Drupal\Core\Utility\Error
*/
class ErrorTest extends UnitTestCase {
public static function getInfo() {
return array(
'name' => 'Error',
'description' => 'Tests the Error utility class.',
'group' => 'Common',
);
}
/**
* Tests the getLastCaller() method.
*
* @param array $backtrace
* The test backtrace array.
* @param array $expected
* The expected return array.
*
* @dataProvider providerTestGetLastCaller
*
*/
public function testGetLastCaller($backtrace, $expected) {
$this->assertSame($expected, Error::getLastCaller($backtrace));
}
/**
* Data provider for testGetLastCaller.
*
* @return array
* An array of parameter data.
*/
public function providerTestGetLastCaller() {
$data = array();
// Test with just one item. This should default to the function being
// main().
$single_item = array($this->createBacktraceItem());
$data[] = array($single_item, $this->createBacktraceItem('main()'));
// Add a second item, without a class.
$two_items = $single_item;
$two_items[] = $this->createBacktraceItem('test_function_two');
$data[] = array($two_items, $this->createBacktraceItem('test_function_two()'));
// Add a second item, with a class.
$two_items = $single_item;
$two_items[] = $this->createBacktraceItem('test_function_two', 'TestClass');
$data[] = array($two_items, $this->createBacktraceItem('TestClass->test_function_two()'));
// Add blacklist functions to backtrace. They should get removed.
foreach (array('debug', '_drupal_error_handler', '_drupal_exception_handler') as $function) {
$two_items = $single_item;
// Push to the start of the backtrace.
array_unshift($two_items, $this->createBacktraceItem($function));
$data[] = array($single_item, $this->createBacktraceItem('main()'));
}
return $data;
}
/**
* Tests the formatBacktrace() method.
*
* @param array $backtrace
* The test backtrace array.
* @param array $expected
* The expected return array.
*
* @dataProvider providerTestFormatBacktrace
*/
public function testFormatBacktrace($backtrace, $expected) {
$this->assertSame($expected, Error::formatBacktrace($backtrace));
}
/**
* Data provider for testFormatBacktrace.
*
* @return array
*/
public function providerTestFormatBacktrace() {
$data = array();
// Test with no function, main should be in the backtrace.
$data[] = array(array($this->createBacktraceItem(NULL, NULL)), "main()\n");
$base = array($this->createBacktraceItem());
$data[] = array($base, "test_function()\n");
// Add a second item.
$second_item = $base;
$second_item[] = $this->createBacktraceItem('test_function_2');
$data[] = array($second_item, "test_function()\ntest_function_2()\n");
// Add a second item, with a class.
$second_item_class = $base;
$second_item_class[] = $this->createBacktraceItem('test_function_2', 'TestClass');
$data[] = array($second_item_class, "test_function()\nTestClass->test_function_2()\n");
// Add a second item, with a class.
$second_item_args = $base;
$second_item_args[] = $this->createBacktraceItem('test_function_2', NULL, array('string', 10, new \stdClass()));