Statistics::getJSChartTemplateWithData()   A
last analyzed

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\Entity\Course;
5
use Chamilo\CoreBundle\Entity\CourseRelUser;
6
use Chamilo\CoreBundle\Entity\MessageRelUser;
7
use Chamilo\CoreBundle\Entity\ResourceLink;
8
use Chamilo\CoreBundle\Entity\TrackEAccess;
9
use Chamilo\CoreBundle\Entity\User;
10
use Chamilo\CoreBundle\Entity\UserRelUser;
11
use Chamilo\CoreBundle\Enums\ActionIcon;
12
use Chamilo\CoreBundle\Framework\Container;
13
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
14
use Doctrine\DBAL\ParameterType;
15
16
/**
17
 * This class provides some functions for statistics.
18
 */
19
class Statistics
20
{
21
    /**
22
     * Converts a number of bytes in a formatted string.
23
     *
24
     * @param int $size
25
     *
26
     * @return string Formatted file size or empty string if no match
27
     */
28
    public static function makeSizeString(int $size): string
29
    {
30
        if ($size < pow(2, 10)) {
31
            return $size." bytes";
32
        }
33
        if ($size >= pow(2, 10) && $size < pow(2, 20)) {
34
            return round($size / pow(2, 10), 0)." KB";
35
        }
36
        if ($size >= pow(2, 20) && $size < pow(2, 30)) {
37
            return round($size / pow(2, 20), 1)." MB";
38
        }
39
        if ($size > pow(2, 30)) {
40
            return round($size / pow(2, 30), 2)." GB";
41
        }
42
43
        return '';
44
    }
45
46
    /**
47
     * Count courses.
48
     *
49
     * @param string|null $categoryCode Code of a course category.
50
     *                                  Default: count all courses.
51
     * @param string|null $dateFrom dateFrom
52
     * @param string|null $dateUntil dateUntil
53
     *
54
     * @return int Number of courses counted
55
     * @throws \Doctrine\DBAL\Exception
56
     * @throws Exception
57
     */
58
    public static function countCourses(string $categoryCode = null, string $dateFrom = null, string $dateUntil = null): int
59
    {
60
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
61
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
62
        $accessUrlUtil = Container::getAccessUrlUtil();
63
64
        if ($accessUrlUtil->isMultiple()) {
65
            $accessUrl = $accessUrlUtil->getCurrent();
66
            $urlId = $accessUrl->getId();
67
            $sql = "SELECT COUNT(*) AS number
68
                    FROM $courseTable AS c, $accessUrlRelCourseTable AS u
69
                    WHERE u.c_id = c.id AND u.access_url_id = $urlId";
70
            if (isset($categoryCode)) {
71
                $sql .= " AND category_code = '".Database::escape_string($categoryCode)."'";
72
            }
73
        } else {
74
            $sql = "SELECT COUNT(*) AS number
75
                    FROM $courseTable AS c
76
                    WHERE 1 = 1";
77
            if (isset($categoryCode)) {
78
                $sql .= " WHERE c.category_code = '".Database::escape_string($categoryCode)."'";
79
            }
80
        }
81
82
        if (!empty($dateFrom)) {
83
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
84
            $sql .= " AND c.creation_date >= '$dateFrom' ";
85
        }
86
        if (!empty($dateUntil)) {
87
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
88
            $sql .= " AND c.creation_date <= '$dateUntil' ";
89
        }
90
91
        $res = Database::query($sql);
92
        $obj = Database::fetch_object($res);
93
94
        return $obj->number;
95
    }
96
97
    /**
98
     * Count courses by visibility.
99
     *
100
     * @param array|null  $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses
101
     * @param string|null $dateFrom dateFrom
102
     * @param string|null $dateUntil dateUntil
103
     *
104
     * @return int Number of courses counted
105
     * @throws \Doctrine\DBAL\Exception
106
     * @throws Exception
107
     */
108
    public static function countCoursesByVisibility(
109
        array $visibility = null,
110
        string $dateFrom = null,
111
        string $dateUntil = null
112
    ): int
113
    {
114
        $visibilityString = '';
115
        if (empty($visibility)) {
116
            return 0;
117
        } else {
118
            $auxArrayVisibility = [];
119
            if (!is_array($visibility)) {
120
                $visibility = [$visibility];
121
            }
122
            foreach ($visibility as $item) {
123
                $auxArrayVisibility[] = (int) $item;
124
            }
125
            $visibilityString = implode(',', $auxArrayVisibility);
126
        }
127
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
128
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
129
        $accessUrlUtil = Container::getAccessUrlUtil();
130
131
        if ($accessUrlUtil->isMultiple()) {
132
            $accessUrl = $accessUrlUtil->getCurrent();
133
            $urlId = $accessUrl->getId();
134
            $sql = "SELECT COUNT(*) AS number
135
                    FROM $courseTable AS c, $accessUrlRelCourseTable AS u
136
                    WHERE u.c_id = c.id AND u.access_url_id = $urlId";
137
        } else {
138
            $sql = "SELECT COUNT(*) AS number
139
                    FROM $courseTable AS c
140
                    WHERE 1 = 1";
141
        }
142
        $sql .= " AND visibility IN ($visibilityString) ";
143
        if (!empty($dateFrom)) {
144
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
145
            $sql .= " AND c.creation_date >= '$dateFrom' ";
146
        }
147
        if (!empty($dateUntil)) {
148
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
149
            $sql .= " AND c.creation_date <= '$dateUntil' ";
150
        }
151
        $res = Database::query($sql);
152
        $obj = Database::fetch_object($res);
153
154
        return $obj->number;
155
    }
156
157
    /**
158
     * Count users.
159
     *
160
     * @param int    $status user status (COURSEMANAGER or STUDENT) if not setted it'll count all users
161
     * @param string $categoryCode course category code. Default: count only users without filtering category
162
     * @param bool   $countInvisibleCourses Count invisible courses (todo)
163
     * @param bool   $onlyActive Count only active users (false to only return currently active users)
164
     *
165
     * @return int Number of users counted
166
     * @throws Exception
167
     */
168
    public static function countUsers(
169
        ?int $status = null,
170
        ?string $categoryCode = null,
171
        ?bool $countInvisibleCourses = true,
172
        ?bool $onlyActive = false
173
    ): int
174
    {
175
        // Database table definitions
176
        $course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
177
        $course_table = Database::get_main_table(TABLE_MAIN_COURSE);
178
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
179
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
180
        $tblCourseCategory = Database::get_main_table(TABLE_MAIN_CATEGORY);
181
        $tblCourseRelCategory = Database::get_main_table(TABLE_MAIN_COURSE_REL_CATEGORY);
182
183
        $conditions = [];
184
        $conditions[] = "u.active <> " . USER_SOFT_DELETED;
185
        if ($onlyActive) {
186
            $conditions[] = "u.active = 1";
187
        }
188
        if (isset($status)) {
189
            $conditions[] = "u.status = " . $status;
190
        }
191
192
        $where = implode(' AND ', $conditions);
193
194
        $accessUrlUtil = Container::getAccessUrlUtil();
195
196
        if ($accessUrlUtil->isMultiple()) {
197
            $accessUrl = $accessUrlUtil->getCurrent();
198
            $urlId = $accessUrl->getId();
199
            $sql = "SELECT COUNT(DISTINCT(u.id)) AS number
200
                FROM $user_table as u
201
                INNER JOIN $access_url_rel_user_table as url ON u.id = url.user_id
202
                WHERE $where AND url.access_url_id = $urlId";
203
204
            if (isset($categoryCode)) {
205
                $categoryCode = Database::escape_string($categoryCode);
206
                $sql = "SELECT COUNT(DISTINCT(cu.user_id)) AS number
207
                    FROM $course_user_table cu
208
                    INNER JOIN $course_table c ON c.id = cu.c_id
209
                    INNER JOIN $access_url_rel_user_table as url ON cu.user_id = url.user_id
210
                    INNER JOIN $tblCourseRelCategory crc ON crc.course_id = c.id
211
                    INNER JOIN $tblCourseCategory cc ON cc.id = crc.course_category_id
212
                    INNER JOIN $user_table u ON cu.user_id = u.id
213
                    WHERE $where AND url.access_url_id = $urlId AND cc.code = '$categoryCode'";
214
            }
215
        } else {
216
            $sql = "SELECT COUNT(DISTINCT(id)) AS number
217
                FROM $user_table u
218
                WHERE $where";
219
220
            if (isset($categoryCode)) {
221
                $categoryCode = Database::escape_string($categoryCode);
222
                $sql = "SELECT COUNT(DISTINCT(cu.user_id)) AS number
223
                    FROM $course_user_table cu
224
                    INNER JOIN $course_table c ON c.id = cu.c_id
225
                    INNER JOIN $tblCourseRelCategory crc ON crc.course_id = c.id
226
                    INNER JOIN $tblCourseCategory cc ON cc.id = crc.course_category_id
227
                    INNER JOIN $user_table u ON u.id = cu.user_id
228
                    WHERE $where AND cc.code = '$categoryCode'";
229
            }
230
        }
231
232
        $res = Database::query($sql);
233
        $obj = Database::fetch_object($res);
234
235
        return $obj->number;
236
    }
237
238
    /**
239
     * Get courses IDs from courses with some access_date between the two given dates
240
     * @param string $startDate
241
     * @param string $endDate
242
     *
243
     * @return array
244
     * @throws Exception
245
     */
246
    public static function getCoursesWithActivity(string $startDate, string $endDate): array
247
    {
248
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
249
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
250
        $startDate = Database::escape_string($startDate);
251
        $endDate = Database::escape_string($endDate);
252
253
        $accessUrlUtil = Container::getAccessUrlUtil();
254
255
        if ($accessUrlUtil->isMultiple()) {
256
            $accessUrl = $accessUrlUtil->getCurrent();
257
            $urlId = $accessUrl->getId();
258
            $sql = "SELECT DISTINCT(t.c_id) FROM $table t , $access_url_rel_course_table a
259
                    WHERE
260
                        t.c_id = a.c_id AND
261
                        access_url_id = $urlId AND
262
                        access_date BETWEEN '$startDate' AND '$endDate'
263
                    ";
264
        } else {
265
            $sql = "SELECT DISTINCT(t.c_id) FROM $table t
266
                   access_date BETWEEN '$startDate' AND '$endDate' ";
267
        }
268
269
        $result = Database::query($sql);
270
271
        return Database::store_result($result);
272
    }
273
274
    /**
275
     * Count activities from track_e_default_table.
276
     *
277
     * @return int Number of activities counted
278
     * @throws Exception
279
     */
280
    public static function getNumberOfActivities(mixed $courseId = 0, ?int $sessionId = 0): int
281
    {
282
        // Database table definitions
283
        $track_e_default = Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT);
284
        $table_user = Database::get_main_table(TABLE_MAIN_USER);
285
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
286
        $accessUrlUtil = Container::getAccessUrlUtil();
287
        if (is_array($courseId)) {
288
            // Usually when no param is given, we get an empty array from SortableTable
289
            $courseId = 0;
290
        }
291
292
        if ($accessUrlUtil->isMultiple()) {
293
            $accessUrl = $accessUrlUtil->getCurrent();
294
            $urlId = $accessUrl->getId();
295
            $sql = "SELECT count(default_id) AS total_number_of_items
296
                    FROM $track_e_default, $table_user user, $access_url_rel_user_table url
297
                    WHERE user.active <> ".USER_SOFT_DELETED." AND
298
                        default_user_id = user.id AND
299
                        user.id = url.user_id AND
300
                        access_url_id = $urlId";
301
        } else {
302
            $sql = "SELECT count(default_id) AS total_number_of_items
303
                    FROM $track_e_default, $table_user user
304
                    WHERE user.active <> ".USER_SOFT_DELETED." AND default_user_id = user.id ";
305
        }
306
307
        if (!empty($courseId)) {
308
            $courseId = (int) $courseId;
309
            $sql .= " AND c_id = $courseId";
310
            $sql .= api_get_session_condition($sessionId);
311
        }
312
313
        if (isset($_GET['keyword'])) {
314
            $keyword = Database::escape_string(trim($_GET['keyword']));
315
            $sql .= " AND (
316
                        user.username LIKE '%".$keyword."%' OR
317
                        default_event_type LIKE '%".$keyword."%' OR
318
                        default_value_type LIKE '%".$keyword."%' OR
319
                        default_value LIKE '%".$keyword."%') ";
320
        }
