Passed
Push — development ( f371cf...087131 )
by Spuds
08:46 queued 19s
created

topPosters()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 59
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 27
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 59
rs 9.1768
ccs 0
cts 42
cp 0
crap 30

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is holds low-level database work used by the Stats.
5
 * Some functions/queries (or all :P) might be duplicate, along Elk.
6
 * They'll be here to avoid including many files in action_stats, and
7
 * perhaps for use of addons in a similar way they were using some
8
 * SSI functions.
9
 *
10
 * @package   ElkArte Forum
11
 * @copyright ElkArte Forum contributors
12
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
13
 *
14
 * This file contains code covered by:
15
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
16
 *
17
 * @version 2.0 dev
18
 *
19
 */
20
21
use ElkArte\Cache\Cache;
22
use ElkArte\MembersList;
23
use ElkArte\User;
24
25
/**
26
 * Return the number of currently online members.
27
 *
28
 * @return double
29
 */
30
function onlineCount()
31
{
32
	$db = database();
33
34
	$result = $db->query('', '
35
		SELECT 
36
			COUNT(*)
37
		FROM {db_prefix}log_online',
38
		[]
39
	);
40
	list ($users_online) = $result->fetch_row();
41
	$result->free_result();
42
43
	return $users_online;
44
}
45
46
/**
47
 * Gets totals for posts, topics, most online, new users, page views, emails
48
 *
49
 * - Can be used (and is) with days up value to generate averages.
50
 *
51
 * @return array
52
 */
53
function getAverages()
54
{
55
	$db = database();
56
57
	$result = $db->query('', '
58
		SELECT
59
			SUM(posts) AS posts, SUM(topics) AS topics, SUM(registers) AS registers,
60
			SUM(most_on) AS most_on, MIN(date) AS date, SUM(hits) AS hits, SUM(email) AS email
61
		FROM {db_prefix}log_activity',
62
		[]
63
	);
64
	$row = $result->fetch_assoc();
65
	$result->free_result();
66
67
	return $row;
68
}
69
70
/**
71
 * Get the count of categories
72
 *
73
 * @return int
74
 */
75
function numCategories()
76
{
77
	$db = database();
78
79
	$result = $db->query('', '
80
		SELECT 
81
			COUNT(*)
82
		FROM {db_prefix}categories',
83
		[]
84
	);
85
	list ($num_categories) = $result->fetch_row();
86
	$result->free_result();
87
88
	return $num_categories;
89
}
90
91
/**
92
 * Gets most online members for a specific date
93
 *
94
 * @param int $date
95
 * @return int
96
 */
97
function mostOnline($date)
98
{
99
	$db = database();
100
101
	$result = $db->query('', '
102
		SELECT 
103
			most_on
104
		FROM {db_prefix}log_activity
105
		WHERE date = {date:given_date}
106
		LIMIT 1',
107
		[
108
			'given_date' => $date,
109
		]
110
	);
111
	list ($online) = $result->fetch_row();
112
	$result->free_result();
113
114
	return (int) $online;
115
}
116
117
/**
118
 * Loads a list of top x posters
119
 *
120
 * - x is configurable via $modSettings['stats_limit'].
121
 *
122
 * @param int|null $limit if empty defaults to 10
123
 * @return array
124
 */
125
function topPosters($limit = null)
126
{
127
	global $modSettings;
128
129
	$db = database();
130
131
	// If there is a default setting, let's not retrieve something bigger
132
	if (isset($modSettings['stats_limit']))
133
	{
134
		$limit = $limit === null ? $modSettings['stats_limit'] : (min($limit, $modSettings['stats_limit']));
135
	}
136
	// Otherwise, fingers crossed and let's grab what is asked
137
	else
138
	{
139
		$limit = $limit ?? 10;
140
	}
141
142
	// Make the query to the x number of top posters
143
	$top_posters = [];
144
	$max_num_posts = 1;
145
	$db->fetchQuery('
146
		SELECT 
147
			id_member, real_name, posts
148
		FROM {db_prefix}members
149
		WHERE posts > {int:no_posts}
150
		ORDER BY posts DESC
151
		LIMIT {int:limit_posts}',
152
		[
153
			'no_posts' => 0,
154
			'limit_posts' => $limit,
155
		]
156
	)->fetch_callback(
157
		function ($row) use (&$top_posters, &$max_num_posts) {
158
			$href = getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['real_name']]);
159
160
			// Build general member information for each top poster
161
			$top_posters[] = [
162
				'name' => $row['real_name'],
163
				'id' => $row['id_member'],
164
				'num_posts' => $row['posts'],
165
				'href' => $href,
166
				'link' => '<a href="' . $href . '">' . $row['real_name'] . '</a>'
167
			];
168
			if ($max_num_posts < $row['posts'])
169
			{
170
				$max_num_posts = $row['posts'];
171
			}
172
		}
