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 }
|