321
        $res = Database::query($sql);
322
        $obj = Database::fetch_object($res);
323
324
        return $obj->total_number_of_items;
325
    }
326
327
    /**
328
     * Get activities data to display.
329
     *
330
     * @param int    $from
331
     * @param int    $numberOfItems
332
     * @param int    $column
333
     * @param string $direction
334
     * @param ?int   $courseId
335
     * @param ?int   $sessionId
336
     *
337
     * @return array
338
     * @throws Exception
339
     */
340
    public static function getActivitiesData(
341
        int $from,
342
        int $numberOfItems,
343
        int $column,
344
        string $direction,
345
        mixed $courseId = 0,
346
        ?int $sessionId = 0
347
    ): array
348
    {
349
        $track_e_default = Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT);
350
        $table_user = Database::get_main_table(TABLE_MAIN_USER);
351
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
352
        $direction = strtoupper($direction);
353
        if (is_array($courseId)) {
354
            // Usually when no param is given, we get an empty array from SortableTable
355
            $courseId = 0;
356
        }
357
358
        if (!in_array($direction, ['ASC', 'DESC'])) {
359
            $direction = 'DESC';
360
        }
361
362
        $accessUrlUtil = Container::getAccessUrlUtil();
363
364
        if ($accessUrlUtil->isMultiple()) {
365
            $accessUrl = $accessUrlUtil->getCurrent();
366
            $urlId = $accessUrl->getId();
367
            $sql = "SELECT
368
                        default_event_type  as col0,
369
                        default_value_type    as col1,
370
                        default_value        as col2,
371
                        c_id         as col3,
372
                        session_id as col4,
373
                        user.username         as col5,
374
                        user.id         as col6,
375
                        default_date         as col7
376
                    FROM $track_e_default as track_default,
377
                    $table_user as user,
378
                    $access_url_rel_user_table as url
379
                    WHERE
380
                        user.active <> -1 AND
381
                        track_default.default_user_id = user.id AND
382
                        url.user_id = user.id AND
383
                        access_url_id= $urlId";
384
        } else {
385
            $sql = "SELECT
386
                       default_event_type  as col0,
387
                       default_value_type    as col1,
388
                       default_value        as col2,
389
                       c_id         as col3,
390
                       session_id as col4,
391
                       user.username         as col5,
392
                       user.id         as col6,
393
                       default_date         as col7
394
                   FROM $track_e_default track_default, $table_user user
395
                   WHERE user.active <> ".USER_SOFT_DELETED." AND track_default.default_user_id = user.id ";
396
        }
397
398
        if (!empty($_GET['keyword'])) {
399
            $keyword = Database::escape_string(trim($_GET['keyword']));
400
            $sql .= " AND (user.username LIKE '%".$keyword."%' OR
401
                        default_event_type LIKE '%".$keyword."%' OR
402
                        default_value_type LIKE '%".$keyword."%' OR
403
                        default_value LIKE '%".$keyword."%') ";
404
        }
405
406
        if (!empty($courseId)) {
407
            $courseId = (int) $courseId;
408
            $sql .= " AND c_id = $courseId";
409
            $sql .= api_get_session_condition($sessionId);
410
        }
411
412
        if (!empty($column)) {
413
            $sql .= " ORDER BY col$column $direction";
414
        } else {
415
            $sql .= " ORDER BY col7 DESC ";
416
        }
417
        $sql .= " LIMIT $from, $numberOfItems ";
418
419
        $res = Database::query($sql);
420
        $activities = [];
421
        while ($row = Database::fetch_row($res)) {
422
            if (false === strpos($row[1], '_object') &&
423
                false === strpos($row[1], '_array')
424
            ) {
425
                $row[2] = $row[2];
426
            } else {
427
                if (!empty($row[2])) {
428
                    $originalData = str_replace('\\', '', $row[2]);
429
                    $row[2] = UnserializeApi::unserialize('not_allowed_classes', $originalData);
430
                    if (is_array($row[2]) && !empty($row[2])) {
431
                        $row[2] = implode_with_key(', ', $row[2]);
432
                    } else {
433
                        $row[2] = $originalData;
434
                    }
435
                }
436
            }
437
438
            if (!empty($row['default_date'])) {
439
                $row['default_date'] = api_get_local_time($row['default_date']);
440
            } else {
441
                $row['default_date'] = '-';
442
            }
443
444
            if (!empty($row[7])) {
445
                $row[7] = api_get_local_time($row[7]);
446
            } else {
447
                $row[7] = '-';
448
            }
449
450
            if (!empty($row[5])) {
451
                // Course
452
                if (!empty($row[3])) {
453
                    $row[3] = Display::url(
454
                        $row[3],
455
                        api_get_path(WEB_CODE_PATH).'admin/course_edit.php?id='.$row[3]
456
                    );
457
                } else {
458
                    $row[3] = '-';
459
                }
460
461
                // session
462
                if (!empty($row[4])) {
463
                    $row[4] = Display::url(
464
                        $row[4],
465
                        api_get_path(WEB_CODE_PATH).'session/resume_session.php?id_session='.$row[4]
466
                    );
467
                } else {
468
                    $row[4] = '-';
469
                }
470
471
                // User id.
472
                $row[5] = Display::url(
473
                    $row[5],
474
                    api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_popup&user_id='.$row[6],
475
                    ['class' => 'ajax']
476
                );
477
478
                $row[6] = Tracking::get_ip_from_user_event(
479
                    $row[6],
480
                    $row[7],
481
                    true
482
                );
483
                if (empty($row[6])) {
484
                    $row[6] = get_lang('Unknown');
485
                }
486
            }
487
            $activities[] = $row;
488
        }
489
490
        return $activities;
491
    }
492
493
    /**
494
     * Show statistics.
495
     *
496
     * @param string $title      The title
497
     * @param array  $stats
498
     * @param ?bool   $showTotal
499
     * @param ?bool   $isFileSize
500
     *
501
     * @return string HTML table
502
     */
503
    public static function printStats(
504
        string $title,
505
        array $stats,
506
        ?bool $showTotal = true,
507
        ?bool $isFileSize = false
508
    ): string
509
    {
510
        $total = 0;
511
        $content = '<table class="table table-hover table-striped data_table stats_table" cellspacing="0" cellpadding="3" width="90%">
512
            <thead><tr><th colspan="'.($showTotal ? '4' : '3').'">'.$title.'</th></tr></thead><tbody>';
513
        $i = 0;
514
        foreach ($stats as $subtitle => $number) {
515
            $total += $number;
516
        }
517
518
        foreach ($stats as $subtitle => $number) {
519
            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...
520
                $number_label = number_format($number, 0, ',', '.');
521
            } else {
522
                $number_label = self::makeSizeString($number);
523
            }
524
            $percentage = ($total > 0 ? number_format(100 * $number / $total, 1, ',', '.') : '0');
525
526
            $content .= '<tr class="row_'.(0 == $i % 2 ? 'odd' : 'even').'">
527
                    <td width="25%" style="vertical-align:top;">'.$subtitle.'</td>
528
                    <td width="60%">'.Display::bar_progress($percentage, false).'</td>
529
                    <td width="5%" align="right" style="vertical-align:top;">'.$number_label.'</td>';
530
            if ($showTotal) {
531
                $content .= '<td width="5%" align="right"> '.$percentage.'%</td>';
532
            }
533
            $content .= '</tr>';
534
            $i++;
535
        }