173
	);
174
175
	// Determine the percents and then format the num_posts
176
	foreach ($top_posters as $i => $poster)
177
	{
178
		$top_posters[$i]['post_percent'] = round(($poster['num_posts'] * 100) / $max_num_posts);
179
		$top_posters[$i]['percent'] = round(($poster['num_posts'] * 100) / $max_num_posts);
180
		$top_posters[$i]['num_posts'] = comma_format($top_posters[$i]['num_posts']);
181
	}
182
183
	return $top_posters;
184
}
185
186
/**
187
 * Loads a list of top x boards with number of board posts and board topics
188
 *
189
 * - x is configurable via $modSettings['stats_limit'].
190
 *
191
 * @param int|null $limit if not supplied, defaults to 10
192
 * @param bool $read_status
193
 * @return array
194
 */
195
function topBoards($limit = null, $read_status = false)
196
{
197
	global $modSettings;
198
199
	$db = database();
200
201
	// If there is a default setting, let's not retrieve something bigger
202
	if (isset($modSettings['stats_limit']))
203
	{
204
		$limit = $limit === null ? $modSettings['stats_limit'] : (min($limit, $modSettings['stats_limit']));
205
	}
206
	// Otherwise, fingers crossed and let's grab what is asked
207
	else
208
	{
209
		$limit = $limit ?? 10;
210
	}
211
212
	$top_boards = [];
213
	$max_num_posts = 1;
214
	$db->fetchQuery('
215
		SELECT 
216
			b.id_board, b.name, b.num_posts, b.num_topics' . ($read_status ? ',' . (User::$info->is_guest === false ? ' 1 AS is_read' : '
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
217
			(COALESCE(lb.id_msg, 0) >= b.id_last_msg) AS is_read') : '') . '
218
		FROM {db_prefix}boards AS b' . ($read_status ? '
219
			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})' : '') . '
220
		WHERE {query_see_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
221
			AND b.id_board != {int:recycle_board}' : '') . '
222
			AND b.redirect = {string:blank_redirect}
223
		ORDER BY num_posts DESC
224
		LIMIT {int:limit_boards}',
225
		[
226
			'recycle_board' => $modSettings['recycle_board'],
227
			'blank_redirect' => '',
228
			'limit_boards' => $limit,
229
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
230
		]
231
	)->fetch_callback(
232
		function ($row) use (&$top_boards, &$max_num_posts, $read_status) {
233
			$href = getUrl('board', ['board' => $row['id_board'], 'start' => '0', 'name' => $row['name']]);
234
235
			// Load the boards info, number of posts, topics etc
236
			$top_boards[$row['id_board']] = [
237
				'id' => $row['id_board'],
238
				'name' => $row['name'],
239
				'num_posts' => $row['num_posts'],
240
				'num_topics' => $row['num_topics'],
241
				'href' => $href,
242
				'link' => '<a href="' . $href . '">' . $row['name'] . '</a>'
243
			];
244
			if ($read_status)
245
			{
246
				$top_boards[$row['id_board']]['is_read'] = !empty($row['is_read']);
247
			}
248
249
			if ($max_num_posts < $row['num_posts'])
250
			{
251
				$max_num_posts = $row['num_posts'];
252
			}
253
		}
254
	);
255
256
	// Determine the post percentages for the boards, then format the numbers
257
	foreach ($top_boards as $i => $board)
258
	{
259
		$top_boards[$i]['post_percent'] = round(($board['num_posts'] * 100) / $max_num_posts);
260
		$top_boards[$i]['percent'] = round(($board['num_posts'] * 100) / $max_num_posts);
261
		$top_boards[$i]['num_posts'] = comma_format($top_boards[$i]['num_posts']);
262
		$top_boards[$i]['num_topics'] = comma_format($top_boards[$i]['num_topics']);
263
	}
264
265
	return $top_boards;
266
}
267
268
/**
269
 * Loads a list of top x topics by replies
270
 *
271
 * - x is configurable via $modSettings['stats_limit'].
272
 *
273
 * @param int $limit if not supplied, defaults to 10
274
 * @return array
275
 */
