annotate core/includes/errors.inc @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /**
Chris@0 4 * @file
Chris@0 5 * Functions for error handling.
Chris@0 6 */
Chris@0 7
Chris@17 8 use Drupal\Component\Render\FormattableMarkup;
Chris@0 9 use Drupal\Component\Utility\Xss;
Chris@0 10 use Drupal\Core\Logger\RfcLogLevel;
Chris@0 11 use Drupal\Core\Render\Markup;
Chris@0 12 use Drupal\Core\Utility\Error;
Chris@0 13 use Symfony\Component\HttpFoundation\Response;
Chris@0 14
Chris@0 15 /**
Chris@0 16 * Maps PHP error constants to watchdog severity levels.
Chris@0 17 *
Chris@0 18 * The error constants are documented at
Chris@0 19 * http://php.net/manual/errorfunc.constants.php
Chris@0 20 *
Chris@0 21 * @ingroup logging_severity_levels
Chris@0 22 */
Chris@0 23 function drupal_error_levels() {
Chris@0 24 $types = [
Chris@0 25 E_ERROR => ['Error', RfcLogLevel::ERROR],
Chris@0 26 E_WARNING => ['Warning', RfcLogLevel::WARNING],
Chris@0 27 E_PARSE => ['Parse error', RfcLogLevel::ERROR],
Chris@0 28 E_NOTICE => ['Notice', RfcLogLevel::NOTICE],
Chris@0 29 E_CORE_ERROR => ['Core error', RfcLogLevel::ERROR],
Chris@0 30 E_CORE_WARNING => ['Core warning', RfcLogLevel::WARNING],
Chris@0 31 E_COMPILE_ERROR => ['Compile error', RfcLogLevel::ERROR],
Chris@0 32 E_COMPILE_WARNING => ['Compile warning', RfcLogLevel::WARNING],
Chris@0 33 E_USER_ERROR => ['User error', RfcLogLevel::ERROR],
Chris@0 34 E_USER_WARNING => ['User warning', RfcLogLevel::WARNING],
Chris@0 35 E_USER_NOTICE => ['User notice', RfcLogLevel::NOTICE],
Chris@0 36 E_STRICT => ['Strict warning', RfcLogLevel::DEBUG],
Chris@0 37 E_RECOVERABLE_ERROR => ['Recoverable fatal error', RfcLogLevel::ERROR],
Chris@0 38 E_DEPRECATED => ['Deprecated function', RfcLogLevel::DEBUG],
Chris@0 39 E_USER_DEPRECATED => ['User deprecated function', RfcLogLevel::DEBUG],
Chris@0 40 ];
Chris@0 41
Chris@0 42 return $types;
Chris@0 43 }
Chris@0 44
Chris@0 45 /**
Chris@0 46 * Provides custom PHP error handling.
Chris@0 47 *
Chris@0 48 * @param $error_level
Chris@0 49 * The level of the error raised.
Chris@0 50 * @param $message
Chris@0 51 * The error message.
Chris@0 52 * @param $filename
Chris@0 53 * The filename that the error was raised in.
Chris@0 54 * @param $line
Chris@0 55 * The line number the error was raised at.
Chris@0 56 * @param $context
Chris@0 57 * An array that points to the active symbol table at the point the error
Chris@0 58 * occurred.
Chris@0 59 */
Chris@0 60 function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) {
Chris@0 61 if ($error_level & error_reporting()) {
Chris@0 62 $types = drupal_error_levels();
Chris@0 63 list($severity_msg, $severity_level) = $types[$error_level];
Chris@0 64 $backtrace = debug_backtrace();
Chris@0 65 $caller = Error::getLastCaller($backtrace);
Chris@0 66
Chris@0 67 // We treat recoverable errors as fatal.
Chris@0 68 $recoverable = $error_level == E_RECOVERABLE_ERROR;
Chris@0 69 // As __toString() methods must not throw exceptions (recoverable errors)
Chris@0 70 // in PHP, we allow them to trigger a fatal error by emitting a user error
Chris@0 71 // using trigger_error().
Chris@0 72 $to_string = $error_level == E_USER_ERROR && substr($caller['function'], -strlen('__toString()')) == '__toString()';
Chris@0 73 _drupal_log_error([
Chris@0 74 '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
Chris@0 75 // The standard PHP error handler considers that the error messages
Chris@0 76 // are HTML. We mimick this behavior here.
Chris@0 77 '@message' => Markup::create(Xss::filterAdmin($message)),
Chris@0 78 '%function' => $caller['function'],
Chris@0 79 '%file' => $caller['file'],
Chris@0 80 '%line' => $caller['line'],
Chris@0 81 'severity_level' => $severity_level,
Chris@0 82 'backtrace' => $backtrace,
Chris@0 83 '@backtrace_string' => (new \Exception())->getTraceAsString(),
Chris@0 84 ], $recoverable || $to_string);
Chris@0 85 }
Chris@14 86 // If the site is a test site then fail for user deprecations so they can be
Chris@14 87 // caught by the deprecation error handler.
Chris@14 88 elseif (DRUPAL_TEST_IN_CHILD_SITE && $error_level === E_USER_DEPRECATED) {
Chris@14 89 $backtrace = debug_backtrace();
Chris@14 90 $caller = Error::getLastCaller($backtrace);
Chris@14 91 _drupal_error_header(
Chris@14 92 Markup::create(Xss::filterAdmin($message)),
Chris@14 93 'User deprecated function',
Chris@14 94 $caller['function'],
Chris@14 95 $caller['file'],
Chris@14 96 $caller['line']
Chris@14 97 );
Chris@14 98 }
Chris@0 99 }
Chris@0 100
Chris@0 101 /**
Chris@0 102 * Determines whether an error should be displayed.
Chris@0 103 *
Chris@0 104 * When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
Chris@0 105 * all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
Chris@0 106 * will be examined to determine if it should be displayed.
Chris@0 107 *
Chris@0 108 * @param $error
Chris@0 109 * Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
Chris@0 110 *
Chris@0 111 * @return
Chris@0 112 * TRUE if an error should be displayed.
Chris@0 113 */
Chris@0 114 function error_displayable($error = NULL) {
Chris@0 115 if (defined('MAINTENANCE_MODE')) {
Chris@0 116 return TRUE;
Chris@0 117 }
Chris@0 118 $error_level = _drupal_get_error_level();
Chris@0 119 if ($error_level == ERROR_REPORTING_DISPLAY_ALL || $error_level == ERROR_REPORTING_DISPLAY_VERBOSE) {
Chris@0 120 return TRUE;
Chris@0 121 }
Chris@0 122 if ($error_level == ERROR_REPORTING_DISPLAY_SOME && isset($error)) {
Chris@0 123 return $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning';
Chris@0 124 }
Chris@0 125 return FALSE;
Chris@0 126 }
Chris@0 127
Chris@0 128 /**
Chris@0 129 * Logs a PHP error or exception and displays an error page in fatal cases.
Chris@0 130 *
Chris@0 131 * @param $error
Chris@0 132 * An array with the following keys: %type, @message, %function, %file,
Chris@0 133 * %line, @backtrace_string, severity_level, and backtrace. All the parameters
Chris@0 134 * are plain-text, with the exception of @message, which needs to be an HTML
Chris@0 135 * string, and backtrace, which is a standard PHP backtrace.
Chris@0 136 * @param bool $fatal
Chris@0 137 * TRUE for:
Chris@0 138 * - An exception is thrown and not caught by something else.
Chris@0 139 * - A recoverable fatal error, which is a fatal error.
Chris@0 140 * Non-recoverable fatal errors cannot be logged by Drupal.
Chris@0 141 */
Chris@0 142 function _drupal_log_error($error, $fatal = FALSE) {
Chris@0 143 $is_installer = drupal_installation_attempted();
Chris@0 144
Chris@0 145 // Backtrace array is not a valid replacement value for t().
Chris@0 146 $backtrace = $error['backtrace'];
Chris@0 147 unset($error['backtrace']);
Chris@0 148
Chris@0 149 // When running inside the testing framework, we relay the errors
Chris@0 150 // to the tested site by the way of HTTP headers.
Chris@0 151 if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
Chris@14 152 _drupal_error_header($error['@message'], $error['%type'], $error['%function'], $error['%file'], $error['%line']);
Chris@0 153 }
Chris@0 154
Chris@0 155 $response = new Response();
Chris@0 156
Chris@0 157 // Only call the logger if there is a logger factory available. This can occur
Chris@0 158 // if there is an error while rebuilding the container or during the
Chris@0 159 // installer.
Chris@0 160 if (\Drupal::hasService('logger.factory')) {
Chris@0 161 try {
Chris@14 162 // Provide the PHP backtrace to logger implementations.
Chris@14 163 \Drupal::logger('php')->log($error['severity_level'], '%type: @message in %function (line %line of %file) @backtrace_string.', $error + ['backtrace' => $backtrace]);
Chris@0 164 }
Chris@0 165 catch (\Exception $e) {
Chris@0 166 // We can't log, for example because the database connection is not
Chris@0 167 // available. At least try to log to PHP error log.
Chris@0 168 error_log(strtr('Failed to log error: %type: @message in %function (line %line of %file). @backtrace_string', $error));
Chris@0 169 }
Chris@0 170 }
Chris@0 171
Chris@0 172 // Log fatal errors, so developers can find and debug them.
Chris@0 173 if ($fatal) {
Chris@0 174 error_log(sprintf('%s: %s in %s on line %d %s', $error['%type'], $error['@message'], $error['%file'], $error['%line'], $error['@backtrace_string']));
Chris@0 175 }
Chris@0 176
Chris@0 177 if (PHP_SAPI === 'cli') {
Chris@0 178 if ($fatal) {
Chris@0 179 // When called from CLI, simply output a plain text message.
Chris@0 180 // Should not translate the string to avoid errors producing more errors.
Chris@17 181 $response->setContent(html_entity_decode(strip_tags(new FormattableMarkup('%type: @message in %function (line %line of %file).', $error))) . "\n");
Chris@0 182 $response->send();
Chris@0 183 exit;
Chris@0 184 }
Chris@0 185 }
Chris@0 186
Chris@0 187 if (\Drupal::hasRequest() && \Drupal::request()->isXmlHttpRequest()) {
Chris@0 188 if ($fatal) {
Chris@0 189 if (error_displayable($error)) {
Chris@0 190 // When called from JavaScript, simply output the error message.
Chris@0 191 // Should not translate the string to avoid errors producing more errors.
Chris@17 192 $response->setContent(new FormattableMarkup('%type: @message in %function (line %line of %file).', $error));
Chris@0 193 $response->send();
Chris@0 194 }
Chris@0 195 exit;
Chris@0 196 }
Chris@0 197 }
Chris@0 198 else {
Chris@0 199 // Display the message if the current error reporting level allows this type
Chris@0 200 // of message to be displayed, and unconditionally in update.php.
Chris@0 201 $message = '';
Chris@0 202 $class = NULL;
Chris@0 203 if (error_displayable($error)) {
Chris@0 204 $class = 'error';
Chris@0 205
Chris@0 206 // If error type is 'User notice' then treat it as debug information
Chris@0 207 // instead of an error message.
Chris@0 208 // @see debug()
Chris@0 209 if ($error['%type'] == 'User notice') {
Chris@0 210 $error['%type'] = 'Debug';
Chris@0 211 $class = 'status';
Chris@0 212 }
Chris@0 213
Chris@0 214 // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
Chris@0 215 // in the message. This does not happen for (false) security.
Chris@0 216 if (\Drupal::hasService('app.root')) {
Chris@0 217 $root_length = strlen(\Drupal::root());
Chris@0 218 if (substr($error['%file'], 0, $root_length) == \Drupal::root()) {
Chris@0 219 $error['%file'] = substr($error['%file'], $root_length + 1);
Chris@0 220 }
Chris@0 221 }
Chris@0 222
Chris@0 223 // Check if verbose error reporting is on.
Chris@0 224 $error_level = _drupal_get_error_level();
Chris@0 225
Chris@0 226 if ($error_level != ERROR_REPORTING_DISPLAY_VERBOSE) {
Chris@0 227 // Without verbose logging, use a simple message.
Chris@0 228
Chris@17 229 // We use \Drupal\Component\Render\FormattableMarkup directly here,
Chris@17 230 // rather than use t() since we are in the middle of error handling, and
Chris@17 231 // we don't want t() to cause further errors.
Chris@17 232 $message = new FormattableMarkup('%type: @message in %function (line %line of %file).', $error);
Chris@0 233 }
Chris@0 234 else {
Chris@0 235 // With verbose logging, we will also include a backtrace.
Chris@0 236
Chris@0 237 // First trace is the error itself, already contained in the message.
Chris@0 238 // While the second trace is the error source and also contained in the
Chris@0 239 // message, the message doesn't contain argument values, so we output it
Chris@0 240 // once more in the backtrace.
Chris@0 241 array_shift($backtrace);
Chris@0 242 // Generate a backtrace containing only scalar argument values.
Chris@0 243 $error['@backtrace'] = Error::formatBacktrace($backtrace);
Chris@17 244 $message = new FormattableMarkup('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
Chris@0 245 }
Chris@0 246 }
Chris@0 247
Chris@0 248 if ($fatal) {
Chris@0 249 // We fallback to a maintenance page at this point, because the page generation
Chris@0 250 // itself can generate errors.
Chris@0 251 // Should not translate the string to avoid errors producing more errors.
Chris@0 252 $message = 'The website encountered an unexpected error. Please try again later.' . '<br />' . $message;
Chris@0 253
Chris@0 254 if ($is_installer) {
Chris@0 255 // install_display_output() prints the output and ends script execution.
Chris@0 256 $output = [
Chris@0 257 '#title' => 'Error',
Chris@0 258 '#markup' => $message,
Chris@0 259 ];
Chris@0 260 install_display_output($output, $GLOBALS['install_state'], $response->headers->all());
Chris@0 261 exit;
Chris@0 262 }
Chris@0 263
Chris@0 264 $response->setContent($message);
Chris@0 265 $response->setStatusCode(500, '500 Service unavailable (with message)');
Chris@0 266
Chris@0 267 $response->send();
Chris@0 268 // An exception must halt script execution.
Chris@0 269 exit;
Chris@0 270 }
Chris@0 271
Chris@0 272 if ($message) {
Chris@0 273 if (\Drupal::hasService('session')) {
Chris@0 274 // Message display is dependent on sessions being available.
Chris@17 275 \Drupal::messenger()->addMessage($message, $class, TRUE);
Chris@0 276 }
Chris@0 277 else {
Chris@0 278 print $message;
Chris@0 279 }
Chris@0 280 }
Chris@0 281 }
Chris@0 282 }
Chris@0 283
Chris@0 284 /**
Chris@0 285 * Returns the current error level.
Chris@0 286 *
Chris@0 287 * This function should only be used to get the current error level prior to the
Chris@0 288 * kernel being booted or before Drupal is installed. In all other situations
Chris@0 289 * the following code is preferred:
Chris@0 290 * @code
Chris@0 291 * \Drupal::config('system.logging')->get('error_level');
Chris@0 292 * @endcode
Chris@0 293 *
Chris@0 294 * @return string
Chris@0 295 * The current error level.
Chris@0 296 */
Chris@0 297 function _drupal_get_error_level() {
Chris@0 298 // Raise the error level to maximum for the installer, so users are able to
Chris@0 299 // file proper bug reports for installer errors. The returned value is
Chris@0 300 // different to the one below, because the installer actually has a
Chris@0 301 // 'config.factory' service, which reads the default 'error_level' value from
Chris@0 302 // System module's default configuration and the default value is not verbose.
Chris@0 303 // @see error_displayable()
Chris@0 304 if (drupal_installation_attempted()) {
Chris@0 305 return ERROR_REPORTING_DISPLAY_VERBOSE;
Chris@0 306 }
Chris@0 307 $error_level = NULL;
Chris@0 308 // Try to get the error level configuration from database. If this fails,
Chris@0 309 // for example if the database connection is not there, try to read it from
Chris@0 310 // settings.php.
Chris@0 311 try {
Chris@0 312 $error_level = \Drupal::config('system.logging')->get('error_level');
Chris@0 313 }
Chris@0 314 catch (\Exception $e) {
Chris@0 315 $error_level = isset($GLOBALS['config']['system.logging']['error_level']) ? $GLOBALS['config']['system.logging']['error_level'] : ERROR_REPORTING_HIDE;
Chris@0 316 }
Chris@0 317
Chris@0 318 // If there is no container or if it has no config.factory service, we are
Chris@0 319 // possibly in an edge-case error situation while trying to serve a regular
Chris@0 320 // request on a public site, so use the non-verbose default value.
Chris@0 321 return $error_level ?: ERROR_REPORTING_DISPLAY_ALL;
Chris@0 322 }
Chris@14 323
Chris@14 324 /**
Chris@14 325 * Adds error information to headers so that tests can access it.
Chris@14 326 *
Chris@14 327 * @param $message
Chris@14 328 * The error message.
Chris@14 329 * @param $type
Chris@14 330 * The type of error.
Chris@14 331 * @param $function
Chris@14 332 * The function that emitted the error.
Chris@14 333 * @param $file
Chris@14 334 * The file that emitted the error.
Chris@14 335 * @param $line
Chris@14 336 * The line number in file that emitted the error.
Chris@14 337 */
Chris@14 338 function _drupal_error_header($message, $type, $function, $file, $line) {
Chris@14 339 // $number does not use drupal_static as it should not be reset
Chris@14 340 // as it uniquely identifies each PHP error.
Chris@14 341 static $number = 0;
Chris@14 342 $assertion = [
Chris@14 343 $message,
Chris@14 344 $type,
Chris@14 345 [
Chris@14 346 'function' => $function,
Chris@14 347 'file' => $file,
Chris@14 348 'line' => $line,
Chris@14 349 ],
Chris@14 350 ];
Chris@14 351 // For non-fatal errors (e.g. PHP notices) _drupal_log_error can be called
Chris@14 352 // multiple times per request. In that case the response is typically
Chris@14 353 // generated outside of the error handler, e.g., in a controller. As a
Chris@14 354 // result it is not possible to use a Response object here but instead the
Chris@14 355 // headers need to be emitted directly.
Chris@14 356 header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
Chris@14 357 $number++;
Chris@14 358 }