Passed
Pull Request — master (#6236)
by
unknown
08:50
created

Statistics::getJSChartTemplateWithData()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 69
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 26
nc 16
nop 8
dl 0
loc 69
rs 9.1928
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
5
use Chamilo\CoreBundle\Entity\MessageRelUser;
6
use Chamilo\CoreBundle\Entity\ResourceLink;
7
use Chamilo\CoreBundle\Entity\UserRelUser;
8
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
9
use Chamilo\CoreBundle\Framework\Container;
10
use Doctrine\DBAL\ParameterType;
11
12
/**
13
 * This class provides some functions for statistics.
14
 */
15
class Statistics
16
{
17
    /**
18
     * Converts a number of bytes in a formatted string.
19
     *
20
     * @param int $size
21
     *
22
     * @return string Formatted file size or empty string if no match
23
     */
24
    public static function makeSizeString(int $size): string
25
    {
26
        if ($size < pow(2, 10)) {
27
            return $size." bytes";
28
        }
29
        if ($size >= pow(2, 10) && $size < pow(2, 20)) {
30
            return round($size / pow(2, 10), 0)." KB";
31
        }
32
        if ($size >= pow(2, 20) && $size < pow(2, 30)) {
33
            return round($size / pow(2, 20), 1)." MB";
34
        }
35
        if ($size > pow(2, 30)) {
36
            return round($size / pow(2, 30), 2)." GB";
37
        }
38
39
        return '';
40
    }
41
42
    /**
43
     * Count courses.
44
     *
45
     * @param string|null $categoryCode Code of a course category.
46
     *                                  Default: count all courses.
47
     * @param string|null $dateFrom dateFrom
48
     * @param string|null $dateUntil dateUntil
49
     *
50
     * @return int Number of courses counted
51
     * @throws \Doctrine\DBAL\Exception
52
     * @throws Exception
53
     */
54
    public static function countCourses(string $categoryCode = null, string $dateFrom = null, string $dateUntil = null): int
55
    {
56
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
57
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
58
        $accessUrlHelper = Container::getAccessUrlHelper();
59
60
        if ($accessUrlHelper->isMultiple()) {
61
            $accessUrl = $accessUrlHelper->getCurrent();
62
            $urlId = $accessUrl->getId();
63
            $sql = "SELECT COUNT(*) AS number
64
                    FROM $courseTable AS c, $accessUrlRelCourseTable AS u
65
                    WHERE u.c_id = c.id AND $accessUrlRelCourseTable = $urlId";
66
            if (isset($categoryCode)) {
67
                $sql .= " AND category_code = '".Database::escape_string($categoryCode)."'";
68
            }
69
        } else {
70
            $sql = "SELECT COUNT(*) AS number
71
                    FROM $courseTable AS c
72
                    WHERE 1 = 1";
73
            if (isset($categoryCode)) {
74
                $sql .= " WHERE c.category_code = '".Database::escape_string($categoryCode)."'";
75
            }
76
        }
77
78
        if (!empty($dateFrom)) {
79
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
80
            $sql .= " AND c.creation_date >= '$dateFrom' ";
81
        }
82
        if (!empty($dateUntil)) {
83
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
84
            $sql .= " AND c.creation_date <= '$dateUntil' ";
85
        }
86
87
        $res = Database::query($sql);
88
        $obj = Database::fetch_object($res);
89
90
        return $obj->number;
91
    }
92
93
    /**
94
     * Count courses by visibility.
95
     *
96
     * @param array|null  $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses
97
     * @param string|null $dateFrom dateFrom
98
     * @param string|null $dateUntil dateUntil
99
     *
100
     * @return int Number of courses counted
101
     * @throws \Doctrine\DBAL\Exception
102
     * @throws Exception
103
     */
104
    public static function countCoursesByVisibility(
105
        array $visibility = null,
106
        string $dateFrom = null,
107
        string $dateUntil = null
108
    ): int
109
    {
110
        $visibilityString = '';
111
        if (empty($visibility)) {
112
            return 0;
113
        } else {
114
            $auxArrayVisibility = [];
115
            if (!is_array($visibility)) {
116
                $visibility = [$visibility];
117
            }
118
            foreach ($visibility as $item) {
119
                $auxArrayVisibility[] = (int) $item;
120
            }
121
            $visibilityString = implode(',', $auxArrayVisibility);
122
        }
123
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
124
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
125
        $accessUrlHelper = Container::getAccessUrlHelper();
126
127
        if ($accessUrlHelper->isMultiple()) {
128
            $accessUrl = $accessUrlHelper->getCurrent();
129
            $urlId = $accessUrl->getId();
130
            $sql = "SELECT COUNT(*) AS number
131
                    FROM $courseTable AS c, $accessUrlRelCourseTable AS u
132
                    WHERE u.c_id = c.id AND u.access_url_id = $urlId";
133
        } else {
134
            $sql = "SELECT COUNT(*) AS number
135
                    FROM $courseTable AS c
136
                    WHERE 1 = 1";
137
        }
138
        $sql .= " AND visibility IN ($visibilityString) ";
139
        if (!empty($dateFrom)) {
140
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
141
            $sql .= " AND c.creation_date >= '$dateFrom' ";
142
        }
143
        if (!empty($dateUntil)) {
144
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
145
            $sql .= " AND c.creation_date <= '$dateUntil' ";
146
        }
147
        $res = Database::query($sql);
148
        $obj = Database::fetch_object($res);
149
150
        return $obj->number;
151
    }
152
153
    /**
154
     * Count users.
155
     *
156
     * @param int    $status user status (COURSEMANAGER or STUDENT) if not setted it'll count all users
157
     * @param string $categoryCode course category code. Default: count only users without filtering category
158
     * @param bool   $countInvisibleCourses Count invisible courses (todo)
159
     * @param bool   $onlyActive Count only active users (false to only return currently active users)
160
     *
161
     * @return int Number of users counted
162
     * @throws Exception
163
     */
164
    public static function countUsers(
165
        ?int $status = null,
166
        ?string $categoryCode = null,
167
        ?bool $countInvisibleCourses = true,
168
        ?bool $onlyActive = false
169
    ): int
170
    {
171
        // Database table definitions
172
        $course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
173
        $course_table = Database::get_main_table(TABLE_MAIN_COURSE);
174
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
175
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
176
        $tblCourseCategory = Database::get_main_table(TABLE_MAIN_CATEGORY);
177
        $tblCourseRelCategory = Database::get_main_table(TABLE_MAIN_COURSE_REL_CATEGORY);
178
179
        $conditions = [];
180
        $conditions[] = "u.active <> " . USER_SOFT_DELETED;
181
        if ($onlyActive) {
182
            $conditions[] = "u.active = 1";
183
        }
184
        if (isset($status)) {
185
            $conditions[] = "u.status = " . $status;
186
        }
187
188
        $where = implode(' AND ', $conditions);
189
190
        $accessUrlHelper = Container::getAccessUrlHelper();
191
192
        if ($accessUrlHelper->isMultiple()) {
193
            $accessUrl = $accessUrlHelper->getCurrent();
194
            $urlId = $accessUrl->getId();
195
            $sql = "SELECT COUNT(DISTINCT(u.id)) AS number
196
                FROM $user_table as u
197
                INNER JOIN $access_url_rel_user_table as url ON u.id = url.user_id
198
                WHERE $where AND url.access_url_id = $urlId";
199
200
            if (isset($categoryCode)) {
201
                $categoryCode = Database::escape_string($categoryCode);
202
                $sql = "SELECT COUNT(DISTINCT(cu.user_id)) AS number
203
                    FROM $course_user_table cu
204
                    INNER JOIN $course_table c ON c.id = cu.c_id
205
                    INNER JOIN $access_url_rel_user_table as url ON cu.user_id = url.user_id
206
                    INNER JOIN $tblCourseRelCategory crc ON crc.course_id = c.id
207
                    INNER JOIN $tblCourseCategory cc ON cc.id = crc.course_category_id
208
                    INNER JOIN $user_table u ON cu.user_id = u.id
209
                    WHERE $where AND url.access_url_id = $urlId AND cc.code = '$categoryCode'";
210
            }
211
        } else {
212
            $sql = "SELECT COUNT(DISTINCT(id)) AS number
213
                FROM $user_table u
214
                WHERE $where";
215
216
            if (isset($categoryCode)) {
217
                $categoryCode = Database::escape_string($categoryCode);
218
                $sql = "SELECT COUNT(DISTINCT(cu.user_id)) AS number
219
                    FROM $course_user_table cu
220
                    INNER JOIN $course_table c ON c.id = cu.c_id
221
                    INNER JOIN $tblCourseRelCategory crc ON crc.course_id = c.id
222
                    INNER JOIN $tblCourseCategory cc ON cc.id = crc.course_category_id
223
                    INNER JOIN $user_table u ON u.id = cu.user_id
224
                    WHERE $where AND cc.code = '$categoryCode'";
225
            }
226
        }
227
228
        $res = Database::query($sql);
229
        $obj = Database::fetch_object($res);
230
231
        return $obj->number;
232
    }
233
234
    /**
235
     * Get courses IDs from courses with some access_date between the two given dates
236
     * @param string $startDate
237
     * @param string $endDate
238
     *
239
     * @return array
240
     * @throws Exception
241
     */
242
    public static function getCoursesWithActivity(string $startDate, string $endDate): array
243
    {
244
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
245
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
246
        $startDate = Database::escape_string($startDate);
247
        $endDate = Database::escape_string($endDate);
248
249
        $accessUrlHelper = Container::getAccessUrlHelper();
250
251
        if ($accessUrlHelper->isMultiple()) {
252
            $accessUrl = $accessUrlHelper->getCurrent();
253
            $urlId = $accessUrl->getId();
254
            $sql = "SELECT DISTINCT(t.c_id) FROM $table t , $access_url_rel_course_table a
255
                    WHERE
256
                        t.c_id = a.c_id AND
257
                        access_url_id = $urlId AND
258
                        access_date BETWEEN '$startDate' AND '$endDate'
259
                    ";
260
        } else {
261
            $sql = "SELECT DISTINCT(t.c_id) FROM $table t
262
                   access_date BETWEEN '$startDate' AND '$endDate' ";
263
        }
264
265
        $result = Database::query($sql);
266
267
        return Database::store_result($result);
268
    }
269
270
    /**
271
     * Count activities from track_e_default_table.
272
     *
273
     * @return int Number of activities counted
274
     * @throws Exception
275
     */
276
    public static function getNumberOfActivities(mixed $courseId = 0, ?int $sessionId = 0): int
277
    {
278
        // Database table definitions
279
        $track_e_default = Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT);
280
        $table_user = Database::get_main_table(TABLE_MAIN_USER);
281
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
282
        $accessUrlHelper = Container::getAccessUrlHelper();
283
        if (is_array($courseId)) {
284
            // Usually when no param is given, we get an empty array from SortableTable
285
            $courseId = 0;
286
        }
287
288
        if ($accessUrlHelper->isMultiple()) {
289
            $accessUrl = $accessUrlHelper->getCurrent();
290
            $urlId = $accessUrl->getId();
291
            $sql = "SELECT count(default_id) AS total_number_of_items
292
                    FROM $track_e_default, $table_user user, $access_url_rel_user_table url
293
                    WHERE user.active <> ".USER_SOFT_DELETED." AND
294
                        default_user_id = user.id AND
295
                        user.id = url.user_id AND
296
                        access_url_id = $urlId";
297
        } else {
298
            $sql = "SELECT count(default_id) AS total_number_of_items
299
                    FROM $track_e_default, $table_user user
300
                    WHERE user.active <> ".USER_SOFT_DELETED." AND default_user_id = user.id ";
301
        }
302
303
        if (!empty($courseId)) {
304
            $courseId = (int) $courseId;
305
            $sql .= " AND c_id = $courseId";
306
            $sql .= api_get_session_condition($sessionId);
307
        }
308
309
        if (isset($_GET['keyword'])) {
310
            $keyword = Database::escape_string(trim($_GET['keyword']));
311
            $sql .= " AND (
312
                        user.username LIKE '%".$keyword."%' OR
313
                        default_event_type LIKE '%".$keyword."%' OR
314
                        default_value_type LIKE '%".$keyword."%' OR
315
                        default_value LIKE '%".$keyword."%') ";
316
        }
317
        $res = Database::query($sql);
318
        $obj = Database::fetch_object($res);
319
320
        return $obj->total_number_of_items;
321
    }
