5 * Functions for error handling.
8 use Drupal\Component\Utility\SafeMarkup;
9 use Drupal\Component\Utility\Xss;
10 use Drupal\Core\Logger\RfcLogLevel;
11 use Drupal\Core\Render\Markup;
12 use Drupal\Core\Utility\Error;
13 use Symfony\Component\HttpFoundation\Response;
16 * Maps PHP error constants to watchdog severity levels.
18 * The error constants are documented at
19 * http://php.net/manual/errorfunc.constants.php
21 * @ingroup logging_severity_levels
23 function drupal_error_levels() {
25 E_ERROR => ['Error', RfcLogLevel::ERROR],
26 E_WARNING => ['Warning', RfcLogLevel::WARNING],
27 E_PARSE => ['Parse error', RfcLogLevel::ERROR],
28 E_NOTICE => ['Notice', RfcLogLevel::NOTICE],
29 E_CORE_ERROR => ['Core error', RfcLogLevel::ERROR],
30 E_CORE_WARNING => ['Core warning', RfcLogLevel::WARNING],
31 E_COMPILE_ERROR => ['Compile error', RfcLogLevel::ERROR],
32 E_COMPILE_WARNING => ['Compile warning', RfcLogLevel::WARNING],
33 E_USER_ERROR => ['User error', RfcLogLevel::ERROR],
34 E_USER_WARNING => ['User warning', RfcLogLevel::WARNING],
35 E_USER_NOTICE => ['User notice', RfcLogLevel::NOTICE],
36 E_STRICT => ['Strict warning', RfcLogLevel::DEBUG],
37 E_RECOVERABLE_ERROR => ['Recoverable fatal error', RfcLogLevel::ERROR],
38 E_DEPRECATED => ['Deprecated function', RfcLogLevel::DEBUG],
39 E_USER_DEPRECATED => ['User deprecated function', RfcLogLevel::DEBUG],
46 * Provides custom PHP error handling.
49 * The level of the error raised.
53 * The filename that the error was raised in.
55 * The line number the error was raised at.
57 * An array that points to the active symbol table at the point the error
60 function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) {
61 if ($error_level & error_reporting()) {
62 $types = drupal_error_levels();
63 list($severity_msg, $severity_level) = $types[$error_level];
64 $backtrace = debug_backtrace();
65 $caller = Error::getLastCaller($backtrace);
67 // We treat recoverable errors as fatal.
68 $recoverable = $error_level == E_RECOVERABLE_ERROR;
69 // As __toString() methods must not throw exceptions (recoverable errors)
70 // in PHP, we allow them to trigger a fatal error by emitting a user error
71 // using trigger_error().
72 $to_string = $error_level == E_USER_ERROR && substr($caller['function'], -strlen('__toString()')) == '__toString()';
74 '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
75 // The standard PHP error handler considers that the error messages
76 // are HTML. We mimick this behavior here.
77 '@message' => Markup::create(Xss::filterAdmin($message)),
78 '%function' => $caller['function'],
79 '%file' => $caller['file'],
80 '%line' => $caller['line'],
81 'severity_level' => $severity_level,
82 'backtrace' => $backtrace,
83 '@backtrace_string' => (new \Exception())->getTraceAsString(),
84 ], $recoverable || $to_string);
89 * Determines whether an error should be displayed.
91 * When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
92 * all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
93 * will be examined to determine if it should be displayed.
96 * Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
99 * TRUE if an error should be displayed.
101 function error_displayable($error = NULL) {
102 if (defined('MAINTENANCE_MODE')) {
105 $error_level = _drupal_get_error_level();
106 if ($error_level == ERROR_REPORTING_DISPLAY_ALL || $error_level == ERROR_REPORTING_DISPLAY_VERBOSE) {
109 if ($error_level == ERROR_REPORTING_DISPLAY_SOME && isset($error)) {
110 return $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning';
116 * Logs a PHP error or exception and displays an error page in fatal cases.
119 * An array with the following keys: %type, @message, %function, %file,
120 * %line, @backtrace_string, severity_level, and backtrace. All the parameters
121 * are plain-text, with the exception of @message, which needs to be an HTML
122 * string, and backtrace, which is a standard PHP backtrace.
125 * - An exception is thrown and not caught by something else.
126 * - A recoverable fatal error, which is a fatal error.
127 * Non-recoverable fatal errors cannot be logged by Drupal.
129 function _drupal_log_error($error, $fatal = FALSE) {
130 $is_installer = drupal_installation_attempted();
132 // Backtrace array is not a valid replacement value for t().
133 $backtrace = $error['backtrace'];
134 unset($error['backtrace']);
136 // When running inside the testing framework, we relay the errors
137 // to the tested site by the way of HTTP headers.
138 if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
139 // $number does not use drupal_static as it should not be reset
140 // as it uniquely identifies each PHP error.
146 'function' => $error['%function'],
147 'file' => $error['%file'],
148 'line' => $error['%line'],
151 // For non-fatal errors (e.g. PHP notices) _drupal_log_error can be called
152 // multiple times per request. In that case the response is typically
153 // generated outside of the error handler, e.g., in a controller. As a
154 // result it is not possible to use a Response object here but instead the
155 // headers need to be emitted directly.
156 header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
160 $response = new Response();
162 // Only call the logger if there is a logger factory available. This can occur
163 // if there is an error while rebuilding the container or during the
165 if (\Drupal::hasService('logger.factory')) {
167 \Drupal::logger('php')->log($error['severity_level'], '%type: @message in %function (line %line of %file) @backtrace_string.', $error);
169 catch (\Exception $e) {
170 // We can't log, for example because the database connection is not
171 // available. At least try to log to PHP error log.
172 error_log(strtr('Failed to log error: %type: @message in %function (line %line of %file). @backtrace_string', $error));
176 // Log fatal errors, so developers can find and debug them.
178 error_log(sprintf('%s: %s in %s on line %d %s', $error['%type'], $error['@message'], $error['%file'], $error['%line'], $error['@backtrace_string']));
181 if (PHP_SAPI === 'cli') {
183 // When called from CLI, simply output a plain text message.
184 // Should not translate the string to avoid errors producing more errors.
185 $response->setContent(html_entity_decode(strip_tags(SafeMarkup::format('%type: @message in %function (line %line of %file).', $error))) . "\n");
191 if (\Drupal::hasRequest() && \Drupal::request()->isXmlHttpRequest()) {
193 if (error_displayable($error)) {
194 // When called from JavaScript, simply output the error message.
195 // Should not translate the string to avoid errors producing more errors.
196 $response->setContent(SafeMarkup::format('%type: @message in %function (line %line of %file).', $error));
203 // Display the message if the current error reporting level allows this type
204 // of message to be displayed, and unconditionally in update.php.
207 if (error_displayable($error)) {
210 // If error type is 'User notice' then treat it as debug information
211 // instead of an error message.
213 if ($error['%type'] == 'User notice') {
214 $error['%type'] = 'Debug';
218 // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
219 // in the message. This does not happen for (false) security.
220 if (\Drupal::hasService('app.root')) {
221 $root_length = strlen(\Drupal::root());
222 if (substr($error['%file'], 0, $root_length) == \Drupal::root()) {
223 $error['%file'] = substr($error['%file'], $root_length + 1);
227 // Check if verbose error reporting is on.
228 $error_level = _drupal_get_error_level();
230 if ($error_level != ERROR_REPORTING_DISPLAY_VERBOSE) {
231 // Without verbose logging, use a simple message.
233 // We call SafeMarkup::format() directly here, rather than use t() since
234 // we are in the middle of error handling, and we don't want t() to
235 // cause further errors.
236 $message = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
239 // With verbose logging, we will also include a backtrace.
241 // First trace is the error itself, already contained in the message.
242 // While the second trace is the error source and also contained in the
243 // message, the message doesn't contain argument values, so we output it
244 // once more in the backtrace.
245 array_shift($backtrace);
246 // Generate a backtrace containing only scalar argument values.
247 $error['@backtrace'] = Error::formatBacktrace($backtrace);
248 $message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
253 // We fallback to a maintenance page at this point, because the page generation
254 // itself can generate errors.
255 // Should not translate the string to avoid errors producing more errors.
256 $message = 'The website encountered an unexpected error. Please try again later.' . '<br />' . $message;
259 // install_display_output() prints the output and ends script execution.
262 '#markup' => $message,
264 install_display_output($output, $GLOBALS['install_state'], $response->headers->all());
268 $response->setContent($message);
269 $response->setStatusCode(500, '500 Service unavailable (with message)');
272 // An exception must halt script execution.
277 if (\Drupal::hasService('session')) {
278 // Message display is dependent on sessions being available.
279 drupal_set_message($message, $class, TRUE);
289 * Returns the current error level.
291 * This function should only be used to get the current error level prior to the
292 * kernel being booted or before Drupal is installed. In all other situations
293 * the following code is preferred:
295 * \Drupal::config('system.logging')->get('error_level');
299 * The current error level.
301 function _drupal_get_error_level() {
302 // Raise the error level to maximum for the installer, so users are able to
303 // file proper bug reports for installer errors. The returned value is
304 // different to the one below, because the installer actually has a
305 // 'config.factory' service, which reads the default 'error_level' value from
306 // System module's default configuration and the default value is not verbose.
307 // @see error_displayable()
308 if (drupal_installation_attempted()) {
309 return ERROR_REPORTING_DISPLAY_VERBOSE;
312 // Try to get the error level configuration from database. If this fails,
313 // for example if the database connection is not there, try to read it from
316 $error_level = \Drupal::config('system.logging')->get('error_level');
318 catch (\Exception $e) {
319 $error_level = isset($GLOBALS['config']['system.logging']['error_level']) ? $GLOBALS['config']['system.logging']['error_level'] : ERROR_REPORTING_HIDE;
322 // If there is no container or if it has no config.factory service, we are
323 // possibly in an edge-case error situation while trying to serve a regular
324 // request on a public site, so use the non-verbose default value.
325 return $error_level ?: ERROR_REPORTING_DISPLAY_ALL;