Passed
Push — master ( 5d8439...1c5d20 )
by Yannick
16:06 queued 08:16
created

Statistics::getSessionsByDuration()   B

Complexity

Conditions 10
Paths 3

Size

Total Lines 46
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 38
nc 3
nop 2
dl 0
loc 46
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/* 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\ServiceHelper\AccessUrlHelper;
9
10
/**
11
 * This class provides some functions for statistics.
12
 */
13
class Statistics
14
{
15
    /**
16
     * Converts a number of bytes in a formatted string.
17
     *
18
     * @param int $size
19
     *
20
     * @return string Formatted file size
21
     */
22
    public static function makeSizeString($size)
23
    {
24
        if ($size < pow(2, 10)) {
25
            return $size." bytes";
26
        }
27
        if ($size >= pow(2, 10) && $size < pow(2, 20)) {
28
            return round($size / pow(2, 10), 0)." KB";
29
        }
30
        if ($size >= pow(2, 20) && $size < pow(2, 30)) {
31
            return round($size / pow(2, 20), 1)." MB";
32
        }
33
        if ($size > pow(2, 30)) {
34
            return round($size / pow(2, 30), 2)." GB";
35
        }
36
    }
37
38
    /**
39
     * Count courses.
40
     *
41
     * @param string|null $categoryCode Code of a course category.
42
     *                                  Default: count all courses.
43
     * @param string|null $dateFrom dateFrom
44
     * @param string|null $dateUntil dateUntil
45
     *
46
     * @return int Number of courses counted
47
     * @throws \Doctrine\DBAL\Exception
48
     */
49
    public static function countCourses(string $categoryCode = null, string $dateFrom = null, string $dateUntil = null): int
50
    {
51
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
52
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
53
        $urlId = api_get_current_access_url_id();
54
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

54
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
55
            $sql = "SELECT COUNT(*) AS number
56
                    FROM ".$courseTable." AS c, $accessUrlRelCourseTable AS u
57
                    WHERE u.c_id = c.id AND $accessUrlRelCourseTable='".$urlId."'";
58
            if (isset($categoryCode)) {
59
                $sql .= " AND category_code = '".Database::escape_string($categoryCode)."'";
60
            }
61
        } else {
62
            $sql = "SELECT COUNT(*) AS number
63
                    FROM $courseTable AS c
64
                    WHERE 1 = 1";
65
            if (isset($categoryCode)) {
66
                $sql .= " WHERE c.category_code = '".Database::escape_string($categoryCode)."'";
67
            }
68
        }
69
70
        if (!empty($dateFrom)) {
71
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
72
            $sql .= " AND c.creation_date >= '$dateFrom' ";
73
        }
74
        if (!empty($dateUntil)) {
75
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
76
            $sql .= " AND c.creation_date <= '$dateUntil' ";
77
        }
78
79
        $res = Database::query($sql);
80
        $obj = Database::fetch_object($res);
81
82
        return $obj->number;
83
    }
84
85
    /**
86
     * Count courses by visibility.
87
     *
88
     * @param array|null  $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses
89
     * @param string|null $dateFrom dateFrom
90
     * @param string|null $dateUntil dateUntil
91
     *
92
     * @return int Number of courses counted
93
     * @throws \Doctrine\DBAL\Exception
94
     */
95
    public static function countCoursesByVisibility(
96
        array $visibility = null,
97
        string $dateFrom = null,
98
        string $dateUntil = null
99
    ): int
100
    {
101
        if (empty($visibility)) {
102
            return 0;
103
        } else {
104
            $visibilityString = '';
105
            $auxArrayVisibility = [];
106
            if (!is_array($visibility)) {
107
                $visibility = [$visibility];
108
            }
109
            foreach ($visibility as $item) {
110
                $auxArrayVisibility[] = (int) $item;
111
            }
112
            $visibilityString = implode(',', $auxArrayVisibility);
113
        }
114
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
115
        $accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
116
        $urlId = api_get_current_access_url_id();
117
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

117
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
118
            $sql = "SELECT COUNT(*) AS number
119
                    FROM $courseTable AS c, $accessUrlRelCourseTable AS u
120
                    WHERE u.c_id = c.id AND u.access_url_id='".$urlId."'";
121
        } else {
122
            $sql = "SELECT COUNT(*) AS number
123
                    FROM $courseTable AS c
124
                    WHERE 1 = 1";
125
        }
126
        $sql .= " AND visibility IN ($visibilityString) ";
127
        if (!empty($dateFrom)) {
128
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
129
            $sql .= " AND c.creation_date >= '$dateFrom' ";
130
        }
131
        if (!empty($dateUntil)) {
132
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
133
            $sql .= " AND c.creation_date <= '$dateUntil' ";
134
        }
135
        $res = Database::query($sql);
136
        $obj = Database::fetch_object($res);
137
138
        return $obj->number;
139
    }
140
141
    /**
142
     * Count users.
143
     *
144
     * @param int    $status                user status (COURSEMANAGER or STUDENT) if not setted it'll count all users
145
     * @param string $categoryCode          course category code. Default: count only users without filtering category
146
     * @param bool   $countInvisibleCourses Count invisible courses (todo)
147
     * @param bool   $onlyActive            Count only active users (false to only return currently active users)
148
     *
149
     * @return int Number of users counted
150
     */
151
    public static function countUsers(
152
        $status = null,
153
        $categoryCode = null,
154
        $countInvisibleCourses = true,
155
        $onlyActive = false
156
    ) {
157
        // Database table definitions
158
        $course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
159
        $course_table = Database::get_main_table(TABLE_MAIN_COURSE);
160
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
161
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
162
        $tblCourseCategory = Database::get_main_table(TABLE_MAIN_CATEGORY);
163
        $tblCourseRelCategory = Database::get_main_table(TABLE_MAIN_COURSE_REL_CATEGORY);
164
        $urlId = api_get_current_access_url_id();
165
166
        $conditions = [];
167
        $conditions[] = "u.active <> " . USER_SOFT_DELETED;
168
        if ($onlyActive) {
169
            $conditions[] = "u.active = 1";
170
        }
171
        if (isset($status)) {
172
            $conditions[] = "u.status = " . intval($status);
173
        }
174
175
        $where = implode(' AND ', $conditions);
176
177
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

177
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
178
            $sql = "SELECT COUNT(DISTINCT(u.id)) AS number
179
                FROM $user_table as u
180
                INNER JOIN $access_url_rel_user_table as url ON u.id = url.user_id
181
                WHERE $where AND url.access_url_id = $urlId";
182
183
            if (isset($categoryCode)) {
184
                $categoryCode = Database::escape_string($categoryCode);
185
                $sql = "SELECT COUNT(DISTINCT(cu.user_id)) AS number
186
                    FROM $course_user_table cu
187
                    INNER JOIN $course_table c ON c.id = cu.c_id
188
                    INNER JOIN $access_url_rel_user_table as url ON cu.user_id = url.user_id
189
                    INNER JOIN $tblCourseRelCategory crc ON crc.course_id = c.id
190
                    INNER JOIN $tblCourseCategory cc ON cc.id = crc.course_category_id
191
                    WHERE $where AND url.access_url_id = $urlId AND cc.code = '$categoryCode'";
192
            }
193
        } else {
194
            $sql = "SELECT COUNT(DISTINCT(id)) AS number
195
                FROM $user_table u
196
                WHERE $where";
197
198
            if (isset($categoryCode)) {
199
                $categoryCode = Database::escape_string($categoryCode);
200
                $sql = "SELECT COUNT(DISTINCT(cu.user_id)) AS number
201
                    FROM $course_user_table cu
202
                    INNER JOIN $course_table c ON c.id = cu.c_id
203
                    INNER JOIN $tblCourseRelCategory crc ON crc.course_id = c.id
204
                    INNER JOIN $tblCourseCategory cc ON cc.id = crc.course_category_id
205
                    INNER JOIN $user_table u ON u.id = cu.user_id
206
                    WHERE $where AND cc.code = '$categoryCode'";
207
            }
208
        }
209
210
        $res = Database::query($sql);
211
        $obj = Database::fetch_object($res);
212
213
        return $obj->number;
214
    }
215
216
    /**
217
     * @param string $startDate
218
     * @param string $endDate
219
     *
220
     * @return array
221
     */
222
    public static function getCoursesWithActivity($startDate, $endDate)
