Passed
Pull Request — master (#3805)
by
unknown
17:20
created

Errors::log_error()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 27
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 10
dl 0
loc 27
rs 9.9332
c 0
b 0
f 0
nc 5
nop 4
ccs 0
cts 12
cp 0
crap 20
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.7
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)
0 ignored issues
show
Bug introduced by
A parse error occurred: The alleged octal '0' is invalid
Loading history...
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('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));
171
		$error_message = strtr($error_message, array('&lt;br /&gt;' => '<br />', '&lt;b&gt;' => '<strong>', '&lt;/b&gt;' => '</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
		{
289
			global $modSettings;
290
291
			// Who knew dying took this much effort
292
			$context['linktree'] = isset($context['linktree']) ? $context['linktree'] : array();
293
			loadUserSettings();
294
295
			$_SESSION['session_var'] = isset($_SESSION['session_var']) ? $_SESSION['session_var'] : '';
296
			$_SESSION['session_value'] = isset($_SESSION['session_value'] ) ? $_SESSION['session_value'] : '';
297
			loadTheme();
298
299
			// Here lies elkarte, dead from a program error. Just a cryptic message, no output could be better.
300
			$context['user']['can_mod'] = false;
301
			$modSettings['default_forum_action'] = '';
302
		}
303
304
		// Don't bother indexing errors mate...
305
		$context['robot_no_index'] = true;
306
307
		// A little something for the template
308
		$context['error_title'] = isset($context['error_title']) ? $context['error_title'] : $txt['error_occurred'];
309
		$context['error_message'] = isset($context['error_message']) ? $context['error_message'] : $error_message;
310
		$context['error_code'] = isset($error_code) ? 'id="' . htmlspecialchars($error_code) . '" ' : '';
311
		$context['page_title'] = empty($context['page_title']) ? $context['error_title'] : $context['page_title'];
312
313
		// Load the template and set the sub template.
314
		loadTemplate('Errors');
315
		$context['sub_template'] = 'fatal_error';
316
317
		if (class_exists('Template_Layers'))
318
			\Template_Layers::instance()->isError();
319
320
		// If this is SSI, what do they want us to do?
321
		if (ELK === 'SSI')
322
		{
323
			if (!empty($ssi_on_error_method) && $ssi_on_error_method !== true && is_callable($ssi_on_error_method))
324
				call_user_func($ssi_on_error_method);
325
			elseif (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
326
				loadSubTemplate('fatal_error');
327
328
			// No layers?
329
			if (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
330
				$this->terminate();
331
		}
332
333
		// We want whatever for the header, and a footer. (footer includes sub template!)
334
		obExit(null, true, false, true);
335
336
		/* DO NOT IGNORE:
337
			If you are creating a bridge or modifying this function, you MUST
338
			make ABSOLUTELY SURE that this function quits and DOES NOT RETURN TO NORMAL
339
			PROGRAM FLOW.  Otherwise, security error messages will not be shown, and
340
			your forum will be in a very easily hackable state.
341
		*/
342
		trigger_error('Hacking attempt...', E_USER_ERROR);
343
	}
344
345
	/**
346
	 * Show a message for the (full block) maintenance mode.
347
	 *
348
	 * What it does:
349
	 *
350
	 * - It shows a complete page independent of language files or themes.
351
	 * - It is used only if $maintenance = 2 in Settings.php.
352
	 * - It stops further execution of the script.
353
	 */
354
	public function display_maintenance_message()
355
	{
356
		global $maintenance, $mtitle, $mmessage;
357
358
		$this->_set_fatal_error_headers();
359
360
		if (!empty($maintenance))
361
			echo '<!DOCTYPE html>
362
	<html>
363
		<head>
364
			<meta name="robots" content="noindex" />
365
			<title>', $mtitle, '</title>
366
		</head>
367
		<body>
368
			<h3>', $mtitle, '</h3>
369
			', $mmessage, '
370
		</body>
371
	</html>';
372
373
		$this->terminate();
374
	}
375
376
	/**
377
	 * Show a message for the (full block) maintenance mode.
378
	 *
379
	 * What it does:
380
	 *
381
	 * - It shows a complete page independent of language files or themes.
382
	 * - It is used only if $maintenance = 2 in Settings.php.
383
	 * - It stops further execution of the script.
384
	 */
385
	public function display_minimal_error($message)
386
	{
387
		global $maintenance, $mtitle, $mmessage;
388
389
		if (!headers_sent())
390
		{
391
			$this->_set_fatal_error_headers();
392
		}
393
394
		echo '<!DOCTYPE html>
395
	<html>
396
		<head>
397
			<meta name="robots" content="noindex" />
398
			<title>Unknown Error</title>
399
		</head>
400
		<body>
401
			<h3>Unknown Error</h3>
402
			', $message, '
403
		</body>
404
	</html>';
405
406
		$this->terminate();
407
	}
408
409
	/**
410
	 * Show an error message for the connection problems.
411
	 *
412
	 * What it does:
413
	 *
414
	 * - It shows a complete page independent of language files or themes.
415
	 * - It is used only if there's no way to connect to the database.
416
	 * - It stops further execution of the script.
417
	 */
418
	public function display_db_error()
419
	{
420
		global $mbname, $maintenance, $webmaster_email, $db_error_send;
421
422
		$cache = \Cache::instance();
423
424
		// Just check we're not in any buffers, just in case.
425
		while (ob_get_level() > 0)
426
		{
427
			@ob_end_clean();
428
		}
429
430
		// Set the output headers
431
		$this->_set_fatal_error_headers();
432
433
		$db_last_error = db_last_error();
434
435
		$temp = '';
436
		if ($cache->getVar($temp, 'db_last_error', 600))
437
			$db_last_error = max($db_last_error, $temp);
438
439
		// Perhaps we want to notify by mail that there was a this->_db error
440
		if ($db_last_error < time() - 3600 * 24 * 3 && empty($maintenance) && !empty($db_error_send))
441
		{
442
			// Try using shared memory if possible.
443
			$cache->put('db_last_error', time(), 600);
444
			if (!$cache->getVar($temp, 'db_last_error', 600))
445
				logLastDatabaseError();
446
447
			// Language files aren't loaded yet :'(
448
			$db_error = $this->_db->last_error($this->_db->connection());
449
			@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.');
450
		}
451
452
		// What to do?  Language files haven't and can't be loaded yet...
453
		echo '<!DOCTYPE html>
454
	<html>
455
		<head>
456
			<meta name="robots" content="noindex" />
457
			<title>Connection Problems</title>
458
		</head>
459
		<body>
460
			<h3>Connection Problems</h3>
461
			Sorry, we were unable to connect to the database.  This may be caused by the server being busy.  Please try again later.
462
		</body>
463
	</html>';
464
465
		$this->terminate();
466
	}
467
468
	/**
469
	 * Show an error message for load average blocking problems.
470
	 *
471
	 * What it does:
472
	 *
473
	 * - It shows a complete page independent of language files or themes.
474
	 * - It is used only if the load averages are too high to continue execution.
475
	 * - It stops further execution of the script.
476
	 */
477
	public function display_loadavg_error()
478
	{
479
		// If this is a load average problem, display an appropriate message (but we still don't have language files!)
480
		$this->_set_fatal_error_headers();
481
482
		echo '<!DOCTYPE html>
483
	<html>
484
		<head>
485
			<meta name="robots" content="noindex" />
486
			<title>Temporarily Unavailable</title>
487
		</head>
488
		<body>
489
			<h3>Temporarily Unavailable</h3>
490
			Due to high stress on the server the forum is temporarily unavailable.  Please try again later.
491
		</body>
492
	</html>';
493
494
		$this->terminate();
495
	}
496
497
	/**
498
	 * Small utility function for fatal error pages, sets the headers.
499
	 *
500
	 * - Used by display_db_error(), display_loadavg_error(),
501
	 * display_maintenance_message()
502
	 */
503
	private function _set_fatal_error_headers()
504
	{
505
		// Don't cache this page!
506
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
507
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
508
		header('Cache-Control: no-cache');
509
510
		// Send the right error codes.
511
		header('HTTP/1.1 503 Service Temporarily Unavailable');
512
		header('Status: 503 Service Temporarily Unavailable');
513
		header('Retry-After: 3600');
514
	}
515
516
	/**
517
	 * Retrieve the sole instance of this class.
518
	 *
519
	 * @return Errors
520
	 */
521
	public static function instance()
522
	{
523
		if (self::$_errors === null)
524
		{
525
			if (function_exists('database'))
526
			{
527
				self::$_errors = new self;
528
			}
529
			else
530
			{
531
				self::$_errors = new self(1);
532
			}
533
		}
534
535
		return self::$_errors;
536
	}
537
}
538