errors.inc 10.6 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Functions for error handling.
6 7
 */

8
use Drupal\Component\Utility\Xss;
9
use Drupal\Core\Page\DefaultHtmlPageRenderer;
10
use Drupal\Core\Utility\Error;
11
use Drupal\Component\Utility\String;
12 13
use Symfony\Component\HttpFoundation\Response;

14
/**
15 16
 * Maps PHP error constants to watchdog severity levels.
 *
17
 * The error constants are documented at
18
 * http://php.net/manual/errorfunc.constants.php
19 20
 *
 * @ingroup logging_severity_levels
21 22 23
 */
function drupal_error_levels() {
  $types = array(
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
    E_ERROR => array('Error', WATCHDOG_ERROR),
    E_WARNING => array('Warning', WATCHDOG_WARNING),
    E_PARSE => array('Parse error', WATCHDOG_ERROR),
    E_NOTICE => array('Notice', WATCHDOG_NOTICE),
    E_CORE_ERROR => array('Core error', WATCHDOG_ERROR),
    E_CORE_WARNING => array('Core warning', WATCHDOG_WARNING),
    E_COMPILE_ERROR => array('Compile error', WATCHDOG_ERROR),
    E_COMPILE_WARNING => array('Compile warning', WATCHDOG_WARNING),
    E_USER_ERROR => array('User error', WATCHDOG_ERROR),
    E_USER_WARNING => array('User warning', WATCHDOG_WARNING),
    E_USER_NOTICE => array('User notice', WATCHDOG_NOTICE),
    E_STRICT => array('Strict warning', WATCHDOG_DEBUG),
    E_RECOVERABLE_ERROR => array('Recoverable fatal error', WATCHDOG_ERROR),
    E_DEPRECATED => array('Deprecated function', WATCHDOG_DEBUG),
    E_USER_DEPRECATED => array('User deprecated function', WATCHDOG_DEBUG),
39
  );
40

41 42 43 44
  return $types;
}

/**
45
 * Provides custom PHP error handling.
46 47 48 49 50 51 52 53 54 55
 *
 * @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
56 57
 *   An array that points to the active symbol table at the point the error
 *   occurred.
58 59 60 61 62
 */
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];
63
    $backtrace = debug_backtrace();
64
    $caller = Error::getLastCaller($backtrace);
65 66 67 68

    // We treat recoverable errors as fatal.
    _drupal_log_error(array(
      '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
69 70
      // The standard PHP error handler considers that the error messages
      // are HTML. We mimick this behavior here.
71
      '!message' => Xss::filterAdmin($message),
72 73 74 75
      '%function' => $caller['function'],
      '%file' => $caller['file'],
      '%line' => $caller['line'],
      'severity_level' => $severity_level,
76
      'backtrace' => $backtrace,
77 78 79 80
    ), $error_level == E_RECOVERABLE_ERROR);
  }
}

81 82 83 84 85 86 87 88 89 90 91 92 93 94
/**
 * 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) {
95 96 97
  if (defined('MAINTENANCE_MODE')) {
    return TRUE;
  }
98
  $error_level = _drupal_get_error_level();
99 100 101 102 103 104 105
  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;
106 107
}

108
/**
109
 * Logs a PHP error or exception and displays an error page in fatal cases.
110 111
 *
 * @param $error
112 113 114 115
 *   An array with the following keys: %type, !message, %function, %file,
 *   %line, severity_level, and backtrace. All the parameters are plain-text,
 *   with the exception of !message, which needs to be a safe HTML string, and
 *   backtrace, which is a standard PHP backtrace.
116 117 118 119
 * @param $fatal
 *   TRUE if the error is fatal.
 */
function _drupal_log_error($error, $fatal = FALSE) {
120
  $is_installer = drupal_installation_attempted();
121
  // Initialize a maintenance theme if the bootstrap was not complete.
122
  // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
123
  if ($fatal && drupal_get_bootstrap_phase() < DRUPAL_BOOTSTRAP_CODE) {
124 125 126 127 128
    // The installer initializes a maintenance theme at the earliest possible
    // point in time already. Do not unset that.
    if (!$is_installer) {
      unset($GLOBALS['theme']);
    }
129 130 131
    if (!defined('MAINTENANCE_MODE')) {
      define('MAINTENANCE_MODE', 'error');
    }
132
    // No-op if $GLOBALS['theme'] is set already.
133 134 135
    drupal_maintenance_theme();
  }

136 137 138 139
  // Backtrace array is not a valid replacement value for t().
  $backtrace = $error['backtrace'];
  unset($error['backtrace']);

140 141
  // When running inside the testing framework, we relay the errors
  // to the tested site by the way of HTTP headers.
142
  if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
143 144 145 146
    // $number does not use drupal_static as it should not be reset
    // as it uniquely identifies each PHP error.
    static $number = 0;
    $assertion = array(
147
      $error['!message'],
148 149 150 151 152 153 154 155 156 157 158
      $error['%type'],
      array(
        'function' => $error['%function'],
        'file' => $error['%file'],
        'line' => $error['%line'],
      ),
    );
    header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
    $number++;
  }

