Passed
Push — development ( 4fadb5...e3a728 )
by Spuds
01:02 queued 21s
created

ManageErrors   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 155
c 0
b 0
f 0
dl 0
loc 344
ccs 0
cts 197
cp 0
rs 9.2
wmc 40

7 Methods

Rating   Name   Duplication   Size   Complexity  
F action_log() 0 99 17
A action_viewfile() 0 38 1
A _setupFiltering() 0 43 4
B _applyFilter() 0 29 7
A action_index() 0 24 5
A action_backtrace() 0 27 1
A _loadMemData() 0 18 5

How to fix   Complexity   

Complex Class

Complex classes like ManageErrors 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.

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 ManageErrors, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * The main purpose of this file is to show a list of all errors that were
5
 * logged on the forum and allow filtering and deleting them.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 Beta 1
15
 *
16
 */
17
18
namespace ElkArte\AdminController;
19
20
use ElkArte\AbstractController;
21
use ElkArte\Errors\Log;
22
use ElkArte\Languages\Txt;
23
use ElkArte\MembersList;
24
25
/**
26
 * ManageErrors controller, administration of error log.
27
 */
28
class ManageErrors extends AbstractController
29
{
30
	/** @var Log */
31
	private Log $errorLog;
32
33
	/**
34
	 * Calls the right handler.
35
	 * Requires admin_forum permission.
36
	 *
37
	 * @see AbstractController::action_index()
38
	 */
39
	public function action_index()
40
	{
41
		// Check for the administrative permission to do this.
42
		isAllowedTo('admin_forum');
43
44
		$this->errorLog = new Log(database());
45
46
		// The error log. View the list, view a file or backtrace?
47
		$activity = $this->_req->getQuery('activity', 'strval');
48
49
		if (isset($activity) && $activity === 'file')
50
		{
51
			// View the file with the error
52
			$this->action_viewfile();
53
		}
54
		elseif (isset($activity) && $activity === 'backtrace')
55
		{
56
			// View the error backtrace
57
		 	$this->action_backtrace();
58
		}
59
		else
60
		{
61
			// View error log
62
			$this->action_log();
63
		}
64
	}
65
66
	/**
67
	 * View a file specified in $_REQUEST['file'], with php highlighting on it
68
	 *
69
	 * What it does:
70
	 *  - File must be readable,
71
	 *  - Full file path must be base64 encoded,
72
	 *  - The line number is specified by $_REQUEST['line']...
73
	 *  - The function will try to get the 20 lines before and after the specified line.
74
	 */
75
	protected function action_viewfile(): void
76
	{
77
		global $context;
78
79
		$err = $this->_req->getQuery('err', 'intval');
80
		$error_details = $this->errorLog->getErrorLogData(
81
			0,
82
			'down',
83
			[
84
				'variable' => 'id_error',
85
				'value' => [
86
					'sql' => $err,
87
				],
88
			]
89
		)['errors'][$err]['file'];
90
91
		$data = iterator_to_array(
92
			theme()->getTemplates()->getHighlightedLinesFromFile(
93
				$error_details['file'],
94
				max($error_details['line'] - 16 - 9, 1),
95
				min($error_details['line'] + 21, count(file($error_details['file'])) + 1)
96
			)
97
		);
98
99
		// Mark the offending line.
100
		$data[$error_details['line']] = sprintf(
101
			'<div class="curline">%s</div>',
102
			$data[$error_details['line']]
103
		);
104
105
		$context['file_data'] = [
106
			'contents' => $data,
107
			'file' => strtr($error_details['file'], ['"' => '\\"']),
108
		];
109
110
		theme()->getTemplates()->load('Errors');
111
		theme()->getLayers()->removeAll();
112
		$context['sub_template'] = 'show_file';
113
	}
114
115
	/**
116
	 * View a debug backtrace
117
	 *
118
	 * What it does:
119
	 *  - Backtrace must exist,
120
	 *  - The error is specified by $_GET['err']...
121
	 */
122
	protected function action_backtrace(): void
123
	{
124
		global $context;
125
126
		Txt::load('Maintenance');
127
		$err = $this->_req->getQuery('err', 'intval');
128
		$error_details = $this->errorLog->getErrorLogData(
129
			0,
130
			'down',
131
			[
132
				'variable' => 'id_error',
133
				'value' => [
134
					'sql' => $err,
135
				],
136
			]
137
		)['errors'][$err]['backtrace'];
138
139
		$context['backtrace_data'] = [
140
			'contents' => json_decode($error_details['backtrace']),
141
			'file' => strtr($error_details['file'], ['"' => '\\"']),
142
			'line' => $error_details['line'],
143
			'message' => $error_details['message'] ?? ''
144
		];
145
146
		theme()->getTemplates()->load('Errors');
147
		theme()->getLayers()->removeAll();
148
		$context['sub_template'] = 'show_backtrace';
149
	}
150
151
	/**
152
	 * View the forum's error log.
153
	 *
154
	 * What it does:
155
	 *
156
	 * - This method sets all the context up to show the error log for maintenance.
157
	 * - It requires the admin_forum permission.
158
	 * - It is accessed from ?action=admin;area=logs;sa=errorlog.
159
	 *
160
	 * @uses the Errors template and error_log sub template.
161
	 */
162
	protected function action_log(): void
163
	{
164
		global $txt, $context, $modSettings;
165
166
		// Templates, etc...
167
		Txt::load('Maintenance');
168
		theme()->getTemplates()->load('Errors');
169
170
		// Set up any filters chosen
171
		$filter = $this->_setupFiltering();
172
173
		// Deleting, are we?
174
		$type = isset($this->_req->post->delall) ? 'delall' : (isset($this->_req->post->delete) ? 'delete' : false);
175
		if ($type !== false)
176
		{
177
			// Make sure the session exists and is correct; otherwise, might be a hacker.
178
			checkSession();
179
			validateToken('admin-el');
180
181
			$error_list = $this->_req->getPost('delete');
182
			$this->errorLog->deleteErrors($type, $filter, $error_list);
183
184
			// Go back to where we were.
185
			if ($type === 'delete')
186
			{
187
				redirectexit('action=admin;area=logs;sa=errorlog' . (isset($this->_req->query->desc) ? ';desc' : '') . ';start=' . $this->_req->query->start . (empty($filter) ? '' : ';filter=' . $this->_req->query->filter . ';value=' . $this->_req->query->value));
188
			}
189
190
			redirectexit('action=admin;area=logs;sa=errorlog' . (isset($this->_req->query->desc) ? ';desc' : ''));
191
		}
192
193
		$num_errors = $this->errorLog->numErrors($filter);
194
		$members = [];
195
196
		// If this filter is empty...
197
		if ($num_errors === 0 && !empty($filter))
198
		{
199
			redirectexit('action=admin;area=logs;sa=errorlog' . (isset($this->_req->query->desc) ? ';desc' : ''));
200
		}
201
202
		// Clean up start.
203
		$start = max($this->_req->getQuery('start', 'intval', 0), 0);
204
205
		// Do we want to reverse the error listing?
206
		$context['sort_direction'] = isset($this->_req->query->desc) ? 'down' : 'up';
207
208
		// How about filter it?
209
		$page_filter = isset($filter['href']) ? ';filter=' . $filter['href']['filter'] . ';value=' . $filter['href']['value'] : '';
210
211
		// Set the page listing up.
212
		$context['page_index'] = constructPageIndex('{scripturl}?action=admin;area=logs;sa=errorlog' . ($context['sort_direction'] === 'down' ? ';desc' : '') . $page_filter, $start, $num_errors, $modSettings['defaultMaxMessages']);
213
		$context['start'] = $start;
214
		$context['$page_filter'] = $page_filter;
215
		$context['errors'] = [];
216
217
		$logdata = $this->errorLog->getErrorLogData($start, $context['sort_direction'], $filter);
218
		if (!empty($logdata))
219
		{
220
			$context['errors'] = $logdata['errors'];
221
			$members = $logdata['members'];
222
		}
223
224
		// Load the member data.
225
		$this->_loadMemData($members);
226
227
		// Filtering anything?
228
		$this->_applyFilter($filter);
229
230
		// What type of errors do we have and how many do we have?
231
		$context['error_types'] = $this->errorLog->fetchErrorsByType($filter, $context['sort_direction']);
232
		$tmp = array_keys($context['error_types']);
233
		$sum = (int) end($tmp);
234
235
		$context['error_types']['all'] = [
236
			'label' => $txt['errortype_all'],
237
			'description' => $txt['errortype_all_desc'] ?? '',
238
			'url' => getUrl('admin', ['action' => 'admin', 'area' => 'logs', 'sa' => 'errorlog', $context['sort_direction'] === 'down' ? 'desc' : '']),
239
			'is_selected' => empty($filter),
240
		];
241
242
		// Update the all errors tab with the total number of errors
243
		$context['error_types']['all']['label'] .= ' (' . $sum . ')';
244
245
		// Finally, work out what the last tab is!
246
		if (isset($context['error_types'][$sum]))
247
		{
248
			$context['error_types'][$sum]['is_last'] = true;
249
		}
250
		else
251
		{
252
			$context['error_types']['all']['is_last'] = true;
253
		}
254
255
		// And this is pretty basic ;).
256
		$context['page_title'] = $txt['errlog'];
257
		$context['has_filter'] = !empty($filter);
258
		$context['sub_template'] = 'error_log';
259
260
		createToken('admin-el');
261
	}
262
263
	/**
264
	 * Set up any filtering the user may have selected
265
	 */
266
	private function _setupFiltering(): array
267
	{
268
		global $txt;
269
270
		// We'll escape some strings...
271
		$db = database();
272
273
		// You can filter by any of the following columns:
274
		$filters = [
275
			'id_member' => $txt['username'],
276
			'ip' => $txt['ip_address'],
277
			'session' => $txt['session'],
278
			'url' => $txt['error_url'],
279
			'message' => $txt['error_message'],
280
			'error_type' => $txt['error_type'],
281
			'file' => $txt['file'],
282
			'line' => $txt['line'],
283
		];
284
285
		// Set up the filtering...
286
		$filter = $this->_req->getQuery('filter', 'trim');
287
		$value = $this->_req->getQuery('value', 'trim');
288
		if (isset($value, $filters[$filter]))
289
		{
290
			return [
291
				'variable' => $filter,
292
				'value' => [
293
					'sql' => in_array($filter, ['message', 'url', 'file'])
294
						? base64_decode(strtr($value, [' ' => '+']))
295
						: $db->escape_wildcard_string($value),
296
				],
297
				'href' => ['filter' => $filter, 'value' => $value],
298
				'entity' => $filters[$filter]
299
			];
300
		}
301
302
		if (isset($filter, $value))
303
		{
304
			$this->_req->clearValue('filter', 'query');
305
			$this->_req->clearValue('value', 'query');
306
		}
307
308
		return [];
309
	}
310
311
	/**
312
	 * Load basic member information for log viewing
313
	 *
314
	 * @param int[] $members
315
	 */
316
	private function _loadMemData(array $members): void
317
	{
318
		global $context, $txt;
319
320
		// Load the member data.
321
		if (!empty($members))
322
		{
323
			require_once(SUBSDIR . '/Members.subs.php');
324
			$members = getBasicMemberData($members, ['add_guest' => true]);
325
326
			// Go through each error and tac the data on.
327
			foreach ($context['errors'] as $id => $dummy)
328
			{
329
				$memID = $context['errors'][$id]['member']['id'];
330
				$context['errors'][$id]['member']['username'] = $members[$memID]['member_name'];
331
				$context['errors'][$id]['member']['name'] = $members[$memID]['real_name'];
332
				$context['errors'][$id]['member']['href'] = empty($memID) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $memID, 'name' => $members[$memID]['real_name']]);
333
				$context['errors'][$id]['member']['link'] = empty($memID) ? $txt['guest_title'] : '<a href="' . $context['errors'][$id]['member']['href'] . '">' . $context['errors'][$id]['member']['name'] . '</a>';
334
			}
335
		}
