Completed
Pull Request — development (#2979)
by Stephen
08:55
created

Errors   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 464
ccs 0
cts 207
cp 0
rs 6.0975
c 0
b 0
f 0
wmc 60
lcom 2
cbo 5

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A terminate() 0 4 1
A addErrorTypes() 0 4 1
A log_lang_error() 0 18 4
A fatal_error() 0 4 1
A fatal_lang_error() 0 4 1
D _setup_fatal_ErrorContext() 0 54 17
A display_maintenance_message() 0 21 2
C display_db_error() 0 49 8
A display_loadavg_error() 0 19 1
A _set_fatal_error_headers() 0 12 1
A getErrorTypes() 0 16 2
B parseQueryString() 0 15 7
B insertLog() 0 26 6
B log_error() 0 28 4
A instance() 0 16 3

How to fix   Complexity   

Complex Class

Complex classes like Errors often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Errors, and based on these observations, apply Extract Interface, too.

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));
0 ignored issues
show
Coding Style Compatibility introduced by
The method terminate() contains an exit expression.

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.

Loading history...
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('<' => '&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
			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