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