322
323
    /**
324
     * Get activities data to display.
325
     *
326
     * @param int    $from
327
     * @param int    $numberOfItems
328
     * @param int    $column
329
     * @param string $direction
330
     * @param ?int   $courseId
331
     * @param ?int   $sessionId
332
     *
333
     * @return array
334
     * @throws Exception
335
     */
336
    public static function getActivitiesData(
337
        int $from,
338
        int $numberOfItems,
339
        int $column,
340
        string $direction,
341
        mixed $courseId = 0,
342
        ?int $sessionId = 0
343
    ): array
344
    {
345
        $track_e_default = Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT);
346
        $table_user = Database::get_main_table(TABLE_MAIN_USER);
347
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
348
        $direction = strtoupper($direction);
349
        if (is_array($courseId)) {
350
            // Usually when no param is given, we get an empty array from SortableTable
351
            $courseId = 0;
352
        }
353
354
        if (!in_array($direction, ['ASC', 'DESC'])) {
355
            $direction = 'DESC';
356
        }
357
358
        $accessUrlHelper = Container::getAccessUrlHelper();
359
360
        if ($accessUrlHelper->isMultiple()) {
361
            $accessUrl = $accessUrlHelper->getCurrent();
362
            $urlId = $accessUrl->getId();
363
            $sql = "SELECT
364
                        default_event_type  as col0,
365
                        default_value_type    as col1,
366
                        default_value        as col2,
367
                        c_id         as col3,
368
                        session_id as col4,
369
                        user.username         as col5,
370
                        user.id         as col6,
371
                        default_date         as col7
372
                    FROM $track_e_default as track_default,
373
                    $table_user as user,
374
                    $access_url_rel_user_table as url
375
                    WHERE
376
                        user.active <> -1 AND
377
                        track_default.default_user_id = user.id AND
378
                        url.user_id = user.id AND
379
                        access_url_id= $urlId";
380
        } else {
381
            $sql = "SELECT
382
                       default_event_type  as col0,
383
                       default_value_type    as col1,
384
                       default_value        as col2,
385
                       c_id         as col3,
386
                       session_id as col4,
387
                       user.username         as col5,
388
                       user.id         as col6,
389
                       default_date         as col7
390
                   FROM $track_e_default track_default, $table_user user
391
                   WHERE user.active <> ".USER_SOFT_DELETED." AND track_default.default_user_id = user.id ";
392
        }
393
394
        if (!empty($_GET['keyword'])) {
395
            $keyword = Database::escape_string(trim($_GET['keyword']));
396
            $sql .= " AND (user.username LIKE '%".$keyword."%' OR
397
                        default_event_type LIKE '%".$keyword."%' OR
398
                        default_value_type LIKE '%".$keyword."%' OR
399
                        default_value LIKE '%".$keyword."%') ";
400
        }
401
402
        if (!empty($courseId)) {
403
            $courseId = (int) $courseId;
404
            $sql .= " AND c_id = $courseId";
405
            $sql .= api_get_session_condition($sessionId);
406
        }
407
408
        if (!empty($column)) {
409
            $sql .= " ORDER BY col$column $direction";
410
        } else {
411
            $sql .= " ORDER BY col7 DESC ";
412
        }
413
        $sql .= " LIMIT $from, $numberOfItems ";
414
415
        $res = Database::query($sql);
416
        $activities = [];
417
        while ($row = Database::fetch_row($res)) {
418
            if (false === strpos($row[1], '_object') &&
419
                false === strpos($row[1], '_array')
420
            ) {
421
                $row[2] = $row[2];
422
            } else {
423
                if (!empty($row[2])) {
424
                    $originalData = str_replace('\\', '', $row[2]);
425
                    $row[2] = UnserializeApi::unserialize('not_allowed_classes', $originalData);
426
                    if (is_array($row[2]) && !empty($row[2])) {
427
                        $row[2] = implode_with_key(', ', $row[2]);
428
                    } else {
429
                        $row[2] = $originalData;
430
                    }
431
                }
432
            }
433
434
            if (!empty($row['default_date'])) {
435
                $row['default_date'] = api_get_local_time($row['default_date']);
436
            } else {
437
                $row['default_date'] = '-';
438
            }
439
440
            if (!empty($row[7])) {
441
                $row[7] = api_get_local_time($row[7]);
442
            } else {
443
                $row[7] = '-';
444
            }
445
446
            if (!empty($row[5])) {
447
                // Course
448
                if (!empty($row[3])) {
449
                    $row[3] = Display::url(
450
                        $row[3],
451
                        api_get_path(WEB_CODE_PATH).'admin/course_edit.php?id='.$row[3]
452
                    );
453
                } else {
454
                    $row[3] = '-';
455
                }
456
457
                // session
458
                if (!empty($row[4])) {
459
                    $row[4] = Display::url(
460
                        $row[4],
461
                        api_get_path(WEB_CODE_PATH).'session/resume_session.php?id_session='.$row[4]
462
                    );
463
                } else {
464
                    $row[4] = '-';
465
                }
466
467
                // User id.
468
                $row[5] = Display::url(
469
                    $row[5],
470
                    api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_popup&user_id='.$row[6],
471
                    ['class' => 'ajax']
472
                );
473
474
                $row[6] = Tracking::get_ip_from_user_event(
475
                    $row[6],
476
                    $row[7],
477
                    true
478
                );
479
                if (empty($row[6])) {
480
                    $row[6] = get_lang('Unknown');
481
                }
482
            }
483
            $activities[] = $row;
484
        }
485
486
        return $activities;
487
    }
488
489
    /**
490
     * Show statistics.
491
     *
492
     * @param string $title      The title
493
     * @param array  $stats
494
     * @param ?bool   $showTotal
495
     * @param ?bool   $isFileSize
496
     *
497
     * @return string HTML table
498
     */
499
    public static function printStats(
500
        string $title,
501
        array $stats,
502
        ?bool $showTotal = true,
503
        ?bool $isFileSize = false
504
    ): string