276
function topTopicReplies($limit = 10)
277
{
278
	global $modSettings;
279
280
	$db = database();
281
282
	// If there is a default setting, let's not retrieve something bigger
283
	$limit = min($limit, $modSettings['stats_limit'] ?? 10);
284
285
	// Must include boards so {query_see_board} can actually resolve b.member_groups
286
	$topic_ids = [];
287
	$db->fetchQuery('
288
		SELECT 
289
			id_topic, num_replies
290
		FROM {db_prefix}topics AS t
291
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
292
		WHERE {query_see_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
293
			AND b.id_board != {int:recycle_board}' : '') . '
294
			AND num_replies != {int:no_replies}' . ($modSettings['postmod_active'] ? '
295
			AND approved = {int:is_approved}' : ''),
296
		[
297
			'no_replies' => 0,
298
			'is_approved' => 1,
299
			'recycle_board' => $modSettings['recycle_board'],
300
		]
301
	)->fetch_callback(
302
		function ($row) use (&$topic_ids) {
303
			$topic_ids[$row['id_topic']] = $row['num_replies'];
304
		}
305
	);
306
307
	arsort($topic_ids);
308
	$topic_ids = array_slice($topic_ids, 0, $limit, true);
309
	$topic_ids = empty($topic_ids) ? [0 => 0] : $topic_ids;
310
	$max_num_replies = max($topic_ids);
311
312
	// Find the top x topics by number of replies
313
	$top_topics_replies = [];
314
	$db->fetchQuery('
315
		SELECT 
316
			m.subject, t.num_replies, t.num_views, t.id_board, t.id_topic
317
		FROM {db_prefix}topics AS t
318
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
319
		WHERE t.id_topic IN ({array_int:topic_list})',
320
		[
321
			'topic_list' => array_keys($topic_ids),
322
		]
323
	)->fetch_callback(
324
		function ($row) use (&$top_topics_replies, &$max_num_replies) {
325
			$href = getUrl('topic', ['topic' => $row['id_topic'], 'start' => '0', 'subject' => $row['subject']]);
326
327
			// Build out this topics details for controller use
328
			$top_topics_replies[$row['id_topic']] = [
329
				'id' => $row['id_topic'],
330
				'subject' => censor($row['subject']),
331
				'num_replies' => comma_format($row['num_replies']),
332
				'post_percent' => round(($row['num_replies'] * 100) / $max_num_replies),
333
				'percent' => round(($row['num_replies'] * 100) / $max_num_replies),
334
				'num_views' => $row['num_views'],
335
				'href' => $href,
336
				'link' => '<a href="' . $href . '">' . $row['subject'] . '</a>'
337
			];
338
		}
339
	);
340
341
	// @todo dedupe this
342
	usort($top_topics_replies, static function ($a, $b) {
343
		return $b['num_replies'] <=> $a['num_replies'];
344
	});
345
346
	return $top_topics_replies;
347
}
348
349
/**
350
 * Loads a list of top x topics by number of views
351
 *
352
 * - x is configurable via $modSettings['stats_limit'].
353
 *
354
 * @param int|null $limit if not supplied, defaults to 10
355
 * @return array
356
 */
357
function topTopicViews($limit = null)
358
{
359
	global $modSettings;
360
361
	$db = database();
362
363
	// If there is a default setting, let's not retrieve something bigger
364
	if (isset($modSettings['stats_limit']))
365
	{
366
		$limit = $limit === null ? $modSettings['stats_limit'] : (min($limit, $modSettings['stats_limit']));
367
	}
368
	// Otherwise, fingers crossed and let's grab what is asked
369
	else
370
	{
371
		$limit = $limit ?? 10;
372
	}
373
374
	// Large forums may need a bit more prodding..
375
	$topic_ids = [];
376
	if ($modSettings['totalMessages'] > 100000)
377
	{
378
		$db->fetchQuery('
379
			SELECT 
380
				id_topic
381
			FROM {db_prefix}topics
382
			WHERE num_views != {int:no_views}
383
			ORDER BY num_views DESC
384
			LIMIT 100',
385
			[
386
				'no_views' => 0,
387
			]
388
		)->fetch_callback(
389
			function ($row) use (&$topic_ids) {
390
				$topic_ids[] = $row['id_topic'];
391
			}
392
		);
393
	}
394
395
	$top_topics_views = [];
396
	$max_num_views = 1;
397
	$db->fetchQuery('
398
		SELECT 
399
			m.subject, t.num_views, t.num_replies, t.id_board, t.id_topic, b.name
400
		FROM {db_prefix}topics AS t
401
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
402
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
403
			AND b.id_board != {int:recycle_board}' : '') . ')
404
		WHERE {query_see_board}' . (!empty($topic_ids) ? '
405
			AND t.id_topic IN ({array_int:topic_list})' : ($modSettings['postmod_active'] ? '
406
			AND t.approved = {int:is_approved}' : '')) . '
407
		ORDER BY t.num_views DESC
408
		LIMIT {int:topic_views}',
409
		[
410
			'topic_list' => $topic_ids,
411
			'recycle_board' => $modSettings['recycle_board'],
412
			'is_approved' => 1,
413
			'topic_views' => $limit,
414
		]
415
	)->fetch_callback(
416
		function ($row) use (&$top_topics_views, &$max_num_views) {
417
			$board_href = getUrl('board', ['board' => $row['id_board'], 'start' => '0', 'name' => $row['name']]);
418
			$topic_href = getUrl('topic', ['topic' => $row['id_topic'], 'start' => '0', 'subject' => $row['subject']]);
419
420
			// Build the topic result array
421
			$row['subject'] = censor($row['subject']);
422
			$top_topics_views[$row['id_topic']] = [
423
				'id' => $row['id_topic'],
424
				'board' => [
425
					'id' => $row['id_board'],
426
					'name' => $row['name'],
427
					'href' => $board_href,
428
					'link' => '<a href="' . $board_href . '">' . $row['name'] . '</a>'
429
				],
430
				'subject' => $row['subject'],
431
				'num_replies' => $row['num_replies'],
432
				'num_views' => $row['num_views'],
433
				'href' => $topic_href,
434
				'link' => '<a href="' . $topic_href . '">' . $row['subject'] . '</a>'
435
			];
436
437
			if ($max_num_views < $row['num_views'])
438
			{
439
				$max_num_views = $row['num_views'];
440
			}
441
		}
442
	);
443
444
	// Percentages and final formatting
445
	foreach ($top_topics_views as $i => $topic)
446
	{
447
		$top_topics_views[$i]['post_percent'] = round(($topic['num_views'] * 100) / $max_num_views);
448
		$top_topics_views[$i]['percent'] = round(($topic['num_views'] * 100) / $max_num_views);
449
		$top_topics_views[$i]['num_views'] = comma_format($top_topics_views[$i]['num_views']);
450
	}
451
452
	return $top_topics_views;
453
}
454
455
/**
456
 * Loads a list of top x topic starters
457
 *
458
 * - x is configurable via $modSettings['stats_limit'].
459
 *
460
 * @return array
461
 */