536
        $content .= '</tbody>';
537
        if ($showTotal) {
538
            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...
539
                $total_label = number_format($total, 0, ',', '.');
540
            } else {
541
                $total_label = self::makeSizeString($total);
542
            }
543
            $content .= '
544
                <tfoot><tr><th colspan="4" align="right">'.get_lang('Total').': '.$total_label.'</td></tr></tfoot>
545
            ';
546
        }
547
        $content .= '</table>';
548
549
        return $content;
550
    }
551
552
    /**
553
     * Show some stats about the number of logins.
554
     *
555
     * @param string $type month, hour or day
556
     * @return string HTML block
557
     * @throws Exception
558
     */
559
    public static function printLoginStats(string $type): string
560
    {
561
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
562
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
563
        $table_url = null;
564
        $where_url = null;
565
        $now = api_get_utc_datetime();
566
        $where_url_last = ' WHERE login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
567
        $accessUrlUtil = Container::getAccessUrlUtil();
568
569
        if ($accessUrlUtil->isMultiple()) {
570
            $accessUrl = $accessUrlUtil->getCurrent();
571
            $urlId = $accessUrl->getId();
572
            $table_url = ", $access_url_rel_user_table";
573
            $where_url = " WHERE login_user_id=user_id AND access_url_id = $urlId";
574
            $where_url_last = ' AND login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
575
        }
576
577
        $period = get_lang('Month');
578
        $periodCollection = api_get_months_long();
579
        $sql = "SELECT
580
                DATE_FORMAT( login_date, '%Y-%m' ) AS stat_date ,
581
                count( login_id ) AS number_of_logins
582
                FROM $table $table_url $where_url
583
                GROUP BY stat_date
584
                ORDER BY login_date DESC";
585
        $sql_last_x = null;
586
587
        switch ($type) {
588
            case 'hour':
589
                $period = get_lang('Hour');
590
                $sql = "SELECT
591
                          DATE_FORMAT( login_date, '%H') AS stat_date,
592
                          count( login_id ) AS number_of_logins
593
                        FROM $table $table_url $where_url
594
                        GROUP BY stat_date
595
                        ORDER BY stat_date ";
596
                $sql_last_x = "SELECT
597
                                DATE_FORMAT( login_date, '%H' ) AS stat_date,
598
                                count( login_id ) AS number_of_logins
599
                               FROM $table $table_url $where_url ".sprintf($where_url_last, 'DAY')."
600
                               GROUP BY stat_date
601
                               ORDER BY stat_date ";
602
                break;
603
            case 'day':
604
                $periodCollection = api_get_week_days_long();
605
                $period = get_lang('Day');
606
                $sql = "SELECT DATE_FORMAT( login_date, '%w' ) AS stat_date ,
607
                        count( login_id ) AS number_of_logins
608
                        FROM  $table $table_url $where_url
609
                        GROUP BY stat_date
610
                        ORDER BY DATE_FORMAT( login_date, '%w' ) ";
611
                $sql_last_x = "SELECT
612
                                DATE_FORMAT( login_date, '%w' ) AS stat_date,
613
                                count( login_id ) AS number_of_logins
614
                               FROM $table $table_url $where_url ".sprintf($where_url_last, 'WEEK')."
615
                               GROUP BY stat_date
616
                               ORDER BY DATE_FORMAT( login_date, '%w' ) ";
617
                break;
618
        }
619
620
        $content = '';
621
        if ($sql_last_x) {
622
            $res_last_x = Database::query($sql_last_x);
623
            $result_last_x = [];
624
            while ($obj = Database::fetch_object($res_last_x)) {
625
                $stat_date = ('day' === $type) ? $periodCollection[$obj->stat_date] : $obj->stat_date;
626
                $result_last_x[$stat_date] = $obj->number_of_logins;
627
            }
628
            $content .= self::printStats(get_lang('Last logins').' ('.$period.')', $result_last_x, true);
629
            flush(); //flush web request at this point to see something already while the full data set is loading
630
            $content .= '<br />';
631
        }
632
        $res = Database::query($sql);
633
        $result = [];
634
        while ($obj = Database::fetch_object($res)) {
635
            $stat_date = $obj->stat_date;
636
            switch ($type) {
637
                case 'month':
638
                    $stat_date = explode('-', $stat_date);
639
                    $stat_date[1] = $periodCollection[(int) $stat_date[1] - 1];
640
                    $stat_date = implode(' ', $stat_date);
641
                    break;
642
                case 'day':
643
                    $stat_date = $periodCollection[$stat_date];
644
                    break;
645
            }
646
            $result[$stat_date] = $obj->number_of_logins;
647
        }
648
        $content .= self::printStats(get_lang('All logins').' ('.$period.')', $result, true);
649
650
        return $content;
651
    }
652
653
    /**
654
     * Print the number of recent logins.
655
     *
656
     * @param ?bool  $distinct        whether to only give distinct users stats, or *all* logins
657
     * @param ?int   $sessionDuration Number of minutes a session must have lasted at a minimum to be taken into account
658
     * @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)
659
     *
660
     * @throws Exception
661
     *
662
     * @return string HTML table
663
     */
664
    public static function printRecentLoginStats(?bool $distinct = false, ?int $sessionDuration = 0, ?array $periods = []): string
665
    {
666
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
667
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
668
        $table_url = '';
669
        $where_url = '';
670
        $accessUrlUtil = Container::getAccessUrlUtil();
671
672
        if ($accessUrlUtil->isMultiple()) {
673
            $accessUrl = $accessUrlUtil->getCurrent();
674
            $urlId = $accessUrl->getId();
675
            $table_url = ", $access_url_rel_user_table";
676
            $where_url = " AND login_user_id=user_id AND access_url_id = $urlId";
677
        }
678
679
        $now = api_get_utc_datetime();
680
        $field = 'login_id';
681
        if ($distinct) {
682
            $field = 'DISTINCT(login_user_id)';
683
        }
684
685
        if (empty($periods)) {
686
            $periods = [1, 7, 31];
687
        }
688
        $sqlList = [];
689
690
        $sessionDuration = (int) $sessionDuration * 60; // convert from minutes to seconds
691
        foreach ($periods as $day) {
692
            $date = new DateTime($now);
693
            $startDate = $date->format('Y-m-d').' 00:00:00';
694
            $endDate = $date->format('Y-m-d').' 23:59:59';
695
696
            if ($day > 1) {
697
                $startDate = $date->sub(new DateInterval('P'.$day.'D'));
698
                $startDate = $startDate->format('Y-m-d').' 00:00:00';
699
            }
700
701
            $localDate = api_get_local_time($startDate, null, null, false, false);
702
            $localEndDate = api_get_local_time($endDate, null, null, false, false);
703
704
            $label = sprintf(get_lang('Last %s days'), $day);
705
            if (1 == $day) {
706
                $label = get_lang('Today');
707
            }
708
            $label .= " <span class=\"muted right\" style=\"float: right; margin-right: 5px;\">[$localDate - $localEndDate]</span>";
709
            $sql = "SELECT count($field) AS number
710
                    FROM $table $table_url
711
                    WHERE ";
712
            if (0 == $sessionDuration) {
713
                $sql .= " logout_date != login_date AND ";
714
            } else {
715
                $sql .= " UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration AND ";
716
            }
717
            $sql .= "login_date BETWEEN '$startDate' AND '$endDate'
718
                        $where_url";
719
            $sqlList[$label] = $sql;
720
        }
721
722
        $sql = "SELECT count($field) AS number
723
                FROM $table $table_url ";
724
        if (0 == $sessionDuration) {
725
            $sql .= " WHERE logout_date != login_date $where_url";
726
        } else {
727
            $sql .= " WHERE UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration $where_url";
728
        }
729
        $sqlList[get_lang('Total')] = $sql;
730
        $totalLogin = [];
731
        foreach ($sqlList as $label => $query) {
732
            $res = Database::query($query);
733
            $obj = Database::fetch_object($res);
734
            $totalLogin[$label] = $obj->number;
735
        }
736
737
        if ($distinct) {
738
            $content = self::printStats(get_lang('Distinct users logins'), $totalLogin, false);
739
        } else {
740
            $content = self::printStats(get_lang('Logins'), $totalLogin, false);
741
        }
742
743
        return $content;
744
    }
745
746
    /**
747
     * Get the number of recent logins.
748
     *
749
     * @param ?bool $distinct            Whether to only give distinct users stats, or *all* logins
750
     * @param ?int  $sessionDuration     Number of minutes a session must have lasted at a minimum to be taken into account
751
     * @param ?bool $completeMissingDays Whether to fill the daily gaps (if any) when getting a list of logins
752
     *
753
     * @throws Exception
754
     *
755
     * @return array
756
     */
757
    public static function getRecentLoginStats(?bool $distinct = false, ?int $sessionDuration = 0, ?bool $completeMissingDays = true): array