505
    {
506
        $total = 0;
507
        $content = '<table class="table table-hover table-striped data_table stats_table" cellspacing="0" cellpadding="3" width="90%">
508
            <thead><tr><th colspan="'.($showTotal ? '4' : '3').'">'.$title.'</th></tr></thead><tbody>';
509
        $i = 0;
510
        foreach ($stats as $subtitle => $number) {
511
            $total += $number;
512
        }
513
514
        foreach ($stats as $subtitle => $number) {
515
            if (!$isFileSize) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isFileSize of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
516
                $number_label = number_format($number, 0, ',', '.');
517
            } else {
518
                $number_label = self::makeSizeString($number);
519
            }
520
            $percentage = ($total > 0 ? number_format(100 * $number / $total, 1, ',', '.') : '0');
521
522
            $content .= '<tr class="row_'.(0 == $i % 2 ? 'odd' : 'even').'">
523
                    <td width="25%" style="vertical-align:top;">'.$subtitle.'</td>
524
                    <td width="60%">'.Display::bar_progress($percentage, false).'</td>
525
                    <td width="5%" align="right" style="vertical-align:top;">'.$number_label.'</td>';
526
            if ($showTotal) {
527
                $content .= '<td width="5%" align="right"> '.$percentage.'%</td>';
528
            }
529
            $content .= '</tr>';
530
            $i++;
531
        }
532
        $content .= '</tbody>';
533
        if ($showTotal) {
534
            if (!$isFileSize) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isFileSize of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
535
                $total_label = number_format($total, 0, ',', '.');
536
            } else {
537
                $total_label = self::makeSizeString($total);
538
            }
539
            $content .= '
540
                <tfoot><tr><th colspan="4" align="right">'.get_lang('Total').': '.$total_label.'</td></tr></tfoot>
541
            ';
542
        }
543
        $content .= '</table>';
544
545
        return $content;
546
    }
547
548
    /**
549
     * Show some stats about the number of logins.
550
     *
551
     * @param string $type month, hour or day
552
     * @return string HTML block
553
     * @throws Exception
554
     */
555
    public static function printLoginStats(string $type): string
556
    {
557
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
558
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
559
        $table_url = null;
560
        $where_url = null;
561
        $now = api_get_utc_datetime();
562
        $where_url_last = ' WHERE login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
563
        $accessUrlHelper = Container::getAccessUrlHelper();
564
565
        if ($accessUrlHelper->isMultiple()) {
566
            $accessUrl = $accessUrlHelper->getCurrent();
567
            $urlId = $accessUrl->getId();
568
            $table_url = ", $access_url_rel_user_table";
569
            $where_url = " WHERE login_user_id=user_id AND access_url_id = $urlId";
570
            $where_url_last = ' AND login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
571
        }
572
573
        $period = get_lang('Month');
574
        $periodCollection = api_get_months_long();
575
        $sql = "SELECT
576
                DATE_FORMAT( login_date, '%Y-%m' ) AS stat_date ,
577
                count( login_id ) AS number_of_logins
578
                FROM $table $table_url $where_url
579
                GROUP BY stat_date
580
                ORDER BY login_date DESC";
581
        $sql_last_x = null;
582
583
        switch ($type) {
584
            case 'hour':
585
                $period = get_lang('Hour');
586
                $sql = "SELECT
587
                          DATE_FORMAT( login_date, '%H') AS stat_date,
588
                          count( login_id ) AS number_of_logins
589
                        FROM $table $table_url $where_url
590
                        GROUP BY stat_date
591
                        ORDER BY stat_date ";
592
                $sql_last_x = "SELECT
593
                                DATE_FORMAT( login_date, '%H' ) AS stat_date,
594
                                count( login_id ) AS number_of_logins
595
                               FROM $table $table_url $where_url ".sprintf($where_url_last, 'DAY')."
596
                               GROUP BY stat_date
597
                               ORDER BY stat_date ";
598
                break;
599
            case 'day':
600
                $periodCollection = api_get_week_days_long();
601
                $period = get_lang('Day');
602
                $sql = "SELECT DATE_FORMAT( login_date, '%w' ) AS stat_date ,
603
                        count( login_id ) AS number_of_logins
604
                        FROM  $table $table_url $where_url
605
                        GROUP BY stat_date
606
                        ORDER BY DATE_FORMAT( login_date, '%w' ) ";
607
                $sql_last_x = "SELECT
608
                                DATE_FORMAT( login_date, '%w' ) AS stat_date,
609
                                count( login_id ) AS number_of_logins
610
                               FROM $table $table_url $where_url ".sprintf($where_url_last, 'WEEK')."
611
                               GROUP BY stat_date
612
                               ORDER BY DATE_FORMAT( login_date, '%w' ) ";
613
                break;
614
        }
615
616
        $content = '';
617
        if ($sql_last_x) {
618
            $res_last_x = Database::query($sql_last_x);
619
            $result_last_x = [];
620
            while ($obj = Database::fetch_object($res_last_x)) {
621
                $stat_date = ('day' === $type) ? $periodCollection[$obj->stat_date] : $obj->stat_date;
622
                $result_last_x[$stat_date] = $obj->number_of_logins;
623
            }
624
            $content .= self::printStats(get_lang('Last logins').' ('.$period.')', $result_last_x, true);
625
            flush(); //flush web request at this point to see something already while the full data set is loading
626
            $content .= '<br />';
627
        }
628
        $res = Database::query($sql);
629
        $result = [];
630
        while ($obj = Database::fetch_object($res)) {
631
            $stat_date = $obj->stat_date;
632
            switch ($type) {
633
                case 'month':
634
                    $stat_date = explode('-', $stat_date);
635
                    $stat_date[1] = $periodCollection[(int) $stat_date[1] - 1];
636
                    $stat_date = implode(' ', $stat_date);
637
                    break;
638
                case 'day':
639
                    $stat_date = $periodCollection[$stat_date];
640
                    break;
641
            }
642
            $result[$stat_date] = $obj->number_of_logins;
643
        }
644
        $content .= self::printStats(get_lang('All logins').' ('.$period.')', $result, true);
645
646
        return $content;
647
    }
648
649
    /**
650
     * Print the number of recent logins.
651
     *
652
     * @param ?bool  $distinct        whether to only give distinct users stats, or *all* logins
653
     * @param ?int   $sessionDuration Number of minutes a session must have lasted at a minimum to be taken into account
654
     * @param ?array $periods         List of number of days we want to query (default: [1, 7, 31] for last 1 day, last 7 days, last 31 days)
655
     *
656
     * @throws Exception
657
     *
658
     * @return string HTML table
659
     */
660
    public static function printRecentLoginStats(?bool $distinct = false, ?int $sessionDuration = 0, ?array $periods = []): string
661
    {
662
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
663
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
664
        $table_url = '';
665
        $where_url = '';
666
        $accessUrlHelper = Container::getAccessUrlHelper();
667
668
        if ($accessUrlHelper->isMultiple()) {
669
            $accessUrl = $accessUrlHelper->getCurrent();
670
            $urlId = $accessUrl->getId();
671
            $table_url = ", $access_url_rel_user_table";
672
            $where_url = " AND login_user_id=user_id AND access_url_id = $urlId";
673
        }
674
675
        $now = api_get_utc_datetime();
676
        $field = 'login_id';
677
        if ($distinct) {
678
            $field = 'DISTINCT(login_user_id)';
679
        }
680
681
        if (empty($periods)) {
682
            $periods = [1, 7, 31];
683
        }
684
        $sqlList = [];
685
686
        $sessionDuration = (int) $sessionDuration * 60; // convert from minutes to seconds
687
        foreach ($periods as $day) {
688
            $date = new DateTime($now);
689
            $startDate = $date->format('Y-m-d').' 00:00:00';
690
            $endDate = $date->format('Y-m-d').' 23:59:59';
691
692
            if ($day > 1) {
693
                $startDate = $date->sub(new DateInterval('P'.$day.'D'));
694
                $startDate = $startDate->format('Y-m-d').' 00:00:00';
695
            }
696
697
            $localDate = api_get_local_time($startDate, null, null, false, false);
698
            $localEndDate = api_get_local_time($endDate, null, null, false, false);
699
700
            $label = sprintf(get_lang('Last %s days'), $day);
701
            if (1 == $day) {
702
                $label = get_lang('Today');
703
            }
704
            $label .= " <span class=\"muted right\" style=\"float: right; margin-right: 5px;\">[$localDate - $localEndDate]</span>";
705
            $sql = "SELECT count($field) AS number
706
                    FROM $table $table_url
707
                    WHERE ";
708
            if (0 == $sessionDuration) {
709
                $sql .= " logout_date != login_date AND ";
710
            } else {
711
                $sql .= " UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration AND ";
712
            }
713
            $sql .= "login_date BETWEEN '$startDate' AND '$endDate'
714
                        $where_url";
715
            $sqlList[$label] = $sql;
716
        }
717
718
        $sql = "SELECT count($field) AS number
719
                FROM $table $table_url ";
720
        if (0 == $sessionDuration) {
721
            $sql .= " WHERE logout_date != login_date $where_url";
722
        } else {
723
            $sql .= " WHERE UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration $where_url";
724
        }
725
        $sqlList[get_lang('Total')] = $sql;
726
        $totalLogin = [];
727
        foreach ($sqlList as $label => $query) {
728
            $res = Database::query($query);
729
            $obj = Database::fetch_object($res);
730
            $totalLogin[$label] = $obj->number;
731
        }
732
733
        if ($distinct) {
734
            $content = self::printStats(get_lang('Distinct users logins'), $totalLogin, false);
735
        } else {
736
            $content = self::printStats(get_lang('Logins'), $totalLogin, false);
737
        }
738
739
        return $content;
740
    }
741
742
    /**
743
     * Get the number of recent logins.
744
     *
745
     * @param ?bool $distinct            Whether to only give distinct users stats, or *all* logins
746
     * @param ?int  $sessionDuration     Number of minutes a session must have lasted at a minimum to be taken into account
747
     * @param ?bool $completeMissingDays Whether to fill the daily gaps (if any) when getting a list of logins
748
     *
749
     * @throws Exception
750
     *
751
     * @return array
752
     */
