Passed
Push — master ( e60e65...6408ae )
by Yannick
09:50
created

Statistics::getJSChartTemplateWithData()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 69
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 26
nc 16
nop 8
dl 0
loc 69
rs 9.1928
c 2
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\UserRelUser;
7
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
8
use Chamilo\CoreBundle\Framework\Container;
9
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
10
11
/**
12
 * This class provides some functions for statistics.
13
 */
14
class Statistics
15
{
16
    /**
17
     * Converts a number of bytes in a formatted string.
18
     *
19
     * @param int $size
20
     *
21
     * @return string Formatted file size or empty string if no match
22
     */
23
    public static function makeSizeString(int $size): string
24
    {
25
        if ($size < pow(2, 10)) {
26
            return $size." bytes";
27
        }
28
        if ($size >= pow(2, 10) && $size < pow(2, 20)) {
29
            return round($size / pow(2, 10), 0)." KB";
30
        }
31
        if ($size >= pow(2, 20) && $size < pow(2, 30)) {
32
            return round($size / pow(2, 20), 1)." MB";
33
        }
34
        if ($size > pow(2, 30)) {
35
            return round($size / pow(2, 30), 2)." GB";
36
        }
37
38
        return '';
39
    }
40
41
    /**
42
     * Count courses.
43
     *
44
     * @param string|null $categoryCode Code of a course category.
45
     *                                  Default: count all courses.
46
     * @param string|null $dateFrom dateFrom
47
     * @param string|null $dateUntil dateUntil
48
     *
49
     * @return int Number of courses counted
50
     * @throws \Doctrine\DBAL\Exception
51
     * @throws Exception
52
     */
53
    public static function countCourses(string $categoryCode = null, string $dateFrom = null, string $dateUntil = null): int
54
    {
55
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
56
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
57
        /** @var AccessUrlHelper $accessUrlHelper */
58
        $accessUrlHelper = Container::$container->get(AccessUrlHelper::class);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

58
        /** @scrutinizer ignore-call */ 
59
        $accessUrlHelper = Container::$container->get(AccessUrlHelper::class);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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