diff core/includes/errors.inc @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/includes/errors.inc	Thu Jul 05 14:24:15 2018 +0000
@@ -0,0 +1,358 @@
+<?php
+
+/**
+ * @file
+ * Functions for error handling.
+ */
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Xss;
+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_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],
+  ];
+
+  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.
+    $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()';
+    _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 mimick 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(),
+    ], $recoverable || $to_string);
+  }
+  // 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']
+    );
+  }
+}
+
+/**
+ * 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
+ *   An array with the following keys: %type, @message, %function, %file,
+ *   %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.
+ * @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 = 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)) {
+    _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 to logger implementations.
+      \Drupal::logger('php')->log($error['severity_level'], '%type: @message in %function (line %line of %file) @backtrace_string.', $error + ['backtrace' => $backtrace]);
+    }
+    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.
+      error_log(strtr('Failed to log error: %type: @message in %function (line %line of %file). @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(SafeMarkup::format('%type: @message in %function (line %line of %file).', $error))) . "\n");
+      $response->send();
+      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.
+        $response->setContent(SafeMarkup::format('%type: @message in %function (line %line of %file).', $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.
+      // @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();
+
+      if ($error_level != ERROR_REPORTING_DISPLAY_VERBOSE) {
+        // Without verbose logging, use a simple message.
+
+        // We call SafeMarkup::format() 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 = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
+      }
+      else {
+        // With verbose logging, we will also include a backtrace.
+
+        // 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.
+        $error['@backtrace'] = Error::formatBacktrace($backtrace);
+        $message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $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. Please try again later.' . '<br />' . $message;
+
+      if ($is_installer) {
+        // install_display_output() prints the output and ends script execution.
+        $output = [
+          '#title' => 'Error',
+          '#markup' => $message,
+        ];
+        install_display_output($output, $GLOBALS['install_state'], $response->headers->all());
+        exit;
+      }
+
+      $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_set_message($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 (drupal_installation_attempted()) {
+    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 $e) {
+    $error_level = isset($GLOBALS['config']['system.logging']['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++;
+}