223
    {
224
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
225
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
226
        $startDate = Database::escape_string($startDate);
227
        $endDate = Database::escape_string($endDate);
228
229
        $urlId = api_get_current_access_url_id();
230
231
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

231
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
232
            $sql = "SELECT DISTINCT(t.c_id) FROM $table t , $access_url_rel_course_table a
233
                    WHERE
234
                        t.c_id = a.c_id AND
235
                        access_url_id='".$urlId."' AND
236
                        access_date BETWEEN '$startDate' AND '$endDate'
237
                    ";
238
        } else {
239
            $sql = "SELECT DISTINCT(t.c_id) FROM $table t
240
                   access_date BETWEEN '$startDate' AND '$endDate' ";
241
        }
242
243
        $result = Database::query($sql);
244
245
        return Database::store_result($result);
246
    }
247
248
    /**
249
     * Count activities from track_e_default_table.
250
     *
251
     * @return int Number of activities counted
252
     */
253
    public static function getNumberOfActivities($courseId = 0, $sessionId = 0)
254
    {
255
        // Database table definitions
256
        $track_e_default = Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT);
257
        $table_user = Database::get_main_table(TABLE_MAIN_USER);
258
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
259
        $urlId = api_get_current_access_url_id();
260
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

260
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
261
            $sql = "SELECT count(default_id) AS total_number_of_items
262
                    FROM $track_e_default, $table_user user, $access_url_rel_user_table url
263
                    WHERE user.active <> ".USER_SOFT_DELETED." AND
264
                        default_user_id = user.id AND
265
                        user.id=url.user_id AND
266
                        access_url_id = '".$urlId."'";
267
        } else {
268
            $sql = "SELECT count(default_id) AS total_number_of_items
269
                    FROM $track_e_default, $table_user user
270
                    WHERE user.active <> ".USER_SOFT_DELETED." AND default_user_id = user.id ";
271
        }
272
273
        if (!empty($courseId)) {
274
            $courseId = (int) $courseId;
275
            $sql .= " AND c_id = $courseId";
276
            $sql .= api_get_session_condition($sessionId);
277
        }
278
279
        if (isset($_GET['keyword'])) {
280
            $keyword = Database::escape_string(trim($_GET['keyword']));
281
            $sql .= " AND (
282
                        user.username LIKE '%".$keyword."%' OR
283
                        default_event_type LIKE '%".$keyword."%' OR
284
                        default_value_type LIKE '%".$keyword."%' OR
285
                        default_value LIKE '%".$keyword."%') ";
286
        }
287
        $res = Database::query($sql);
288
        $obj = Database::fetch_object($res);
289
290
        return $obj->total_number_of_items;
291
    }
292
293
    /**
294
     * Get activities data to display.
295
     *
296
     * @param int    $from
297
     * @param int    $numberOfItems
298
     * @param int    $column
299
     * @param string $direction
300
     * @param int    $courseId
301
     * @param int    $sessionId
302
     *
303
     * @return array
304
     */
305
    public static function getActivitiesData(
306
        $from,
307
        $numberOfItems,
308
        $column,
309
        $direction,
310
        $courseId = 0,
311
        $sessionId = 0
312
    ) {
313
        $track_e_default = Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT);
314
        $table_user = Database::get_main_table(TABLE_MAIN_USER);
315
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
316
        $urlId = api_get_current_access_url_id();
317
        $column = (int) $column;
318
        $from = (int) $from;
319
        $numberOfItems = (int) $numberOfItems;
320
        $direction = strtoupper($direction);
321
322
        if (!in_array($direction, ['ASC', 'DESC'])) {
323
            $direction = 'DESC';
324
        }
325
326
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

326
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
327
            $sql = "SELECT
328
                        default_event_type  as col0,
329
                        default_value_type    as col1,
330
                        default_value        as col2,
331
                        c_id         as col3,
332
                        session_id as col4,
333
                        user.username         as col5,
334
                        user.id         as col6,
335
                        default_date         as col7
336
                    FROM $track_e_default as track_default,
337
                    $table_user as user,
338
                    $access_url_rel_user_table as url
339
                    WHERE
340
                        user.active <> -1 AND
341
                        track_default.default_user_id = user.id AND
342
                        url.user_id = user.id AND
343
                        access_url_id= $urlId ";
344
        } else {
345
            $sql = "SELECT
346
                       default_event_type  as col0,
347
                       default_value_type    as col1,
348
                       default_value        as col2,
349
                       c_id         as col3,
350
                       session_id as col4,
351
                       user.username         as col5,
352
                       user.id         as col6,
353
                       default_date         as col7
354
                   FROM $track_e_default track_default, $table_user user
355
                   WHERE user.active <> ".USER_SOFT_DELETED." AND track_default.default_user_id = user.id ";
356
        }
357
358
        if (!empty($_GET['keyword'])) {
359
            $keyword = Database::escape_string(trim($_GET['keyword']));
360
            $sql .= " AND (user.username LIKE '%".$keyword."%' OR
361
                        default_event_type LIKE '%".$keyword."%' OR
362
                        default_value_type LIKE '%".$keyword."%' OR
363
                        default_value LIKE '%".$keyword."%') ";
364
        }
365
366
        if (!empty($courseId)) {
367
            $courseId = (int) $courseId;
368
            $sql .= " AND c_id = $courseId";
369
            $sql .= api_get_session_condition($sessionId);
370
        }
371
372
        if (!empty($column) && !empty($direction)) {
373
            $sql .= " ORDER BY col$column $direction";
374
        } else {
375
            $sql .= " ORDER BY col7 DESC ";
376
        }
377
        $sql .= " LIMIT $from, $numberOfItems ";
378
379
        $res = Database::query($sql);
380
        $activities = [];
381
        while ($row = Database::fetch_row($res)) {
382
            if (false === strpos($row[1], '_object') &&
383
                false === strpos($row[1], '_array')
384
            ) {
385
                $row[2] = $row[2];
386
            } else {
387
                if (!empty($row[2])) {
388
                    $originalData = str_replace('\\', '', $row[2]);
389
                    $row[2] = UnserializeApi::unserialize('not_allowed_classes', $originalData);
390
                    if (is_array($row[2]) && !empty($row[2])) {
391
                        $row[2] = implode_with_key(', ', $row[2]);
392
                    } else {
393
                        $row[2] = $originalData;
394
                    }
395
                }
396
            }
397
398
            if (!empty($row['default_date'])) {
399
                $row['default_date'] = api_get_local_time($row['default_date']);
400
            } else {
401
                $row['default_date'] = '-';
402
            }
403
404
            if (!empty($row[7])) {
405
                $row[7] = api_get_local_time($row[7]);
406
            } else {
407
                $row[7] = '-';
408
            }
409
410
            if (!empty($row[5])) {
411
                // Course
412
                if (!empty($row[3])) {
413
                    $row[3] = Display::url(
414
                        $row[3],
415
                        api_get_path(WEB_CODE_PATH).'admin/course_edit.php?id='.$row[3]
416
                    );
417
                } else {
418
                    $row[3] = '-';
419
                }
420
421
                // session
422
                if (!empty($row[4])) {
423
                    $row[4] = Display::url(
424
                        $row[4],
425
                        api_get_path(WEB_CODE_PATH).'session/resume_session.php?id_session='.$row[4]
426
                    );
427
                } else {
428
                    $row[4] = '-';
429
                }
430
431
                // User id.
432
                $row[5] = Display::url(
433
                    $row[5],
434
                    api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_popup&user_id='.$row[6],
435
                    ['class' => 'ajax']
436
                );
437
438
                $row[6] = Tracking::get_ip_from_user_event(
439
                    $row[6],
440
                    $row[7],
441
                    true
442
                );
443
                if (empty($row[6])) {
444
                    $row[6] = get_lang('Unknown');
445
                }
446
            }
447
            $activities[] = $row;
448
        }
449
450
        return $activities;
451
    }
452
453
    /**
454
     * Rescale data.
455
     *
456
     * @param array $data The data that should be rescaled
457
     * @param int   $max  The maximum value in the rescaled data (default = 500);
458
     *
459
     * @return array The rescaled data, same key as $data
460
     */
461
    public static function rescale($data, $max = 500)
