Completed
Push — development ( 7ace62...380f58 )
by Stephen
20s
created

Errors::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 4
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
use Template_Layers;
24
use AbstractModel;
25
26
/**
27
 * Class to handle all forum errors and exceptions
28
 */
29
class Errors extends AbstractModel
30
{
31
	/** @var Errors Sole private Errors instance */
32
	private static $_errors = null;
33
34
	/** @var string[] The types of categories we have */
35
	private $errorTypes = array(
36
		'general',
37
		'critical',
38
		'database',
39
		'undefined_vars',
40
		'user',
41
		'template',
42
		'debug',
43
	);
44
45
	/**
46
	 * In case of maintenance of very early errors, the database may not be available,
47
	 * this __construct will feed AbstractModel with a value just to stop it
48
	 * from trying to initialize the database connection.
49
	 *
50
	 * @param $db Database|null
51
	 */
52
	public function __construct($db = null)
53
	{
54
		parent::__construct($db);
55
	}
56
57
	/**
58
	 * Halts execution, optionally displays an error message
59
	 *
60
	 * @param string|integer $error
61
	 */
62
	protected function terminate($error = '')
63
	{
64
		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...
65
	}
66
67
	/**
68
	 * @param string $errorType
69
	 */
70
	public function addErrorTypes($errorType)
71
	{
72
		$this->errorTypes[] = $errorType;
73
	}
74
75
	/**
76
	 * @return string[]
77
	 */
78
	protected function getErrorTypes()
79
	{
80
		static $tried_hook = false;
81
82
		// Perhaps integration wants to add specific error types for the log
83
		$errorTypes = array();
84
		if (empty($tried_hook))
85
		{
86
			// This prevents us from infinite looping if the hook or call produces an error.
87
			$tried_hook = true;
88
			call_integration_hook('integrate_error_types', array(&$errorTypes));
89
			$this->errorTypes += $errorTypes;
90
		}
91
92
		return $this->errorTypes;
93
	}
94
95
	/**
96
	 * @return string
97
	 */
98
	private function parseQueryString()
99
	{
100
		global $scripturl;
101
102
		$query_string = empty($_SERVER['QUERY_STRING']) ? (empty($_SERVER['REQUEST_URL']) ? '' : str_replace($scripturl, '', $_SERVER['REQUEST_URL'])) : $_SERVER['QUERY_STRING'];
103
104
		// Don't log the session hash in the url twice, it's a waste.
105
		$query_string = htmlspecialchars((ELK === 'SSI' ? '' : '?') . preg_replace(array('~;sesc=[^&;]+~', '~' . session_name() . '=' . session_id() . '[&;]~'), array(';sesc', ''), $query_string), ENT_COMPAT, 'UTF-8');
106
107
		// Just so we know what board error messages are from.
108
		if (isset($_POST['board']) && !isset($_GET['board']))
109
			$query_string .= ($query_string == '' ? 'board=' : ';board=') . $_POST['board'];
110
111
		return $query_string;
112
	}
113
114
	/**
115
	 * Insert an error entry in to the log_errors table
116
	 *
117
	 * @param string $query_string
118
	 * @param string $error_message
119
	 * @param string $error_type
120
	 * @param string $file
121
	 * @param int $line
122
	 */
123
	private function insertLog($query_string, $error_message, $error_type, $file, $line)
124
	{
125
		global $user_info, $last_error;
126
127
		$this->_db = database();
128
129
		// Just in case there's no id_member or IP set yet.
130
		if (empty($user_info['id']))
131
			$user_info['id'] = 0;
132
		if (empty($user_info['ip']))
133
			$user_info['ip'] = '';
134
135
		// Don't log the same error countless times, as we can get in a cycle of depression...
136
		$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);
137
		if (empty($last_error) || $last_error != $error_info)
138
		{
139
			// Insert the error into the database.
140
			$this->_db->insert('',
141
				'{db_prefix}log_errors',
142
				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'),
143
				$error_info,
144
				array('id_error')
145
			);
146
			$last_error = $error_info;
147
		}
148
	}