462
function topTopicStarter()
463
{
464
	global $modSettings;
465
466
	$db = database();
467
	$members = [];
468
	$top_starters = [];
469
470
	// Try to cache this when possible, because it's a little unavoidably slow.
471
	if (!Cache::instance()->getVar($members, 'stats_top_starters', 360) || empty($members))
472
	{
473
		$db->fetchQuery('
474
			SELECT 
475
				id_member_started, COUNT(*) AS hits
476
			FROM {db_prefix}topics' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
477
			WHERE id_board != {int:recycle_board}' : '') . '
478
			GROUP BY id_member_started',
479
			[
480
				'recycle_board' => $modSettings['recycle_board'],
481
			]
482
		)->fetch_callback(
483
			function ($row) use (&$members) {
484
				if ($row['id_member_started'] !== '0')
485
				{
486
					$members[(int) $row['id_member_started']] = (int) $row['hits'];
487
				}
488
			}
489
		);
490
491
		arsort($members);
492
		$members = array_slice($members, 0, $modSettings['stats_limit'] ?? 10, true);
493
494
		Cache::instance()->put('stats_top_starters', $members, 360);
495
	}
496
	$max_num_topics = max($members);
497
498
	if (empty($members))
499
	{
500
		$members = [0 => 0];
501
	}
502
503
	// Find the top starters of topics
504
	$db->fetchQuery('
505
		SELECT 
506
			id_member, real_name
507
		FROM {db_prefix}members
508
		WHERE id_member IN ({array_int:member_list})',
509
		[
510
			'member_list' => array_keys($members),
511
		]
512
	)->fetch_callback(
513
		function ($row) use (&$top_starters, $members, $max_num_topics) {
514
			$href = getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['real_name']]);
515
516
			// Our array of spammers, er topic starters !
517
			$top_starters[$row['id_member']] = [
518
				'name' => $row['real_name'],
519
				'id' => $row['id_member'],
520
				'num_topics' => comma_format($members[$row['id_member']]),
521
				'post_percent' => round(($members[$row['id_member']] * 100) / $max_num_topics),
522
				'percent' => round(($members[$row['id_member']] * 100) / $max_num_topics),
523
				'href' => $href,
524
				'link' => '<a href="' . $href . '">' . $row['real_name'] . '</a>'
525
			];
526
		}
527
	);
528
529
	// Even spammers must be orderly.