753
    public static function getRecentLoginStats(?bool $distinct = false, ?int $sessionDuration = 0, ?bool $completeMissingDays = true): array
754
    {
755
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
756
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
757
        $table_url = '';
758
        $where_url = '';
759
        $accessUrlHelper = Container::getAccessUrlHelper();
760
761
        if ($accessUrlHelper->isMultiple()) {
762
            $accessUrl = $accessUrlHelper->getCurrent();
763
            $urlId = $accessUrl->getId();
764
            $table_url = ", $access_url_rel_user_table";
765
            $where_url = " AND login_user_id=user_id AND access_url_id = $urlId";
766
        }
767
768
        $now = api_get_utc_datetime();
769
        $date = new DateTime($now);
770
        $date->sub(new DateInterval('P31D'));
771
        $newDate = $date->format('Y-m-d h:i:s');
772
        $totalLogin = self::buildDatesArray($newDate, $now, true);
773
774
        $field = 'login_id';
775
        if ($distinct) {
776
            $field = 'DISTINCT(login_user_id)';
777
        }
778
        $sessionDuration = (int) $sessionDuration * 60; //Convert from minutes to seconds
779
780
        $sql = "SELECT count($field) AS number, date(login_date) as login_date
781
                FROM $table $table_url
782
                WHERE ";
783
        if (0 == $sessionDuration) {
784
            $sql .= " logout_date != login_date AND ";
785
        } else {
786
            $sql .= " UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration AND ";
787
        }
788
        $sql .= " login_date >= '$newDate' $where_url
789
                GROUP BY date(login_date)";
790
791
        $res = Database::query($sql);
792
        while ($row = Database::fetch_assoc($res)) {
793
            $monthAndDay = substr($row['login_date'], 5, 5);
794
            $totalLogin[$monthAndDay] = $row['number'];
795
        }
796
797
        return $totalLogin;
798
    }
799
800
    /**
801
     * Get course tools usage statistics for the whole platform (by URL if multi-url).
802
     * @throws Exception
803
     */
804
    public static function getToolsStats(): array
805
    {
806
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ACCESS);
807
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
808
809
        $tools = [
810
            'announcement',
811
            'assignment',
812
            'calendar_event',
813
            'chat',
814
            'course_description',
815
            'document',
816
            'dropbox',
817
            'group',
818
            'learnpath',
819
            'link',
820
            'quiz',
821
            'student_publication',
822
            'user',
823
            'forum',
824
        ];
825
        $tool_names = [];
826
        foreach ($tools as $tool) {
827
            $tool_names[$tool] = get_lang(ucfirst($tool), '');
828
        }
829
        $accessUrlHelper = Container::getAccessUrlHelper();
830
831
        if ($accessUrlHelper->isMultiple()) {
832
            $accessUrl = $accessUrlHelper->getCurrent();
833
            $urlId = $accessUrl->getId();
834
            $sql = "SELECT access_tool, count( access_id ) AS number_of_logins
835
                    FROM $table t , $access_url_rel_course_table a
836
                    WHERE
837
                        access_tool IN ('".implode("','", $tools)."') AND
838
                        t.c_id = a.c_id AND
839
                        access_url_id = $urlId
840
                        GROUP BY access_tool
841
                    ";
842
        } else {
843
            $sql = "SELECT access_tool, count( access_id ) AS number_of_logins
844
                    FROM $table
845
                    WHERE access_tool IN ('".implode("','", $tools)."')
846
                    GROUP BY access_tool ";
847
        }
848
849
        $res = Database::query($sql);
850
        $result = [];
851
        while ($obj = Database::fetch_object($res)) {
852
            $result[$tool_names[$obj->access_tool]] = $obj->number_of_logins;
853
        }
854
855
        return $result;
856
    }
857
858
    /**
859
     * Show some stats about the accesses to the different course tools.
860
     *
861
     * @param array $result If defined, this serves as data. Otherwise, will get the data from getToolsStats()
862
     *
863
     * @return string HTML table
864
     * @throws Exception
865
     */
866
    public static function printToolStats($result = null): string
867
    {
868
        if (empty($result)) {
869
            $result = self::getToolsStats();
870
        }
871
872
        return self::printStats(get_lang('Tools access'), $result, true);
873
    }
874
875
    /**
876
     * Returns some stats about the number of courses per language.
877
     * @throws Exception
878
     */
879
    public static function printCourseByLanguageStats(): array
880
    {
881
        $table = Database::get_main_table(TABLE_MAIN_COURSE);
882
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
883
        $accessUrlHelper = Container::getAccessUrlHelper();
884
885
        if ($accessUrlHelper->isMultiple()) {
886
            $accessUrl = $accessUrlHelper->getCurrent();
887
            $urlId = $accessUrl->getId();
888
            $sql = "SELECT course_language, count( c.code ) AS number_of_courses
889
                    FROM $table as c, $access_url_rel_course_table as u
890
                    WHERE u.c_id = c.id AND access_url_id = $urlId
891
                    GROUP BY course_language
892
                    ORDER BY number_of_courses DESC";
893
        } else {
894
            $sql = "SELECT course_language, count( code ) AS number_of_courses
895
                   FROM $table GROUP BY course_language
896
                   ORDER BY number_of_courses DESC";
897
        }
898
        $res = Database::query($sql);
899
        $result = [];
900
        while ($obj = Database::fetch_object($res)) {
901
            $result[$obj->course_language] = $obj->number_of_courses;
902
        }
903
904
        return $result;
905
    }
906
907
    /**
908
     * Shows the number of users having their picture uploaded in Dokeos.
909
     * @throws Exception
910
     */
911
    public static function printUserPicturesStats(): string
912
    {
913
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
914
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
915
        $url_condition = null;
916
        $url_condition2 = null;
917
        $table = null;
918
        $accessUrlHelper = Container::getAccessUrlHelper();
919
920
        if ($accessUrlHelper->isMultiple()) {
921
            $accessUrl = $accessUrlHelper->getCurrent();
922
            $urlId = $accessUrl->getId();
923
            $url_condition = ", $access_url_rel_user_table as url WHERE url.user_id=u.id AND access_url_id='".$urlId."'";
924
            $url_condition2 = " AND url.user_id=u.id AND access_url_id = $urlId";
925
            $table = ", $access_url_rel_user_table as url ";
926
        }
927
        $sql = "SELECT COUNT(*) AS n FROM $user_table as u ".$url_condition;
928
        $res = Database::query($sql);
929
        $count1 = Database::fetch_object($res);
930
        $sql = "SELECT COUNT(*) AS n FROM $user_table as u $table
931
               WHERE LENGTH(picture_uri) > 0 $url_condition2";
932
933
        $sql .= !str_contains($sql, 'WHERE') ? ' WHERE u.active <> '.USER_SOFT_DELETED : ' AND u.active <> '.USER_SOFT_DELETED;
934
935
        $res = Database::query($sql);
936
        $count2 = Database::fetch_object($res);
937
        // #users without picture
938
        $result[get_lang('No')] = $count1->n - $count2->n;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.
Loading history...
939
        $result[get_lang('Yes')] = $count2->n; // #users with picture
940
941
        return self::printStats(get_lang('Number of users').' ('.get_lang('Picture').')', $result, true);
942
    }
943
944
    /**
945
     * Print important activities report page
946
     */
947
    public static function printActivitiesStats(): string
948
    {
949
        $content = '<h4>'.get_lang('Important activities').'</h4>';
950
        // Create a search-box
951
        $form = new FormValidator(
952
            'search_simple',
953
            'get',
954
            api_get_path(WEB_CODE_PATH).'admin/statistics/index.php',
955
            '',
956
            ['style' => 'width:200px']
957
        );
958
        $renderer = &$form->defaultRenderer();
959
        $renderer->setCustomElementTemplate('<span>{element}</span> ');
960
        $form->addHidden('report', 'activities');
961
        $form->addHidden('activities_direction', 'DESC');
962
        $form->addHidden('activities_column', '4');
963
        $form->addElement('text', 'keyword', get_lang('Keyword'));
964
        $form->addButtonSearch(get_lang('Search'), 'submit');
965
        $content .= '<div class="actions">';
966
        $content .= $form->returnForm();
967
        $content .= '</div>';
968
969
        if (!empty($_GET['keyword'])) {
970
            $table = new SortableTable(
971
                'activities',
972
                ['Statistics', 'getNumberOfActivities'],
973
                ['Statistics', 'getActivitiesData'],
974
                7,
975
                50,
976
                'DESC'
977
            );
978
            $parameters = [];
979
980
            $parameters['report'] = 'activities';
981
            $parameters['keyword'] = Security::remove_XSS($_GET['keyword']);
982
983
            $table->set_additional_parameters($parameters);
984
            $table->set_header(0, get_lang('Event type'));
985
            $table->set_header(1, get_lang('Data type'));
986
            $table->set_header(2, get_lang('Value'));
987
            $table->set_header(3, get_lang('Course'));
988
            $table->set_header(4, get_lang('Session'));
989
            $table->set_header(5, get_lang('Username'));
990
            $table->set_header(6, get_lang('IP address'));
991
            $table->set_header(7, get_lang('Date'));
992
            $content .= $table->return_table();
993
        }
994
995
        $content .= '<div class="alert alert-info">'.get_lang('Important activities').' : '.'<br>';
996
        $prefix = 'LOG_';
997
        $userDefinedConstants = get_defined_constants(true)['user'];
998
        $filteredConstants = array_filter($userDefinedConstants, function ($constantName) use ($prefix) {
999
            return strpos($constantName, $prefix) === 0;
1000
        }, ARRAY_FILTER_USE_KEY);
1001
        $constantNames = array_keys($filteredConstants);
1002
        $link = api_get_self().'?report=activities&activities_direction=DESC&activities_column=7&keyword=';
1003
        foreach ($constantNames as $constantName) {
1004
            if ($constantName != 'LOG_WS') {
1005
                if (substr($constantName, -3) == '_ID') {
1006
                    continue;
1007
                }
1008
                $content .= '- <a href="'.$link.constant($constantName).'">'.constant($constantName).'</a><br>'.PHP_EOL;
1009
            } else {
1010
                $constantValue = constant($constantName);
1011
                $reflection = new ReflectionClass('Rest');
1012
                $constants = $reflection->getConstants();
1013
                foreach ($constants as $name => $value) {
1014
                    $content .= '- <a href="'.$link.$constantValue.$value.'">'.$constantValue.$value.'</a><br>'.PHP_EOL;
1015
                }
1016
            }
1017
        }
1018
        $content .= '</div>';
1019
1020
        return $content;
1021
    }
