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

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

8
9
use Symfony\Component\HttpFoundation\Response;

10
/**
11
12
 * Maps PHP error constants to watchdog severity levels.
 *
13
 * The error constants are documented at
14
 * http://php.net/manual/errorfunc.constants.php
15
16
 *
 * @ingroup logging_severity_levels
17
18
19
 */
function drupal_error_levels() {
  $types = array(
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    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),
35
  );
36

37
38
39
40
  return $types;
}

/**
41
 * Provides custom PHP error handling.
42
43
44
45
46
47
48
49
50
51
 *
 * @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
52
53
 *   An array that points to the active symbol table at the point the error
 *   occurred.
54
55
56
57
58
 */
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];
59
60
    $backtrace = debug_backtrace();
    $caller = _drupal_get_last_caller($backtrace);
61

62
    if (!function_exists('filter_xss_admin')) {
63
      require_once DRUPAL_ROOT . '/core/includes/common.inc';
64
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
71
      // The standard PHP error handler considers that the error messages
      // are HTML. We mimick this behavior here.
      '!message' => filter_xss_admin($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
81
    ), $error_level == E_RECOVERABLE_ERROR);
  }
}

/**
82
 * Decodes an exception and retrieves the correct caller.
83
84
85
 *
 * @param $exception
 *   The exception object that was thrown.
86
 *
87
88
 * @return
 *   An error in the format expected by _drupal_log_error().
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
 */
function _drupal_decode_exception($exception) {
  $message = $exception->getMessage();

  $backtrace = $exception->getTrace();
  // Add the line throwing the exception to the backtrace.
  array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile()));

  // For PDOException errors, we try to return the initial caller,
  // skipping internal functions of the database layer.
  if ($exception instanceof PDOException) {
    // The first element in the stack is the call, the second element gives us the caller.
    // We skip calls that occurred in one of the classes of the database layer
    // or in one of its global functions.
    $db_functions = array('db_query',  'db_query_range');
    while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
        ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
        in_array($caller['function'], $db_functions))) {
      // We remove that call.
      array_shift($backtrace);
    }
    if (isset($exception->query_string, $exception->args)) {
      $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
    }
  }
  $caller = _drupal_get_last_caller($backtrace);

  return array(
    '%type' => get_class($exception),
118
119
120
    // The standard PHP exception handler considers that the exception message
    // is plain-text. We mimick this behavior here.
    '!message' => check_plain($message),
121
122
123
    '%function' => $caller['function'],
    '%file' => $caller['file'],
    '%line' => $caller['line'],
124
    'severity_level' => WATCHDOG_ERROR,
125
126
127
  );
}

128
/**
129
 * Renders an exception error message without further exceptions.
130
131
132
 *
 * @param $exception
 *   The exception object that was thrown.
133
 *
134
135
136
137
 * @return
 *   An error message.
 */
function _drupal_render_exception_safe($exception) {
138
  return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)));
139
140
}

141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
 * 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) {
155
  $error_level = config('system.logging')->get('error_level');
156
  $updating = (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update');
157
158
  $all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL) ||
    ($error_level == ERROR_REPORTING_DISPLAY_VERBOSE);
159
160
161
162
163
164
  $error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME &&
    isset($error) && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning');

  return ($updating || $all_errors_displayed || $error_needs_display);
}

165
/**
166
 * Logs a PHP error or exception and displays an error page in fatal cases.
167
168
 *
 * @param $error
169
170
171
172
 *   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.
173
174
175
176
177
178
179
180
181
182
183
184
185
186
 * @param $fatal
 *   TRUE if the error is fatal.
 */
function _drupal_log_error($error, $fatal = FALSE) {
  // Initialize a maintenance theme if the boostrap was not complete.
  // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
  if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
    unset($GLOBALS['theme']);
    if (!defined('MAINTENANCE_MODE')) {
      define('MAINTENANCE_MODE', 'error');
    }
    drupal_maintenance_theme();
  }

187
188
189
190
  // Backtrace array is not a valid replacement value for t().
  $backtrace = $error['backtrace'];
  unset($error['backtrace']);