462
    {
463
        $data_max = 1;
464
        foreach ($data as $index => $value) {
465
            $data_max = ($data_max < $value ? $value : $data_max);
466
        }
467
        reset($data);
468
        $result = [];
469
        $delta = $max / $data_max;
470
        foreach ($data as $index => $value) {
471
            $result[$index] = (int) round($value * $delta);
472
        }
473
474
        return $result;
475
    }
476
477
    /**
478
     * Show statistics.
479
     *
480
     * @param string $title      The title
481
     * @param array  $stats
482
     * @param bool   $showTotal
483
     * @param bool   $isFileSize
484
     *
485
     * @return string HTML table
486
     */
487
    public static function printStats(
488
        $title,
489
        $stats,
490
        $showTotal = true,
491
        $isFileSize = false
492
    ) {
493
        $total = 0;
494
        $content = '<table class="table table-hover table-striped data_table stats_table" cellspacing="0" cellpadding="3" width="90%">
495
            <thead><tr><th colspan="'.($showTotal ? '4' : '3').'">'.$title.'</th></tr></thead><tbody>';
496
        $i = 0;
497
        foreach ($stats as $subtitle => $number) {
498
            $total += $number;
499
        }
500
501
        foreach ($stats as $subtitle => $number) {
502
            if (!$isFileSize) {
503
                $number_label = number_format($number, 0, ',', '.');
504
            } else {
505
                $number_label = self::makeSizeString($number);
506
            }
507
            $percentage = ($total > 0 ? number_format(100 * $number / $total, 1, ',', '.') : '0');
508
509
            $content .= '<tr class="row_'.(0 == $i % 2 ? 'odd' : 'even').'">
510
                    <td width="25%" style="vertical-align:top;">'.$subtitle.'</td>
511
                    <td width="60%">'.Display::bar_progress($percentage, false).'</td>
512
                    <td width="5%" align="right" style="vertical-align:top;">'.$number_label.'</td>';
513
            if ($showTotal) {
514
                $content .= '<td width="5%" align="right"> '.$percentage.'%</td>';
515
            }
516
            $content .= '</tr>';
517
            $i++;
518
        }
519
        $content .= '</tbody>';
520
        if ($showTotal) {
521
            if (!$isFileSize) {
522
                $total_label = number_format($total, 0, ',', '.');
523
            } else {
524
                $total_label = self::makeSizeString($total);
525
            }
526
            $content .= '
527
                <tfoot><tr><th colspan="4" align="right">'.get_lang('Total').': '.$total_label.'</td></tr></tfoot>
528
            ';
529
        }
530
        $content .= '</table>';
531
532
        return $content;
533
    }
534
535
    /**
536
     * Show some stats about the number of logins.
537
     *
538
     * @param string $type month, hour or day
539
     */
540
    public static function printLoginStats($type)
541
    {
542
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
543
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
544
        $urlId = api_get_current_access_url_id();
545
546
        $table_url = null;
547
        $where_url = null;
548
        $now = api_get_utc_datetime();
549
        $where_url_last = ' WHERE login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
550
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

550
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
551
            $table_url = ", $access_url_rel_user_table";
552
            $where_url = " WHERE login_user_id=user_id AND access_url_id='".$urlId."'";
553
            $where_url_last = ' AND login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
554
        }
555
556
        $period = get_lang('Month');
557
        $periodCollection = api_get_months_long();
558
        $sql = "SELECT
559
                DATE_FORMAT( login_date, '%Y-%m' ) AS stat_date ,
560
                count( login_id ) AS number_of_logins
561
                FROM $table $table_url $where_url
562
                GROUP BY stat_date
563
                ORDER BY login_date DESC";
564
        $sql_last_x = null;
565
566
        switch ($type) {
567
            case 'hour':
568
                $period = get_lang('Hour');
569
                $sql = "SELECT
570
                          DATE_FORMAT( login_date, '%H') AS stat_date,
571
                          count( login_id ) AS number_of_logins
572
                        FROM $table $table_url $where_url
573
                        GROUP BY stat_date
574
                        ORDER BY stat_date ";
575
                $sql_last_x = "SELECT
576
                                DATE_FORMAT( login_date, '%H' ) AS stat_date,
577
                                count( login_id ) AS number_of_logins
578
                               FROM $table $table_url $where_url ".sprintf($where_url_last, 'DAY')."
579
                               GROUP BY stat_date
580
                               ORDER BY stat_date ";
581
                break;
582
            case 'day':
583
                $periodCollection = api_get_week_days_long();
584
                $period = get_lang('Day');
585
                $sql = "SELECT DATE_FORMAT( login_date, '%w' ) AS stat_date ,
586
                        count( login_id ) AS number_of_logins
587
                        FROM  $table $table_url $where_url
588
                        GROUP BY stat_date
589
                        ORDER BY DATE_FORMAT( login_date, '%w' ) ";
590
                $sql_last_x = "SELECT
591
                                DATE_FORMAT( login_date, '%w' ) AS stat_date,
592
                                count( login_id ) AS number_of_logins
593
                               FROM $table $table_url $where_url ".sprintf($where_url_last, 'WEEK')."
594
                               GROUP BY stat_date
595
                               ORDER BY DATE_FORMAT( login_date, '%w' ) ";
596
                break;
597
        }
598
599
        $content = '';
600
        if ($sql_last_x) {
601
            $res_last_x = Database::query($sql_last_x);
602
            $result_last_x = [];
603
            while ($obj = Database::fetch_object($res_last_x)) {
604
                $stat_date = ('day' === $type) ? $periodCollection[$obj->stat_date] : $obj->stat_date;
605
                $result_last_x[$stat_date] = $obj->number_of_logins;
606
            }
607
            $content .= self::printStats(get_lang('LastLogins').' ('.$period.')', $result_last_x, true);
608
            flush(); //flush web request at this point to see something already while the full data set is loading
609
            $content .= '<br />';
610
        }
611
        $res = Database::query($sql);
612
        $result = [];
613
        while ($obj = Database::fetch_object($res)) {
614
            $stat_date = $obj->stat_date;
615
            switch ($type) {
616
                case 'month':
617
                    $stat_date = explode('-', $stat_date);
618
                    $stat_date[1] = $periodCollection[$stat_date[1] - 1];
619
                    $stat_date = implode(' ', $stat_date);
620
                    break;
621
                case 'day':
622
                    $stat_date = $periodCollection[$stat_date];
623
                    break;
624
            }
625
            $result[$stat_date] = $obj->number_of_logins;
626
        }
627
        $content .= self::printStats(get_lang('AllLogins').' ('.$period.')', $result, true);
628
629
        return $content;
630
    }
631
632
    /**
633
     * Print the number of recent logins.
634
     *
635
     * @param bool  $distinct        whether to only give distinct users stats, or *all* logins
636
     * @param int   $sessionDuration Number of minutes a session must have lasted at a minimum to be taken into account
637
     * @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)
638
     *
639
     * @throws Exception
640
     *
641
     * @return string HTML table
642
     */
643
    public static function printRecentLoginStats($distinct = false, $sessionDuration = 0, $periods = [])
644
    {
645
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
646
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
647
        $urlId = api_get_current_access_url_id();
648
        $table_url = '';
649
        $where_url = '';
650
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

650
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
651
            $table_url = ", $access_url_rel_user_table";
652
            $where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'";
653
        }
654
655
        $now = api_get_utc_datetime();
656
        $field = 'login_id';
657
        if ($distinct) {
658
            $field = 'DISTINCT(login_user_id)';
659
        }
660
661
        if (empty($periods)) {
662
            $periods = [1, 7, 31];
663
        }
664
        $sqlList = [];
665
666
        $sessionDuration = (int) $sessionDuration * 60; // convert from minutes to seconds
667
        foreach ($periods as $day) {
668
            $date = new DateTime($now);
669
            $startDate = $date->format('Y-m-d').' 00:00:00';
670
            $endDate = $date->format('Y-m-d').' 23:59:59';
671
672
            if ($day > 1) {
673
                $startDate = $date->sub(new DateInterval('P'.$day.'D'));
674
                $startDate = $startDate->format('Y-m-d').' 00:00:00';
675
            }
676
677
            $localDate = api_get_local_time($startDate, null, null, false, false);
678
            $localEndDate = api_get_local_time($endDate, null, null, false, false);
679
680
            $label = sprintf(get_lang('Last %s days'), $day);
681
            if (1 == $day) {
682
                $label = get_lang('Today');
683
            }
684
            $label .= " <span class=\"muted right\" style=\"float: right; margin-right: 5px;\">[$localDate - $localEndDate]</span>";
685
            $sql = "SELECT count($field) AS number
686
                    FROM $table $table_url
687
                    WHERE ";
688
            if (0 == $sessionDuration) {
689
                $sql .= " logout_date != login_date AND ";
690
            } else {
691
                $sql .= " UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration AND ";
692
            }
693
            $sql .= "login_date BETWEEN '$startDate' AND '$endDate'
694
                        $where_url";
695
            $sqlList[$label] = $sql;
696
        }