1022
1023
    /**
1024
     * Shows statistics about the time of last visit to each course.
1025
     * @throws Exception
1026
     */
1027
    public static function printCourseLastVisit(): string
1028
    {
1029
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
1030
        $columns[0] = 'c_id';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$columns was never initialized. Although not strictly required by PHP, it is generally a good practice to add $columns = array(); before regardless.
Loading history...
1031
        $columns[1] = 'access_date';
1032
        $sql_order[SORT_ASC] = 'ASC';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sql_order was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sql_order = array(); before regardless.
Loading history...
1033
        $sql_order[SORT_DESC] = 'DESC';
1034
        $per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 10;
1035
        $page_nr = isset($_GET['page_nr']) ? intval($_GET['page_nr']) : 1;
1036
        $column = isset($_GET['column']) ? intval($_GET['column']) : 0;
1037
        $direction = isset($_GET['direction']) ? $_GET['direction'] : SORT_ASC;
1038
1039
        if (!in_array($direction, [SORT_ASC, SORT_DESC])) {
1040
            $direction = SORT_ASC;
1041
        }
1042
        $form = new FormValidator('courselastvisit', 'get');
1043
        $form->addElement('hidden', 'report', 'courselastvisit');
1044
        $form->addText('date_diff', get_lang('days'), true);
1045
        $form->addRule('date_diff', 'InvalidNumber', 'numeric');
1046
        $form->addButtonSearch(get_lang('Search'), 'submit');
1047
        if (!isset($_GET['date_diff'])) {
1048
            $defaults['date_diff'] = 60;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$defaults was never initialized. Although not strictly required by PHP, it is generally a good practice to add $defaults = array(); before regardless.
Loading history...
1049
        } else {
1050
            $defaults['date_diff'] = Security::remove_XSS($_GET['date_diff']);
1051
        }
1052
        $form->setDefaults($defaults);
1053
        $content = $form->returnForm();
1054
1055
        $values = $form->exportValues();
1056
        $date_diff = $values['date_diff'];
1057
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
1058
        $accessUrlHelper = Container::getAccessUrlHelper();
1059
1060
        if ($accessUrlHelper->isMultiple()) {
1061
            $accessUrl = $accessUrlHelper->getCurrent();
1062
            $urlId = $accessUrl->getId();
1063
            $sql = "SELECT * FROM $table t , $access_url_rel_course_table a
1064
                   WHERE
1065
                        c_id = a.c_id AND
1066
                        access_url_id = $urlId
1067
                   GROUP BY c_id
1068
                   HAVING c_id <> ''
1069
                   AND DATEDIFF( '".api_get_utc_datetime()."' , access_date ) <= ".$date_diff;
1070
        } else {
1071
            $sql = "SELECT * FROM $table t
1072
                   GROUP BY c_id
1073
                   HAVING c_id <> ''
1074
                   AND DATEDIFF( '".api_get_utc_datetime()."' , access_date ) <= ".$date_diff;
1075
        }
1076
        $sql .= ' ORDER BY `'.$columns[$column].'` '.$sql_order[$direction];
1077
        $from = ($page_nr - 1) * $per_page;
1078
        $sql .= ' LIMIT '.$from.','.$per_page;
1079
1080
        $content .= '<p>'.get_lang('Latest access').' &gt;= '.$date_diff.' '.get_lang('days').'</p>';
1081
        $res = Database::query($sql);
1082
        if (Database::num_rows($res) > 0) {
1083
            $courses = [];
1084
            while ($obj = Database::fetch_object($res)) {
1085
                $courseInfo = api_get_course_info_by_id($obj->c_id);
1086
                $course = [];
1087
                $course[] = '<a href="'.api_get_path(WEB_COURSE_PATH).$courseInfo['code'].'">'.$courseInfo['code'].' <a>';
1088
                // Allow sort by date hiding the numerical date
1089
                $course[] = '<span style="display:none;">'.$obj->access_date.'</span>'.api_convert_and_format_date($obj->access_date);
1090
                $courses[] = $course;
1091
            }
1092
            $parameters['date_diff'] = $date_diff;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$parameters was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parameters = array(); before regardless.
Loading history...
1093
            $parameters['report'] = 'courselastvisit';
1094
            $table_header[] = [get_lang("Course code"), true];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$table_header was never initialized. Although not strictly required by PHP, it is generally a good practice to add $table_header = array(); before regardless.
Loading history...
1095
            $table_header[] = [get_lang("Latest access"), true];
1096
1097
            ob_start();
1098
            Display:: display_sortable_table(
1099
                $table_header,
1100
                $courses,
1101
                ['column' => $column, 'direction' => $direction],
1102
                [],
1103
                $parameters
1104
            );
1105
            $content .= ob_get_contents();
1106
            ob_end_clean();
1107
        } else {
1108
            $content = get_lang('No search results');
1109
        }
1110
1111
        return $content;
1112
    }
1113
1114
    /**
1115
     * Displays the statistics of the messages sent and received by each user in the social network.
1116
     *
1117
     * @param string $messageType Type of message: 'sent' or 'received'
1118
     *
1119
     * @return array Message list
1120
     */
1121
    public static function getMessages(string $messageType): array
1122
    {
1123
        $messageTable = Database::get_main_table(TABLE_MESSAGE);
1124
        $messageRelUserTable = Database::get_main_table(TABLE_MESSAGE_REL_USER);
1125
        $userTable = Database::get_main_table(TABLE_MAIN_USER);
1126
        $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1127
1128
        switch ($messageType) {
1129
            case 'sent':
1130
                $field = 'm.user_sender_id';
1131
                $joinCondition = "m.id = mru.message_id AND mru.receiver_type = " . MessageRelUser::TYPE_SENDER;
1132
                break;
1133
            case 'received':
1134
                $field = 'mru.user_id';
1135
                $joinCondition = "m.id = mru.message_id AND mru.receiver_type = " . MessageRelUser::TYPE_TO;
1136
                break;
1137
        }
1138
1139
        $accessUrlHelper = Container::getAccessUrlHelper();
1140
1141
        if ($accessUrlHelper->isMultiple()) {
1142
            $accessUrl = $accessUrlHelper->getCurrent();
1143
            $urlId = $accessUrl->getId();
1144
            $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
1145
            FROM $messageTable m
1146
            INNER JOIN $messageRelUserTable mru ON $joinCondition
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $joinCondition does not seem to be defined for all execution paths leading up to this point.
Loading history...
1147
            INNER JOIN $userTable u ON $field = u.id
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $field does not seem to be defined for all execution paths leading up to this point.
Loading history...
1148
            INNER JOIN $accessUrlRelUserTable url ON u.id = url.user_id
1149
            WHERE url.access_url_id = $urlId
1150
            AND u.active <> " . USER_SOFT_DELETED . "
1151
            GROUP BY $field
1152
            ORDER BY count_message DESC";
1153
        } else {
1154
            $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
1155
            FROM $messageTable m
1156
            INNER JOIN $messageRelUserTable mru ON $joinCondition
1157
            INNER JOIN $userTable u ON $field = u.id
1158
            WHERE u.active <> " . USER_SOFT_DELETED . "
1159
            GROUP BY $field
1160
            ORDER BY count_message DESC";
1161
        }
1162
        $res = Database::query($sql);
1163
        $messages_sent = [];
1164
        while ($messages = Database::fetch_array($res)) {
1165
            if (empty($messages['username'])) {
1166
                $messages['username'] = get_lang('Unknown');
1167
            }
1168
            $users = api_get_person_name(
1169
                    $messages['firstname'],
1170
                    $messages['lastname']
1171
                ) . '<br />(' . $messages['username'] . ')';
1172
            $messages_sent[$users] = $messages['count_message'];
1173
        }
1174
1175
        return $messages_sent;
1176
    }
1177
1178
    /**
1179
     * Count the number of friends for each social network users.
1180
     * @throws Exception
1181
     */
1182
    public static function getFriends(): array
