errors.inc
Same filename in other branches
Functions for error handling.
File
-
core/
includes/ errors.inc
View source
<?php
/**
* @file
* Functions for error handling.
*/
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Render\Markup;
use Drupal\Core\Utility\Error;
use Symfony\Component\HttpFoundation\Response;
/**
* Maps PHP error constants to watchdog severity levels.
*
* The error constants are documented at
* http://php.net/manual/errorfunc.constants.php
*
* @ingroup logging_severity_levels
*/
function drupal_error_levels() {
$types = [
E_ERROR => [
'Error',
RfcLogLevel::ERROR,
],
E_WARNING => [
'Warning',
RfcLogLevel::WARNING,
],
E_PARSE => [
'Parse error',
RfcLogLevel::ERROR,
],
E_NOTICE => [
'Notice',
RfcLogLevel::NOTICE,
],
E_CORE_ERROR => [
'Core error',
RfcLogLevel::ERROR,
],
E_CORE_WARNING => [
'Core warning',
RfcLogLevel::WARNING,
],
E_COMPILE_ERROR => [
'Compile error',
RfcLogLevel::ERROR,
],
E_COMPILE_WARNING => [
'Compile warning',
RfcLogLevel::WARNING,
],
E_USER_ERROR => [
'User error',
RfcLogLevel::ERROR,
],
E_USER_WARNING => [
'User warning',
RfcLogLevel::WARNING,
],
E_USER_NOTICE => [
'User notice',
RfcLogLevel::NOTICE,
],
E_RECOVERABLE_ERROR => [
'Recoverable fatal error',
RfcLogLevel::ERROR,
],
E_DEPRECATED => [
'Deprecated function',
RfcLogLevel::DEBUG,
],
E_USER_DEPRECATED => [
'User deprecated function',
RfcLogLevel::DEBUG,
],
];
return $types;
}
/**
* Provides custom PHP error handling.
*
* @param $error_level
* The level of the error raised.
* @param $message
* The error message.
* @param $filename
* The filename that the error was raised in.
* @param $line
* The line number the error was raised at.
*/
function _drupal_error_handler_real($error_level, $message, $filename, $line) {
if ($error_level & error_reporting()) {
$types = drupal_error_levels();
[
$severity_msg,
$severity_level,
] = $types[$error_level];
$backtrace = debug_backtrace();
$caller = Error::getLastCaller($backtrace);
// We treat recoverable errors as fatal.
$recoverable = $error_level == E_RECOVERABLE_ERROR;
// As __toString() methods must not throw exceptions (recoverable errors)
// in PHP, we allow them to trigger a fatal error by emitting a user error
// using trigger_error().
$to_string = $error_level == E_USER_ERROR && str_ends_with($caller['function'], '__toString()');
_drupal_log_error([
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
// The standard PHP error handler considers that the error messages
// are HTML. We mimic this behavior here.
'@message' => Markup::create(Xss::filterAdmin($message)),
'%function' => $caller['function'],
'%file' => $caller['file'],
'%line' => $caller['line'],
'severity_level' => $severity_level,
'backtrace' => $backtrace,
'@backtrace_string' => (new \Exception())->getTraceAsString(),
'exception' => NULL,
], $recoverable || $to_string);
}
elseif (defined('DRUPAL_TEST_IN_CHILD_SITE') && DRUPAL_TEST_IN_CHILD_SITE && $error_level === E_USER_DEPRECATED) {
static $seen = [];
if (array_search($message, $seen, TRUE) === FALSE) {
// Only report each deprecation once. Too many headers can break some
// Chrome and web driver testing.
$seen[] = $message;
$backtrace = debug_backtrace();
$caller = Error::getLastCaller($backtrace);
_drupal_error_header(Markup::create(Xss::filterAdmin($message)), 'User deprecated function', $caller['function'], $caller['file'], $caller['line']);
}
}
}
/**
* Determines whether an error should be displayed.
*
* When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
* all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
* will be examined to determine if it should be displayed.
*
* @param $error
* Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
*
* @return bool
* TRUE if an error should be displayed.
*/
function error_displayable($error = NULL) {
if (defined('MAINTENANCE_MODE')) {
return TRUE;
}
$error_level = _drupal_get_error_level();
if ($error_level == ERROR_REPORTING_DISPLAY_ALL || $error_level == ERROR_REPORTING_DISPLAY_VERBOSE) {
return TRUE;
}
if ($error_level == ERROR_REPORTING_DISPLAY_SOME && isset($error)) {
return $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning';
}
return FALSE;
}
/**
* Logs a PHP error or exception and displays an error page in fatal cases.
*
* @param $error
* An array with the following keys: %type, @message, %function, %file, %line,
* @backtrace_string, severity_level, backtrace, and exception. All the
* parameters are plain-text, with the exception of @message, which needs to
* be an HTML string, backtrace, which is a standard PHP backtrace, and
* exception, which is the exception object (or NULL if the error is not an
* exception).
* @param bool $fatal
* TRUE for:
* - An exception is thrown and not caught by something else.
* - A recoverable fatal error, which is a fatal error.
* Non-recoverable fatal errors cannot be logged by Drupal.
*/
function _drupal_log_error($error, $fatal = FALSE) {
$is_installer = InstallerKernel::installationAttempted() && \Drupal::hasContainer();
// Backtrace, exception and 'severity_level' are not valid replacement values
// for t().
$backtrace = $error['backtrace'];
$exception = $error['exception'];
$severity = $error['severity_level'];
unset($error['backtrace'], $error['exception'], $error['severity_level']);
// When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers.
if (defined('DRUPAL_TEST_IN_CHILD_SITE') && DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
_drupal_error_header($error['@message'], $error['%type'], $error['%function'], $error['%file'], $error['%line']);
}
$response = new Response();
// Only call the logger if there is a logger factory available. This can occur
// if there is an error while rebuilding the container or during the
// installer.
if (\Drupal::hasService('logger.factory')) {
try {
// Provide the PHP backtrace and exception to logger implementations. Add
// 'severity_level' to the context to maintain BC and allow logging
// implementations to use it.
\Drupal::logger('php')->log($severity, '%type: @message in %function (line %line of %file) @backtrace_string.', $error + [
'backtrace' => $backtrace,
'exception' => $exception,
'severity_level' => $severity,
]);
} catch (\Throwable) {
// We can't log, for example because the database connection is not
// available. At least try to log to PHP error log.
error_log(strtr('Failed to log error: ' . Error::DEFAULT_ERROR_MESSAGE . ' @backtrace_string', $error));
}
}
// Log fatal errors, so developers can find and debug them.
if ($fatal) {
error_log(sprintf('%s: %s in %s on line %d %s', $error['%type'], $error['@message'], $error['%file'], $error['%line'], $error['@backtrace_string']));
}
if (PHP_SAPI === 'cli') {
if ($fatal) {
// When called from CLI, simply output a plain text message.
// Should not translate the string to avoid errors producing more errors.
$response->setContent(html_entity_decode(strip_tags(new FormattableMarkup(Error::DEFAULT_ERROR_MESSAGE, $error))) . "\n");
$response->send();
exit(1);
}
}
if (\Drupal::hasRequest() && \Drupal::request()->isXmlHttpRequest()) {
if ($fatal) {
if (error_displayable($error)) {
// When called from JavaScript, simply output the error message.
// Should not translate the string to avoid errors producing more errors.
$response->setContent(new FormattableMarkup(Error::DEFAULT_ERROR_MESSAGE, $error));
$response->send();
}
exit;
}
}
else {
// Display the message if the current error reporting level allows this type
// of message to be displayed, and unconditionally in update.php.
$message = '';
$class = NULL;
if (error_displayable($error)) {
$class = 'error';
// If error type is 'User notice' then treat it as debug information
// instead of an error message.
if ($error['%type'] == 'User notice') {
$error['%type'] = 'Debug';
$class = 'status';
}
// Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
// in the message. This also prevents full path disclosure, see
// https://owasp.org/www-community/attacks/Full_Path_Disclosure.
try {
$root = \Drupal::root();
} catch (\Throwable) {
$root = realpath(dirname(__DIR__, 2));
}
if (str_starts_with($error['%file'], $root)) {
$error['%file'] = substr($error['%file'], strlen($root) + 1);
}
// Check if verbose error reporting is on.
$error_level = _drupal_get_error_level();
if ($error_level != ERROR_REPORTING_DISPLAY_VERBOSE) {
// Without verbose logging, use a simple message.
// We use \Drupal\Component\Render\FormattableMarkup directly here,
// rather than use t() since we are in the middle of error handling, and
// we don't want t() to cause further errors.
$message = new FormattableMarkup(Error::DEFAULT_ERROR_MESSAGE, $error);
}
else {
// With verbose logging, we will also include a backtrace.
// First trace is the error itself, already contained in the message.
array_shift($backtrace);
// Strip arguments from the backtrace.
$error['@backtrace'] = Error::formatBacktrace(array_map(function ($trace) {
unset($trace['args']);
return $trace;
}, $backtrace));
$message = new FormattableMarkup('<details class="error-with-backtrace"><summary>' . Error::DEFAULT_ERROR_MESSAGE . '</summary><pre class="backtrace">@backtrace</pre></details>', $error);
}
}
if ($fatal) {
// We fallback to a maintenance page at this point, because the page generation
// itself can generate errors.
// Should not translate the string to avoid errors producing more errors.
$message = 'The website encountered an unexpected error. Try again later.' . '<br />' . $message;
if ($is_installer) {
// install_display_output() prints the output and ends script execution.
$output = [
'#title' => 'Error',
'#markup' => $message,
];
try {
install_display_output($output, $GLOBALS['install_state']);
exit;
} catch (\Throwable) {
// The maintenance page failed, so fall back to a plain error message.
}
}
$response->setContent($message);
$response->setStatusCode(500, '500 Service unavailable (with message)');
$response->send();
// An exception must halt script execution.
exit;
}
if ($message) {
if (\Drupal::hasService('session')) {
// Message display is dependent on sessions being available.
\Drupal::messenger()->addMessage($message, $class, TRUE);
}
else {
print $message;
}
}
}
}
/**
* Returns the current error level.
*
* This function should only be used to get the current error level prior to the
* kernel being booted or before Drupal is installed. In all other situations
* the following code is preferred:
* @code
* \Drupal::config('system.logging')->get('error_level');
* @endcode
*
* @return string
* The current error level.
*/
function _drupal_get_error_level() {
// Raise the error level to maximum for the installer, so users are able to
// file proper bug reports for installer errors. The returned value is
// different to the one below, because the installer actually has a
// 'config.factory' service, which reads the default 'error_level' value from
// System module's default configuration and the default value is not verbose.
// @see error_displayable()
if (InstallerKernel::installationAttempted()) {
return ERROR_REPORTING_DISPLAY_VERBOSE;
}
$error_level = NULL;
// Try to get the error level configuration from database. If this fails,
// for example if the database connection is not there, try to read it from
// settings.php.
try {
$error_level = \Drupal::config('system.logging')->get('error_level');
} catch (\Exception) {
$error_level = $GLOBALS['config']['system.logging']['error_level'] ?? ERROR_REPORTING_HIDE;
}
// If there is no container or if it has no config.factory service, we are
// possibly in an edge-case error situation while trying to serve a regular
// request on a public site, so use the non-verbose default value.
return $error_level ?: ERROR_REPORTING_DISPLAY_ALL;
}
/**
* Adds error information to headers so that tests can access it.
*
* @param $message
* The error message.
* @param $type
* The type of error.
* @param $function
* The function that emitted the error.
* @param $file
* The file that emitted the error.
* @param $line
* The line number in file that emitted the error.
*/
function _drupal_error_header($message, $type, $function, $file, $line) {
// $number does not use drupal_static as it should not be reset
// as it uniquely identifies each PHP error.
static $number = 0;
$assertion = [
$message,
$type,
[
'function' => $function,
'file' => $file,
'line' => $line,
],
];
// For non-fatal errors (e.g. PHP notices) _drupal_log_error can be called
// multiple times per request. In that case the response is typically
// generated outside of the error handler, e.g., in a controller. As a
// result it is not possible to use a Response object here but instead the
// headers need to be emitted directly.
header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
$number++;
}
Functions
Title | Deprecated | Summary |
---|---|---|
drupal_error_levels | Maps PHP error constants to watchdog severity levels. | |
error_displayable | Determines whether an error should be displayed. | |
_drupal_error_handler_real | Provides custom PHP error handling. | |
_drupal_error_header | Adds error information to headers so that tests can access it. | |
_drupal_get_error_level | Returns the current error level. | |
_drupal_log_error | Logs a PHP error or exception and displays an error page in fatal cases. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.