1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* The purpose of this file is... errors. (hard to guess, I guess?) It takes |
5
|
|
|
* care of logging, error messages, error handling, database errors, and |
6
|
|
|
* error log administration. |
7
|
|
|
* |
8
|
|
|
* @name ElkArte Forum |
9
|
|
|
* @copyright ElkArte Forum contributors |
10
|
|
|
* @license BSD http://opensource.org/licenses/BSD-3-Clause |
11
|
|
|
* |
12
|
|
|
* This file contains code covered by: |
13
|
|
|
* copyright: 2011 Simple Machines (http://www.simplemachines.org) |
14
|
|
|
* license: BSD, See included LICENSE.TXT for terms and conditions. |
15
|
|
|
* |
16
|
|
|
* @version 1.1 Release Candidate 1 |
17
|
|
|
* |
18
|
|
|
*/ |
19
|
|
|
|
20
|
|
|
namespace ElkArte\Errors; |
21
|
|
|
|
22
|
|
|
use Elk_Exception; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Class to handle all forum errors and exceptions |
26
|
|
|
*/ |
27
|
|
|
class Errors extends \AbstractModel |
28
|
|
|
{ |
29
|
|
|
/** @var Errors Sole private Errors instance */ |
30
|
|
|
private static $_errors = null; |
31
|
|
|
|
32
|
|
|
/** @var string[] The types of categories we have */ |
33
|
|
|
private $errorTypes = array( |
34
|
|
|
'general', |
35
|
|
|
'critical', |
36
|
|
|
'database', |
37
|
|
|
'undefined_vars', |
38
|
|
|
'user', |
39
|
|
|
'template', |
40
|
|
|
'debug', |
41
|
|
|
); |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* In case of maintenance of very early errors, the database may not be available, |
45
|
|
|
* this __construct will feed AbstractModel with a value just to stop it |
46
|
|
|
* from trying to initialize the database connection. |
47
|
|
|
* |
48
|
|
|
* @param $db Database|null |
49
|
|
|
*/ |
50
|
|
|
public function __construct($db = null) |
51
|
|
|
{ |
52
|
|
|
parent::__construct($db); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Halts execution, optionally displays an error message |
57
|
|
|
* |
58
|
|
|
* @param string|integer $error |
59
|
|
|
*/ |
60
|
|
|
protected function terminate($error = '') |
61
|
|
|
{ |
62
|
|
|
die(htmlspecialchars($error)); |
|
|
|
|
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @param string $errorType |
67
|
|
|
*/ |
68
|
|
|
public function addErrorTypes($errorType) |
69
|
|
|
{ |
70
|
|
|
$this->errorTypes[] = $errorType; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @return string[] |
75
|
|
|
*/ |
76
|
|
|
protected function getErrorTypes() |
77
|
|
|
{ |
78
|
|
|
static $tried_hook = false; |
79
|
|
|
|
80
|
|
|
// Perhaps integration wants to add specific error types for the log |
81
|
|
|
$errorTypes = array(); |
82
|
|
|
if (empty($tried_hook)) |
83
|
|
|
{ |
84
|
|
|
// This prevents us from infinite looping if the hook or call produces an error. |
85
|
|
|
$tried_hook = true; |
86
|
|
|
call_integration_hook('integrate_error_types', array(&$errorTypes)); |
87
|
|
|
$this->errorTypes += $errorTypes; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
return $this->errorTypes; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* @return string |
95
|
|
|
*/ |
96
|
|
|
private function parseQueryString() |
97
|
|
|
{ |
98
|
|
|
global $scripturl; |
99
|
|
|
|
100
|
|
|
$query_string = empty($_SERVER['QUERY_STRING']) ? (empty($_SERVER['REQUEST_URL']) ? '' : str_replace($scripturl, '', $_SERVER['REQUEST_URL'])) : $_SERVER['QUERY_STRING']; |
101
|
|
|
|
102
|
|
|
// Don't log the session hash in the url twice, it's a waste. |
103
|
|
|
$query_string = htmlspecialchars((ELK === 'SSI' ? '' : '?') . preg_replace(array('~;sesc=[^&;]+~', '~' . session_name() . '=' . session_id() . '[&;]~'), array(';sesc', ''), $query_string), ENT_COMPAT, 'UTF-8'); |
104
|
|
|
|
105
|
|
|
// Just so we know what board error messages are from. |
106
|
|
|
if (isset($_POST['board']) && !isset($_GET['board'])) |
107
|
|
|
$query_string .= ($query_string == '' ? 'board=' : ';board=') . $_POST['board']; |
108
|
|
|
|
109
|
|
|
return $query_string; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Insert an error entry in to the log_errors table |
114
|
|
|
* |
115
|
|
|
* @param string $query_string |
116
|
|
|
* @param string $error_message |
117
|
|
|
* @param string|boolean $error_type |
118
|
|
|
* @param string $file |
119
|
|
|
* @param int $line |
120
|
|
|
*/ |
121
|
|
|
private function insertLog($query_string, $error_message, $error_type, $file, $line) |
122
|
|
|
{ |
123
|
|
|
global $user_info, $last_error; |
124
|
|
|
|
125
|
|
|
$this->_db = database(); |
126
|
|
|
|
127
|
|
|
// Just in case there's no id_member or IP set yet. |
128
|
|
|
if (empty($user_info['id'])) |
129
|
|
|
$user_info['id'] = 0; |
130
|
|
|
if (empty($user_info['ip'])) |
131
|
|
|
$user_info['ip'] = ''; |
132
|
|
|
|
133
|
|
|
// Don't log the same error countless times, as we can get in a cycle of depression... |
134
|
|
|
$error_info = array($user_info['id'], time(), $user_info['ip'], $query_string, $error_message, isset($_SESSION['session_value']) ? (string) $_SESSION['session_value'] : 'no_session_data', $error_type, $file, $line); |
135
|
|
|
if (empty($last_error) || $last_error != $error_info) |
136
|
|
|
{ |
137
|
|
|
// Insert the error into the database. |
138
|
|
|
$this->_db->insert('', |
139
|
|
|
'{db_prefix}log_errors', |
140
|
|
|
array('id_member' => 'int', 'log_time' => 'int', 'ip' => 'string-16', 'url' => 'string-65534', 'message' => 'string-65534', 'session' => 'string', 'error_type' => 'string', 'file' => 'string-255', 'line' => 'int'), |
141
|
|
|
$error_info, |
142
|
|
|
array('id_error') |
143
|
|
|
); |
144
|
|
|
$last_error = $error_info; |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Log an error to the error log if the error logging is enabled. |
150
|
|
|
* |
151
|
|
|
* - filename and line should be __FILE__ and __LINE__, respectively. |
152
|
|
|
* |
153
|
|
|
* Example use: |
154
|
|
|
* - die(Errors::instance()->log_error($msg)); |
155
|
|
|
* |
156
|
|
|
* @param string $error_message |
157
|
|
|
* @param string|boolean $error_type = 'general' |
158
|
|
|
* @param string $file = '' |
159
|
|
|
* @param int $line = 0 |
160
|
|
|
* |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
public function log_error($error_message, $error_type = 'general', $file = '', $line = 00) |
164
|
|
|
{ |
165
|
|
|
// Check if error logging is actually on. |
166
|
|
|
if (empty($this->_modSettings['enableErrorLogging'])) |
167
|
|
|
return $error_message; |
168
|
|
|
|
169
|
|
|
// Basically, htmlspecialchars it minus &. (for entities!) |
170
|
|
|
$error_message = strtr($error_message, array('<' => '<', '>' => '>', '"' => '"')); |
171
|
|
|
$error_message = strtr($error_message, array('<br />' => '<br />', '<b>' => '<strong>', '</b>' => '</strong>', "\n" => '<br />')); |
172
|
|
|
|
173
|
|
|
// Add a file and line to the error message? |
174
|
|
|
// Don't use the actual txt entries for file and line but instead use %1$s for file and %2$s for line |
175
|
|
|
// Windows-style slashes don't play well, lets convert them to the unix style. |
176
|
|
|
$file = str_replace('\\', '/', $file); |
177
|
|
|
$line = (int) $line; |
178
|
|
|
|
179
|
|
|
// Find the best query string we can... |
180
|
|
|
$query_string = $this->parseQueryString(); |
181
|
|
|
|
182
|
|
|
// Make sure the category that was specified is a valid one |
183
|
|
|
$error_type = in_array($error_type, $this->getErrorTypes()) && $error_type !== true ? $error_type : 'general'; |
184
|
|
|
|
185
|
|
|
// Insert the error into the database. |
186
|
|
|
$this->insertLog($query_string, $error_message, $error_type, $file, $line); |
187
|
|
|
|
188
|
|
|
// Return the message to make things simpler. |
189
|
|
|
return $error_message; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Similar to log_error, it accepts a language index as the error. |
194
|
|
|
* |
195
|
|
|
* What it does: |
196
|
|
|
* |
197
|
|
|
* - Takes care of loading the forum default language |
198
|
|
|
* - Logs the error (forwarding to log_error) |
199
|
|
|
* |
200
|
|
|
* @param string $error |
201
|
|
|
* @param string $error_type = 'general' |
202
|
|
|
* @param string|mixed[] $sprintf = array() |
203
|
|
|
* @param string $file = '' |
204
|
|
|
* @param int $line = 0 |
205
|
|
|
* |
206
|
|
|
* @return string |
207
|
|
|
*/ |
208
|
|
|
public function log_lang_error($error, $error_type = 'general', $sprintf = array(), $file = '', $line = 0) |
209
|
|
|
{ |
210
|
|
|
global $user_info, $language, $txt; |
211
|
|
|
|
212
|
|
|
loadLanguage('Errors', $language); |
213
|
|
|
|
214
|
|
|
$reload_lang_file = $language != $user_info['language']; |
215
|
|
|
|
216
|
|
|
$error_message = !isset($txt[$error]) ? $error : (empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf)); |
217
|
|
|
$this->log_error($error_message, $error_type, $file, $line); |
218
|
|
|
|
219
|
|
|
// Load the language file, only if it needs to be reloaded |
220
|
|
|
if ($reload_lang_file) |
221
|
|
|
loadLanguage('Errors'); |
222
|
|
|
|
223
|
|
|
// Return the message to make things simpler. |
224
|
|
|
return $error_message; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* An irrecoverable error. |
229
|
|
|
* |
230
|
|
|
* What it does: |
231
|
|
|
* |
232
|
|
|
* - This function stops execution and displays an error message. |
233
|
|
|
* - It logs the error message if $log is specified. |
234
|
|
|
* |
235
|
|
|
* @param string $error |
236
|
|
|
* @param string|boolean $log defaults to 'general' false will skip logging, true will use general |
237
|
|
|
* |
238
|
|
|
* @throws \Elk_Exception |
239
|
|
|
*/ |
240
|
|
|
public function fatal_error($error = '', $log = 'general') |
241
|
|
|
{ |
242
|
|
|
throw new \Elk_Exception($error, $log); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Shows a fatal error with a message stored in the language file. |
247
|
|
|
* |
248
|
|
|
* What it does: |
249
|
|
|
* |
250
|
|
|
* - This function stops execution and displays an error message by key. |
251
|
|
|
* - uses the string with the error_message_key key. |
252
|
|
|
* - logs the error in the forum's default language while displaying the error |
253
|
|
|
* message in the user's language. |
254
|
|
|
* - uses Errors language file and applies the $sprintf information if specified. |
255
|
|
|
* - the information is logged if log is specified. |
256
|
|
|
* |
257
|
|
|
* @param string $error |
258
|
|
|
* @param string|boolean $log defaults to 'general' false will skip logging, true will use general |
259
|
|
|
* @param string[] $sprintf defaults to empty array() |
260
|
|
|
* |
261
|
|
|
* @throws \Elk_Exception |
262
|
|
|
*/ |
263
|
|
|
public function fatal_lang_error($error, $log = 'general', $sprintf = array()) |
264
|
|
|
{ |
265
|
|
|
throw new \Elk_Exception($error, $log, $sprintf); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* It is called by Errors::fatal_error() and Errors::fatal_lang_error(). |
270
|
|
|
* |
271
|
|
|
* @uses Errors template, fatal_error sub template |
272
|
|
|
* @param string $error_message |
273
|
|
|
* @param string $error_code string or int code |
274
|
|
|
* @throws \Elk_Exception |
275
|
|
|
*/ |
276
|
|
|
final protected function _setup_fatal_ErrorContext($error_message, $error_code) |
277
|
|
|
{ |
278
|
|
|
global $context, $txt, $ssi_on_error_method; |
279
|
|
|
static $level = 0; |
280
|
|
|
|
281
|
|
|
// Attempt to prevent a recursive loop. |
282
|
|
|
++$level; |
283
|
|
|
if ($level > 1) |
284
|
|
|
return false; |
285
|
|
|
|
286
|
|
|
// Maybe they came from dlattach or similar? |
287
|
|
|
if (ELK !== 'SSI' && empty($context['theme_loaded'])) |
288
|
|
|
loadTheme(); |
289
|
|
|
|
290
|
|
|
// Don't bother indexing errors mate... |
291
|
|
|
$context['robot_no_index'] = true; |
292
|
|
|
|
293
|
|
|
// A little something for the template |
294
|
|
|
$context['error_title'] = isset($context['error_title']) ? $context['error_title'] : $txt['error_occurred']; |
295
|
|
|
$context['error_message'] = isset($context['error_message']) ? $context['error_message'] : $error_message; |
296
|
|
|
$context['error_code'] = isset($error_code) ? 'id="' . htmlspecialchars($error_code) . '" ' : ''; |
297
|
|
|
$context['page_title'] = empty($context['page_title']) ? $context['error_title'] : $context['page_title']; |
298
|
|
|
|
299
|
|
|
// Load the template and set the sub template. |
300
|
|
|
loadTemplate('Errors'); |
301
|
|
|
$context['sub_template'] = 'fatal_error'; |
302
|
|
|
|
303
|
|
|
if (class_exists('Template_Layers')) |
304
|
|
|
\Template_Layers::instance()->isError(); |
305
|
|
|
|
306
|
|
|
// If this is SSI, what do they want us to do? |
307
|
|
|
if (ELK === 'SSI') |
308
|
|
|
{ |
309
|
|
|
if (!empty($ssi_on_error_method) && $ssi_on_error_method !== true && is_callable($ssi_on_error_method)) |
310
|
|
|
call_user_func($ssi_on_error_method); |
311
|
|
|
elseif (empty($ssi_on_error_method) || $ssi_on_error_method !== true) |
312
|
|
|
loadSubTemplate('fatal_error'); |
313
|
|
|
|
314
|
|
|
// No layers? |
315
|
|
|
if (empty($ssi_on_error_method) || $ssi_on_error_method !== true) |
316
|
|
|
$this->terminate(); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
// We want whatever for the header, and a footer. (footer includes sub template!) |
320
|
|
|
obExit(null, true, false, true); |
321
|
|
|
|
322
|
|
|
/* DO NOT IGNORE: |
323
|
|
|
If you are creating a bridge or modifying this function, you MUST |
324
|
|
|
make ABSOLUTELY SURE that this function quits and DOES NOT RETURN TO NORMAL |
325
|
|
|
PROGRAM FLOW. Otherwise, security error messages will not be shown, and |
326
|
|
|
your forum will be in a very easily hackable state. |
327
|
|
|
*/ |
328
|
|
|
trigger_error('Hacking attempt...', E_USER_ERROR); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Show a message for the (full block) maintenance mode. |
333
|
|
|
* |
334
|
|
|
* What it does: |
335
|
|
|
* |
336
|
|
|
* - It shows a complete page independent of language files or themes. |
337
|
|
|
* - It is used only if $maintenance = 2 in Settings.php. |
338
|
|
|
* - It stops further execution of the script. |
339
|
|
|
*/ |
340
|
|
|
public function display_maintenance_message() |
341
|
|
|
{ |
342
|
|
|
global $maintenance, $mtitle, $mmessage; |
343
|
|
|
|
344
|
|
|
$this->_set_fatal_error_headers(); |
345
|
|
|
|
346
|
|
|
if (!empty($maintenance)) |
347
|
|
|
echo '<!DOCTYPE html> |
348
|
|
|
<html> |
349
|
|
|
<head> |
350
|
|
|
<meta name="robots" content="noindex" /> |
351
|
|
|
<title>', $mtitle, '</title> |
352
|
|
|
</head> |
353
|
|
|
<body> |
354
|
|
|
<h3>', $mtitle, '</h3> |
355
|
|
|
', $mmessage, ' |
356
|
|
|
</body> |
357
|
|
|
</html>'; |
358
|
|
|
|
359
|
|
|
$this->terminate(); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Show an error message for the connection problems. |
364
|
|
|
* |
365
|
|
|
* What it does: |
366
|
|
|
* |
367
|
|
|
* - It shows a complete page independent of language files or themes. |
368
|
|
|
* - It is used only if there's no way to connect to the database. |
369
|
|
|
* - It stops further execution of the script. |
370
|
|
|
*/ |
371
|
|
|
public function display_db_error() |
372
|
|
|
{ |
373
|
|
|
global $mbname, $maintenance, $webmaster_email, $db_error_send; |
374
|
|
|
|
375
|
|
|
$cache = \Cache::instance(); |
376
|
|
|
|
377
|
|
|
// Just check we're not in any buffers, just in case. |
378
|
|
|
while (ob_get_level() > 0) |
379
|
|
|
{ |
380
|
|
|
@ob_end_clean(); |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
// Set the output headers |
384
|
|
|
$this->_set_fatal_error_headers(); |
385
|
|
|
|
386
|
|
|
$db_last_error = db_last_error(); |
387
|
|
|
|
388
|
|
|
$temp = ''; |
389
|
|
|
if ($cache->getVar($temp, 'db_last_error', 600)) |
390
|
|
|
$db_last_error = max($db_last_error, $temp); |
391
|
|
|
|
392
|
|
|
// Perhaps we want to notify by mail that there was a this->_db error |
393
|
|
|
if ($db_last_error < time() - 3600 * 24 * 3 && empty($maintenance) && !empty($db_error_send)) |
394
|
|
|
{ |
395
|
|
|
// Try using shared memory if possible. |
396
|
|
|
$cache->put('db_last_error', time(), 600); |
397
|
|
|
if (!$cache->getVar($temp, 'db_last_error', 600)) |
398
|
|
|
logLastDatabaseError(); |
399
|
|
|
|
400
|
|
|
// Language files aren't loaded yet :'( |
401
|
|
|
$db_error = $this->_db->last_error($this->_db->connection()); |
402
|
|
|
@mail($webmaster_email, $mbname . ': Database Error!', 'There has been a problem with the database!' . ($db_error == '' ? '' : "\n" . $this->_db->db_title() . ' reported:' . "\n" . $db_error) . "\n\n" . 'This is a notice email to let you know that the system could not connect to the database, contact your host if this continues.'); |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
// What to do? Language files haven't and can't be loaded yet... |
406
|
|
|
echo '<!DOCTYPE html> |
407
|
|
|
<html> |
408
|
|
|
<head> |
409
|
|
|
<meta name="robots" content="noindex" /> |
410
|
|
|
<title>Connection Problems</title> |
411
|
|
|
</head> |
412
|
|
|
<body> |
413
|
|
|
<h3>Connection Problems</h3> |
414
|
|
|
Sorry, we were unable to connect to the database. This may be caused by the server being busy. Please try again later. |
415
|
|
|
</body> |
416
|
|
|
</html>'; |
417
|
|
|
|
418
|
|
|
$this->terminate(); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Show an error message for load average blocking problems. |
423
|
|
|
* |
424
|
|
|
* What it does: |
425
|
|
|
* |
426
|
|
|
* - It shows a complete page independent of language files or themes. |
427
|
|
|
* - It is used only if the load averages are too high to continue execution. |
428
|
|
|
* - It stops further execution of the script. |
429
|
|
|
*/ |
430
|
|
|
public function display_loadavg_error() |
431
|
|
|
{ |
432
|
|
|
// If this is a load average problem, display an appropriate message (but we still don't have language files!) |
433
|
|
|
$this->_set_fatal_error_headers(); |
434
|
|
|
|
435
|
|
|
echo '<!DOCTYPE html> |
436
|
|
|
<html> |
437
|
|
|
<head> |
438
|
|
|
<meta name="robots" content="noindex" /> |
439
|
|
|
<title>Temporarily Unavailable</title> |
440
|
|
|
</head> |
441
|
|
|
<body> |
442
|
|
|
<h3>Temporarily Unavailable</h3> |
443
|
|
|
Due to high stress on the server the forum is temporarily unavailable. Please try again later. |
444
|
|
|
</body> |
445
|
|
|
</html>'; |
446
|
|
|
|
447
|
|
|
$this->terminate(); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Small utility function for fatal error pages, sets the headers. |
452
|
|
|
* |
453
|
|
|
* - Used by display_db_error(), display_loadavg_error(), |
454
|
|
|
* display_maintenance_message() |
455
|
|
|
*/ |
456
|
|
|
private function _set_fatal_error_headers() |
457
|
|
|
{ |
458
|
|
|
// Don't cache this page! |
459
|
|
|
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); |
460
|
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); |
461
|
|
|
header('Cache-Control: no-cache'); |
462
|
|
|
|
463
|
|
|
// Send the right error codes. |
464
|
|
|
header('HTTP/1.1 503 Service Temporarily Unavailable'); |
465
|
|
|
header('Status: 503 Service Temporarily Unavailable'); |
466
|
|
|
header('Retry-After: 3600'); |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* Retrieve the sole instance of this class. |
471
|
|
|
* |
472
|
|
|
* @return Errors |
473
|
|
|
*/ |
474
|
|
|
public static function instance() |
475
|
|
|
{ |
476
|
|
|
if (self::$_errors === null) |
477
|
|
|
{ |
478
|
|
|
if (function_exists('database')) |
479
|
|
|
{ |
480
|
|
|
self::$_errors = new self; |
481
|
|
|
} |
482
|
|
|
else |
483
|
|
|
{ |
484
|
|
|
self::$_errors = new self(1); |
485
|
|
|
} |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
return self::$_errors; |
489
|
|
|
} |
490
|
|
|
} |
491
|
|
|
|
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exit
expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.