1183
    {
1184
        $user_friend_table = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
1185
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
1186
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1187
1188
        $accessUrlHelper = Container::getAccessUrlHelper();
1189
1190
        if ($accessUrlHelper->isMultiple()) {
1191
            $accessUrl = $accessUrlHelper->getCurrent();
1192
            $urlId = $accessUrl->getId();
1193
            $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
1194
                    FROM $access_url_rel_user_table as url, $user_friend_table uf
1195
                    LEFT JOIN $user_table u
1196
                    ON (uf.user_id = u.id) AND u.active <> ".USER_SOFT_DELETED."
1197
                    WHERE
1198
                        uf.relation_type <> '".UserRelUser::USER_RELATION_TYPE_RRHH."' AND
1199
                        uf.user_id = url.user_id AND
1200
                        access_url_id = $urlId
1201
                    GROUP BY uf.user_id
1202
                    ORDER BY count_friend DESC ";
1203
        } else {
1204
            $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
1205
                    FROM $user_friend_table uf
1206
                    LEFT JOIN $user_table u
1207
                    ON (uf.user_id = u.id) AND u.active <> ".USER_SOFT_DELETED."
1208
                    WHERE uf.relation_type <> '".UserRelUser::USER_RELATION_TYPE_RRHH."'
1209
                    GROUP BY uf.user_id
1210
                    ORDER BY count_friend DESC ";
1211
        }
1212
        $res = Database::query($sql);
1213
        $list_friends = [];
1214
        while ($friends = Database::fetch_array($res)) {
1215
            $users = api_get_person_name($friends['firstname'], $friends['lastname']).'<br />('.$friends['username'].')';
1216
            $list_friends[$users] = $friends['count_friend'];
1217
        }
1218
1219
        return $list_friends;
1220
    }
1221
1222
    /**
1223
     * Returns the number of users that didn't log in for a certain period of time.
1224
     * @throws Exception
1225
     */
1226
    public static function printUsersNotLoggedInStats(): string
1227
    {
1228
        $totalLogin = [];
1229
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1230
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1231
        $total = self::countUsers();
1232
        $accessUrlHelper = Container::getAccessUrlHelper();
1233
1234
        if ($accessUrlHelper->isMultiple()) {
1235
            $accessUrl = $accessUrlHelper->getCurrent();
1236
            $urlId = $accessUrl->getId();
1237
            $table_url = ", $access_url_rel_user_table";
1238
            $where_url = " AND login_user_id=user_id AND access_url_id = $urlId";
1239
        } else {
1240
            $table_url = '';
1241
            $where_url = '';
1242
        }
1243
        $now = api_get_utc_datetime();
1244
        $sql[get_lang('This day')] =
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sql was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sql = array(); before regardless.
Loading history...
1245
            "SELECT count(distinct(login_user_id)) AS number ".
1246
            " FROM $table $table_url ".
1247
            " WHERE DATE_ADD(login_date, INTERVAL 1 DAY) >= '$now' $where_url";
1248
        $sql[get_lang('In the last 7 days')] =
1249
            "SELECT count(distinct(login_user_id)) AS number ".
1250
            " FROM $table $table_url ".
1251
            " WHERE DATE_ADD(login_date, INTERVAL 7 DAY) >= '$now' $where_url";
1252
        $sql[get_lang('In the last 31 days')] =
1253
            "SELECT count(distinct(login_user_id)) AS number ".
1254
            " FROM $table $table_url ".
1255
            " WHERE DATE_ADD(login_date, INTERVAL 31 DAY) >= '$now' $where_url";
1256
        $sql[sprintf(get_lang('Last %d months'), 6)] =
1257
            "SELECT count(distinct(login_user_id)) AS number ".
1258
            " FROM $table $table_url ".
1259
            " WHERE DATE_ADD(login_date, INTERVAL 6 MONTH) >= '$now' $where_url";
1260
        $sql[get_lang('Never connected')] =
1261
            "SELECT count(distinct(login_user_id)) AS number ".
1262
            " FROM $table $table_url WHERE 1=1 $where_url";
1263
        foreach ($sql as $index => $query) {
1264
            $res = Database::query($query);
1265
            $obj = Database::fetch_object($res);
1266
            $r = $total - $obj->number;
1267
            $totalLogin[$index] = $r < 0 ? 0 : $r;
1268
        }
1269
1270
        return self::printStats(
1271
            get_lang('Not logged in for some time'),
1272
            $totalLogin,
1273
            false
1274
        );
1275
    }
1276
1277
    /**
1278
     * Returns an array with indexes as the 'yyyy-mm-dd' format of each date
1279
     * within the provided range (including limits). Dates are assumed to be
1280
     * given in UTC.
1281
     *
1282
     * @param string $startDate  Start date, in Y-m-d or Y-m-d h:i:s format
1283
     * @param string $endDate    End date, in Y-m-d or Y-m-d h:i:s format
1284
     * @param ?bool   $removeYear Whether to remove the year in the results (for easier reading)
1285
     *
1286
     * @return array|bool False on error in the params, array of [date1 => 0, date2 => 0, ...] otherwise
1287
     */
1288
    public static function buildDatesArray(string $startDate, string $endDate, ?bool $removeYear = false): mixed
1289
    {
1290
        if (strlen($startDate) > 10) {
1291
            $startDate = substr($startDate, 0, 10);
1292
        }
1293
        if (strlen($endDate) > 10) {
1294
            $endDate = substr($endDate, 0, 10);
1295
        }
1296
        if (!preg_match('/\d\d\d\d-\d\d-\d\d/', $startDate)) {
1297
            return false;
1298
        }
1299
        if (!preg_match('/\d\d\d\d-\d\d-\d\d/', $startDate)) {
1300
            return false;
1301
        }
1302
        $startTimestamp = strtotime($startDate);
1303
        $endTimestamp = strtotime($endDate);
1304
        $list = [];
1305
        for ($time = $startTimestamp; $time < $endTimestamp; $time += 86400) {
1306
            $datetime = api_get_utc_datetime($time);
1307
            if ($removeYear) {
1308
                $datetime = substr($datetime, 5, 5);
1309
            } else {
1310
                $dateTime = substr($datetime, 0, 10);
1311
            }
1312
            $list[$datetime] = 0;
1313
        }
1314
1315
        return $list;
1316
    }
1317
1318
    /**
1319
     * Prepare the JS code to load a chart.
1320
     *
1321
     * @param string $url     URL for AJAX data generator
1322
     * @param ?string $type    bar, line, pie, etc (defaults to 'pie')
1323
     * @param ?string $options Additional options to the chart (see chart-specific library)
1324
     * @param ?string A JS code for loading the chart together with a call to AJAX data generator
1325
     */
1326
    public static function getJSChartTemplate(string $url, ?string $type = 'pie', ?string $options = '', ?string $elementId = 'canvas')
1327
    {
1328
        return '
1329
        <script>
1330
        $(function() {
1331
            $.ajax({
1332
                url: "'.$url.'",
1333
                type: "POST",
1334
                success: function(data) {
1335
                    Chart.defaults.responsive = false;
1336
                    var ctx = document.getElementById("'.$elementId.'").getContext("2d");
1337
                    ctx.canvas.width = 420;
1338
                    ctx.canvas.height = 420;
1339
                    var chart = new Chart(ctx, {
1340
                        type: "'.$type.'",
1341
                        data: data,
1342
                        options: {
1343
                            plugins: {
1344
                                '.$options.'
1345
                            },
1346
                            cutout: "25%"
1347
                        }
1348
                    });
1349
                    var title = chart.options.plugins.title.text;
1350
                    $("#'.$elementId.'_title").html(title);
1351
                    $("#'.$elementId.'_table").html(data.table);
1352
                }
1353
            });
1354
        });
1355
        </script>';
1356
1357
    }
1358
1359
    /**
1360
     * Return template for a JS chart
1361
     * @param $data
1362
     * @param $type
1363
     * @param $options
1364
     * @param $elementId
1365
     * @param $responsive
1366
     * @param $onClickHandler
1367
     * @param $extraButtonHandler
1368
     * @param $canvasDimensions
1369
     * @return string
1370
     */
1371
    public static function getJSChartTemplateWithData(
1372
        $data,
1373
        ?string $type = 'pie',
1374
        ?string $options = '',
1375
        ?string $elementId = 'canvas',
1376
        ?bool $responsive = true,
1377
        ?string $onClickHandler = '',
1378
        ?string $extraButtonHandler = '',
1379
        ?array $canvasDimensions = ['width' => 420, 'height' => 420]
1380
    ): string {
1381
        $data = json_encode($data);
1382
        $responsiveValue = $responsive ? 'true' : 'false';
1383
1384
        $indexAxisOption = '';
1385
        if ($type === 'bar') {
1386
            $indexAxisOption = 'indexAxis: "y",';
1387
        }
1388
1389
        $onClickScript = '';
1390
        if (!empty($onClickHandler)) {
1391
            $onClickScript = '
1392
                onClick: function(evt) {
1393
                    '.$onClickHandler.'
1394
                },
1395
            ';
1396
        }
1397
1398
        $canvasSize = '';
1399
        if ($responsiveValue === 'false') {
1400
            $canvasSize = '
1401
            ctx.canvas.width = '.$canvasDimensions['width'].';
1402
            ctx.canvas.height = '.$canvasDimensions['height'].';
1403
            ';
1404
        }
1405
1406
        return '
1407
        <script>
1408
            $(function() {
1409
                Chart.defaults.responsive = '.$responsiveValue.';
1410
                var ctx = document.getElementById("'.$elementId.'").getContext("2d");
1411
                '.$canvasSize.'
1412
                var chart = new Chart(ctx, {
1413
                    type: "'.$type.'",
1414
                    data: '.$data.',
1415
                    options: {
1416
                        plugins: {
1417
                            '.$options.',
1418
                            datalabels: {
1419
                                anchor: "end",
1420
                                align: "left",
1421
                                formatter: function(value) {
1422
                                    return value;
1423
                                },
1424
                                color: "#000"
1425
                            },
1426
                        },
1427
                        '.$indexAxisOption.'
1428
                        scales: {
1429
                            x: { beginAtZero: true },
1430
                            y: { barPercentage: 0.5 }
1431
                        },
1432
                        '.$onClickScript.'
1433
                    }
1434
                });
1435
                var title = chart.options.plugins.title.text;
1436
                $("#'.$elementId.'_title").html(title);
1437
                $("#'.$elementId.'_table").html(chart.data.datasets[0].data);
1438
1439
                '.$extraButtonHandler.'
1440
            });
1441
        </script>';
1442
    }