697
698
        $sql = "SELECT count($field) AS number
699
                FROM $table $table_url ";
700
        if (0 == $sessionDuration) {
701
            $sql .= " WHERE logout_date != login_date $where_url";
702
        } else {
703
            $sql .= " WHERE UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration $where_url";
704
        }
705
        $sqlList[get_lang('Total')] = $sql;
706
        $totalLogin = [];
707
        foreach ($sqlList as $label => $query) {
708
            $res = Database::query($query);
709
            $obj = Database::fetch_object($res);
710
            $totalLogin[$label] = $obj->number;
711
        }
712
713
        if ($distinct) {
714
            $content = self::printStats(get_lang('DistinctUsersLogins'), $totalLogin, false);
715
        } else {
716
            $content = self::printStats(get_lang('Logins'), $totalLogin, false);
717
        }
718
719
        return $content;
720
    }
721
722
    /**
723
     * Get the number of recent logins.
724
     *
725
     * @param bool $distinct            Whether to only give distinct users stats, or *all* logins
726
     * @param int  $sessionDuration     Number of minutes a session must have lasted at a minimum to be taken into account
727
     * @param bool $completeMissingDays Whether to fill the daily gaps (if any) when getting a list of logins
728
     *
729
     * @throws Exception
730
     *
731
     * @return array
732
     */
733
    public static function getRecentLoginStats($distinct = false, $sessionDuration = 0, $completeMissingDays = true)
734
    {
735
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
736
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
737
        $urlId = api_get_current_access_url_id();
738
        $table_url = '';
739
        $where_url = '';
740
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

740
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
741
            $table_url = ", $access_url_rel_user_table";
742
            $where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'";
743
        }
744
745
        $now = api_get_utc_datetime();
746
        $date = new DateTime($now);
747
        $date->sub(new DateInterval('P31D'));
748
        $newDate = $date->format('Y-m-d h:i:s');
749
        $totalLogin = self::buildDatesArray($newDate, $now, true);
750
751
        $field = 'login_id';
752
        if ($distinct) {
753
            $field = 'DISTINCT(login_user_id)';
754
        }
755
        $sessionDuration = (int) $sessionDuration * 60; //Convert from minutes to seconds
756
757
        $sql = "SELECT count($field) AS number, date(login_date) as login_date
758
                FROM $table $table_url
759
                WHERE ";
760
        if (0 == $sessionDuration) {
761
            $sql .= " logout_date != login_date AND ";
762
        } else {
763
            $sql .= " UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date) > $sessionDuration AND ";
764
        }
765
        $sql .= " login_date >= '$newDate' $where_url
766
                GROUP BY date(login_date)";
767
768
        $res = Database::query($sql);
769
        while ($row = Database::fetch_assoc($res)) {
770
            $monthAndDay = substr($row['login_date'], 5, 5);
771
            $totalLogin[$monthAndDay] = $row['number'];
772
        }
773
774
        return $totalLogin;
775
    }
776
777
    /**
778
     * Get course tools usage statistics for the whole platform (by URL if multi-url).
779
     */
780
    public static function getToolsStats()
781
    {
782
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ACCESS);
783
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
784
        $urlId = api_get_current_access_url_id();
785
786
        $tools = [
787
            'announcement',
788
            'assignment',
789
            'calendar_event',
790
            'chat',
791
            'course_description',
792
            'document',
793
            'dropbox',
794
            'group',
795
            'learnpath',
796
            'link',
797
            'quiz',
798
            'student_publication',
799
            'user',
800
            'forum',
801
        ];
802
        $tool_names = [];
803
        foreach ($tools as $tool) {
804
            $tool_names[$tool] = get_lang(ucfirst($tool), '');
805
        }
806
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

806
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
807
            $sql = "SELECT access_tool, count( access_id ) AS number_of_logins
808
                    FROM $table t , $access_url_rel_course_table a
809
                    WHERE
810
                        access_tool IN ('".implode("','", $tools)."') AND
811
                        t.c_id = a.c_id AND
812
                        access_url_id='".$urlId."'
813
                        GROUP BY access_tool
814
                    ";
815
        } else {
816
            $sql = "SELECT access_tool, count( access_id ) AS number_of_logins
817
                    FROM $table
818
                    WHERE access_tool IN ('".implode("','", $tools)."')
819
                    GROUP BY access_tool ";
820
        }
821
822
        $res = Database::query($sql);
823
        $result = [];
824
        while ($obj = Database::fetch_object($res)) {
825
            $result[$tool_names[$obj->access_tool]] = $obj->number_of_logins;
826
        }
827
828
        return $result;
829
    }
830
831
    /**
832
     * Show some stats about the accesses to the different course tools.
833
     *
834
     * @param array $result If defined, this serves as data. Otherwise, will get the data from getToolsStats()
835
     *
836
     * @return string HTML table
837
     */
838
    public static function printToolStats($result = null)
839
    {
840
        if (empty($result)) {
841
            $result = self::getToolsStats();
842
        }
843
844
        return self::printStats(get_lang('Tools access'), $result, true);
845
    }
846
847
    /**
848
     * Show some stats about the number of courses per language.
849
     */
850
    public static function printCourseByLanguageStats()
851
    {
852
        $table = Database::get_main_table(TABLE_MAIN_COURSE);
853
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
854
        $urlId = api_get_current_access_url_id();
855
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

855
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
856
            $sql = "SELECT course_language, count( c.code ) AS number_of_courses
857
                    FROM $table as c, $access_url_rel_course_table as u
858
                    WHERE u.c_id = c.id AND access_url_id='".$urlId."'
859
                    GROUP BY course_language
860
                    ORDER BY number_of_courses DESC";
861
        } else {
862
            $sql = "SELECT course_language, count( code ) AS number_of_courses
863
                   FROM $table GROUP BY course_language
864
                   ORDER BY number_of_courses DESC";
865
        }
866
        $res = Database::query($sql);
867
        $result = [];
868
        while ($obj = Database::fetch_object($res)) {
869
            $result[$obj->course_language] = $obj->number_of_courses;
870
        }
871
872
        return $result;
873
    }
874
875
    /**
876
     * Shows the number of users having their picture uploaded in Dokeos.
877
     */
878
    public static function printUserPicturesStats()
879
    {
880
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
881
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
882
        $urlId = api_get_current_access_url_id();
883
        $url_condition = null;
884
        $url_condition2 = null;
885
        $table = null;
886
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

886
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
887
            $url_condition = ", $access_url_rel_user_table as url WHERE url.user_id=u.id AND access_url_id='".$urlId."'";
888
            $url_condition2 = " AND url.user_id=u.id AND access_url_id='".$urlId."'";
889
            $table = ", $access_url_rel_user_table as url ";
890
        }
891
        $sql = "SELECT COUNT(*) AS n FROM $user_table as u ".$url_condition;
892
        $res = Database::query($sql);
893
        $count1 = Database::fetch_object($res);
894
        $sql = "SELECT COUNT(*) AS n FROM $user_table as u $table ".
895
               "WHERE LENGTH(picture_uri) > 0 $url_condition2";
896
897
        $sql .= !str_contains($sql, 'WHERE') ? ' WHERE u.active <> '.USER_SOFT_DELETED : ' AND u.active <> '.USER_SOFT_DELETED;
898
899
        $res = Database::query($sql);
900
        $count2 = Database::fetch_object($res);
901
        // #users without picture
902
        $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...
903
        $result[get_lang('Yes')] = $count2->n; // #users with picture
904
905
        return self::printStats(get_lang('Number of users').' ('.get_lang('Picture').')', $result, true);
906
    }
907
908
    /**
909
     * Important activities.
910
     */
911
    public static function printActivitiesStats()