758
    {
759
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
760
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
761
        $table_url = '';
762
        $where_url = '';
763
        $accessUrlUtil = Container::getAccessUrlUtil();
764
765
        if ($accessUrlUtil->isMultiple()) {
766
            $accessUrl = $accessUrlUtil->getCurrent();
767
            $urlId = $accessUrl->getId();
768
            $table_url = ", $access_url_rel_user_table";
769
            $where_url = " AND login_user_id=user_id AND access_url_id = $urlId";
770
        }
771
772
        $now = api_get_utc_datetime();
773
        $date = new DateTime($now);
774
        $date->sub(new DateInterval('P31D'));
775
        $newDate = $date->format('Y-m-d h:i:s');
776
        $totalLogin = self::buildDatesArray($newDate, $now, true);
777
778
        $field = 'login_id';
779
        if ($distinct) {
780
            $field = 'DISTINCT(login_user_id)';
781
        }
782
        $sessionDuration = (int) $sessionDuration * 60; //Convert from minutes to seconds
783
784
        $sql = "SELECT count($field) AS number, date(login_date) as login_date
785
                FROM $table $table_url
786
                WHERE ";
787
        if (0 == $sessionDuration) {
788
            $sql .= " logout_date != login_date AND ";
789
        } else {
790
            $sql .= " UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration AND ";
791
        }
792
        $sql .= " login_date >= '$newDate' $where_url
793
                GROUP BY date(login_date)";
794
795
        $res = Database::query($sql);
796
        while ($row = Database::fetch_assoc($res)) {
797
            $monthAndDay = substr($row['login_date'], 5, 5);
798
            $totalLogin[$monthAndDay] = $row['number'];
799
        }
800
801
        return $totalLogin;
802
    }
803
804
    /**
805
     * Get course tools usage statistics for the whole platform (by URL if multi-url).
806
     * @throws Exception
807
     */
808
    public static function getToolsStats(): array
809
    {
810
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ACCESS);
811
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
812
813
        $tools = [
814
            'announcement',
815
            'assignment',
816
            'calendar_event',
817
            'chat',
818
            'course_description',
819
            'document',
820
            'dropbox',
821
            'group',
822
            'learnpath',
823
            'link',
824
            'quiz',
825
            'student_publication',
826
            'user',
827
            'forum',
828
        ];
829
        $tool_names = [];
830
        foreach ($tools as $tool) {
831
            $tool_names[$tool] = get_lang(ucfirst($tool), '');
832
        }
833
        $accessUrlUtil = Container::getAccessUrlUtil();
834
835
        if ($accessUrlUtil->isMultiple()) {
836
            $accessUrl = $accessUrlUtil->getCurrent();
837
            $urlId = $accessUrl->getId();
838
            $sql = "SELECT access_tool, count( access_id ) AS number_of_logins
839
                    FROM $table t , $access_url_rel_course_table a
840
                    WHERE
841
                        access_tool IN ('".implode("','", $tools)."') AND
842
                        t.c_id = a.c_id AND
843
                        access_url_id = $urlId
844
                        GROUP BY access_tool
845
                    ";
846
        } else {
847
            $sql = "SELECT access_tool, count( access_id ) AS number_of_logins
848
                    FROM $table
849
                    WHERE access_tool IN ('".implode("','", $tools)."')
850
                    GROUP BY access_tool ";
851
        }
852
853
        $res = Database::query($sql);
854
        $result = [];
855
        while ($obj = Database::fetch_object($res)) {
856
            $result[$tool_names[$obj->access_tool]] = $obj->number_of_logins;
857
        }
858
859
        return $result;
860
    }
861
862
    /**
863
     * Show some stats about the accesses to the different course tools.
864
     *
865
     * @param array $result If defined, this serves as data. Otherwise, will get the data from getToolsStats()
866
     *
867
     * @return string HTML table
868
     * @throws Exception
869
     */
870
    public static function printToolStats($result = null): string
871
    {
872
        if (empty($result)) {
873
            $result = self::getToolsStats();
874
        }
875
876
        return self::printStats(get_lang('Tools access'), $result, true);
877
    }
878
879
    /**
880
     * Returns some stats about the number of courses per language.
881
     * @throws Exception
882
     */
883
    public static function printCourseByLanguageStats(): array
884
    {
885
        $table = Database::get_main_table(TABLE_MAIN_COURSE);
886
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
887
        $accessUrlUtil = Container::getAccessUrlUtil();
888
889
        if ($accessUrlUtil->isMultiple()) {
890
            $accessUrl = $accessUrlUtil->getCurrent();
891
            $urlId = $accessUrl->getId();
892
            $sql = "SELECT course_language, count( c.code ) AS number_of_courses
893
                    FROM $table as c, $access_url_rel_course_table as u
894
                    WHERE u.c_id = c.id AND access_url_id = $urlId
895
                    GROUP BY course_language
896
                    ORDER BY number_of_courses DESC";
897
        } else {
898
            $sql = "SELECT course_language, count( code ) AS number_of_courses
899
                   FROM $table GROUP BY course_language
900
                   ORDER BY number_of_courses DESC";
901
        }
902
        $res = Database::query($sql);
903
        $result = [];
904
        while ($obj = Database::fetch_object($res)) {
905
            $result[$obj->course_language] = $obj->number_of_courses;
906
        }
907
908
        return $result;
909
    }
910
911
    /**
912
     * Shows the number of users having their picture uploaded in Dokeos.
913
     * @throws Exception
914
     */
915
    public static function printUserPicturesStats(): string
916
    {
917
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
918
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
919
        $url_condition = null;
920
        $url_condition2 = null;
921
        $table = null;
922
        $accessUrlUtil = Container::getAccessUrlUtil();
923
924
        if ($accessUrlUtil->isMultiple()) {
925
            $accessUrl = $accessUrlUtil->getCurrent();
926
            $urlId = $accessUrl->getId();
927
            $url_condition = ", $access_url_rel_user_table as url WHERE url.user_id=u.id AND access_url_id='".$urlId."'";
928
            $url_condition2 = " AND url.user_id=u.id AND access_url_id = $urlId";
929
            $table = ", $access_url_rel_user_table as url ";
930
        }
931
        $sql = "SELECT COUNT(*) AS n FROM $user_table as u ".$url_condition;
932
        $res = Database::query($sql);
933
        $count1 = Database::fetch_object($res);
934
        $sql = "SELECT COUNT(*) AS n FROM $user_table as u $table
935
               WHERE LENGTH(picture_uri) > 0 $url_condition2";
936
937
        $sql .= !str_contains($sql, 'WHERE') ? ' WHERE u.active <> '.USER_SOFT_DELETED : ' AND u.active <> '.USER_SOFT_DELETED;
938
939
        $res = Database::query($sql);
940
        $count2 = Database::fetch_object($res);
941
        // #users without picture
942
        $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...
943
        $result[get_lang('Yes')] = $count2->n; // #users with picture
944
945
        return self::printStats(get_lang('Number of users').' ('.get_lang('Picture').')', $result, true);
946
    }
947
948
    /**
949
     * Print important activities report page
950
     */
951
    public static function printActivitiesStats(): string
952
    {
953
        $content = '<h4>'.get_lang('Important activities').'</h4>';
954
        // Create a search-box
955
        $form = new FormValidator(
956
            'search_simple',
957
            'get',
958
            api_get_path(WEB_CODE_PATH).'admin/statistics/index.php',
959
            '',
960
            ['style' => 'width:200px']
961
        );
962
        $renderer = &$form->defaultRenderer();
963
        $renderer->setCustomElementTemplate('<span>{element}</span> ');
964
        $form->addHidden('report', 'activities');
965
        $form->addHidden('activities_direction', 'DESC');
966
        $form->addHidden('activities_column', '4');
967
        $form->addElement('text', 'keyword', get_lang('Keyword'));
968
        $form->addButtonSearch(get_lang('Search'), 'submit');
969
        $content .= '<div class="actions">';
970
        $content .= $form->returnForm();
971
        $content .= '</div>';
972
973
        if (!empty($_GET['keyword'])) {
974
            $table = new SortableTable(
975
                'activities',
976
                ['Statistics', 'getNumberOfActivities'],
977
                ['Statistics', 'getActivitiesData'],
978
                7,
979
                50,
980
                'DESC'
981
            );
982
            $parameters = [];
983
984
            $parameters['report'] = 'activities';
985
            $parameters['keyword'] = Security::remove_XSS($_GET['keyword']);
986
987
            $table->set_additional_parameters($parameters);
988
            $table->set_header(0, get_lang('Event type'));
989
            $table->set_header(1, get_lang('Data type'));
990
            $table->set_header(2, get_lang('Value'));
991
            $table->set_header(3, get_lang('Course'));
992
            $table->set_header(4, get_lang('Session'));
993
            $table->set_header(5, get_lang('Username'));
994
            $table->set_header(6, get_lang('IP address'));
995
            $table->set_header(7, get_lang('Date'));
996
            $content .= $table->return_table();
997
        }
998
999
        $content .= '<div class="alert alert-info">'.get_lang('Important activities').' : '.'<br>';
1000
        $prefix = 'LOG_';
1001
        $userDefinedConstants = get_defined_constants(true)['user'];
1002
        $filteredConstants = array_filter($userDefinedConstants, function ($constantName) use ($prefix) {
1003
            return strpos($constantName, $prefix) === 0;
1004
        }, ARRAY_FILTER_USE_KEY);
1005
        $constantNames = array_keys($filteredConstants);
1006
        $link = api_get_self().'?report=activities&activities_direction=DESC&activities_column=7&keyword=';
1007
        foreach ($constantNames as $constantName) {
1008
            if ($constantName != 'LOG_WS') {
1009
                if (substr($constantName, -3) == '_ID') {
1010
                    continue;
1011
                }
1012
                $content .= '- <a href="'.$link.constant($constantName).'">'.constant($constantName).'</a><br>'.PHP_EOL;
1013
            } else {
1014
                $constantValue = constant($constantName);
1015
                $reflection = new ReflectionClass('Rest');
1016
                $constants = $reflection->getConstants();
1017
                foreach ($constants as $name => $value) {
1018
                    $content .= '- <a href="'.$link.$constantValue.$value.'">'.$constantValue.$value.'</a><br>'.PHP_EOL;
1019
                }
1020
            }
1021
        }
1022
        $content .= '</div>';
1023
1024
        return $content;
1025
    }