530
	uksort($top_starters, static function ($a, $b) use ($members) {
531
		return $members[$b] <=> $members[$a];
532
	});
533
534
	return $top_starters;
535
}
536
537
/**
538
 * Loads a list of top users by online time
539
 *
540
 * - x is configurable via $modSettings['stats_limit'], defaults to 10
541
 *
542
 * @return array
543
 */
544
function topTimeOnline()
545
{
546
	global $modSettings, $txt;
547
548
	$db = database();
549
550
	$max_members = $modSettings['stats_limit'] ?? 10;
551
552
	// Do we have something cached that will help speed this up?
553
	$temp = Cache::instance()->get('stats_total_time_members', 600);
554
555
	// Get the member data, sorted by total time logged in
556
	$result = $db->query('', '
557
		SELECT 
558
			id_member, real_name, total_time_logged_in
559
		FROM {db_prefix}members' . (!empty($temp) ? '
560
		WHERE id_member IN ({array_int:member_list_cached})' : '') . '
561
		ORDER BY total_time_logged_in DESC
562
		LIMIT {int:top_online}',
563
		[
564
			'member_list_cached' => $temp,
565
			'top_online' => $modSettings['stats_limit'] ?? 20,
566
		]
567
	);
568
	$top_time_online = [];
569
	$temp2 = [];
570
	$max_time_online = 1;
571
	while (($row = $result->fetch_assoc()))
572
	{
573
		$temp2[] = (int) $row['id_member'];
574
		if (count($top_time_online) >= $max_members)
575
		{
576
			continue;
577
		}
578
579
		// Figure out the days, hours and minutes.
580
		$timeDays = floor($row['total_time_logged_in'] / 86400);
581
		$timeHours = floor(($row['total_time_logged_in'] % 86400) / 3600);
582
583
		// Figure out which things to show... (days, hours, minutes, etc.)
584
		$timelogged = '';
585
		if ($timeDays > 0)
586
		{
587
			$timelogged .= $timeDays . $txt['totalTimeLogged5'];
588
		}
589
590
		if ($timeHours > 0)
591
		{
592
			$timelogged .= $timeHours . $txt['totalTimeLogged6'];
593
		}
594
595
		$timelogged .= floor(($row['total_time_logged_in'] % 3600) / 60) . $txt['totalTimeLogged7'];
596
597
		$href = getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $row['real_name']]);
598
599
		// Finally add it to the stats array
600
		$top_time_online[] = [
601
			'id' => $row['id_member'],
602
			'name' => $row['real_name'],
603
			'time_online' => $timelogged,
604
			'seconds_online' => $row['total_time_logged_in'],
605
			'href' => $href,
606
			'link' => '<a href="' . $href . '">' . $row['real_name'] . '</a>'
607
		];
608
609
		if ($max_time_online < $row['total_time_logged_in'])
610
		{
611
			$max_time_online = $row['total_time_logged_in'];
612
		}
613
	}
614
	$result->free_result();
615
616
	// As always percentages are next
617
	foreach ($top_time_online as $i => $member)
618
	{
619
		$top_time_online[$i]['time_percent'] = round(($member['seconds_online'] * 100) / $max_time_online);
620
		$top_time_online[$i]['percent'] = round(($member['seconds_online'] * 100) / $max_time_online);
621
	}
622
623
	// Cache the ones we found for a bit, just so we don't have to look again.
624
	if ($temp !== $temp2)
625
	{
626
		Cache::instance()->put('stats_total_time_members', $temp2, 600);
627
	}
628
629
	return $top_time_online;
630
}
631
632
/**
633
 * Loads the monthly statistics and returns them in $context
634
 *
635
 * - page views, new registrations, topics posts, most on etc
636
 *
637
 */