912
    {
913
        $content = '<h4>'.get_lang('Important activities').'</h4>';
914
        // Create a search-box
915
        $form = new FormValidator(
916
            'search_simple',
917
            'get',
918
            api_get_path(WEB_CODE_PATH).'admin/statistics/index.php',
919
            '',
920
            ['style' => 'width:200px'],
921
            false
922
        );
923
        $renderer = &$form->defaultRenderer();
924
        $renderer->setCustomElementTemplate('<span>{element}</span> ');
925
        $form->addHidden('report', 'activities');
926
        $form->addHidden('activities_direction', 'DESC');
927
        $form->addHidden('activities_column', '4');
928
        $form->addElement('text', 'keyword', get_lang('Keyword'));
929
        $form->addButtonSearch(get_lang('Search'), 'submit');
930
        $content .= '<div class="actions">';
931
        $content .= $form->returnForm();
932
        $content .= '</div>';
933
934
        $table = new SortableTable(
935
            'activities',
936
            ['Statistics', 'getNumberOfActivities'],
937
            ['Statistics', 'getActivitiesData'],
938
            7,
939
            50,
940
            'DESC'
941
        );
942
        $parameters = [];
943
944
        $parameters['report'] = 'activities';
945
        if (isset($_GET['keyword'])) {
946
            $parameters['keyword'] = Security::remove_XSS($_GET['keyword']);
947
        }
948
949
        $table->set_additional_parameters($parameters);
950
        $table->set_header(0, get_lang('Event type'));
951
        $table->set_header(1, get_lang('Data type'));
952
        $table->set_header(2, get_lang('Value'));
953
        $table->set_header(3, get_lang('Course'));
954
        $table->set_header(4, get_lang('Session'));
955
        $table->set_header(5, get_lang('Username'));
956
        $table->set_header(6, get_lang('IP address'));
957
        $table->set_header(7, get_lang('Date'));
958
        $content .= $table->return_table();
959
960
        return $content;
961
    }
962
963
    /**
964
     * Shows statistics about the time of last visit to each course.
965
     */
966
    public static function printCourseLastVisit()
967
    {
968
        $access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
969
        $urlId = api_get_current_access_url_id();
970
971
        $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...
972
        $columns[1] = 'access_date';
973
        $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...
974
        $sql_order[SORT_DESC] = 'DESC';
975
        $per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 10;
976
        $page_nr = isset($_GET['page_nr']) ? intval($_GET['page_nr']) : 1;
977
        $column = isset($_GET['column']) ? intval($_GET['column']) : 0;
978
        $direction = isset($_GET['direction']) ? $_GET['direction'] : SORT_ASC;
979
980
        if (!in_array($direction, [SORT_ASC, SORT_DESC])) {
981
            $direction = SORT_ASC;
982
        }
983
        $form = new FormValidator('courselastvisit', 'get');
984
        $form->addElement('hidden', 'report', 'courselastvisit');
985
        $form->addText('date_diff', get_lang('days'), true);
986
        $form->addRule('date_diff', 'InvalidNumber', 'numeric');
987
        $form->addButtonSearch(get_lang('Search'), 'submit');
988
        if (!isset($_GET['date_diff'])) {
989
            $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...
990
        } else {
991
            $defaults['date_diff'] = Security::remove_XSS($_GET['date_diff']);
992
        }
993
        $form->setDefaults($defaults);
994
        $content = $form->returnForm();
995
996
        $values = $form->exportValues();
997
        $date_diff = $values['date_diff'];
998
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
999
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

999
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
1000
            $sql = "SELECT * FROM $table t , $access_url_rel_course_table a
1001
                   WHERE
1002
                        c_id = a.c_id AND
1003
                        access_url_id='".$urlId."'
1004
                   GROUP BY c_id
1005
                   HAVING c_id <> ''
1006
                   AND DATEDIFF( '".api_get_utc_datetime()."' , access_date ) <= ".$date_diff;
1007
        } else {
1008
            $sql = "SELECT * FROM $table t
1009
                   GROUP BY c_id
1010
                   HAVING c_id <> ''
1011
                   AND DATEDIFF( '".api_get_utc_datetime()."' , access_date ) <= ".$date_diff;
1012
        }
1013
        $sql .= ' ORDER BY `'.$columns[$column].'` '.$sql_order[$direction];
1014
        $from = ($page_nr - 1) * $per_page;
1015
        $sql .= ' LIMIT '.$from.','.$per_page;
1016
1017
        $content .= '<p>'.get_lang('Latest access').' &gt;= '.$date_diff.' '.get_lang('days').'</p>';
1018
        $res = Database::query($sql);
1019
        if (Database::num_rows($res) > 0) {
1020
            $courses = [];
1021
            while ($obj = Database::fetch_object($res)) {
1022
                $courseInfo = api_get_course_info_by_id($obj->c_id);
1023
                $course = [];
1024
                $course[] = '<a href="'.api_get_path(WEB_COURSE_PATH).$courseInfo['code'].'">'.$courseInfo['code'].' <a>';
1025
                // Allow sort by date hiding the numerical date
1026
                $course[] = '<span style="display:none;">'.$obj->access_date.'</span>'.api_convert_and_format_date($obj->access_date);
1027
                $courses[] = $course;
1028
            }
1029
            $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...
1030
            $parameters['report'] = 'courselastvisit';
1031
            $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...
1032
            $table_header[] = [get_lang("Latest access"), true];
1033
1034
            ob_start();
1035
            Display:: display_sortable_table(
1036
                $table_header,
1037
                $courses,
1038
                ['column' => $column, 'direction' => $direction],
1039
                [],
1040
                $parameters
1041
            );
1042
            $content .= ob_get_contents();
1043
            ob_end_clean();
1044
        } else {
1045
            $content = get_lang('No search results');
1046
        }
1047
1048
        return $content;
1049
    }
1050
1051
    /**
1052
     * Displays the statistics of the messages sent and received by each user in the social network.
1053
     *
1054
     * @param string $messageType Type of message: 'sent' or 'received'
1055
     *
1056
     * @return array Message list
1057
     */
1058
    public static function getMessages($messageType)
1059
    {
1060
        $messageTable = Database::get_main_table(TABLE_MESSAGE);
1061
        $messageRelUserTable = Database::get_main_table(TABLE_MESSAGE_REL_USER);
1062
        $userTable = Database::get_main_table(TABLE_MAIN_USER);
1063
        $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1064
1065
        $urlId = api_get_current_access_url_id();
1066
1067
        switch ($messageType) {
1068
            case 'sent':
1069
                $field = 'm.user_sender_id';
1070
                $joinCondition = "m.id = mru.message_id AND mru.receiver_type = " . MessageRelUser::TYPE_SENDER;
1071
                break;
1072
            case 'received':
1073
                $field = 'mru.user_id';
1074
                $joinCondition = "m.id = mru.message_id AND mru.receiver_type = " . MessageRelUser::TYPE_TO;
1075
                break;
1076
        }
1077
1078
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

1078
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
1079
            $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
1080
            FROM $messageTable m
1081
            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...
1082
            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...
1083
            INNER JOIN $accessUrlRelUserTable url ON u.id = url.user_id
1084
            WHERE url.access_url_id = $urlId
1085
            AND u.active <> " . USER_SOFT_DELETED . "
1086
            GROUP BY $field
1087
            ORDER BY count_message DESC";
1088
        } else {
1089
            $sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
1090
            FROM $messageTable m
1091
            INNER JOIN $messageRelUserTable mru ON $joinCondition
1092
            INNER JOIN $userTable u ON $field = u.id
1093
            WHERE u.active <> " . USER_SOFT_DELETED . "
1094
            GROUP BY $field
1095
            ORDER BY count_message DESC";
1096
        }
1097
        $res = Database::query($sql);
1098
        $messages_sent = [];
1099
        while ($messages = Database::fetch_array($res)) {
1100
            if (empty($messages['username'])) {
1101
                $messages['username'] = get_lang('Unknown');
1102
            }
1103
            $users = api_get_person_name(
1104
                    $messages['firstname'],
1105
                    $messages['lastname']
1106
                ) . '<br />(' . $messages['username'] . ')';
1107
            $messages_sent[$users] = $messages['count_message'];
1108
        }
1109
1110
        return $messages_sent;
1111
    }
1112
1113
    /**
1114
     * Count the number of friends for social network users.
1115
     */
1116
    public static function getFriends()