149
150
	/**
151
	 * Log an error to the error log if the error logging is enabled.
152
	 *
153
	 * - filename and line should be __FILE__ and __LINE__, respectively.
154
	 *
155
	 * Example use:
156
	 *   - die(Errors::instance()->log_error($msg));
157
	 *
158
	 * @param string $error_message
159
	 * @param string|boolean $error_type = 'general'
160
	 * @param string $file = ''
161
	 * @param int $line = 0
162
	 *
163
	 * @return string
164
	 */
165
	public function log_error($error_message, $error_type = 'general', $file = '', $line = 00)
166
	{
167
		// Check if error logging is actually on.
168
		if (empty($this->_modSettings['enableErrorLogging']))
169
			return $error_message;
170
171
		// Basically, htmlspecialchars it minus &. (for entities!)
172
		$error_message = strtr($error_message, array('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));
173
		$error_message = strtr($error_message, array('&lt;br /&gt;' => '<br />', '&lt;b&gt;' => '<strong>', '&lt;/b&gt;' => '</strong>', "\n" => '<br />'));
174
175
		// Add a file and line to the error message?
176
		// Don't use the actual txt entries for file and line but instead use %1$s for file and %2$s for line
177
		// Windows-style slashes don't play well, lets convert them to the unix style.
178
		$file = str_replace('\\', '/', $file);
179
		$line = (int) $line;
180
181
		// Find the best query string we can...
182
		$query_string = $this->parseQueryString();
183
184
		// Make sure the category that was specified is a valid one
185
		$error_type = in_array($error_type, $this->getErrorTypes()) && $error_type !== true ? $error_type : 'general';
186
187
		// Insert the error into the database.
188
		$this->insertLog($query_string, $error_message, $error_type, $file, $line);
0 ignored issues
show
Bug introduced by
It seems like $error_type defined by in_array($error_type, $t...$error_type : 'general' on line 185 can also be of type boolean; however, ElkArte\Errors\Errors::insertLog() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
189
190
		// Return the message to make things simpler.
191
		return $error_message;
192
	}
193
194
	/**
195
	 * Similar to log_error, it accepts a language index as the error.
196
	 *
197
	 * What it does:
198
	 *
199
	 * - Takes care of loading the forum default language
200
	 * - Logs the error (forwarding to log_error)
201
	 *
202
	 * @param string $error
203
	 * @param string $error_type = 'general'
204
	 * @param string|mixed[] $sprintf = array()
205
	 * @param string $file = ''
206
	 * @param int $line = 0
207
	 *
208
	 * @return string
209
	 */
210
	public function log_lang_error($error, $error_type = 'general', $sprintf = array(), $file = '', $line = 0)
211
	{
212
		global $user_info, $language, $txt;
213
214
		loadLanguage('Errors', $language);
215
216
		$reload_lang_file = $language != $user_info['language'];
217
218
		$error_message = !isset($txt[$error]) ? $error : (empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf));
219
		$this->log_error($error_message, $error_type, $file, $line);
220
221
		// Load the language file, only if it needs to be reloaded
222
		if ($reload_lang_file)
223
			loadLanguage('Errors');
224
225
		// Return the message to make things simpler.
226
		return $error_message;
227
	}
228
229
	/**
230
	 * An irrecoverable error.
231
	 *
232
	 * What it does:
233
	 *
234
	 * - This function stops execution and displays an error message.
235
	 * - It logs the error message if $log is specified.
236
	 *
237
	 * @param string         $error
238
	 * @param string|boolean $log defaults to 'general' false will skip logging, true will use general
239
	 *
240
	 * @throws \Elk_Exception
241
	 */
242
	public function fatal_error($error = '', $log = 'general')
243
	{
244
		throw new \Elk_Exception($error, $log);
245
	}
246
247
	/**
248
	 * Shows a fatal error with a message stored in the language file.
249
	 *
250
	 * What it does:
251
	 *
252
	 * - This function stops execution and displays an error message by key.
253
	 * - uses the string with the error_message_key key.
254
	 * - logs the error in the forum's default language while displaying the error
255
	 * message in the user's language.
256
	 * - uses Errors language file and applies the $sprintf information if specified.
257
	 * - the information is logged if log is specified.
258
	 *
259
	 * @param string         $error
260
	 * @param string|boolean $log defaults to 'general' false will skip logging, true will use general
261
	 * @param string[]       $sprintf defaults to empty array()
262
	 *
263
	 * @throws \Elk_Exception
264
	 */
265
	public function fatal_lang_error($error, $log = 'general', $sprintf = array())
266
	{
267
		throw new \Elk_Exception($error, $log, $sprintf);
268
	}
269
270
	/**
271
	 * It is called by Errors::fatal_error() and Errors::fatal_lang_error().
272
	 *
273
	 * @uses Errors template, fatal_error sub template
274
	 * @param string $error_message
275
	 * @param string $error_code string or int code
276
	 * @throws Elk_Exception
277
	 */
278
	final protected function _setup_fatal_ErrorContext($error_message, $error_code)
279
	{
280
		global $context, $txt, $ssi_on_error_method;
281
		static $level = 0;
282
283
		// Attempt to prevent a recursive loop.
284
		++$level;
285
		if ($level > 1)
286
			return false;
287
288
		// Maybe they came from dlattach or similar?
289
		if (ELK !== 'SSI' && empty($context['theme_loaded']))
290
			loadTheme();
291
292
		// Don't bother indexing errors mate...
293
		$context['robot_no_index'] = true;
294
295
		// A little something for the template
296
		$context['error_title'] = isset($context['error_title']) ? $context['error_title'] : $txt['error_occurred'];
297
		$context['error_message'] = isset($context['error_message']) ? $context['error_message'] : $error_message;
298
		$context['error_code'] = isset($error_code) ? 'id="' . htmlspecialchars($error_code) . '" ' : '';
299
		$context['page_title'] = empty($context['page_title']) ? $context['error_title'] : $context['page_title'];
300
301
		// Load the template and set the sub template.
302
		loadTemplate('Errors');
303
		$context['sub_template'] = 'fatal_error';
304
305
		if (class_exists('Template_Layers'))
306
			\Template_Layers::getInstance()->isError();
307
308
		// If this is SSI, what do they want us to do?
309
		if (ELK === 'SSI')
310
		{
311
			if (!empty($ssi_on_error_method) && $ssi_on_error_method !== true && is_callable($ssi_on_error_method))
312
				call_user_func($ssi_on_error_method);
313
			elseif (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
314
				loadSubTemplate('fatal_error');
315
316
			// No layers?
317
			if (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
318
				$this->terminate();
319
		}
320
321
		// We want whatever for the header, and a footer. (footer includes sub template!)
322
		obExit(null, true, false, true);
323
324
		/* DO NOT IGNORE:
325
			If you are creating a bridge or modifying this function, you MUST
326
			make ABSOLUTELY SURE that this function quits and DOES NOT RETURN TO NORMAL
327
			PROGRAM FLOW.  Otherwise, security error messages will not be shown, and
328
			your forum will be in a very easily hackable state.
329
		*/
330
		trigger_error('Hacking attempt...', E_USER_ERROR);
331
	}
332
333
	/**
334
	 * Show a message for the (full block) maintenance mode.
335
	 *
336
	 * What it does:
337
	 *
338
	 * - It shows a complete page independent of language files or themes.
339
	 * - It is used only if $maintenance = 2 in Settings.php.
340
	 * - It stops further execution of the script.
341
	 */
342
	public function display_maintenance_message()
343
	{
344
		global $maintenance, $mtitle, $mmessage;
345
346
		$this->_set_fatal_error_headers();
347
348
		if (!empty($maintenance))
349
			echo '<!DOCTYPE html>
350
	<html>
351
		<head>
352
			<meta name="robots" content="noindex" />
353
			<title>', $mtitle, '</title>
354
		</head>
355
		<body>
356
			<h3>', $mtitle, '</h3>
357
			', $mmessage, '
358
		</body>
359
	</html>';
360
361
		$this->terminate();
362
	}
363
364
	/**
365
	 * Show an error message for the connection problems.
366
	 *
367
	 * What it does:
368
	 *
369
	 * - It shows a complete page independent of language files or themes.
370
	 * - It is used only if there's no way to connect to the database.
371
	 * - It stops further execution of the script.
372
	 */
373
	public function display_db_error()
374
	{
375
		global $mbname, $maintenance, $webmaster_email, $db_error_send;
376
377
		$cache = \Cache::instance();
378
379
		// Just check we're not in any buffers, just in case.
380
		while (ob_get_level() > 0)
381
		{
382
			@ob_end_clean();
383
		}
384
385
		// Set the output headers
386
		$this->_set_fatal_error_headers();
387
388
		$db_last_error = db_last_error();
389
390
		$temp = '';
391
		if ($cache->getVar($temp, 'db_last_error', 600))
392
			$db_last_error = max($db_last_error, $temp);
393
394
		// Perhaps we want to notify by mail that there was a this->_db error
395
		if ($db_last_error < time() - 3600 * 24 * 3 && empty($maintenance) && !empty($db_error_send))
396
		{
397
			// Try using shared memory if possible.
398
			$cache->put('db_last_error', time(), 600);
399
			if (!$cache->getVar($temp, 'db_last_error', 600))
400
				logLastDatabaseError();
401
402
			// Language files aren't loaded yet :'(
403
			$db_error = $this->_db->last_error($this->_db->connection());
404
			@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.');
405
		}
406
407
		// What to do?  Language files haven't and can't be loaded yet...
408
		echo '<!DOCTYPE html>
409
	<html>
410
		<head>
411
			<meta name="robots" content="noindex" />
412
			<title>Connection Problems</title>
413
		</head>
414
		<body>
415
			<h3>Connection Problems</h3>
416
			Sorry, we were unable to connect to the database.  This may be caused by the server being busy.  Please try again later.
417
		</body>
418
	</html>';
419
420
		$this->terminate();
421
	}
422
423
	/**
424
	 * Show an error message for load average blocking problems.
425
	 *
426
	 * What it does:
427
	 *
428
	 * - It shows a complete page independent of language files or themes.
429
	 * - It is used only if the load averages are too high to continue execution.
430
	 * - It stops further execution of the script.
431
	 */
432
	public function display_loadavg_error()
433
	{
434
		// If this is a load average problem, display an appropriate message (but we still don't have language files!)
435
		$this->_set_fatal_error_headers();
436
437
		echo '<!DOCTYPE html>
438
	<html>
439
		<head>
440
			<meta name="robots" content="noindex" />
441
			<title>Temporarily Unavailable</title>
442
		</head>
443
		<body>
444
			<h3>Temporarily Unavailable</h3>
445
			Due to high stress on the server the forum is temporarily unavailable.  Please try again later.
446
		</body>
447
	</html>';
448
449
		$this->terminate();
450
	}
451
452
	/**
453
	 * Small utility function for fatal error pages, sets the headers.
454
	 *
455
	 * - Used by display_db_error(), display_loadavg_error(),
456
	 * display_maintenance_message()
457
	 */
458
	private function _set_fatal_error_headers()
459
	{
460
		// Don't cache this page!
461
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
462
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
463
		header('Cache-Control: no-cache');
464
465
		// Send the right error codes.
466
		header('HTTP/1.1 503 Service Temporarily Unavailable');
467
		header('Status: 503 Service Temporarily Unavailable');
468
		header('Retry-After: 3600');
469
	}
470
471
	/**
472
	 * Retrieve the sole instance of this class.
473
	 *
474
	 * @return Errors
475
	 */
476
	public static function instance()
477
	{
478
		if (self::$_errors === null)
479
		{
480
			if (function_exists('database'))
481
			{
482
				self::$_errors = new self;
483
			}
484
			else
485
			{
486
				self::$_errors = new self(1);
487
			}
488
		}
489
490
		return self::$_errors;
491
	}
492
}
493