336
	}
337
338
	/**
339
	 * Applys the filter to the template
340
	 *
341
	 * @param array $filter
342
	 */
343
	private function _applyFilter(array $filter): void
344
	{
345
		global $context, $scripturl;
346
347
		if (isset($filter['variable']))
348
		{
349
			$context['filter'] = &$filter;
350
351
			// Set the filtering context.
352
			switch ($filter['variable'])
353
			{
354
				case 'id_member':
355
					$id = $filter['value']['sql'];
356
					MembersList::load($id, false, 'minimal');
357
					$name = MembersList::get($id)->real_name;
0 ignored issues
show
Bug Best Practice introduced by
The property real_name does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
358
					$context['filter']['value']['html'] = '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $id, 'name' => $name]) . '">' . $name . '</a>';
359
					break;
360
				case 'url':
361
					$context['filter']['value']['html'] = "'" . strtr(htmlspecialchars((str_starts_with($filter['value']['sql'], '?') ? $scripturl : '') . $filter['value']['sql'], ENT_COMPAT, 'UTF-8'), ['\_' => '_']) . "'";
362
					break;
363
				case 'message':
364
					$context['filter']['value']['html'] = "'" . strtr(htmlspecialchars($filter['value']['sql'], ENT_COMPAT, 'UTF-8'), ["\n" => '<br />', '&lt;br /&gt;' => '<br />', "\t" => '&nbsp;&nbsp;&nbsp;', '\_' => '_', '\\%' => '%', '\\\\' => '\\']) . "'";
365
					$context['filter']['value']['html'] = preg_replace('~&amp;lt;span class=&amp;quot;remove&amp;quot;&amp;gt;(.+?)&amp;lt;/span&amp;gt;~', '$1', $context['filter']['value']['html']);
366
					break;
367
				case 'error_type':
368
					$context['filter']['value']['html'] = "'" . strtr(htmlspecialchars($filter['value']['sql'], ENT_COMPAT, 'UTF-8'), ["\n" => '<br />', '&lt;br /&gt;' => '<br />', "\t" => '&nbsp;&nbsp;&nbsp;', '\_' => '_', '\\%' => '%', '\\\\' => '\\']) . "'";
369
					break;
370
				default:
371
					$context['filter']['value']['html'] = &$filter['value']['sql'];
372
			}
373
		}
374
	}
375
}
376