2015-08-18 00:00:26 +00:00
< ? php
/**
* @ file
* Functions for error handling .
*/
2018-11-23 12:29:20 +00:00
use Drupal\Component\Render\FormattableMarkup ;
2015-08-18 00:00:26 +00:00
use Drupal\Component\Utility\Xss ;
use Drupal\Core\Logger\RfcLogLevel ;
2015-10-08 18:40:12 +00:00
use Drupal\Core\Render\Markup ;
2015-08-18 00:00:26 +00:00
use Drupal\Core\Utility\Error ;
2015-08-27 19:03:05 +00:00
use Symfony\Component\HttpFoundation\Response ;
2015-08-18 00:00:26 +00:00
/**
* 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 () {
2017-04-13 14:53:35 +00:00
$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_STRICT => [ 'Strict warning' , RfcLogLevel :: DEBUG ],
E_RECOVERABLE_ERROR => [ 'Recoverable fatal error' , RfcLogLevel :: ERROR ],
E_DEPRECATED => [ 'Deprecated function' , RfcLogLevel :: DEBUG ],
E_USER_DEPRECATED => [ 'User deprecated function' , RfcLogLevel :: DEBUG ],
];
2015-08-18 00:00:26 +00:00
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 .
* @ param $context
* An array that points to the active symbol table at the point the error
* occurred .
*/
function _drupal_error_handler_real ( $error_level , $message , $filename , $line , $context ) {
if ( $error_level & error_reporting ()) {
$types = drupal_error_levels ();
list ( $severity_msg , $severity_level ) = $types [ $error_level ];
$backtrace = debug_backtrace ();
$caller = Error :: getLastCaller ( $backtrace );
// We treat recoverable errors as fatal.
2015-10-08 18:40:12 +00:00
$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 && substr ( $caller [ 'function' ], - strlen ( '__toString()' )) == '__toString()' ;
2017-04-13 14:53:35 +00:00
_drupal_log_error ([
2015-08-18 00:00:26 +00:00
'%type' => isset ( $types [ $error_level ]) ? $severity_msg : 'Unknown error' ,
// The standard PHP error handler considers that the error messages
// are HTML. We mimick this behavior here.
2015-10-08 18:40:12 +00:00
'@message' => Markup :: create ( Xss :: filterAdmin ( $message )),
2015-08-18 00:00:26 +00:00
'%function' => $caller [ 'function' ],
'%file' => $caller [ 'file' ],
'%line' => $caller [ 'line' ],
'severity_level' => $severity_level ,
'backtrace' => $backtrace ,
2016-10-06 22:16:20 +00:00
'@backtrace_string' => ( new \Exception ()) -> getTraceAsString (),
2017-04-13 14:53:35 +00:00
], $recoverable || $to_string );
2015-08-18 00:00:26 +00:00
}
2018-11-23 12:29:20 +00:00
// If the site is a test site then fail for user deprecations so they can be
// caught by the deprecation error handler.
elseif ( DRUPAL_TEST_IN_CHILD_SITE && $error_level === E_USER_DEPRECATED ) {
$backtrace = debug_backtrace ();
$caller = Error :: getLastCaller ( $backtrace );
_drupal_error_header (
Markup :: create ( Xss :: filterAdmin ( $message )),
'User deprecated function' ,
$caller [ 'function' ],
$caller [ 'file' ],
$caller [ 'line' ]
);
}
2015-08-18 00:00:26 +00:00
}
/**
* 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
* 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
2015-09-04 20:20:09 +00:00
* An array with the following keys : % type , @ message , % function , % file ,
2016-10-06 22:16:20 +00:00
* % line , @ backtrace_string , severity_level , and backtrace . All the parameters
* are plain - text , with the exception of @ message , which needs to be an HTML
* string , and backtrace , which is a standard PHP backtrace .
2015-11-17 21:42:33 +00:00
* @ param bool $fatal
2016-04-20 16:56:34 +00:00
* TRUE for :
2015-11-17 21:42:33 +00:00
* - An exception is thrown and not caught by something else .
* - A recoverable fatal error , which is a fatal error .
2016-04-20 16:56:34 +00:00
* Non - recoverable fatal errors cannot be logged by Drupal .
2015-08-18 00:00:26 +00:00
*/
function _drupal_log_error ( $error , $fatal = FALSE ) {
$is_installer = drupal_installation_attempted ();
// Backtrace array is not a valid replacement value for t().
$backtrace = $error [ 'backtrace' ];
unset ( $error [ 'backtrace' ]);
// When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers.
if ( DRUPAL_TEST_IN_CHILD_SITE && ! headers_sent () && ( ! defined ( 'SIMPLETEST_COLLECT_ERRORS' ) || SIMPLETEST_COLLECT_ERRORS )) {
2018-11-23 12:29:20 +00:00
_drupal_error_header ( $error [ '@message' ], $error [ '%type' ], $error [ '%function' ], $error [ '%file' ], $error [ '%line' ]);
2015-08-18 00:00:26 +00:00
}
2015-08-27 19:03:05 +00:00
$response = new Response ();
2015-08-18 00:00:26 +00:00
// 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' )) {
2015-08-27 19:03:05 +00:00
try {
2018-11-23 12:29:20 +00:00
// Provide the PHP backtrace to logger implementations.
\Drupal :: logger ( 'php' ) -> log ( $error [ 'severity_level' ], '%type: @message in %function (line %line of %file) @backtrace_string.' , $error + [ 'backtrace' => $backtrace ]);
2015-08-27 19:03:05 +00:00
}
catch ( \Exception $e ) {
// We can't log, for example because the database connection is not
// available. At least try to log to PHP error log.
2016-10-06 22:16:20 +00:00
error_log ( strtr ( 'Failed to log error: %type: @message in %function (line %line of %file). @backtrace_string' , $error ));
2015-08-27 19:03:05 +00:00
}
2015-08-18 00:00:26 +00:00
}
2015-11-17 21:42:33 +00:00
// Log fatal errors, so developers can find and debug them.
if ( $fatal ) {
2016-10-06 22:16:20 +00:00
error_log ( sprintf ( '%s: %s in %s on line %d %s' , $error [ '%type' ], $error [ '@message' ], $error [ '%file' ], $error [ '%line' ], $error [ '@backtrace_string' ]));
2015-11-17 21:42:33 +00:00
}
2015-08-18 00:00:26 +00:00
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.
2018-11-23 12:29:20 +00:00
$response -> setContent ( html_entity_decode ( strip_tags ( new FormattableMarkup ( '%type: @message in %function (line %line of %file).' , $error ))) . " \n " );
2015-08-27 19:03:05 +00:00
$response -> send ();
2015-08-18 00:00:26 +00:00
exit ;
}
}
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.
2018-11-23 12:29:20 +00:00
$response -> setContent ( new FormattableMarkup ( '%type: @message in %function (line %line of %file).' , $error ));
2015-08-27 19:03:05 +00:00
$response -> send ();
2015-08-18 00:00:26 +00:00
}
exit ;
}
}
else {
// Display the message if the current error reporting level allows this type
// of message to be displayed, and unconditionally in update.php.
2015-08-27 19:03:05 +00:00
$message = '' ;
$class = NULL ;
2015-08-18 00:00:26 +00:00
if ( error_displayable ( $error )) {
$class = 'error' ;
// If error type is 'User notice' then treat it as debug information
// instead of an error message.
// @see debug()
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 does not happen for (false) security.
if ( \Drupal :: hasService ( 'app.root' )) {
$root_length = strlen ( \Drupal :: root ());
if ( substr ( $error [ '%file' ], 0 , $root_length ) == \Drupal :: root ()) {
$error [ '%file' ] = substr ( $error [ '%file' ], $root_length + 1 );
}
}
// Check if verbose error reporting is on.
$error_level = _drupal_get_error_level ();
2015-09-04 20:20:09 +00:00
if ( $error_level != ERROR_REPORTING_DISPLAY_VERBOSE ) {
// Without verbose logging, use a simple message.
2018-11-23 12:29:20 +00:00
// 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 ( '%type: @message in %function (line %line of %file).' , $error );
2015-09-04 20:20:09 +00:00
}
else {
// With verbose logging, we will also include a backtrace.
2015-08-18 00:00:26 +00:00
// First trace is the error itself, already contained in the message.
// While the second trace is the error source and also contained in the
// message, the message doesn't contain argument values, so we output it
// once more in the backtrace.
array_shift ( $backtrace );
// Generate a backtrace containing only scalar argument values.
2015-09-04 20:20:09 +00:00
$error [ '@backtrace' ] = Error :: formatBacktrace ( $backtrace );
2018-11-23 12:29:20 +00:00
$message = new FormattableMarkup ( '%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>' , $error );
2015-08-18 00:00:26 +00:00
}
}
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.
2015-08-27 19:03:05 +00:00
$message = 'The website encountered an unexpected error. Please try again later.' . '<br />' . $message ;
2015-08-18 00:00:26 +00:00
if ( $is_installer ) {
// install_display_output() prints the output and ends script execution.
2017-04-13 14:53:35 +00:00
$output = [
2015-08-18 00:00:26 +00:00
'#title' => 'Error' ,
'#markup' => $message ,
2017-04-13 14:53:35 +00:00
];
2015-08-27 19:03:05 +00:00
install_display_output ( $output , $GLOBALS [ 'install_state' ], $response -> headers -> all ());
2015-08-18 00:00:26 +00:00
exit ;
}
2015-08-27 19:03:05 +00:00
$response -> setContent ( $message );
2015-08-18 00:00:26 +00:00
$response -> setStatusCode ( 500 , '500 Service unavailable (with message)' );
2015-08-27 19:03:05 +00:00
2015-08-18 00:00:26 +00:00
$response -> send ();
2015-08-27 19:03:05 +00:00
// An exception must halt script execution.
2015-08-18 00:00:26 +00:00
exit ;
}
2015-09-04 20:20:09 +00:00
if ( $message ) {
2015-08-27 19:03:05 +00:00
if ( \Drupal :: hasService ( 'session' )) {
// Message display is dependent on sessions being available.
2018-11-23 12:29:20 +00:00
\Drupal :: messenger () -> addMessage ( $message , $class , TRUE );
2015-08-27 19:03:05 +00:00
}
else {
print $message ;
}
}
2015-08-18 00:00:26 +00:00
}
}
/**
* 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 ( drupal_installation_attempted ()) {
return ERROR_REPORTING_DISPLAY_VERBOSE ;
}
$error_level = NULL ;
2015-08-27 19:03:05 +00:00
// 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 {
2015-08-18 00:00:26 +00:00
$error_level = \Drupal :: config ( 'system.logging' ) -> get ( 'error_level' );
}
2015-08-27 19:03:05 +00:00
catch ( \Exception $e ) {
$error_level = isset ( $GLOBALS [ 'config' ][ 'system.logging' ][ 'error_level' ]) ? $GLOBALS [ 'config' ][ 'system.logging' ][ 'error_level' ] : ERROR_REPORTING_HIDE ;
}
2015-08-18 00:00:26 +00:00
// 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 ;
}
2018-11-23 12:29:20 +00:00
/**
* 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 ++ ;
}