1117
    {
1118
        $user_friend_table = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
1119
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
1120
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1121
        $urlId = api_get_current_access_url_id();
1122
1123
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

1123
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
1124
            $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
1125
                    FROM $access_url_rel_user_table as url, $user_friend_table uf
1126
                    LEFT JOIN $user_table u
1127
                    ON (uf.user_id = u.id) AND u.active <> ".USER_SOFT_DELETED."
1128
                    WHERE
1129
                        uf.relation_type <> '".UserRelUser::USER_RELATION_TYPE_RRHH."' AND
1130
                        uf.user_id = url.user_id AND
1131
                        access_url_id = '".$urlId."'
1132
                    GROUP BY uf.user_id
1133
                    ORDER BY count_friend DESC ";
1134
        } else {
1135
            $sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
1136
                    FROM $user_friend_table uf
1137
                    LEFT JOIN $user_table u
1138
                    ON (uf.user_id = u.id) AND u.active <> ".USER_SOFT_DELETED."
1139
                    WHERE uf.relation_type <> '".UserRelUser::USER_RELATION_TYPE_RRHH."'
1140
                    GROUP BY uf.user_id
1141
                    ORDER BY count_friend DESC ";
1142
        }
1143
        $res = Database::query($sql);
1144
        $list_friends = [];
1145
        while ($friends = Database::fetch_array($res)) {
1146
            $users = api_get_person_name($friends['firstname'], $friends['lastname']).'<br />('.$friends['username'].')';
1147
            $list_friends[$users] = $friends['count_friend'];
1148
        }
1149
1150
        return $list_friends;
1151
    }
1152
1153
    /**
1154
     * Print the number of users that didn't login for a certain period of time.
1155
     */
1156
    public static function printUsersNotLoggedInStats()
1157
    {
1158
        $totalLogin = [];
1159
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1160
        $access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1161
        $urlId = api_get_current_access_url_id();
1162
        $total = self::countUsers();
1163
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

1163
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
1164
            $table_url = ", $access_url_rel_user_table";
1165
            $where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'";
1166
        } else {
1167
            $table_url = '';
1168
            $where_url = '';
1169
        }
1170
        $now = api_get_utc_datetime();
1171
        $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...
1172
            "SELECT count(distinct(login_user_id)) AS number ".
1173
            " FROM $table $table_url ".
1174
            " WHERE DATE_ADD(login_date, INTERVAL 1 DAY) >= '$now' $where_url";
1175
        $sql[get_lang('In the last 7 days')] =
1176
            "SELECT count(distinct(login_user_id)) AS number ".
1177
            " FROM $table $table_url ".
1178
            " WHERE DATE_ADD(login_date, INTERVAL 7 DAY) >= '$now' $where_url";
1179
        $sql[get_lang('In the last 31 days')] =
1180
            "SELECT count(distinct(login_user_id)) AS number ".
1181
            " FROM $table $table_url ".
1182
            " WHERE DATE_ADD(login_date, INTERVAL 31 DAY) >= '$now' $where_url";
1183
        $sql[sprintf(get_lang('Last %d months'), 6)] =
1184
            "SELECT count(distinct(login_user_id)) AS number ".
1185
            " FROM $table $table_url ".
1186
            " WHERE DATE_ADD(login_date, INTERVAL 6 MONTH) >= '$now' $where_url";
1187
        $sql[get_lang('Never connected')] =
1188
            "SELECT count(distinct(login_user_id)) AS number ".
1189
            " FROM $table $table_url WHERE 1=1 $where_url";
1190
        foreach ($sql as $index => $query) {
1191
            $res = Database::query($query);
1192
            $obj = Database::fetch_object($res);
1193
            $r = $total - $obj->number;
1194
            $totalLogin[$index] = $r < 0 ? 0 : $r;
1195
        }
1196
1197
        return self::printStats(
1198
            get_lang('Not logged in for some time'),
1199
            $totalLogin,
1200
            false
1201
        );
1202
    }
1203
1204
    /**
1205
     * Returns an array with indexes as the 'yyyy-mm-dd' format of each date
1206
     * within the provided range (including limits). Dates are assumed to be
1207
     * given in UTC.
1208
     *
1209
     * @param string $startDate  Start date, in Y-m-d or Y-m-d h:i:s format
1210
     * @param string $endDate    End date, in Y-m-d or Y-m-d h:i:s format
1211
     * @param bool   $removeYear Whether to remove the year in the results (for easier reading)
1212
     *
1213
     * @return array|bool False on error in the params, array of [date1 => 0, date2 => 0, ...] otherwise
1214
     */
1215
    public static function buildDatesArray($startDate, $endDate, $removeYear = false)
1216
    {
1217
        if (strlen($startDate) > 10) {
1218
            $startDate = substr($startDate, 0, 10);
1219
        }
1220
        if (strlen($endDate) > 10) {
1221
            $endDate = substr($endDate, 0, 10);
1222
        }
1223
        if (!preg_match('/\d\d\d\d-\d\d-\d\d/', $startDate)) {
1224
            return false;
1225
        }
1226
        if (!preg_match('/\d\d\d\d-\d\d-\d\d/', $startDate)) {
1227
            return false;
1228
        }
1229
        $startTimestamp = strtotime($startDate);
1230
        $endTimestamp = strtotime($endDate);
1231
        $list = [];
1232
        for ($time = $startTimestamp; $time < $endTimestamp; $time += 86400) {
1233
            $datetime = api_get_utc_datetime($time);
1234
            if ($removeYear) {
1235
                $datetime = substr($datetime, 5, 5);
1236
            } else {
1237
                $dateTime = substr($datetime, 0, 10);
1238
            }
1239
            $list[$datetime] = 0;
1240
        }
1241
1242
        return $list;
1243
    }
1244
1245
    /**
1246
     * Prepare the JS code to load a chart.
1247
     *
1248
     * @param string $url     URL for AJAX data generator
1249
     * @param string $type    bar, line, pie, etc
1250
     * @param string $options Additional options to the chart (see chart-specific library)
1251
     * @param string A JS code for loading the chart together with a call to AJAX data generator
1252
     */
1253
    public static function getJSChartTemplate($url, $type = 'pie', $options = '', $elementId = 'canvas')
1254
    {
1255
        $chartCode = '
1256
        <script>
1257
        $(function() {
1258
            $.ajax({
1259
                url: "'.$url.'",
1260
                type: "POST",
1261
                success: function(data) {
1262
                    Chart.defaults.responsive = false;
1263
                    var ctx = document.getElementById("'.$elementId.'").getContext("2d");
1264
                    ctx.canvas.width = 420;
1265
                    ctx.canvas.height = 420;
1266
                    var chart = new Chart(ctx, {
1267
                        type: "'.$type.'",
1268
                        data: data,
1269
                        options: {
1270
                            plugins: {
1271
                                '.$options.'
1272
                            },
1273
                            cutout: "25%"
1274
                        }
1275
                    });
1276
                    var title = chart.options.plugins.title.text;
1277
                    $("#'.$elementId.'_title").html(title);
1278
                    $("#'.$elementId.'_table").html(data.table);
1279
                }
1280
            });
1281
        });
1282
        </script>';
1283
1284
        return $chartCode;
1285
    }
1286
1287
    public static function getJSChartTemplateWithData(
1288
        $data,
1289
        $type = 'pie',
1290
        $options = '',
1291
        $elementId = 'canvas',
1292
        $responsive = true,
1293
        $onClickHandler = '',
1294
        $extraButtonHandler = '',
1295
        $canvasDimensions = ['width' => 420, 'height' => 420]
1296
    ): string {
1297
        $data = json_encode($data);
1298
        $responsiveValue = $responsive ? 'true' : 'false';
1299
1300
        $indexAxisOption = '';
1301
        if ($type === 'bar') {
1302
            $indexAxisOption = 'indexAxis: "y",';
1303
        }
1304
1305
        $onClickScript = '';
1306
        if (!empty($onClickHandler)) {
1307
            $onClickScript = '
1308
                onClick: function(evt) {
1309
                    '.$onClickHandler.'
1310
                },
1311
            ';
1312
        }
1313
1314
        $canvasSize = '';
1315
        if ($responsiveValue === 'false') {
1316
            $canvasSize = '
1317
            ctx.canvas.width = '.$canvasDimensions['width'].';
1318
            ctx.canvas.height = '.$canvasDimensions['height'].';
1319
            ';
1320
        }
1321
1322
        $chartCode = '
1323
        <script>
1324
            $(function() {
1325
                Chart.defaults.responsive = '.$responsiveValue.';
1326
                var ctx = document.getElementById("'.$elementId.'").getContext("2d");
1327
                '.$canvasSize.'
1328
                var chart = new Chart(ctx, {
1329
                    type: "'.$type.'",
1330
                    data: '.$data.',
1331
                    options: {
1332
                        plugins: {
1333
                            '.$options.',
1334
                            datalabels: {
1335
                                anchor: "end",
1336
                                align: "left",
1337
                                formatter: function(value) {
1338
                                    return value;
1339
                                },
1340
                                color: "#000"
1341
                            },
1342
                        },
1343
                        '.$indexAxisOption.'
1344
                        scales: {
1345
                            x: { beginAtZero: true },
1346
                            y: { barPercentage: 0.5 }
1347
                        },
1348
                        '.$onClickScript.'
1349
                    }
1350
                });
1351
                var title = chart.options.plugins.title.text;
1352
                $("#'.$elementId.'_title").html(title);
1353
                $("#'.$elementId.'_table").html(chart.data.datasets[0].data);
1354
1355
                '.$extraButtonHandler.'
1356
            });
1357
        </script>';
1358
1359
        return $chartCode;
1360
    }