1026
1027
    /**
1028
     * Shows statistics about the time of last visit to each course.
1029
     * @throws Exception
1030
     */
1031
    public static function printCourseLastVisit(): string
1032
    {
1033
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
1034
        $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...
1035
        $columns[1] = 'access_date';
1036
        $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...
1037
        $sql_order[SORT_DESC] = 'DESC';
1038
        $per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 10;
1039
        $page_nr = isset($_GET['page_nr']) ? intval($_GET['page_nr']) : 1;
1040
        $column = isset($_GET['column']) ? intval($_GET['column']) : 0;
1041
        $direction = isset($_GET['direction']) ? $_GET['direction'] : SORT_ASC;
1042
1043
        if (!in_array($direction, [SORT_ASC, SORT_DESC])) {
1044
            $direction = SORT_ASC;
1045
        }
1046
        $form = new FormValidator('courselastvisit', 'get');
1047
        $form->addElement('hidden', 'report', 'courselastvisit');
1048
        $form->addText('date_diff', get_lang('days'), true);
1049
        $form->addRule('date_diff', 'InvalidNumber', 'numeric');
1050
        $form->addButtonSearch(get_lang('Search'), 'submit');
1051
        if (!isset($_GET['date_diff'])) {
1052
            $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...
1053
        } else {
1054
            $defaults['date_diff'] = Security::remove_XSS($_GET['date_diff']);
1055
        }
1056
        $form->setDefaults($defaults);
1057
        $content = $form->returnForm();
1058
1059
        $values = $form->exportValues();
1060
        $date_diff = $values['date_diff'];
1061
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
1062
        $accessUrlUtil = Container::getAccessUrlUtil();
1063
1064
        if ($accessUrlUtil->isMultiple()) {
1065
            $accessUrl = $accessUrlUtil->getCurrent();
1066
            $urlId = $accessUrl->getId();
1067
            $sql = "SELECT * FROM $table t , $access_url_rel_course_table a
1068
                   WHERE
1069
                        c_id = a.c_id AND
1070
                        access_url_id = $urlId
1071
                   GROUP BY c_id
1072
                   HAVING c_id <> ''
1073
                   AND DATEDIFF( '".api_get_utc_datetime()."' , access_date ) <= ".$date_diff;
1074
        } else {
1075
            $sql = "SELECT * FROM $table t
1076
                   GROUP BY c_id
1077
                   HAVING c_id <> ''
1078
                   AND DATEDIFF( '".api_get_utc_datetime()."' , access_date ) <= ".$date_diff;
1079
        }
1080
        $sql .= ' ORDER BY `'.$columns[$column].'` '.$sql_order[$direction];
1081
        $from = ($page_nr - 1) * $per_page;
1082
        $sql .= ' LIMIT '.$from.','.$per_page;
1083
1084
        $content .= '<p>'.get_lang('Latest access').' &gt;= '.$date_diff.' '.get_lang('days').'</p>';
1085
        $res = Database::query($sql);
1086
        if (Database::num_rows($res) > 0) {
1087
            $courses = [];
1088
            while ($obj = Database::fetch_object($res)) {
1089
                $courseInfo = api_get_course_info_by_id($obj->c_id);
1090
                $course = [];
1091
                $course[] = '<a href="'.api_get_path(WEB_COURSE_PATH).$courseInfo['code'].'">'.$courseInfo['code'].' <a>';
1092
                // Allow sort by date hiding the numerical date
1093
                $course[] = '<span style="display:none;">'.$obj->access_date.'</span>'.api_convert_and_format_date($obj->access_date);
1094
                $courses[] = $course;
1095
            }
1096
            $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...
1097
            $parameters['report'] = 'courselastvisit';
1098
            $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...
1099
            $table_header[] = [get_lang("Latest access"), true];
1100
1101
            ob_start();
1102
            Display:: display_sortable_table(
1103
                $table_header,
1104
                $courses,
1105
                ['column' => $column, 'direction' => $direction],
1106
                [],
1107
                $parameters
1108
            );
1109
            $content .= ob_get_contents();
1110
            ob_end_clean();
1111
        } else {
1112
            $content = get_lang('No search results');
1113
        }
1114
1115
        return $content;
1116
    }
1117
1118
    /**
1119
     * Displays the statistics of the messages sent and received by each user in the social network.
1120
     *
1121
     * @param string $messageType Type of message: 'sent' or 'received'
1122
     *
1123
     * @return array Message list
1124
     */
1125
    public static function getMessages(string $messageType): array
1126
    {
1127
        $messageTable = Database::get_main_table(TABLE_MESSAGE);
1128
        $messageRelUserTable = Database::get_main_table(TABLE_MESSAGE_REL_USER);
1129
        $userTable = Database::get_main_table(TABLE_MAIN_USER);
1130
        $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1131
1132
        switch ($messageType) {
1133
            case 'sent':
1134
                $field = 'm.user_sender_id';
1135
                $joinCondition = "m.id = mru.message_id AND mru.receiver_type = " . MessageRelUser::TYPE_SENDER;
1136
                break;
1137
            case 'received':
1138
                $field = 'mru.user_id';
1139
                $joinCondition = "m.id = mru.message_id AND mru.receiver_type = " . MessageRelUser::TYPE_TO;
1140
                break;
1141
        }
1142
1143
        $accessUrlUtil = Container::getAccessUrlUtil();
1144
1145
        if ($accessUrlUtil->isMultiple()) {
1146
            $accessUrl = $accessUrlUtil->getCurrent();
1147
            $urlId = $accessUrl->getId();
1148
            $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
1149
            FROM $messageTable m
1150
            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...
1151
            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...
1152
            INNER JOIN $accessUrlRelUserTable url ON u.id = url.user_id
1153
            WHERE url.access_url_id = $urlId
1154
            AND u.active <> " . USER_SOFT_DELETED . "
1155
            GROUP BY $field
1156
            ORDER BY count_message DESC";
1157
        } else {
1158
            $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
1159
            FROM $messageTable m
1160
            INNER JOIN $messageRelUserTable mru ON $joinCondition
1161
            INNER JOIN $userTable u ON $field = u.id
1162
            WHERE u.active <> " . USER_SOFT_DELETED . "
1163
            GROUP BY $field
1164
            ORDER BY count_message DESC";
1165
        }
1166
        $res = Database::query($sql);
1167
        $messages_sent = [];
1168
        while ($messages = Database::fetch_array($res)) {
1169
            if (empty($messages['username'])) {
1170
                $messages['username'] = get_lang('Unknown');
1171
            }
1172
            $users = api_get_person_name(
1173
                    $messages['firstname'],
1174
                    $messages['lastname']
1175
                ) . '<br />(' . $messages['username'] . ')';
1176
            $messages_sent[$users] = $messages['count_message'];
1177
        }
1178
1179
        return $messages_sent;
1180
    }
1181
1182
    /**
1183
     * Count the number of friends for each social network users.
1184
     * @throws Exception
1185
     */
1186
    public static function getFriends(): array
1187
    {
1188
        $user_friend_table = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
1189
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
1190
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1191
1192
        $accessUrlUtil = Container::getAccessUrlUtil();
1193
1194
        if ($accessUrlUtil->isMultiple()) {
1195
            $accessUrl = $accessUrlUtil->getCurrent();
1196
            $urlId = $accessUrl->getId();
1197
            $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
1198
                    FROM $access_url_rel_user_table as url, $user_friend_table uf
1199
                    LEFT JOIN $user_table u
1200
                    ON (uf.user_id = u.id) AND u.active <> ".USER_SOFT_DELETED."
1201
                    WHERE
1202
                        uf.relation_type <> '".UserRelUser::USER_RELATION_TYPE_RRHH."' AND
1203
                        uf.user_id = url.user_id AND
1204
                        access_url_id = $urlId
1205
                    GROUP BY uf.user_id
1206
                    ORDER BY count_friend DESC ";
1207
        } else {
1208
            $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
1209
                    FROM $user_friend_table uf
1210
                    LEFT JOIN $user_table u
1211
                    ON (uf.user_id = u.id) AND u.active <> ".USER_SOFT_DELETED."
1212
                    WHERE uf.relation_type <> '".UserRelUser::USER_RELATION_TYPE_RRHH."'
1213
                    GROUP BY uf.user_id
1214
                    ORDER BY count_friend DESC ";
1215
        }
1216
        $res = Database::query($sql);
1217
        $list_friends = [];
1218
        while ($friends = Database::fetch_array($res)) {
1219
            $users = api_get_person_name($friends['firstname'], $friends['lastname']).'<br />('.$friends['username'].')';
1220
            $list_friends[$users] = $friends['count_friend'];
1221
        }
1222
1223
        return $list_friends;
1224
    }
1225
1226
    /**
1227
     * Returns the number of users that didn't log in for a certain period of time.
1228
     * @throws Exception
1229
     */
1230
    public static function printUsersNotLoggedInStats(): string