1443
1444
    public static function buildJsChartData(array $all, string $chartName): array
1445
    {
1446
        $list = [];
1447
        $palette = ChamiloApi::getColorPalette(true, true);
1448
        foreach ($all as $tick => $tock) {
1449
            $list['labels'][] = $tick;
1450
        }
1451
1452
        $list['datasets'][0]['label'] = $chartName;
1453
        $list['datasets'][0]['borderColor'] = 'rgba(255,255,255,1)';
1454
1455
        $i = 0;
1456
        foreach ($all as $tick => $tock) {
1457
            $j = $i % count($palette);
1458
            $list['datasets'][0]['data'][] = $tock;
1459
            $list['datasets'][0]['backgroundColor'][] = $palette[$j];
1460
            $i++;
1461
        }
1462
1463
        $scoreDisplay = ScoreDisplay::instance();
1464
        $table = new HTML_Table(['class' => 'data_table stats_table']);
1465
        $headers = [
1466
            get_lang('Name'),
1467
            get_lang('Count'),
1468
            get_lang('Percentage'),
1469
        ];
1470
        $row = 0;
1471
        $column = 0;
1472
        foreach ($headers as $header) {
1473
            $table->setHeaderContents($row, $column, $header);
1474
            $column++;
1475
        }
1476
1477
        $total = 0;
1478
        foreach ($all as $name => $value) {
1479
            $total += $value;
1480
        }
1481
        $row++;
1482
        foreach ($all as $name => $value) {
1483
            $table->setCellContents($row, 0, $name);
1484
            $table->setCellContents($row, 1, $value);
1485
            $table->setCellContents($row, 2, $scoreDisplay->display_score([$value, $total], SCORE_PERCENT));
1486
            $row++;
1487
        }
1488
        $table = Display::page_subheader2($chartName).$table->toHtml();
1489
1490
        return ['chart' => $list, 'table' => $table];
1491
    }
1492
1493
    /**
1494
     * Display the Logins By Date report and allow export its result to XLS.
1495
     */
1496
    public static function printLoginsByDate(): mixed