1361
1362
    public static function buildJsChartData($all, $chartName)
1363
    {
1364
        $list = [];
1365
        $palette = ChamiloApi::getColorPalette(true, true);
1366
        foreach ($all as $tick => $tock) {
1367
            $list['labels'][] = $tick;
1368
        }
1369
1370
        $list['datasets'][0]['label'] = $chartName;
1371
        $list['datasets'][0]['borderColor'] = 'rgba(255,255,255,1)';
1372
1373
        $i = 0;
1374
        foreach ($all as $tick => $tock) {
1375
            $j = $i % count($palette);
1376
            $list['datasets'][0]['data'][] = $tock;
1377
            $list['datasets'][0]['backgroundColor'][] = $palette[$j];
1378
            $i++;
1379
        }
1380
1381
        $scoreDisplay = ScoreDisplay::instance();
1382
        $table = new HTML_Table(['class' => 'data_table stats_table']);
1383
        $headers = [
1384
            get_lang('Name'),
1385
            get_lang('Count'),
1386
            get_lang('Percentage'),
1387
        ];
1388
        $row = 0;
1389
        $column = 0;
1390
        foreach ($headers as $header) {
1391
            $table->setHeaderContents($row, $column, $header);
1392
            $column++;
1393
        }
1394
1395
        $total = 0;
1396
        foreach ($all as $name => $value) {
1397
            $total += $value;
1398
        }
1399
        $row++;
1400
        foreach ($all as $name => $value) {
1401
            $table->setCellContents($row, 0, $name);
1402
            $table->setCellContents($row, 1, $value);
1403
            $table->setCellContents($row, 2, $scoreDisplay->display_score([$value, $total], SCORE_PERCENT));
1404
            $row++;
1405
        }
1406
        $table = Display::page_subheader2($chartName).$table->toHtml();
1407
1408
        return ['chart' => $list, 'table' => $table];
1409
    }
1410
1411
    /**
1412
     * Display the Logins By Date report and allow export its result to XLS.
1413
     */
1414
    public static function printLoginsByDate()
1415
    {
1416
        if (isset($_GET['export']) && 'xls' === $_GET['export']) {
1417
            $result = self::getLoginsByDate($_GET['start'], $_GET['end']);
1418
            $data = [[get_lang('Username'), get_lang('First name'), get_lang('Last name'), get_lang('Total time')]];
1419
1420
            foreach ($result as $i => $item) {
1421
                $data[] = [
1422
                    $item['username'],
1423
                    $item['firstname'],
1424
                    $item['lastname'],
1425
                    api_time_to_hms($item['time_count']),
1426
                ];
1427
            }
1428
1429
            Export::arrayToXls($data);
1430
            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...
1431
        }
1432
1433
        $content = Display::page_header(get_lang('Logins by date'));
1434
1435
        $actions = '';
1436
        $form = new FormValidator('frm_logins_by_date', 'get');
1437
        $form->addDateRangePicker(
1438
            'daterange',
1439
            get_lang('Date range'),
1440
            true,
1441
            ['format' => 'YYYY-MM-DD', 'timePicker' => 'false', 'validate_format' => 'Y-m-d']
1442
        );
1443
        $form->addHidden('report', 'logins_by_date');
1444
        $form->addButtonFilter(get_lang('Search'));
1445
1446
        if ($form->validate()) {
1447
            $values = $form->exportValues();
1448
1449
            $result = self::getLoginsByDate($values['daterange_start'], $values['daterange_end']);
1450
1451
            if (!empty($result)) {
1452
                $actions = Display::url(
1453
                    Display::getMdiIcon(ActionIcon::EXPORT_SPREADSHEET, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('ExportToXls')),
1454
                    api_get_self().'?'.http_build_query(
1455
                        [
1456
                            'report' => 'logins_by_date',
1457
                            'export' => 'xls',
1458
                            'start' => Security::remove_XSS($values['daterange_start']),
1459
                            'end' => Security::remove_XSS($values['daterange_end']),
1460
                        ]
1461
                    )
1462
                );
1463
            }
1464
1465
            $table = new HTML_Table(['class' => 'data_table stats_table']);
1466
            $table->setHeaderContents(0, 0, get_lang('Username'));
1467
            $table->setHeaderContents(0, 1, get_lang('First name'));
1468
            $table->setHeaderContents(0, 2, get_lang('Last name'));
1469
            $table->setHeaderContents(0, 3, get_lang('Total time'));
1470
1471
            foreach ($result as $i => $item) {
1472
                $table->setCellContents($i + 1, 0, $item['username']);
1473
                $table->setCellContents($i + 1, 1, $item['firstname']);
1474
                $table->setCellContents($i + 1, 2, $item['lastname']);
1475
                $table->setCellContents($i + 1, 3, api_time_to_hms($item['time_count']));
1476
            }
1477
1478
            $table->setColAttributes(0, ['class' => 'text-center']);
1479
            $table->setColAttributes(3, ['class' => 'text-center']);
1480
            $content = $table->toHtml();
1481
        }
1482
1483
        $content .= $form->returnForm();
1484
1485
        if (!empty($actions)) {
1486
            $content .= Display::toolbarAction('logins_by_date_toolbar', [$actions]);
1487
        }
1488
1489
        return $content;
1490
    }
1491
1492
    public static function getBossTable($bossId)
1493
    {
1494
        $students = UserManager::getUsersFollowedByStudentBoss(
1495
            $bossId,
1496
            0,
1497
            false,
1498
            false,
1499
            false,
1500
            null,
1501
            null,
1502
            null,
1503
            null,
1504
            1
1505
        );
1506
1507
        if (!empty($students)) {
1508
            $table = new HTML_Table(['class' => 'table table-responsive', 'id' => 'table_'.$bossId]);
1509
            $headers = [
1510
                get_lang('Name'),
1511
                //get_lang('LastName'),
1512
            ];
1513
            $row = 0;
1514
            $column = 0;
1515
            foreach ($headers as $header) {
1516
                $table->setHeaderContents($row, $column, $header);
1517
                $column++;
1518
            }
1519
            $row++;
1520
            foreach ($students as $student) {
1521
                $column = 0;
1522
                $content = api_get_person_name($student['firstname'], $student['lastname']).'';
1523
                $content = '<div style="width: 200px; overflow-wrap: break-word;">'.$content.'</div>';
1524
                $table->setCellContents(
1525
                    $row,
1526
                    $column++,
1527
                    $content
1528
                );
1529
                $row++;
1530
            }
1531
1532
            return $table->toHtml();
1533
        }
1534
1535
        return '<table id="table_'.$bossId.'"></table>';
1536
    }
1537
1538
    /**
1539
     * @param string $startDate
1540
     * @param string $endDate
1541
     *
1542
     * @return array
1543
     */
1544
    public static function getLoginsByDate(string $startDate, string $endDate): array
1545
    {
1546
        $startDate = api_get_utc_datetime("$startDate 00:00:00");
1547
        $endDate = api_get_utc_datetime("$endDate 23:59:59");
1548
1549
        if (empty($startDate) || empty($endDate)) {
1550
            return [];
1551
        }
1552
1553
        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
1554
        $tblLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1555
        $urlJoin = '';
1556
        $urlWhere = '';
1557
1558
        if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

1558
        if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
1559
            $tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1560
1561
            $urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id";
1562
            $urlWhere = 'AND au.access_url_id = '.api_get_current_access_url_id();
1563
        }