638
function monthlyActivity()
639
{
640
	global $context, $txt;
641
642
	$db = database();
643
644
	$result = $db->fetchQuery('
645
		SELECT
646
			YEAR(date) AS stats_year, MONTH(date) AS stats_month, SUM(hits) AS hits, SUM(registers) AS registers, SUM(topics) AS topics, SUM(posts) AS posts, MAX(most_on) AS most_on, COUNT(*) AS num_days
647
		FROM {db_prefix}log_activity
648
		GROUP BY stats_year, stats_month',
649
		[]
650
	);
651
	while (($row = $result->fetch_assoc()))
652
	{
653
		$id_month = $row['stats_year'] . sprintf('%02d', $row['stats_month']);
654
		$expanded = !empty($_SESSION['expanded_stats'][$row['stats_year']]) && in_array($row['stats_month'], $_SESSION['expanded_stats'][$row['stats_year']]);
655
656
		if (!isset($context['yearly'][$row['stats_year']]))
657
		{
658
			$context['yearly'][$row['stats_year']] = [
659
				'year' => $row['stats_year'],
660
				'new_topics' => 0,
661
				'new_posts' => 0,
662
				'new_members' => 0,
663
				'most_members_online' => 0,
664
				'hits' => 0,
665
				'num_months' => 0,
666
				'months' => [],
667
				'expanded' => false,
668
				'current_year' => $row['stats_year'] == date('Y'),
669
			];
670
		}
671
672
		$href = getUrl('action', ['action' => 'stats', ($expanded ? 'collapse' : 'expand') => $id_month]) . '#m' . $id_month;
673
		$context['yearly'][$row['stats_year']]['months'][(int) $row['stats_month']] = [
674
			'id' => $id_month,
675
			'date' => [
676
				'month' => sprintf('%02d', $row['stats_month']),
677
				'year' => $row['stats_year']
678
			],
679
			'href' => $href,
680
			'link' => '<a href="' . $href . '">' . $txt['months'][(int) $row['stats_month']] . ' ' . $row['stats_year'] . '</a>',
681
			'month' => $txt['months'][(int) $row['stats_month']],
682
			'year' => $row['stats_year'],
683
			'new_topics' => comma_format($row['topics']),
684
			'new_posts' => comma_format($row['posts']),
685
			'new_members' => comma_format($row['registers']),
686
			'most_members_online' => comma_format($row['most_on']),
687
			'hits' => comma_format($row['hits']),
688
			'num_days' => $row['num_days'],
689
			'days' => [],
690
			'expanded' => $expanded
691
		];
692
693
		$context['yearly'][$row['stats_year']]['new_topics'] += $row['topics'];
694
		$context['yearly'][$row['stats_year']]['new_posts'] += $row['posts'];
695
		$context['yearly'][$row['stats_year']]['new_members'] += $row['registers'];
696
		$context['yearly'][$row['stats_year']]['hits'] += $row['hits'];
697
		$context['yearly'][$row['stats_year']]['num_months']++;
698
		$context['yearly'][$row['stats_year']]['expanded'] |= $expanded;
699
		$context['yearly'][$row['stats_year']]['most_members_online'] = max($context['yearly'][$row['stats_year']]['most_members_online'], $row['most_on']);
700
	}
701
702
	krsort($context['yearly']);
703
}
704
705
/**
706
 * Loads the statistics on a daily basis in $context.
707
 *
708
 * - called by action_stats().
709
 *
710
 * @param string $condition_string
711
 * @param array $condition_parameters = array()
712
 */
713
function getDailyStats($condition_string, $condition_parameters = [])
714
{
715
	$db = database();
716
717
	// Activity by day.
718
	$db->fetchQuery('
719
		SELECT 
720
			YEAR(date) AS stats_year, MONTH(date) AS stats_month, DAYOFMONTH(date) AS stats_day, topics, posts, registers, most_on, hits
721
		FROM {db_prefix}log_activity
722
		WHERE ' . $condition_string . '
723
		ORDER BY stats_day DESC',
724
		$condition_parameters
725
	)->fetch_callback(
726
		function ($row) {
727
			global $context;
728
729
			$context['yearly'][$row['stats_year']]['months'][(int) $row['stats_month']]['days'][] = [
730
				'day' => sprintf('%02d', $row['stats_day']),
731
				'month' => sprintf('%02d', $row['stats_month']),
732
				'year' => $row['stats_year'],
733
				'new_topics' => comma_format($row['topics']),
734
				'new_posts' => comma_format($row['posts']),
735
				'new_members' => comma_format($row['registers']),
736
				'most_members_online' => comma_format($row['most_on']),
737
				'hits' => comma_format($row['hits'])
738
			];
739
		}
740
	);
741
}
742
743
/**
744
 * Returns the number of topics a user has started, including ones on boards
745
 * they may no longer have access on.
746
 *
747
 * - Does not count topics that are in the recycle board
748
 *
749
 * @param int $memID
750
 *
751
 * @return int
752
 */
753
function UserStatsTopicsStarted($memID)
754
{
755
	global $modSettings;
756
757
	$db = database();
758
759
	// Number of topics started.
760
	$result = $db->query('', '
761
		SELECT 
762
			COUNT(*)
763
		FROM {db_prefix}topics
764
		WHERE id_member_started = {int:current_member}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
765
			AND id_board != {int:recycle_board}' : ''),
766
		[
767
			'current_member' => $memID,
768
			'recycle_board' => $modSettings['recycle_board'],
769
		]
770
	);
771
	list ($num_topics) = $result->fetch_row();
772
	$result->free_result();
773
774
	return $num_topics;
775
}
776
777
/**
778
 * Returns the number of polls a user has started, including ones on boards
779
 * they may no longer have access on.
780
 *
781
 * - Does not count topics that are in the recycle board
782
 *
783
 * @param int $memID
784
 *
785
 * @return int
786
 */