1497
    {
1498
        if (isset($_GET['export']) && 'xls' === $_GET['export']) {
1499
            $result = self::getLoginsByDate($_GET['start'], $_GET['end']);
1500
            $data = [[get_lang('Username'), get_lang('First name'), get_lang('Last name'), get_lang('Total time')]];
1501
1502
            foreach ($result as $i => $item) {
1503
                $data[] = [
1504
                    $item['username'],
1505
                    $item['firstname'],
1506
                    $item['lastname'],
1507
                    api_time_to_hms($item['time_count']),
1508
                ];
1509
            }
1510
1511
            Export::arrayToXls($data);
1512
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1513
        }
1514
1515
        $content = Display::page_header(get_lang('Logins by date'));
1516
1517
        $actions = '';
1518
        $form = new FormValidator('frm_logins_by_date', 'get');
1519
        $form->addDateRangePicker(
1520
            'daterange',
1521
            get_lang('Date range'),
1522
            true,
1523
            ['format' => 'YYYY-MM-DD', 'timePicker' => 'false', 'validate_format' => 'Y-m-d']
1524
        );
1525
        $form->addHidden('report', 'logins_by_date');
1526
        $form->addButtonFilter(get_lang('Search'));
1527
1528
        if ($form->validate()) {
1529
            $values = $form->exportValues();
1530
1531
            $result = self::getLoginsByDate($values['daterange_start'], $values['daterange_end']);
1532
1533
            if (!empty($result)) {
1534
                $actions = Display::url(
1535
                    Display::getMdiIcon(ActionIcon::EXPORT_SPREADSHEET, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export to XLS')),
1536
                    api_get_self().'?'.http_build_query(
1537
                        [
1538
                            'report' => 'logins_by_date',
1539
                            'export' => 'xls',
1540
                            'start' => Security::remove_XSS($values['daterange_start']),
1541
                            'end' => Security::remove_XSS($values['daterange_end']),
1542
                        ]
1543
                    )
1544
                );
1545
            }
1546
1547
            $table = new HTML_Table(['class' => 'data_table stats_table']);
1548
            $table->setHeaderContents(0, 0, get_lang('Username'));
1549
            $table->setHeaderContents(0, 1, get_lang('First name'));
1550
            $table->setHeaderContents(0, 2, get_lang('Last name'));
1551
            $table->setHeaderContents(0, 3, get_lang('Total time'));
1552
1553
            foreach ($result as $i => $item) {
1554
                $table->setCellContents($i + 1, 0, $item['username']);
1555
                $table->setCellContents($i + 1, 1, $item['firstname']);
1556
                $table->setCellContents($i + 1, 2, $item['lastname']);
1557
                $table->setCellContents($i + 1, 3, api_time_to_hms($item['time_count']));
1558
            }
1559
1560
            $table->setColAttributes(0, ['class' => 'text-center']);
1561
            $table->setColAttributes(3, ['class' => 'text-center']);
1562
            $content = $table->toHtml();
1563
        }
1564
1565
        $content .= $form->returnForm();
1566
1567
        if (!empty($actions)) {
1568
            $content .= Display::toolbarAction('logins_by_date_toolbar', [$actions]);
1569
        }
1570
1571
        return $content;
1572
    }
1573
1574
    /**
1575
     * Return HTML table for the student boss role, for the given user ID
1576
     * @param int $bossId
1577
     * @return string
1578
     */
1579
    public static function getBossTable(int $bossId): string
1580
    {
1581
        $students = UserManager::getUsersFollowedByStudentBoss(
1582
            $bossId,
1583
            0,
1584
            false,
1585
            false,
1586
            false,
1587
            null,
1588
            null,
1589
            null,
1590
            null,
1591
            1
1592
        );
1593
1594
        if (!empty($students)) {
1595
            $table = new HTML_Table(['class' => 'table table-responsive', 'id' => 'table_'.$bossId]);
1596
            $headers = [
1597
                get_lang('Name'),
1598
            ];
1599
            $row = 0;
1600
            $column = 0;
1601
            foreach ($headers as $header) {
1602
                $table->setHeaderContents($row, $column, $header);
1603
                $column++;
1604
            }
1605
            $row++;
1606
            foreach ($students as $student) {
1607
                $column = 0;
1608
                $content = api_get_person_name($student['firstname'], $student['lastname']);
1609
                $content = '<div style="width: 200px; overflow-wrap: break-word;">'.$content.'</div>';
1610
                $table->setCellContents(
1611
                    $row,
1612
                    $column++,
1613
                    $content
1614
                );
1615
                $row++;
1616
            }
1617
1618
            return $table->toHtml();
1619
        }
1620
1621
        return '<table id="table_'.$bossId.'"></table>';
1622
    }
1623
1624
    /**
1625
     * @param string $startDate
1626
     * @param string $endDate
1627
     *
1628
     * @return array
1629
     * @throws Exception
1630
     */
1631
    public static function getLoginsByDate(string $startDate, string $endDate): array
1632
    {
1633
        $startDate = api_get_utc_datetime("$startDate 00:00:00");
1634
        $endDate = api_get_utc_datetime("$endDate 23:59:59");
1635
1636
        if (empty($startDate) || empty($endDate)) {
1637
            return [];
1638
        }
1639
1640
        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
1641
        $tblLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1642
        $urlJoin = '';
1643
        $urlWhere = '';
1644
1645
        $accessUrlHelper = Container::getAccessUrlHelper();
1646
1647
        if ($accessUrlHelper->isMultiple()) {
1648
            $accessUrl = $accessUrlHelper->getCurrent();
1649
            $urlId = $accessUrl->getId();
1650
            $tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1651
1652
            $urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id";
1653
            $urlWhere = "AND au.access_url_id = $urlId";
1654
        }
1655
1656
        $sql = "SELECT u.id,
1657
                    u.firstname,
1658
                    u.lastname,
1659
                    u.username,
1660
                    SUM(TIMESTAMPDIFF(SECOND, l.login_date, l.logout_date)) AS time_count
1661
                FROM $tblUser u
1662
                INNER JOIN $tblLogin l
1663
                ON u.id = l.login_user_id
1664
                $urlJoin
1665
                WHERE u.active <> ".USER_SOFT_DELETED." AND l.login_date BETWEEN '$startDate' AND '$endDate'
1666
                $urlWhere
1667
                GROUP BY u.id";
1668
1669
        $stmt = Database::query($sql);
1670
1671
        return Database::store_result($stmt, 'ASSOC');
1672
    }
1673
1674
    /**
1675
     * Gets the number of new users registered between two dates.
1676
     * @throws Exception
1677
     */
1678
    public static function getNewUserRegistrations(string $startDate, string $endDate): array
1679
    {
1680
        $sql = "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as reg_date, COUNT(*) as user_count
1681
            FROM user
1682
            WHERE created_at BETWEEN '$startDate' AND '$endDate'
1683
            GROUP BY reg_date";
1684
1685
        $result = Database::query($sql);
1686
        $data = [];
1687
        while ($row = Database::fetch_array($result)) {
1688
            $userCount = is_numeric($row['user_count']) ? (int) $row['user_count'] : 0;
1689
            $data[] = ['date' => $row['reg_date'], 'count' => $userCount];
1690
        }
1691
1692
        return $data;
1693
    }
1694
1695
    /**
1696
     * Gets the number of users registered by creator (creator_id) between two dates.
1697
     * @throws Exception
1698
     */
1699
    public static function getUserRegistrationsByCreator(string $startDate, string $endDate): array
1700
    {
1701
        $sql = "SELECT u.creator_id, COUNT(u.id) as user_count, c.firstname, c.lastname
1702
                FROM user u
1703
                LEFT JOIN user c ON u.creator_id = c.id
1704
                WHERE u.created_at BETWEEN '$startDate' AND '$endDate'
1705
                AND u.creator_id IS NOT NULL
1706
                GROUP BY u.creator_id";
1707
1708
        $result = Database::query($sql);
1709
        $data = [];
1710
        while ($row = Database::fetch_array($result)) {
1711
            $userCount = is_numeric($row['user_count']) ? (int) $row['user_count'] : 0;
1712
            $name = trim($row['firstname'] . ' ' . $row['lastname']);
1713
            if (!empty($name)) {
1714
                $data[] = [
1715
                    'name' => $name,
1716
                    'count' => $userCount
1717
                ];
1718
            }
1719
        }
1720
1721
        return $data;
1722
    }
1723
1724
    /**
1725
     * Initializes an array with dates between two given dates, setting each date's value to 0.
1726
     * @throws Exception
1727
     */
1728
    public static function initializeDateRangeArray(string $startDate, string $endDate): array
1729
    {
1730
        $dateRangeArray = [];
1731
        $currentDate = new DateTime($startDate);
1732
        $endDate = new DateTime($endDate);
1733
1734
        // Loop through the date range and initialize each date with 0
1735
        while ($currentDate <= $endDate) {
1736
            $formattedDate = $currentDate->format('Y-m-d');
1737
            $dateRangeArray[$formattedDate] = 0;
1738
            $currentDate->modify('+1 day');
1739
        }
1740
1741
        return $dateRangeArray;
1742
    }
1743
1744
    /**
1745
     * Checks if the difference between two dates is more than one month.
1746
     * @throws Exception
1747
     */
1748
    public static function isMoreThanAMonth(string $dateStart, string $dateEnd): bool
1749
    {
1750
        $startDate = new DateTime($dateStart);
1751
        $endDate = new DateTime($dateEnd);
1752
1753
        $diff = $startDate->diff($endDate);
1754
1755
        if ($diff->y >= 1) {
1756
            return true;
1757
        }
1758
1759
        if ($diff->m > 1) {
1760
            return true;
1761
        }
1762
1763
        if ($diff->m == 1) {
1764
            return $diff->d > 0;
1765
        }
1766
1767
        return false;
1768
    }
1769
1770
    /**
1771
     * Groups registration data by month.
1772
     * @throws Exception
1773
     */
1774
    public static function groupByMonth(array $registrations): array
1775
    {
1776
        $groupedData = [];
1777
1778
        foreach ($registrations as $registration) {
1779
            $monthYear = (new DateTime($registration['date']))->format('Y-m');
1780
            if (isset($groupedData[$monthYear])) {
1781
                $groupedData[$monthYear] += $registration['count'];
1782
            } else {
1783
                $groupedData[$monthYear] = $registration['count'];
1784
            }
1785
        }
1786
1787
        return $groupedData;
1788
    }
1789
1790
    /**
1791
     * Retrieves the available tools using the repository.
1792
     */
1793
    public static function getAvailableTools(): array
1794
    {
1795
        $em = Database::getManager();
1796
        $repo = $em->getRepository(ResourceLink::class);
1797
1798
        return $repo->getAvailableTools();
1799
    }
1800
1801
    /**
1802
     * Generates a report of tool usage based on the provided tool IDs.
1803
     */
1804
    public static function getToolUsageReportByTools(array $toolIds): array
1805
    {
1806
        $em = Database::getManager();
1807
        $repo = $em->getRepository(ResourceLink::class);
1808
1809
        return $repo->getToolUsageReportByTools($toolIds);
1810
    }
1811
1812
    /**
1813
     * Return de number of certificates generated.
1814
     * This function is resource intensive.
1815
     * @throws \Doctrine\DBAL\Exception
1816
     * @throws Exception
1817
     */
1818
    public static function countCertificatesByQuarter(string $dateFrom = null, string $dateUntil = null): int
1819
    {
1820
        $tableGradebookCertificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
1821
        $condition = "";
1822
        if (!empty($dateFrom) && !empty($dateUntil)) {
1823
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1824
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1825
            $condition = "WHERE (created_at BETWEEN '$dateFrom' AND '$dateUntil')";
1826
        } elseif (!empty($dateFrom)) {
1827
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1828
            $condition = "WHERE created_at >= '$dateFrom'";
1829
        } elseif (!empty($dateUntil)) {
1830
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1831
            $condition = "WHERE created_at <= '$dateUntil'";
1832
        }
1833
        $sql = "
1834
            SELECT count(*) AS count
1835
            FROM $tableGradebookCertificate
1836
            $condition
1837
        ";
1838
        $response = Database::query($sql);
1839
        $obj = Database::fetch_object($response);
1840
        return $obj->count;
1841
    }
1842
1843
    /**
1844
     * Get the number of logins by dates.
1845
     * This function is resource intensive.
1846
     * @throws Exception
1847
     */
1848
    public static function getSessionsByDuration(string $dateFrom, string $dateUntil): array
1849
    {
1850
        $results = [
1851
            '0' => 0,
1852
            '5' => 0,
1853
            '10' => 0,
1854
            '15' => 0,
1855
            '30' => 0,
1856
            '60' => 0,
1857
        ];
1858
        if (!empty($dateFrom) && !empty($dateUntil)) {
1859
            $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1860
            $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1861
            $urlId = api_get_current_access_url_id();
1862
            $tableUrl = '';
1863
            $whereUrl = '';
1864
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1865
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1866
            $accessUrlHelper = Container::getAccessUrlHelper();
1867
1868
            if ($accessUrlHelper->isMultiple()) {
1869
                $accessUrl = $accessUrlHelper->getCurrent();
1870
                $urlId = $accessUrl->getId();
1871
                $tableUrl = ", $accessUrlRelUserTable";
1872
                $whereUrl = " AND login_user_id = user_id AND access_url_id = $urlId";
1873
            }
1874
            $sql = "SELECT login_id, TIMESTAMPDIFF(SECOND, login_date, logout_date) AS duration
1875
            FROM $table $tableUrl
1876
            WHERE login_date >= '$dateFrom'
1877
            AND logout_date <= '$dateUntil'
1878
            $whereUrl
1879
            ";
1880
            $res = Database::query($sql);
1881
            while ($session = Database::fetch_array($res)) {
1882
                if ($session['duration'] > 3600) {
1883
                    $results['60']++;
1884
                } elseif ($session['duration'] > 1800) {
1885
                    $results['30']++;
1886
                } elseif ($session['duration'] > 900) {
1887
                    $results['15']++;
1888
                } elseif ($session['duration'] > 600) {
1889
                    $results['10']++;
1890
                } elseif ($session['duration'] > 300) {
1891
                    $results['5']++;
1892
                } else {
1893
                    $results['0']++;
1894
                }
1895
            }
1896
        }
1897
        return $results;
1898
    }
1899
1900
    /**
1901
     * Returns the number of user subscriptions grouped by day.
1902
     */
1903
    public static function getSubscriptionsByDay(string $startDate, string $endDate): array
1904
    {
1905
        $conn = Database::getManager()->getConnection();
1906
        $sql = "
1907
        SELECT DATE(default_date) AS date, COUNT(default_id) AS count
1908
        FROM track_e_default
1909
        WHERE default_event_type = :eventType
1910
        AND default_date BETWEEN :start AND :end
1911
        GROUP BY DATE(default_date)
1912
        ORDER BY DATE(default_date)
1913
    ";
1914
1915
        return $conn->executeQuery($sql, [
1916
            'eventType' => 'user_subscribed',
1917
            'start' => $startDate.' 00:00:00',
1918
            'end' => $endDate.' 23:59:59',
1919
        ])->fetchAllAssociative();
1920
    }
1921
1922
    /**
1923
     * Returns the number of user unsubscriptions grouped by day.
1924
     */
1925
    public static function getUnsubscriptionsByDay(string $startDate, string $endDate): array
1926
    {
1927
        $conn = Database::getManager()->getConnection();
1928
        $sql = "
1929
        SELECT DATE(default_date) AS date, COUNT(default_id) AS count
1930
        FROM track_e_default
1931
        WHERE default_event_type IN (:eventType1, :eventType2)
1932
        AND default_date BETWEEN :start AND :end
1933
        GROUP BY DATE(default_date)
1934
        ORDER BY DATE(default_date)
1935
    ";
1936
1937
        return $conn->executeQuery($sql, [
1938
            'eventType1' => 'user_unsubscribed',
1939
            'eventType2' => 'session_user_deleted',
1940
            'start' => $startDate.' 00:00:00',
1941
            'end' => $endDate.' 23:59:59',
1942
        ], [
1943
            'eventType1' => ParameterType::STRING,
1944
            'eventType2' => ParameterType::STRING,
1945
        ])->fetchAllAssociative();
1946
    }
1947
}
1948