1564
1565
        $sql = "SELECT u.id,
1566
                    u.firstname,
1567
                    u.lastname,
1568
                    u.username,
1569
                    SUM(TIMESTAMPDIFF(SECOND, l.login_date, l.logout_date)) AS time_count
1570
                FROM $tblUser u
1571
                INNER JOIN $tblLogin l
1572
                ON u.id = l.login_user_id
1573
                $urlJoin
1574
                WHERE u.active <> ".USER_SOFT_DELETED." AND l.login_date BETWEEN '$startDate' AND '$endDate'
1575
                $urlWhere
1576
                GROUP BY u.id";
1577
1578
        $stmt = Database::query($sql);
1579
1580
        return Database::store_result($stmt, 'ASSOC');
1581
    }
1582
1583
    /**
1584
     * Gets the number of new users registered between two dates.
1585
     */
1586
    public static function getNewUserRegistrations(string $startDate, string $endDate): array
1587
    {
1588
        $sql = "SELECT DATE_FORMAT(registration_date, '%Y-%m-%d') as reg_date, COUNT(*) as user_count
1589
            FROM user
1590
            WHERE registration_date BETWEEN '$startDate' AND '$endDate'
1591
            GROUP BY reg_date";
1592
1593
        $result = Database::query($sql);
1594
        $data = [];
1595
        while ($row = Database::fetch_array($result)) {
1596
            $userCount = is_numeric($row['user_count']) ? (int) $row['user_count'] : 0;
1597
            $data[] = ['date' => $row['reg_date'], 'count' => $userCount];
1598
        }
1599
1600
        return $data;
1601
    }
1602
1603
    /**
1604
     * Gets the number of users registered by creator (creator_id) between two dates.
1605
     */
1606
    public static function getUserRegistrationsByCreator(string $startDate, string $endDate): array
1607
    {
1608
        $sql = "SELECT u.creator_id, COUNT(u.id) as user_count, c.firstname, c.lastname
1609
                FROM user u
1610
                LEFT JOIN user c ON u.creator_id = c.id
1611
                WHERE u.registration_date BETWEEN '$startDate' AND '$endDate'
1612
                AND u.creator_id IS NOT NULL
1613
                GROUP BY u.creator_id";
1614
1615
        $result = Database::query($sql);
1616
        $data = [];
1617
        while ($row = Database::fetch_array($result)) {
1618
            $userCount = is_numeric($row['user_count']) ? (int) $row['user_count'] : 0;
1619
            $name = trim($row['firstname'] . ' ' . $row['lastname']);
1620
            if (!empty($name)) {
1621
                $data[] = [
1622
                    'name' => $name,
1623
                    'count' => $userCount
1624
                ];
1625
            }
1626
        }
1627
1628
        return $data;
1629
    }
1630
1631
    /**
1632
     * Initializes an array with dates between two given dates, setting each date's value to 0.
1633
     */
1634
    public static function initializeDateRangeArray(string $startDate, string $endDate): array
1635
    {
1636
        $dateRangeArray = [];
1637
        $currentDate = new DateTime($startDate);
1638
        $endDate = new DateTime($endDate);
1639
1640
        // Loop through the date range and initialize each date with 0
1641
        while ($currentDate <= $endDate) {
1642
            $formattedDate = $currentDate->format('Y-m-d');
1643
            $dateRangeArray[$formattedDate] = 0;
1644
            $currentDate->modify('+1 day');
1645
        }
1646
1647
        return $dateRangeArray;
1648
    }
1649
1650
    /**
1651
     * Checks if the difference between two dates is more than one month.
1652
     */
1653
    public static function isMoreThanAMonth(string $dateStart, string $dateEnd): bool
1654
    {
1655
        $startDate = new DateTime($dateStart);
1656
        $endDate = new DateTime($dateEnd);
1657
1658
        $diff = $startDate->diff($endDate);
1659
1660
        if ($diff->y >= 1) {
1661
            return true;
1662
        }
1663
1664
        if ($diff->m > 1) {
1665
            return true;
1666
        }
1667
1668
        if ($diff->m == 1) {
1669
            return $diff->d > 0;
1670
        }
1671
1672
        return false;
1673
    }
1674
1675
    /**
1676
     * Groups registration data by month.
1677
     */
1678
    public static function groupByMonth(array $registrations): array
1679
    {
1680
        $groupedData = [];
1681
1682
        foreach ($registrations as $registration) {
1683
            $monthYear = (new DateTime($registration['date']))->format('Y-m');
1684
            if (isset($groupedData[$monthYear])) {
1685
                $groupedData[$monthYear] += $registration['count'];
1686
            } else {
1687
                $groupedData[$monthYear] = $registration['count'];
1688
            }
1689
        }
1690
1691
        return $groupedData;
1692
    }
1693
1694
    /**
1695
     * Return de number of certificates generated.
1696
     * This function is resource intensive.
1697
     * @throws \Doctrine\DBAL\Exception
1698
     * @throws Exception
1699
     */
1700
    public static function countCertificatesByQuarter(string $dateFrom = null, string $dateUntil = null): int
1701
    {
1702
        $tableGradebookCertificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
1703
        $condition = "";
1704
        if (!empty($dateFrom) && !empty($dateUntil)) {
1705
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1706
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1707
            $condition = "WHERE (created_at BETWEEN '$dateFrom' AND '$dateUntil')";
1708
        } elseif (!empty($dateFrom)) {
1709
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1710
            $condition = "WHERE created_at >= '$dateFrom'";
1711
        } elseif (!empty($dateUntil)) {
1712
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1713
            $condition = "WHERE created_at <= '$dateUntil'";
1714
        }
1715
        $sql = "
1716
            SELECT count(*) AS count
1717
            FROM $tableGradebookCertificate
1718
            $condition
1719
        ";
1720
        $response = Database::query($sql);
1721
        $obj = Database::fetch_object($response);
1722
        return $obj->count;
1723
    }
1724
1725
    /**
1726
     * Get the number of logins by dates.
1727
     * This function is resource intensive.
1728
     * @throws Exception
1729
     */
1730
    public static function getSessionsByDuration(string $dateFrom, string $dateUntil): array
1731
    {
1732
        $results = [
1733
            '0' => 0,
1734
            '5' => 0,
1735
            '10' => 0,
1736
            '15' => 0,
1737
            '30' => 0,
1738
            '60' => 0,
1739
        ];
1740
        if (!empty($dateFrom) && !empty($dateUntil)) {
1741
            $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
1742
            $accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
1743
            $urlId = api_get_current_access_url_id();
1744
            $tableUrl = '';
1745
            $whereUrl = '';
1746
            $dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
1747
            $dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
1748
            if (AccessUrlHelper::isMultiple()) {
0 ignored issues
show
Bug Best Practice introduced by
The method Chamilo\CoreBundle\Servi...UrlHelper::isMultiple() is not static, but was called statically. ( Ignorable by Annotation )

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

1748
            if (AccessUrlHelper::/** @scrutinizer ignore-call */ isMultiple()) {
Loading history...
1749
                $tableUrl = ", $accessUrlRelUserTable";
1750
                $whereUrl = " AND login_user_id = user_id AND access_url_id = $urlId";
1751
            }
1752
            $sql = "SELECT login_id, TIMESTAMPDIFF(SECOND, login_date, logout_date) AS duration
1753
            FROM $table $tableUrl
1754
            WHERE login_date >= '$dateFrom'
1755
            AND logout_date <= '$dateUntil'
1756
            $whereUrl
1757
            ";
1758
            $res = Database::query($sql);
1759
            while ($session = Database::fetch_array($res)) {
1760
                if ($session['duration'] > 3600) {
1761
                    $results['60']++;
1762
                } elseif ($session['duration'] > 1800) {
1763
                    $results['30']++;
1764
                } elseif ($session['duration'] > 900) {
1765
                    $results['15']++;
1766
                } elseif ($session['duration'] > 600) {
1767
                    $results['10']++;
1768
                } elseif ($session['duration'] > 300) {
1769
                    $results['5']++;
1770
                } else {
1771
                    $results['0']++;
1772
                }
1773
            }
1774
        }
1775
        return $results;
1776
    }
1777
}
1778