787
function UserStatsPollsStarted($memID)
788
{
789
	global $modSettings;
790
791
	$db = database();
792
793
	// Number polls started.
794
	$result = $db->query('', '
795
		SELECT 
796
			COUNT(*)
797
		FROM {db_prefix}topics
798
		WHERE id_member_started = {int:current_member}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
799
			AND id_board != {int:recycle_board}' : '') . '
800
			AND id_poll != {int:no_poll}',
801
		[
802
			'current_member' => $memID,
803
			'recycle_board' => $modSettings['recycle_board'],
804
			'no_poll' => 0,
805
		]
806
	);
807
	list ($num_polls) = $result->fetch_row();
808
	$result->free_result();
809
810
	return $num_polls;
811
}
812
813
/**
814
 * Returns the number of polls a user has voted in, including ones on boards
815
 * they may no longer have access on.
816
 *
817
 * @param int $memID
818
 *
819
 * @return int
820
 */
821
function UserStatsPollsVoted($memID)
822
{
823
	$db = database();
824
825
	// Number polls voted in.
826
	$result = $db->fetchQuery('
827
		SELECT 
828
			COUNT(DISTINCT id_poll)
829
		FROM {db_prefix}log_polls
830
		WHERE id_member = {int:current_member}',
831
		[
832
			'current_member' => $memID,
833
		]
834
	);
835
	list ($num_votes) = $result->fetch_row();
836
	$result->free_result();
837
838
	return $num_votes;
839
}
840
841
/**
842
 * Finds the 1-N list of boards that a user posts in most often
843
 *
844
 * - Returns array with some basic stats of post percent per board
845
 *
846
 * @param int $memID
847
 * @param int $limit
848
 *
849
 * @return array
850
 */
851
function UserStatsMostPostedBoard($memID, $limit = 10)
852
{
853
	$db = database();
854
855
	// Find the board this member spammed most often.
856
	$popular_boards = [];
857
	$db->fetchQuery('
858
		SELECT
859
			b.id_board, MAX(b.name) AS name, MAX(b.num_posts) AS num_posts, COUNT(*) AS message_count
860
		FROM {db_prefix}messages AS m
861
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
862
		WHERE m.id_member = {int:current_member}
863
			AND b.count_posts = {int:count_enabled}
864
			AND {query_see_board}
865
		GROUP BY b.id_board
866
		ORDER BY message_count DESC
867
		LIMIT {int:limit}',
868
		[
869
			'current_member' => $memID,
870
			'count_enabled' => 0,
871
			'limit' => (int) $limit,
872
		]
873
	)->fetch_callback(
874
		function ($row) use (&$popular_boards, $memID) {
875
			$href = getUrl('board', ['board' => $row['id_board'], 'start' => '0', 'name' => $row['name']]);
876
			$posts = (int) MembersList::get($memID)->posts;
0 ignored issues
show
Bug Best Practice introduced by
The property posts does not exist on anonymous//sources/ElkArte/MembersList.php$0. Since you implemented __get, consider adding a @property annotation.
Loading history...
877
878
			// Build the board details that this member is responsible for
879
			$popular_boards[$row['id_board']] = [
880
				'id' => $row['id_board'],
881
				'posts' => $row['message_count'],
882
				'name' => $row['name'],
883
				'href' => $href,
884
				'link' => '<a href="' . $href . '">' . $row['name'] . '</a>',
885
				'posts_percent' => $posts === 0 ? 0 : ($row['message_count'] * 100) / $posts,
886
				'percent' => $posts === 0 ? 0 : ($row['message_count'] * 100) / $posts,
887
				'total_posts' => $row['num_posts'],
888
				'total_posts_member' => $posts,
889
			];
890
		}
891
	);
892
893
	return $popular_boards;
894
}
895
896
/**
897
 * Finds the 1-N list of boards that a user participates in most often
898
 *
899
 * - Returns array with some basic stats of post percent per board as a percent of board activity
900
 *
901
 * @param int $memID
902
 * @param int $limit
903
 *
904
 * @return array
905
 */