159
  watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
160

161 162 163
  if (drupal_is_cli()) {
    if ($fatal) {
      // When called from CLI, simply output a plain text message.
164 165
      // Should not translate the string to avoid errors producing more errors.
      print html_entity_decode(strip_tags(format_string('%type: !message in %function (line %line of %file).', $error))). "\n";
166 167 168 169
      exit;
    }
  }

170
  if (\Drupal::hasRequest() && \Drupal::request()->isXmlHttpRequest()) {
171
    if ($fatal) {
172 173
      if (error_displayable($error)) {
        // When called from JavaScript, simply output the error message.
174 175
        // Should not translate the string to avoid errors producing more errors.
        print format_string('%type: !message in %function (line %line of %file).', $error);
176
      }
177 178 179 180 181 182
      exit;
    }
  }
  else {
    // Display the message if the current error reporting level allows this type
    // of message to be displayed, and unconditionnaly in update.php.
183
    if (error_displayable($error)) {
184 185 186
      $class = 'error';

      // If error type is 'User notice' then treat it as debug information
187 188
      // instead of an error message.
      // @see debug()
189 190 191 192 193
      if ($error['%type'] == 'User notice') {
        $error['%type'] = 'Debug';
        $class = 'status';
      }

194 195 196 197 198 199
      // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
      // in the message. This does not happen for (false) security.
      $root_length = strlen(DRUPAL_ROOT);
      if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) {
        $error['%file'] = substr($error['%file'], $root_length + 1);
      }
200 201
      // Should not translate the string to avoid errors producing more errors.
      $message = format_string('%type: !message in %function (line %line of %file).', $error);
202 203

      // Check if verbose error reporting is on.
204
      $error_level = _drupal_get_error_level();
205 206 207 208 209 210 211 212 213 214

      if ($error_level == ERROR_REPORTING_DISPLAY_VERBOSE) {
        // 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.
        $message .= '<pre class="backtrace">' . format_backtrace($backtrace) . '</pre>';
      }
215
      drupal_set_message($message, $class, TRUE);
216 217 218 219 220
    }

    if ($fatal) {
      // We fallback to a maintenance page at this point, because the page generation
      // itself can generate errors.
221
      // Should not translate the string to avoid errors producing more errors.
222 223 224
      $message = 'The website has encountered an error. Please try again later.';
      if ($is_installer) {
        // install_display_output() prints the output and ends script execution.
225 226 227 228 229
        $output = array(
          '#title' => 'Error',
          '#markup' => $message,
        );
        install_display_output($output, $GLOBALS['install_state']);
230 231
      }
      else {
232
        $output = DefaultHtmlPageRenderer::renderPage($message, 'Error');
233 234
      }

235 236 237 238 239
      $response = new Response($output, 500);
      $response->setStatusCode(500, '500 Service unavailable (with message)');
      // An exception must halt script execution.
      $response->send();
      exit;
240 241 242 243
    }
  }
}

244 245 246
/**
 * Returns the current error level.
 *
247 248 249
 * This function should only be used to get the current error level prior to
 * DRUPAL_BOOTSTRAP_KERNEL or before Drupal is installed. In all other situations
 * the following code is preferred:
250
 * @code
251
 * \Drupal::config('system.logging')->get('error_level');
252 253 254 255 256 257
 * @endcode
 *
 * @return string
 *   The current error level.
 */
function _drupal_get_error_level() {
258 259 260 261 262 263 264 265
  // 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;
266
  }
267 268 269
  $error_level = NULL;
  if (\Drupal::hasService('config.factory')) {
    $error_level = \Drupal::config('system.logging')->get('error_level');
270
  }
271 272 273 274
  // 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;
275 276
}

277 278 279 280 281 282 283 284 285 286
/**
 * Formats a backtrace into a plain-text string.
 *
 * The calls show values for scalar arguments and type names for complex ones.
 *
 * @param array $backtrace
 *   A standard PHP backtrace.
 *
 * @return string
 *   A plain-text line-wrapped string ready to be put inside <pre>.
287
 *
288 289
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal\Core\Utility\Error::formatBacktrace().
290 291
 */
function format_backtrace(array $backtrace) {
292
  return Error::formatBacktrace($backtrace);
293
}