1231
    {
1232
        $totalLogin = [];
1233
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1234
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1235
        $total = self::countUsers();
1236
        $accessUrlUtil = Container::getAccessUrlUtil();
1237
1238
        if ($accessUrlUtil->isMultiple()) {
1239
            $accessUrl = $accessUrlUtil->getCurrent();
1240
            $urlId = $accessUrl->getId();
1241
            $table_url = ", $access_url_rel_user_table";
1242
            $where_url = " AND login_user_id=user_id AND access_url_id = $urlId";
1243
        } else {
1244
            $table_url = '';
1245
            $where_url = '';
1246
        }
1247
        $now = api_get_utc_datetime();
1248
        $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...
1249
            "SELECT count(distinct(login_user_id)) AS number ".
1250
            " FROM $table $table_url ".
1251
            " WHERE DATE_ADD(login_date, INTERVAL 1 DAY) >= '$now' $where_url";
1252
        $sql[get_lang('In the last 7 days')] =
1253
            "SELECT count(distinct(login_user_id)) AS number ".
1254
            " FROM $table $table_url ".
1255
            " WHERE DATE_ADD(login_date, INTERVAL 7 DAY) >= '$now' $where_url";
1256
        $sql[get_lang('In the last 31 days')] =
1257
            "SELECT count(distinct(login_user_id)) AS number ".
1258
            " FROM $table $table_url ".
1259
            " WHERE DATE_ADD(login_date, INTERVAL 31 DAY) >= '$now' $where_url";
1260
        $sql[sprintf(get_lang('Last %d months'), 6)] =
1261
            "SELECT count(distinct(login_user_id)) AS number ".
1262
            " FROM $table $table_url ".
1263
            " WHERE DATE_ADD(login_date, INTERVAL 6 MONTH) >= '$now' $where_url";
1264
        $sql[get_lang('Never connected')] =
1265
            "SELECT count(distinct(login_user_id)) AS number ".
1266
            " FROM $table $table_url WHERE 1=1 $where_url";
1267
        foreach ($sql as $index => $query) {
1268
            $res = Database::query($query);
1269
            $obj = Database::fetch_object($res);
1270
            $r = $total - $obj->number;
1271
            $totalLogin[$index] = $r < 0 ? 0 : $r;
1272
        }
1273
1274
        return self::printStats(
1275
            get_lang('Not logged in for some time'),
1276
            $totalLogin,
1277
            false
1278
        );
1279
    }
1280
1281
    /**
1282
     * Returns an array with indexes as the 'yyyy-mm-dd' format of each date
1283
     * within the provided range (including limits). Dates are assumed to be
1284
     * given in UTC.
1285
     *
1286
     * @param string $startDate  Start date, in Y-m-d or Y-m-d h:i:s format
1287
     * @param string $endDate    End date, in Y-m-d or Y-m-d h:i:s format
1288
     * @param ?bool   $removeYear Whether to remove the year in the results (for easier reading)
1289
     *
1290
     * @return array|bool False on error in the params, array of [date1 => 0, date2 => 0, ...] otherwise
1291
     */
1292
    public static function buildDatesArray(string $startDate, string $endDate, ?bool $removeYear = false): mixed
1293
    {
1294
        if (strlen($startDate) > 10) {
1295
            $startDate = substr($startDate, 0, 10);
1296
        }
1297
        if (strlen($endDate) > 10) {
1298
            $endDate = substr($endDate, 0, 10);
1299
        }
1300
        if (!preg_match('/\d\d\d\d-\d\d-\d\d/', $startDate)) {
1301
            return false;
1302
        }
1303
        if (!preg_match('/\d\d\d\d-\d\d-\d\d/', $startDate)) {
1304
            return false;
1305
        }
1306
        $startTimestamp = strtotime($startDate);
1307
        $endTimestamp = strtotime($endDate);
1308
        $list = [];
1309
        for ($time = $startTimestamp; $time < $endTimestamp; $time += 86400) {
1310
            $datetime = api_get_utc_datetime($time);
1311
            if ($removeYear) {
1312
                $datetime = substr($datetime, 5, 5);
1313
            } else {
1314
                $dateTime = substr($datetime, 0, 10);
1315
            }
1316
            $list[$datetime] = 0;
1317
        }
1318
1319
        return $list;
1320
    }
1321
1322
    /**
1323
     * Prepare the JS code to load a chart.
1324
     *
1325
     * @param string $url     URL for AJAX data generator
1326
     * @param ?string $type    bar, line, pie, etc (defaults to 'pie')
1327
     * @param ?string $options Additional options to the chart (see chart-specific library)
1328
     * @param ?string A JS code for loading the chart together with a call to AJAX data generator
1329
     */
1330
    public static function getJSChartTemplate(string $url, ?string $type = 'pie', ?string $options = '', ?string $elementId = 'canvas')
1331
    {
1332
        return '
1333
        <script>
1334
        $(function() {
1335
            $.ajax({
1336
                url: "'.$url.'",
1337
                type: "POST",
1338
                success: function(data) {
1339
                    Chart.defaults.responsive = false;
1340
                    var ctx = document.getElementById("'.$elementId.'").getContext("2d");
1341
                    ctx.canvas.width = 420;
1342
                    ctx.canvas.height = 420;
1343
                    var chart = new Chart(ctx, {
1344
                        type: "'.$type.'",
1345
                        data: data,
1346
                        options: {
1347
                            plugins: {
1348
                                '.$options.'
1349
                            },
1350
                            cutout: "25%"
1351
                        }
1352
                    });
1353
                    var title = chart.options.plugins.title.text;
1354
                    $("#'.$elementId.'_title").html(title);
1355
                    $("#'.$elementId.'_table").html(data.table);
1356
                }
1357
            });
1358
        });
1359
        </script>';
1360
1361
    }
1362
1363
    /**
1364
     * Return template for a JS chart
1365
     * @param $data
1366
     * @param $type
1367
     * @param $options
1368
     * @param $elementId
1369
     * @param $responsive
1370
     * @param $onClickHandler
1371
     * @param $extraButtonHandler
1372
     * @param $canvasDimensions
1373
     * @return string
1374
     */
1375
    public static function getJSChartTemplateWithData(
1376
        $data,
1377
        ?string $type = 'pie',
1378
        ?string $options = '',
1379
        ?string $elementId = 'canvas',
1380
        ?bool $responsive = true,
1381
        ?string $onClickHandler = '',
1382
        ?string $extraButtonHandler = '',
1383
        ?array $canvasDimensions = ['width' => 420, 'height' => 420]
1384
    ): string {
1385
        $data = json_encode($data);
1386
        $responsiveValue = $responsive ? 'true' : 'false';
1387
1388
        $indexAxisOption = '';
1389
        if ($type === 'bar') {
1390
            $indexAxisOption = 'indexAxis: "y",';
1391
        }
1392
1393
        $onClickScript = '';
1394
        if (!empty($onClickHandler)) {
1395
            $onClickScript = '
1396
                onClick: function(evt) {
1397
                    '.$onClickHandler.'
1398
                },
1399
            ';
1400
        }
1401
1402
        $canvasSize = '';
1403
        if ($responsiveValue === 'false') {
1404
            $canvasSize = '
1405
            ctx.canvas.width = '.$canvasDimensions['width'].';
1406
            ctx.canvas.height = '.$canvasDimensions['height'].';
1407
            ';
1408
        }
1409
1410
        return '
1411
        <script>
1412
            $(function() {
1413
                Chart.defaults.responsive = '.$responsiveValue.';
1414
                var ctx = document.getElementById("'.$elementId.'").getContext("2d");
1415
                '.$canvasSize.'
1416
                var chart = new Chart(ctx, {
1417
                    type: "'.$type.'",
1418
                    data: '.$data.',
1419
                    options: {
1420
                        plugins: {
1421
                            '.$options.',
1422
                            datalabels: {
1423
                                anchor: "end",
1424
                                align: "left",
1425
                                formatter: function(value) {
1426
                                    return value;
1427
                                },
1428
                                color: "#000"
1429
                            },
1430
                        },
1431
                        '.$indexAxisOption.'
1432
                        scales: {
1433
                            x: { beginAtZero: true },
1434
                            y: { barPercentage: 0.5 }
1435
                        },
1436
                        '.$onClickScript.'
1437
                    }
1438
                });
1439
                var title = chart.options.plugins.title.text;
1440
                $("#'.$elementId.'_title").html(title);
1441
                $("#'.$elementId.'_table").html(chart.data.datasets[0].data);
1442
1443
                '.$extraButtonHandler.'
1444
            });
1445
        </script>';
1446
    }
1447
1448
    public static function buildJsChartData(array $all, string $chartName): array
1449
    {
1450
        $list = [];
1451
        $palette = ChamiloHelper::getColorPalette(true, true);
1452
        foreach ($all as $tick => $tock) {
1453
            $list['labels'][] = $tick;
1454
        }
1455
1456
        $list['datasets'][0]['label'] = $chartName;
1457
        $list['datasets'][0]['borderColor'] = 'rgba(255,255,255,1)';
1458
1459
        $i = 0;
1460
        foreach ($all as $tick => $tock) {
1461
            $j = $i % count($palette);
1462
            $list['datasets'][0]['data'][] = $tock;
1463
            $list['datasets'][0]['backgroundColor'][] = $palette[$j];
1464
            $i++;
1465
        }
1466
1467
        $scoreDisplay = ScoreDisplay::instance();
1468
        $table = new HTML_Table(['class' => 'data_table stats_table']);
1469
        $headers = [
1470
            get_lang('Name'),
1471
            get_lang('Count'),
1472
            get_lang('Percentage'),
1473
        ];
1474
        $row = 0;
1475
        $column = 0;
1476
        foreach ($headers as $header) {
1477
            $table->setHeaderContents($row, $column, $header);
1478
            $column++;
1479
        }
1480
1481
        $total = 0;
1482
        foreach ($all as $name => $value) {
1483
            $total += $value;
1484
        }
1485
        $row++;
1486
        foreach ($all as $name => $value) {
1487
            $table->setCellContents($row, 0, $name);
1488
            $table->setCellContents($row, 1, $value);
1489
            $table->setCellContents($row, 2, $scoreDisplay->display_score([$value, $total], SCORE_PERCENT));
1490
            $row++;
1491
        }
1492
        $table = Display::page_subheader2($chartName).$table->toHtml();
1493
1494
        return ['chart' => $list, 'table' => $table];
1495
    }