191
192
  // When running inside the testing framework, we relay the errors
  // to the tested site by the way of HTTP headers.
193
194
  $test_info = &$GLOBALS['drupal_test_info'];
  if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
195
196
197
198
    // $number does not use drupal_static as it should not be reset
    // as it uniquely identifies each PHP error.
    static $number = 0;
    $assertion = array(
199
      $error['!message'],
200
201
202
203
204
205
206
207
208
209
210
      $error['%type'],
      array(
        'function' => $error['%function'],
        'file' => $error['%file'],
        'line' => $error['%line'],
      ),
    );
    header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
    $number++;
  }

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

213
214
215
  if (drupal_is_cli()) {
    if ($fatal) {
      // When called from CLI, simply output a plain text message.
216
      print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n";
217
218
219
220
      exit;
    }
  }

221
222
  if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
    if ($fatal) {
223
224
225
226
      if (error_displayable($error)) {
        // When called from JavaScript, simply output the error message.
        print t('%type: !message in %function (line %line of %file).', $error);
      }
227
228
229
230
231
232
      exit;
    }
  }
  else {
    // Display the message if the current error reporting level allows this type
    // of message to be displayed, and unconditionnaly in update.php.
233
    if (error_displayable($error)) {
234
235
236
      $class = 'error';

      // If error type is 'User notice' then treat it as debug information
237
238
      // instead of an error message.
      // @see debug()
239
240
241
242
243
      if ($error['%type'] == 'User notice') {
        $error['%type'] = 'Debug';
        $class = 'status';
      }

244
245
246
247
248
249
250
251
252
      // 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);
      }
      $message = t('%type: !message in %function (line %line of %file).', $error);

      // Check if verbose error reporting is on.
253
      $error_level = config('system.logging')->get('error_level');
254
255
256
257
258
259
260
261
262
263
264

      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>';
      }
      drupal_set_message($message, $class);
265
266
267
268
269
270
    }

    if ($fatal) {
      drupal_set_title(t('Error'));
      // We fallback to a maintenance page at this point, because the page generation
      // itself can generate errors.
271
272
273
274
275
276
277
278
      $output = theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.')));

      $response = new Response($output, 500);
      if ($fatal) {
        $response->setStatusCode(500, '500 Service unavailable (with message)');
      }

      return $response;
279
280
281
282
283
284
285
286
    }
  }
}

/**
 * Gets the last caller from a backtrace.
 *
 * @param $backtrace
287
 *   A standard PHP backtrace. Passed by reference.
288
 *
289
290
291
 * @return
 *   An associative array with keys 'file', 'line' and 'function'.
 */
292
function _drupal_get_last_caller(&$backtrace) {
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
  // Errors that occur inside PHP internal functions do not generate
  // information about file and line. Ignore black listed functions.
  $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
  while (($backtrace && !isset($backtrace[0]['line'])) ||
         (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
    array_shift($backtrace);
  }

  // The first trace is the call itself.
  // It gives us the line and the file of the last call.
  $call = $backtrace[0];

  // The second call give us the function where the call originated.
  if (isset($backtrace[1])) {
    if (isset($backtrace[1]['class'])) {
      $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
    }
    else {
      $call['function'] = $backtrace[1]['function'] . '()';
    }
  }
  else {
    $call['function'] = 'main()';
  }
  return $call;
}
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355

/**
 * 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>.
 */
function format_backtrace(array $backtrace) {
  $return = '';
  foreach ($backtrace as $trace) {
    $call = array('function' => '', 'args' => array());
    if (isset($trace['class'])) {
      $call['function'] = $trace['class'] . $trace['type'] . $trace['function'];
    }
    elseif (isset($trace['function'])) {
      $call['function'] = $trace['function'];
    }
    else {
      $call['function'] = 'main';
    }
    foreach ($trace['args'] as $arg) {
      if (is_scalar($arg)) {
        $call['args'][] = is_string($arg) ? '\'' . filter_xss($arg) . '\'' : $arg;
      }
      else {
        $call['args'][] = ucfirst(gettype($arg));
      }
    }
    $return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n";
  }
  return $return;
}