|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * Functions for error handling. |
|
6 */ |
|
7 |
|
8 /** |
|
9 * Maps PHP error constants to watchdog severity levels. |
|
10 * |
|
11 * The error constants are documented at |
|
12 * http://php.net/manual/errorfunc.constants.php |
|
13 * |
|
14 * @ingroup logging_severity_levels |
|
15 */ |
|
16 function drupal_error_levels() { |
|
17 $types = array( |
|
18 E_ERROR => array('Error', WATCHDOG_ERROR), |
|
19 E_WARNING => array('Warning', WATCHDOG_WARNING), |
|
20 E_PARSE => array('Parse error', WATCHDOG_ERROR), |
|
21 E_NOTICE => array('Notice', WATCHDOG_NOTICE), |
|
22 E_CORE_ERROR => array('Core error', WATCHDOG_ERROR), |
|
23 E_CORE_WARNING => array('Core warning', WATCHDOG_WARNING), |
|
24 E_COMPILE_ERROR => array('Compile error', WATCHDOG_ERROR), |
|
25 E_COMPILE_WARNING => array('Compile warning', WATCHDOG_WARNING), |
|
26 E_USER_ERROR => array('User error', WATCHDOG_ERROR), |
|
27 E_USER_WARNING => array('User warning', WATCHDOG_WARNING), |
|
28 E_USER_NOTICE => array('User notice', WATCHDOG_NOTICE), |
|
29 E_STRICT => array('Strict warning', WATCHDOG_DEBUG), |
|
30 E_RECOVERABLE_ERROR => array('Recoverable fatal error', WATCHDOG_ERROR), |
|
31 ); |
|
32 // E_DEPRECATED and E_USER_DEPRECATED were added in PHP 5.3.0. |
|
33 if (defined('E_DEPRECATED')) { |
|
34 $types[E_DEPRECATED] = array('Deprecated function', WATCHDOG_DEBUG); |
|
35 $types[E_USER_DEPRECATED] = array('User deprecated function', WATCHDOG_DEBUG); |
|
36 } |
|
37 return $types; |
|
38 } |
|
39 |
|
40 /** |
|
41 * Provides custom PHP error handling. |
|
42 * |
|
43 * @param $error_level |
|
44 * The level of the error raised. |
|
45 * @param $message |
|
46 * The error message. |
|
47 * @param $filename |
|
48 * The filename that the error was raised in. |
|
49 * @param $line |
|
50 * The line number the error was raised at. |
|
51 * @param $context |
|
52 * An array that points to the active symbol table at the point the error |
|
53 * occurred. |
|
54 */ |
|
55 function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) { |
|
56 if ($error_level & error_reporting()) { |
|
57 $types = drupal_error_levels(); |
|
58 list($severity_msg, $severity_level) = $types[$error_level]; |
|
59 $caller = _drupal_get_last_caller(debug_backtrace()); |
|
60 |
|
61 if (!function_exists('filter_xss_admin')) { |
|
62 require_once DRUPAL_ROOT . '/includes/common.inc'; |
|
63 } |
|
64 |
|
65 // We treat recoverable errors as fatal. |
|
66 _drupal_log_error(array( |
|
67 '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error', |
|
68 // The standard PHP error handler considers that the error messages |
|
69 // are HTML. We mimic this behavior here. |
|
70 '!message' => filter_xss_admin($message), |
|
71 '%function' => $caller['function'], |
|
72 '%file' => $caller['file'], |
|
73 '%line' => $caller['line'], |
|
74 'severity_level' => $severity_level, |
|
75 ), $error_level == E_RECOVERABLE_ERROR); |
|
76 } |
|
77 } |
|
78 |
|
79 /** |
|
80 * Decodes an exception and retrieves the correct caller. |
|
81 * |
|
82 * @param $exception |
|
83 * The exception object that was thrown. |
|
84 * |
|
85 * @return |
|
86 * An error in the format expected by _drupal_log_error(). |
|
87 */ |
|
88 function _drupal_decode_exception($exception) { |
|
89 $message = $exception->getMessage(); |
|
90 |
|
91 $backtrace = $exception->getTrace(); |
|
92 // Add the line throwing the exception to the backtrace. |
|
93 array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); |
|
94 |
|
95 // For PDOException errors, we try to return the initial caller, |
|
96 // skipping internal functions of the database layer. |
|
97 if ($exception instanceof PDOException) { |
|
98 // The first element in the stack is the call, the second element gives us the caller. |
|
99 // We skip calls that occurred in one of the classes of the database layer |
|
100 // or in one of its global functions. |
|
101 $db_functions = array('db_query', 'db_query_range'); |
|
102 while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && |
|
103 ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || |
|
104 in_array($caller['function'], $db_functions))) { |
|
105 // We remove that call. |
|
106 array_shift($backtrace); |
|
107 } |
|
108 if (isset($exception->query_string, $exception->args)) { |
|
109 $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE); |
|
110 } |
|
111 } |
|
112 $caller = _drupal_get_last_caller($backtrace); |
|
113 |
|
114 return array( |
|
115 '%type' => get_class($exception), |
|
116 // The standard PHP exception handler considers that the exception message |
|
117 // is plain-text. We mimic this behavior here. |
|
118 '!message' => check_plain($message), |
|
119 '%function' => $caller['function'], |
|
120 '%file' => $caller['file'], |
|
121 '%line' => $caller['line'], |
|
122 'severity_level' => WATCHDOG_ERROR, |
|
123 ); |
|
124 } |
|
125 |
|
126 /** |
|
127 * Renders an exception error message without further exceptions. |
|
128 * |
|
129 * @param $exception |
|
130 * The exception object that was thrown. |
|
131 * @return |
|
132 * An error message. |
|
133 */ |
|
134 function _drupal_render_exception_safe($exception) { |
|
135 return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception))); |
|
136 } |
|
137 |
|
138 /** |
|
139 * Determines whether an error should be displayed. |
|
140 * |
|
141 * When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL, |
|
142 * all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error |
|
143 * will be examined to determine if it should be displayed. |
|
144 * |
|
145 * @param $error |
|
146 * Optional error to examine for ERROR_REPORTING_DISPLAY_SOME. |
|
147 * |
|
148 * @return |
|
149 * TRUE if an error should be displayed. |
|
150 */ |
|
151 function error_displayable($error = NULL) { |
|
152 $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL); |
|
153 $updating = (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update'); |
|
154 $all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL); |
|
155 $error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME && |
|
156 isset($error) && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning'); |
|
157 |
|
158 return ($updating || $all_errors_displayed || $error_needs_display); |
|
159 } |
|
160 |
|
161 /** |
|
162 * Logs a PHP error or exception and displays an error page in fatal cases. |
|
163 * |
|
164 * @param $error |
|
165 * An array with the following keys: %type, !message, %function, %file, %line |
|
166 * and severity_level. All the parameters are plain-text, with the exception |
|
167 * of !message, which needs to be a safe HTML string. |
|
168 * @param $fatal |
|
169 * TRUE if the error is fatal. |
|
170 */ |
|
171 function _drupal_log_error($error, $fatal = FALSE) { |
|
172 // Initialize a maintenance theme if the bootstrap was not complete. |
|
173 // Do it early because drupal_set_message() triggers a drupal_theme_initialize(). |
|
174 if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) { |
|
175 unset($GLOBALS['theme']); |
|
176 if (!defined('MAINTENANCE_MODE')) { |
|
177 define('MAINTENANCE_MODE', 'error'); |
|
178 } |
|
179 drupal_maintenance_theme(); |
|
180 } |
|
181 |
|
182 // When running inside the testing framework, we relay the errors |
|
183 // to the tested site by the way of HTTP headers. |
|
184 $test_info = &$GLOBALS['drupal_test_info']; |
|
185 if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { |
|
186 // $number does not use drupal_static as it should not be reset |
|
187 // as it uniquely identifies each PHP error. |
|
188 static $number = 0; |
|
189 $assertion = array( |
|
190 $error['!message'], |
|
191 $error['%type'], |
|
192 array( |
|
193 'function' => $error['%function'], |
|
194 'file' => $error['%file'], |
|
195 'line' => $error['%line'], |
|
196 ), |
|
197 ); |
|
198 header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion))); |
|
199 $number++; |
|
200 } |
|
201 |
|
202 // Log the error immediately, unless this is a non-fatal error which has been |
|
203 // triggered via drupal_trigger_error_with_delayed_logging(); in that case |
|
204 // trigger it in a shutdown function. Fatal errors are always triggered |
|
205 // immediately since for a fatal error the page request will end here anyway. |
|
206 if (!$fatal && drupal_static('_drupal_trigger_error_with_delayed_logging')) { |
|
207 drupal_register_shutdown_function('watchdog', 'php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); |
|
208 } |
|
209 else { |
|
210 watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); |
|
211 } |
|
212 |
|
213 if ($fatal) { |
|
214 drupal_add_http_header('Status', '500 Service unavailable (with message)'); |
|
215 } |
|
216 |
|
217 if (drupal_is_cli()) { |
|
218 if ($fatal) { |
|
219 // When called from CLI, simply output a plain text message. |
|
220 print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n"; |
|
221 exit; |
|
222 } |
|
223 } |
|
224 |
|
225 if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { |
|
226 if ($fatal) { |
|
227 if (error_displayable($error)) { |
|
228 // When called from JavaScript, simply output the error message. |
|
229 print t('%type: !message in %function (line %line of %file).', $error); |
|
230 } |
|
231 exit; |
|
232 } |
|
233 } |
|
234 else { |
|
235 // Display the message if the current error reporting level allows this type |
|
236 // of message to be displayed, and unconditionally in update.php. |
|
237 if (error_displayable($error)) { |
|
238 $class = 'error'; |
|
239 |
|
240 // If error type is 'User notice' then treat it as debug information |
|
241 // instead of an error message, see dd(). |
|
242 if ($error['%type'] == 'User notice') { |
|
243 $error['%type'] = 'Debug'; |
|
244 $class = 'status'; |
|
245 } |
|
246 |
|
247 drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class); |
|
248 } |
|
249 |
|
250 if ($fatal) { |
|
251 drupal_set_title(t('Error')); |
|
252 // We fallback to a maintenance page at this point, because the page generation |
|
253 // itself can generate errors. |
|
254 print theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.'))); |
|
255 exit; |
|
256 } |
|
257 } |
|
258 } |
|
259 |
|
260 /** |
|
261 * Gets the last caller from a backtrace. |
|
262 * |
|
263 * @param $backtrace |
|
264 * A standard PHP backtrace. |
|
265 * |
|
266 * @return |
|
267 * An associative array with keys 'file', 'line' and 'function'. |
|
268 */ |
|
269 function _drupal_get_last_caller($backtrace) { |
|
270 // Errors that occur inside PHP internal functions do not generate |
|
271 // information about file and line. Ignore black listed functions. |
|
272 $blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler'); |
|
273 while (($backtrace && !isset($backtrace[0]['line'])) || |
|
274 (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { |
|
275 array_shift($backtrace); |
|
276 } |
|
277 |
|
278 // The first trace is the call itself. |
|
279 // It gives us the line and the file of the last call. |
|
280 $call = $backtrace[0]; |
|
281 |
|
282 // The second call give us the function where the call originated. |
|
283 if (isset($backtrace[1])) { |
|
284 if (isset($backtrace[1]['class'])) { |
|
285 $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()'; |
|
286 } |
|
287 else { |
|
288 $call['function'] = $backtrace[1]['function'] . '()'; |
|
289 } |
|
290 } |
|
291 else { |
|
292 $call['function'] = 'main()'; |
|
293 } |
|
294 return $call; |
|
295 } |