1496
1497
    /**
1498
     * Display the Logins By Date report and allow export its result to XLS.
1499
     */
1500
    public static function printLoginsByDate(): mixed
1501
    {
1502
        if (isset($_GET['export']) && 'xls' === $_GET['export']) {
1503
            $result = self::getLoginsByDate($_GET['start'], $_GET['end']);
1504
            $data = [[get_lang('Username'), get_lang('First name'), get_lang('Last name'), get_lang('Total time')]];
1505
1506
            foreach ($result as $i => $item) {
1507
                $data[] = [
1508
                    $item['username'],
1509
                    $item['firstname'],
1510
                    $item['lastname'],
1511
                    api_time_to_hms($item['time_count']),
1512
                ];
1513
            }
1514
1515
            Export::arrayToXls($data);
1516
            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...
1517
        }
1518
1519
        $content = Display::page_header(get_lang('Logins by date'));
1520
1521
        $actions = '';
1522
        $form = new FormValidator('frm_logins_by_date', 'get');
1523
        $form->addDateRangePicker(
1524
            'daterange',
1525
            get_lang('Date range'),
1526
            true,
1527
            ['format' => 'YYYY-MM-DD', 'timePicker' => 'false', 'validate_format' => 'Y-m-d']
1528
        );
1529
        $form->addHidden('report', 'logins_by_date');
1530
        $form->addButtonFilter(get_lang('Search'));
1531
1532
        if ($form->validate()) {
1533
            $values = $form->exportValues();
1534
1535
            $result = self::getLoginsByDate($values['daterange_start'], $values['daterange_end']);
1536
1537
            if (!empty($result)) {
1538
                $actions = Display::url(
1539
                    Display::getMdiIcon(ActionIcon::EXPORT_SPREADSHEET, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export to XLS')),
1540
                    api_get_self().'?'.http_build_query(
1541
                        [
1542
                            'report' => 'logins_by_date',
1543
                            'export' => 'xls',
1544
                            'start' => Security::remove_XSS($values['daterange_start']),
1545
                            'end' => Security::remove_XSS($values['daterange_end']),
1546
                        ]
1547
                    )
1548
                );
1549
            }
1550
1551
            $table = new HTML_Table(['class' => 'data_table stats_table']);
1552
            $table->setHeaderContents(0, 0, get_lang('Username'));
1553
            $table->setHeaderContents(0, 1, get_lang('First name'));
1554
            $table->setHeaderContents(0, 2, get_lang('Last name'));
1555
            $table->setHeaderContents(0, 3, get_lang('Total time'));
1556
1557
            foreach ($result as $i => $item) {
1558
                $table->setCellContents($i + 1, 0, $item['username']);
1559
                $table->setCellContents($i + 1, 1, $item['firstname']);
1560
                $table->setCellContents($i + 1, 2, $item['lastname']);
1561
                $table->setCellContents($i + 1, 3, api_time_to_hms($item['time_count']));
1562
            }
1563
1564
            $table->setColAttributes(0, ['class' => 'text-center']);
1565
            $table->setColAttributes(3, ['class' => 'text-center']);
1566
            $content = $table->toHtml();
1567
        }
1568
1569
        $content .= $form->returnForm();
1570
1571
        if (!empty($actions)) {
1572
            $content .= Display::toolbarAction('logins_by_date_toolbar', [$actions]);
1573
        }
1574
1575
        return $content;
1576
    }
1577
1578
    /**
1579
     * Return HTML table for the student boss role, for the given user ID
1580
     * @param int $bossId
1581
     * @return string
1582
     */
1583
    public static function getBossTable(int $bossId): string
1584
    {
1585
        $students = UserManager::getUsersFollowedByStudentBoss(
1586
            $bossId,
1587
            0,
1588
            false,
1589
            false,
1590
            false,
1591
            null,
1592
            null,
1593
            null,
1594
            null,
1595
            1
1596
        );
1597
1598
        if (!empty($students)) {
1599
            $table = new HTML_Table(['class' => 'table table-responsive', 'id' => 'table_'.$bossId]);
1600
            $headers = [
1601
                get_lang('Name'),
1602
            ];
1603
            $row = 0;
1604
            $column = 0;
1605
            foreach ($headers as $header) {
1606
                $table->setHeaderContents($row, $column, $header);
1607
                $column++;
1608
            }
1609
            $row++;
1610
            foreach ($students as $student) {
1611
                $column = 0;
1612
                $content = api_get_person_name($student['firstname'], $student['lastname']);
1613
                $content = '<div style="width: 200px; overflow-wrap: break-word;">'.$content.'</div>';
1614
                $table->setCellContents(
1615
                    $row,
1616
                    $column++,
1617
                    $content
1618
                );
1619
                $row++;
1620
            }
1621
1622
            return $table->toHtml();
1623
        }
1624
1625
        return '<table id="table_'.$bossId.'"></table>';
1626
    }
1627
1628
    /**
1629
     * @param string $startDate
1630
     * @param string $endDate
1631
     *
1632
     * @return array
1633
     * @throws Exception
1634
     */
1635
    public static function getLoginsByDate(string $startDate, string $endDate): array
1636
    {
1637
        $startDate = api_get_utc_datetime("$startDate 00:00:00");
1638
        $endDate = api_get_utc_datetime("$endDate 23:59:59");
1639
1640
        if (empty($startDate) || empty($endDate)) {
1641
            return [];
1642
        }
1643
1644
        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
1645
        $tblLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1646
        $urlJoin = '';
1647
        $urlWhere = '';
1648
1649
        $accessUrlUtil = Container::getAccessUrlUtil();
1650
1651
        if ($accessUrlUtil->isMultiple()) {
1652
            $accessUrl = $accessUrlUtil->getCurrent();
1653
            $urlId = $accessUrl->getId();
1654
            $tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1655
1656
            $urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id";
1657
            $urlWhere = "AND au.access_url_id = $urlId";
1658
        }
1659
1660
        $sql = "SELECT u.id,
1661
                    u.firstname,
1662
                    u.lastname,
1663
                    u.username,
1664
                    SUM(TIMESTAMPDIFF(SECOND, l.login_date, l.logout_date)) AS time_count
1665
                FROM $tblUser u
1666
                INNER JOIN $tblLogin l
1667
                ON u.id = l.login_user_id
1668
                $urlJoin
1669
                WHERE u.active <> ".USER_SOFT_DELETED." AND l.login_date BETWEEN '$startDate' AND '$endDate'
1670
                $urlWhere
1671
                GROUP BY u.id";
1672
1673
        $stmt = Database::query($sql);
1674
1675
        return Database::store_result($stmt, 'ASSOC');
1676
    }
1677
1678
    /**
1679
     * Gets the number of new users registered between two dates.
1680
     * @throws Exception
1681
     */
1682
    public static function getNewUserRegistrations(string $startDate, string $endDate): array
1683
    {
1684
        $sql = "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as reg_date, COUNT(*) as user_count
1685
            FROM user
1686
            WHERE created_at BETWEEN '$startDate' AND '$endDate'
1687
            GROUP BY reg_date";
1688
1689
        $result = Database::query($sql);
1690
        $data = [];
1691
        while ($row = Database::fetch_array($result)) {
1692
            $userCount = is_numeric($row['user_count']) ? (int) $row['user_count'] : 0;
1693
            $data[] = ['date' => $row['reg_date'], 'count' => $userCount];
1694
        }
1695
1696
        return $data;
1697
    }
1698
1699
    /**
1700
     * Gets the number of users registered by creator (creator_id) between two dates.
1701
     * @throws Exception
1702
     */
1703
    public static function getUserRegistrationsByCreator(string $startDate, string $endDate): array
1704
    {
1705
        $sql = "SELECT u.creator_id, COUNT(u.id) as user_count, c.firstname, c.lastname
1706
                FROM user u
1707
                LEFT JOIN user c ON u.creator_id = c.id
1708
                WHERE u.created_at BETWEEN '$startDate' AND '$endDate'
1709
                AND u.creator_id IS NOT NULL
1710
                GROUP BY u.creator_id";
1711
1712
        $result = Database::query($sql);
1713
        $data = [];
1714
        while ($row = Database::fetch_array($result)) {
1715
            $userCount = is_numeric($row['user_count']) ? (int) $row['user_count'] : 0;
1716
            $name = trim($row['firstname'] . ' ' . $row['lastname']);
1717
            if (!empty($name)) {
1718
                $data[] = [
1719
                    'name' => $name,
1720
                    'count' => $userCount
1721
                ];
1722
            }
1723
        }
1724
1725
        return $data;
1726
    }
1727
1728
    /**
1729
     * Initializes an array with dates between two given dates, setting each date's value to 0.
1730
     * @throws Exception
1731
     */
1732
    public static function initializeDateRangeArray(string $startDate, string $endDate): array
1733
    {
1734
        $dateRangeArray = [];
1735
        $currentDate = new DateTime($startDate);
1736
        $endDate = new DateTime($endDate);
1737
1738
        // Loop through the date range and initialize each date with 0
1739
        while ($currentDate <= $endDate) {
1740
            $formattedDate = $currentDate->format('Y-m-d');
1741
            $dateRangeArray[$formattedDate] = 0;
1742
            $currentDate->modify('+1 day');
1743
        }
1744
1745
        return $dateRangeArray;
1746
    }
1747
1748
    /**
1749
     * Checks if the difference between two dates is more than one month.
1750
     * @throws Exception
1751
     */
1752
    public static function isMoreThanAMonth(string $dateStart, string $dateEnd): bool
1753
    {
1754
        $startDate = new DateTime($dateStart);
1755
        $endDate = new DateTime($dateEnd);
1756
1757
        $diff = $startDate->diff($endDate);
1758
1759
        if ($diff->y >= 1) {
1760
            return true;
1761
        }
1762
1763
        if ($diff->m > 1) {
1764
            return true;
1765
        }
1766
1767
        if ($diff->m == 1) {
1768
            return $diff->d > 0;
1769
        }
1770
1771
        return false;
1772
    }
1773
1774
    /**
1775
     * Groups registration data by month.
1776
     * @throws Exception
1777
     */
1778
    public static function groupByMonth(array $registrations): array
1779
    {
1780
        $groupedData = [];
1781
1782
        foreach ($registrations as $registration) {
1783
            $monthYear = (new DateTime($registration['date']))->format('Y-m');
1784
            if (isset($groupedData[$monthYear])) {
1785
                $groupedData[$monthYear] += $registration['count'];
1786
            } else {
1787
                $groupedData[$monthYear] = $registration['count'];
1788
            }
1789
        }
1790
1791
        return $groupedData;
1792
    }
1793
1794
    /**
1795
     * Retrieves the available tools using the repository.
1796
     */
1797
    public static function getAvailableTools(): array
1798
    {
1799
        $em = Database::getManager();
1800
        $repo = $em->getRepository(ResourceLink::class);
1801
1802
        return $repo->getAvailableTools();
1803
    }
1804
1805
    /**
1806
     * Generates a report of tool usage based on the provided tool IDs.
1807
     */
1808
    public static function getToolUsageReportByTools(array $toolIds): array
1809
    {
1810
        $em = Database::getManager();
1811
        $repo = $em->getRepository(ResourceLink::class);
1812
1813
        return $repo->getToolUsageReportByTools($toolIds);
1814
    }
1815
1816
    /**
1817
     * Return de number of certificates generated.
1818
     * This function is resource intensive.
1819
     * @throws \Doctrine\DBAL\Exception
1820
     * @throws Exception
1821
     */
1822
    public static function countCertificatesByQuarter(string $dateFrom = null, string $dateUntil = null): int
1823
    {
1824
        $tableGradebookCertificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
1825
        $condition = "";
1826
        if (!empty($dateFrom) && !empty($dateUntil)) {
1827
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1828
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1829
            $condition = "WHERE (created_at BETWEEN '$dateFrom' AND '$dateUntil')";
1830
        } elseif (!empty($dateFrom)) {
1831
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1832
            $condition = "WHERE created_at >= '$dateFrom'";
1833
        } elseif (!empty($dateUntil)) {
1834
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1835
            $condition = "WHERE created_at <= '$dateUntil'";
1836
        }
1837
        $sql = "
1838
            SELECT count(*) AS count
1839
            FROM $tableGradebookCertificate
1840
            $condition
1841
        ";
1842
        $response = Database::query($sql);
1843
        $obj = Database::fetch_object($response);
1844
        return $obj->count;
1845
    }
1846
1847
    /**
1848
     * Get the number of logins by dates.
1849
     * This function is resource intensive.
1850
     * @throws Exception
1851
     */
1852
    public static function getSessionsByDuration(string $dateFrom, string $dateUntil): array
1853
    {
1854
        $results = [
1855
            '0' => 0,
1856
            '5' => 0,
1857
            '10' => 0,
1858
            '15' => 0,
1859
            '30' => 0,
1860
            '60' => 0,
1861
        ];
1862
        if (!empty($dateFrom) && !empty($dateUntil)) {
1863
            $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1864
            $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1865
            $urlId = api_get_current_access_url_id();
1866
            $tableUrl = '';
1867
            $whereUrl = '';
1868
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1869
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1870
            $accessUrlUtil = Container::getAccessUrlUtil();
1871
1872
            if ($accessUrlUtil->isMultiple()) {
1873
                $accessUrl = $accessUrlUtil->getCurrent();
1874
                $urlId = $accessUrl->getId();
1875
                $tableUrl = ", $accessUrlRelUserTable";
1876
                $whereUrl = " AND login_user_id = user_id AND access_url_id = $urlId";
1877
            }
1878
            $sql = "SELECT login_id, TIMESTAMPDIFF(SECOND, login_date, logout_date) AS duration
1879
            FROM $table $tableUrl
1880
            WHERE login_date >= '$dateFrom'
1881
            AND logout_date <= '$dateUntil'
1882
            $whereUrl
1883
            ";
1884
            $res = Database::query($sql);
1885
            while ($session = Database::fetch_array($res)) {
1886
                if ($session['duration'] > 3600) {
1887
                    $results['60']++;
1888
                } elseif ($session['duration'] > 1800) {
1889
                    $results['30']++;
1890
                } elseif ($session['duration'] > 900) {
1891
                    $results['15']++;
1892
                } elseif ($session['duration'] > 600) {
1893
                    $results['10']++;
1894
                } elseif ($session['duration'] > 300) {
1895
                    $results['5']++;
1896
                } else {
1897
                    $results['0']++;
1898
                }
1899
            }
1900
        }
1901
        return $results;
1902
    }
1903
1904
    /**
1905
     * Returns the number of user subscriptions grouped by day.
1906
     */
1907
    public static function getSubscriptionsByDay(string $startDate, string $endDate): array
1908
    {
1909
        $conn = Database::getManager()->getConnection();
1910
        $sql = "
1911
        SELECT DATE(default_date) AS date, COUNT(default_id) AS count
1912
        FROM track_e_default
1913
        WHERE default_event_type = :eventType
1914
        AND default_date BETWEEN :start AND :end
1915
        GROUP BY DATE(default_date)
1916
        ORDER BY DATE(default_date)
1917
    ";
1918
1919
        return $conn->executeQuery($sql, [
1920
            'eventType' => 'user_subscribed',
1921
            'start' => $startDate.' 00:00:00',
1922
            'end' => $endDate.' 23:59:59',
1923
        ])->fetchAllAssociative();
1924
    }
1925
1926
    /**
1927
     * Returns the number of user unsubscriptions grouped by day.
1928
     */
1929
    public static function getUnsubscriptionsByDay(string $startDate, string $endDate): array
1930
    {
1931
        $conn = Database::getManager()->getConnection();
1932
        $sql = "
1933
        SELECT DATE(default_date) AS date, COUNT(default_id) AS count
1934
        FROM track_e_default
1935
        WHERE default_event_type IN (:eventType1, :eventType2)
1936
        AND default_date BETWEEN :start AND :end
1937
        GROUP BY DATE(default_date)
1938
        ORDER BY DATE(default_date)
1939
    ";
1940
1941
        return $conn->executeQuery($sql, [
1942
            'eventType1' => 'user_unsubscribed',
1943
            'eventType2' => 'session_user_deleted',
1944
            'start' => $startDate.' 00:00:00',
1945
            'end' => $endDate.' 23:59:59',
1946
        ], [
1947
            'eventType1' => ParameterType::STRING,
1948
            'eventType2' => ParameterType::STRING,
1949
        ])->fetchAllAssociative();
1950
    }
1951
1952
    /**
1953
     * Returns users who have activity in open courses without being officially enrolled.
1954
     */
1955
    public static function getUsersWithActivityButNotRegistered(int $sessionId = 0): array
1956
    {
1957
        $em = Database::getManager();
1958
1959
        $qb = $em->createQueryBuilder();
1960
        $qb->select('t.accessUserId AS userId, t.cId AS courseId, MAX(t.accessDate) AS lastAccess')
1961
            ->from(TrackEAccess::class, 't')
1962
            ->where('t.accessUserId IS NOT NULL')
1963
            ->andWhere('t.cId IS NOT NULL')
1964
            ->groupBy('t.accessUserId, t.cId');
1965
1966
        if ($sessionId > 0) {
1967
            $qb->andWhere('t.sessionId = :sessionId')
1968
                ->setParameter('sessionId', $sessionId);
1969
        }
1970
1971
        $results = $qb->getQuery()->getArrayResult();
1972
1973
        $nonRegistered = [];
1974
1975
        foreach ($results as $row) {
1976
            $userId = $row['userId'];
1977
            $courseId = $row['courseId'];
1978
1979
            $course = $em->getRepository(Course::class)->find($courseId);
1980
            if (!$course) {
1981
                continue;
1982
            }
1983
1984
            if (!\in_array($course->getVisibility(), [Course::OPEN_PLATFORM, Course::OPEN_WORLD], true)) {
1985
                continue;
1986
            }
1987
1988
            $isRegistered = $em->createQueryBuilder()
1989
                ->select('1')
1990
                ->from(CourseRelUser::class, 'cu')
1991
                ->where('cu.user = :userId AND cu.course = :courseId')
1992
                ->setParameter('userId', $userId)
1993
                ->setParameter('courseId', $courseId);
1994
1995
            if ($sessionId > 0) {
1996
                $isRegistered->andWhere('cu.session = :sessionId')
1997
                    ->setParameter('sessionId', $sessionId);
1998
            }
1999
2000
            if (empty($isRegistered->getQuery()->getResult())) {
2001
                $user = $em->getRepository(User::class)->find($userId);
2002
                if (!$user) {
2003
                    continue;
2004
                }
2005
2006
                $nonRegistered[] = [
2007
                    'id' => $user->getId(),
2008
                    'firstname' => $user->getFirstname(),
2009
                    'lastname' => $user->getLastname(),
2010
                    'email' => $user->getEmail(),
2011
                    'courseTitle' => $course->getTitle(),
2012
                    'courseCode' => $course->getCode(),
2013
                    'lastAccess' => $row['lastAccess'] ? (new \DateTime($row['lastAccess']))->format('Y-m-d H:i:s') : '',
2014
                ];
2015
            }
2016
        }
2017
2018
        return $nonRegistered;
2019
    }
2020
}
2021