906
function UserStatsMostActiveBoard($memID, $limit = 10)
907
{
908
	$db = database();
909
910
	// Find the board this member spammed most often.
911
	$result = $db->fetchQuery('
912
		SELECT
913
			b.id_board, MAX(b.name) AS name, b.num_posts, COUNT(*) AS message_count,
914
			MAX(b.num_posts) as max_posts_per_board
915
		FROM {db_prefix}messages AS m
916
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
917
		WHERE m.id_member = {int:current_member}
918
			AND {query_see_board}
919
		GROUP BY b.id_board, b.num_posts
920
		LIMIT {int:limit}',
921
		[
922
			'current_member' => $memID,
923
			'limit' => (int) $limit,
924
		]
925
	);
926
	$board_activity = [];
927
	$percent = [];
928
	while (($row = $result->fetch_assoc()))
929
	{
930
		$href = getUrl('board', ['board' => $row['id_board'], 'start' => '0', 'name' => $row['name']]);
931
932
		// min/max take care of cases when b.num_posts is broken for wwhatever reason
933
		$percentage = min($row['message_count'] / max(1, $row['max_posts_per_board']), 1) * 100;
934
935
		// What have they been doing in this board
936
		$board_activity[$row['id_board']] = [
937
			'id' => $row['id_board'],
938
			'posts' => $row['message_count'],
939
			'href' => $href,
940
			'name' => $row['name'],
941
			'link' => '<a href="' . $href . '">' . $row['name'] . '</a>',
942
			'percent' => comma_format($percentage, 2),
943
			'posts_percent' => $percentage,
944
			'total_posts' => $row['num_posts'],
945
		];
946
947
		$percent[$row['id_board']] = $percentage;
948
	}
949
	$result->free_result();
950
951
	array_multisort($percent, SORT_DESC, $board_activity);
0 ignored issues
show
Bug introduced by
SORT_DESC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

951
	array_multisort($percent, /** @scrutinizer ignore-type */ SORT_DESC, $board_activity);
Loading history...
952
953
	return $board_activity;
954
}
955
956
/**
957
 * Finds the users posting activity by time of day
958
 *
959
 * - Returns array with some basic stats of post percent per hour
960
 *
961
 * @param int $memID
962
 *
963
 * @return array
964
 */
965
function UserStatsPostingTime($memID)
966
{
967
	global $modSettings;
968
969
	$posts_by_time = [];
970
	$hours = [];
971
	for ($hour = 0; $hour < 24; $hour++)
972
	{
973
		$posts_by_time[$hour] = [
974
			'hour' => $hour,
975
			'hour_format' => stripos(User::$info->time_format, '%p') === false ? $hour : date('g a', mktime($hour)),
0 ignored issues
show
Bug introduced by
It seems like ElkArte\User::info->time_format can also be of type null; however, parameter $haystack of stripos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

975
			'hour_format' => stripos(/** @scrutinizer ignore-type */ User::$info->time_format, '%p') === false ? $hour : date('g a', mktime($hour)),
Loading history...
Bug Best Practice introduced by
The property time_format does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
976
			'posts' => 0,
977
			'posts_percent' => 0,
978
			'percent' => 0,
979
			'relative_percent' => 0,
980
		];
981
	}
982
983
	$db = database();
984
985
	// Find the times when the users posts
986
	$result = $db->query('', '
987
		SELECT
988
			poster_time
989
		FROM {db_prefix}messages
990
		WHERE id_member = {int:current_member}' . ($modSettings['totalMessages'] > 100000 ? '
991
			AND id_topic > {int:top_ten_thousand_topics}' : ''),
992
		[
993
			'current_member' => $memID,
994
			'top_ten_thousand_topics' => $modSettings['totalTopics'] - 10000,
995
		]
996
	);
997
	while ((list ($poster_time) = $result->fetch_row()))
998
	{
999
		// Cast as an integer to remove the leading 0.
1000
		$hour = (int) standardTime($poster_time, '%H');
1001
1002
		if (!isset($hours[$hour]))
1003
		{
1004
			$hours[$hour] = 0;
1005
		}
1006
1007
		$hours[$hour]++;
1008
	}
1009
	$result->free_result();
1010
	$maxPosts = max($hours);
1011
	$totalPosts = array_sum($hours);
1012
1013
	foreach ($hours as $hour => $num)
1014
	{
1015
		// When they post, hour by hour
1016
		$posts_by_time[$hour] = array_merge($posts_by_time[$hour], [
1017
			'posts' => comma_format($num),
1018
			'posts_percent' => round(($num * 100) / $totalPosts),
1019
			'percent' => round(($num * 100) / $totalPosts),
1020
			'relative_percent' => round(($num * 100) / $maxPosts),
1021
		]);
1022
	}
1023
1024
	return $posts_by_time;
1025
}
1026