Passed
Push — master ( 21a798...446031 )
by Yannick
11:04
created

Event::saveQuestionAttempt()   F

Complexity

Conditions 23
Paths > 20000

Size

Total Lines 179
Code Lines 109

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 23
eloc 109
c 1
b 0
f 0
nc 57888
nop 15
dl 0
loc 179
rs 0

How to fix   Long Method    Complexity    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
3
/* See license terms in /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\Course as CourseEntity;
7
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
8
use Chamilo\CoreBundle\Entity\TrackEAttemptQualify;
9
use Chamilo\CoreBundle\Entity\TrackEDefault;
10
use Chamilo\CoreBundle\Entity\TrackEDownloads;
11
use Chamilo\CoreBundle\Entity\TrackEExercise;
12
use Chamilo\CoreBundle\Entity\User;
13
use Chamilo\CoreBundle\Framework\Container;
14
use ChamiloSession as Session;
15
16
/**
17
 * Class Event
18
 * Functions of this library are used to record informations when some kind
19
 * of event occur. Each event has his own types of informations then each event
20
 * use its own function.
21
 */
22
class Event
23
{
24
    /**
25
     * Record information for login event when an user identifies himself with username & password
26
     * @param int $userId
27
     *
28
     * @return bool
29
     *
30
     * @throws Exception
31
     * @author Julio Montoya
32
     * @author Sebastien Piraux <[email protected]> old code
33
     */
34
    public static function eventLogin(int $userId): bool
35
    {
36
        $userInfo = api_get_user_info($userId);
37
38
        if (empty($userInfo)) {
39
            return false;
40
        }
41
42
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
43
        $reallyNow = api_get_utc_datetime();
44
        $userIp = Database::escape_string(api_get_real_ip());
45
46
        $sql = "INSERT INTO $table (login_user_id, user_ip, login_date, logout_date) VALUES
47
                    ($userId,
48
                    '$userIp',
49
                    '$reallyNow',
50
                    '$reallyNow'
51
                )";
52
        Database::query($sql);
53
54
        $status = 'student';
55
        if (SESSIONADMIN == $userInfo['status']) {
56
            $status = 'sessionadmin';
57
        }
58
        if (COURSEMANAGER == $userInfo['status']) {
59
            $status = 'teacher';
60
        }
61
        if (DRH == $userInfo['status']) {
62
            $status = 'DRH';
63
        }
64
65
        // Auto subscribe
66
        $autoSubscribe = api_get_setting($status.'_autosubscribe');
67
        if ($autoSubscribe) {
68
            $autoSubscribe = explode('|', $autoSubscribe);
69
            foreach ($autoSubscribe as $code) {
70
                if (CourseManager::course_exists($code)) {
71
                    $courseInfo = api_get_course_info($code);
72
                    CourseManager::subscribeUser($userId, $courseInfo['real_id']);
73
                }
74
            }
75
        }
76
77
        return true;
78
    }
79
80
    /**
81
     * Check if we need to log a session access (based on visibility and extra field 'disable_log_after_session_ends')
82
     * @param int $sessionId
83
     *
84
     * @return bool
85
     */
86
    public static function isSessionLogNeedToBeSave(int $sessionId): bool
87
    {
88
        if (!empty($sessionId)) {
89
            $visibility = api_get_session_visibility($sessionId);
90
            if (!empty($visibility) && SESSION_AVAILABLE != $visibility) {
91
                $extraFieldValue = new ExtraFieldValue('session');
92
                $value = $extraFieldValue->get_values_by_handler_and_field_variable(
93
                    $sessionId,
94
                    'disable_log_after_session_ends'
95
                );
96
                if (!empty($value) && isset($value['value']) && 1 == (int) $value['value']) {
97
                    return false;
98
                }
99
            }
100
        }
101
102
        return true;
103
    }
104
105
    /**
106
     * Record information for access event for tools
107
     * @param string $tool name of the tool
108
     *
109
     * @return bool
110
     * @throws \Doctrine\DBAL\Exception
111
     * @throws \Doctrine\DBAL\Exception
112
     * @throws Exception
113
     * @author Sebastien Piraux <[email protected]>
114
     *
115
     *  $tool can take this values :
116
     *  Links, Calendar, Document, Announcements,
117
     *  Group, Video, Works, Users, Exercises, Course Desc
118
     *  ...
119
     *  Values can be added if new modules are created (15char max)
120
     *  I encourage to use $nameTool as $tool when calling this function
121
     *
122
     * Functionality for "what's new" notification is added by Toon Van Hoecke
123
     *
124
     */
125
    public static function event_access_tool(string $tool): bool
126
    {
127
        if (Session::read('login_as')) {
128
            return false;
129
        }
130
131
        $tool = Database::escape_string($tool);
132
133
        if (empty($tool)) {
134
            return false;
135
        }
136
137
        $courseInfo = api_get_course_info();
138
        $sessionId = api_get_session_id();
139
        $reallyNow = api_get_utc_datetime();
140
        $userId = api_get_user_id();
141
142
        if (empty($courseInfo)) {
143
            return false;
144
        }
145
146
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
147
            return false;
148
        }
149
150
        $courseId = $courseInfo['real_id'];
151
152
        $tableAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ACCESS);
153
        //for "what's new" notification
154
        $tableLastAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
155
156
        // record information
157
        // only if user comes from the course $_cid
158
        //if( eregi($_configuration['root_web'].$_cid,$_SERVER['HTTP_REFERER'] ) )
159
        //$pos = strpos($_SERVER['HTTP_REFERER'],$_configuration['root_web'].$_cid);
160
        $coursePath = isset($courseInfo['path']) ? $courseInfo['path'] : null;
161
162
        $pos = isset($_SERVER['HTTP_REFERER']) ? strpos(
163
            strtolower($_SERVER['HTTP_REFERER']),
164
            strtolower(api_get_path(WEB_COURSE_PATH).$coursePath)
165
        ) : false;
166
        // added for "what's new" notification
167
        $pos2 = isset($_SERVER['HTTP_REFERER']) ? strpos(
168
            strtolower($_SERVER['HTTP_REFERER']),
169
            strtolower(api_get_path(WEB_PATH)."index")
170
        ) : false;
171
172
        // end "what's new" notification
173
        if (false !== $pos || false !== $pos2) {
174
            $params = [
175
                'access_user_id' => $userId,
176
                'c_id' => $courseId,
177
                'access_tool' => $tool,
178
                'access_date' => $reallyNow,
179
                'session_id' => $sessionId,
180
                'user_ip' => Database::escape_string(api_get_real_ip()),
181
            ];
182
            Database::insert($tableAccess, $params);
183
        }
184
185
        // "what's new" notification
186
        $sql = "UPDATE $tableLastAccess
187
                SET access_date = '$reallyNow'
188
                WHERE
189
                    access_user_id = $userId AND
190
                    c_id = $courseId AND
191
                    access_tool = '$tool' AND
192
                    session_id = $sessionId";
193
        $result = Database::query($sql);
194
195
        if (0 == Database::affected_rows($result)) {
196
            $params = [
197
                'access_user_id' => $userId,
198
                'c_id' => $courseId,
199
                'access_tool' => $tool,
200
                'access_date' => $reallyNow,
201
                'session_id' => $sessionId,
202
            ];
203
            Database::insert($tableLastAccess, $params);
204
        }
205
206
        return true;
207
    }
208
209
    /**
210
     * Record information for download event (when a user clicks to d/l a
211
     * document).
212
     *
213
     * @param string $documentUrl
214
     *
215
     * @return int
216
     *
217
     * @throws \Doctrine\ORM\Exception\NotSupported
218
     * @author Evie Embrechts
219
     * @author Sebastien Piraux <[email protected]>
220
     */
221
    public static function event_download(string $documentUrl): int
222
    {
223
        if (Session::read('login_as')) {
224
            return 0;
225
        }
226
227
        $user = api_get_user_entity();
228
        $course = api_get_course_entity();
229
230
        $em = Database::getManager();
231
232
        $repository = $em->getRepository(TrackEDownloads::class);
233
234
        $resourceLinkId = $course->getFirstResourceLink()->getId();
235
236
        return $repository->saveDownload($user->getId(), $resourceLinkId, $documentUrl);
237
    }
238
239
    /**
240
     * Record information of upload event.
241
     * Used in the works tool to record information when a user uploads 1 work.
242
     * @param int $documentId of document (id in mainDb.document table)
243
     *
244
     * @return int
245
     * @throws Exception
246
     * @author Sebastien Piraux <[email protected]>
247
     */
248
    public static function event_upload($documentId): int
249
    {
250
        if (Session::read('login_as')) {
251
            return 0;
252
        }
253
254
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_UPLOADS);
255
        $courseId = api_get_course_int_id();
256
        $reallyNow = api_get_utc_datetime();
257
        $userId = api_get_user_id();
258
        $documentId = (int) $documentId;
259
        $sessionId = api_get_session_id();
260
261
        $sql = "INSERT INTO $table
262
                ( upload_user_id,
263
                  c_id,
264
                  upload_work_id,
265
                  upload_date,
266
                  session_id
267
                )
268
                VALUES (
269
                 $userId,
270
                 $courseId,
271
                 $documentId,
272
                 '$reallyNow',
273
                 $sessionId
274
                )";
275
        Database::query($sql);
276
277
        return 1;
278
    }
279
280
    /**
281
     * Record information for link event (when a user clicks on an added link).
282
     *
283
     * @param int $linkId (id in c_link table)
284
     *
285
     * @return int
286
     *
287
     * @throws Exception
288
     * @author Sebastien Piraux <[email protected]>
289
     */
290
    public static function event_link(int $linkId): int
291
    {
292
        if (Session::read('login_as')) {
293
            return 0;
294
        }
295
296
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LINKS);
297
        $reallyNow = api_get_utc_datetime();
298
        $userId = api_get_user_id();
299
        $courseId = api_get_course_int_id();
300
        $linkId = (int) $linkId;
301
        $sessionId = api_get_session_id();
302
        $sql = "INSERT INTO ".$table."
303
                    ( links_user_id,
304
                     c_id,
305
                     links_link_id,
306
                     links_date,
307
                     session_id
308
                    ) VALUES (
309
                     $userId,
310
                     $courseId,
311
                     $linkId,
312
                     '$reallyNow',
313
                     $sessionId
314
                    )";
315
        Database::query($sql);
316
317
        return 1;
318
    }
319
320
    /**
321
     * Update the TRACK_E_EXERCICES exercises.
322
     * Record result of user when an exercise was done.
323
     *
324
     * @param int    $exeId
325
     * @param int    $exoId
326
     * @param mixed  $score
327
     * @param int    $weighting
328
     * @param int    $sessionId
329
     * @param ?int    $learnpathId
330
     * @param ?int    $learnpathItemId
331
     * @param ?int    $learnpathItemViewId
332
     * @param ?int    $duration
333
     * @param ?array  $questionsList
334
     * @param ?string $status
335
     * @param ?array  $remindList
336
     * @param ?string   $endDate
337
     *
338
     * @return bool
339
     *
340
     * @throws Exception
341
     * @author Sebastien Piraux <[email protected]>
342
     * @author Julio Montoya Armas <[email protected]> Reworked 2010
343
     */
344
    public static function updateEventExercise(
345
        int $exeId,
346
        int $exoId,
347
        float $score,
348
        int $weighting,
349
        int $sessionId,
350
        ?int $learnpathId = 0,
351
        ?int $learnpathItemId = 0,
352
        ?int $learnpathItemViewId = 0,
353
        ?int $duration = 0,
354
        ?array $questionsList = [],
355
        ?string $status = '',
356
        ?array $remindList = [],
357
        ?string $endDate = null
358
    ):bool
359
    {
360
        if (empty($exeId)) {
361
            return false;
362
        }
363
364
        if (!isset($status) || empty($status)) {
365
            $status = '';
366
        } else {
367
            $status = Database::escape_string($status);
368
        }
369
370
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
371
372
        if (!empty($questionsList)) {
373
            $questionsList = array_map('intval', $questionsList);
374
        }
375
376
        if (!empty($remindList)) {
377
            $remindList = array_map('intval', $remindList);
378
            $remindList = array_filter($remindList);
379
            $remindList = implode(",", $remindList);
380
        } else {
381
            $remindList = '';
382
        }
383
384
        if (empty($endDate)) {
385
            $endDate = api_get_utc_datetime();
386
        }
387
        $score = Database::escape_string($score);
388
        $weighting = Database::escape_string($weighting);
389
        $questions = implode(',', $questionsList);
390
        $userIp = Database::escape_string(api_get_real_ip());
391
392
        $sql = "UPDATE $table SET
393
               exe_exo_id = $exoId,
394
               score = '$score',
395
               max_score = '$weighting',
396
               orig_lp_id = $learnpathId,
397
               orig_lp_item_id = $learnpathItemId,
398
               orig_lp_item_view_id = $learnpathItemViewId,
399
               exe_duration = $duration,
400
               exe_date = '$endDate',
401
               status = '$status',
402
               questions_to_check = '$remindList',
403
               data_tracking = '$questions',
404
               user_ip = '$userIp'
405
             WHERE exe_id = $exeId";
406
        Database::query($sql);
407
408
        return true;
409
    }
410
411
    /**
412
     * Record an event for this attempt at answering an exercise.
413
     * @param Exercise $exercise
414
     * @param float  $score Score achieved
415
     * @param string $answer Answer given
416
     * @param int    $question_id
417
     * @param int    $exe_id Exercise attempt ID a.k.a exe_id (from track_e_exercise)
418
     * @param int    $position
419
     * @param ?int    $exercise_id From c_quiz
420
     * @param ?bool   $updateResults
421
     * @param ?int    $questionDuration Time spent in seconds
422
     * @param ?string $fileName Filename (for audio answers - using nanogong)
423
     * @param ?int    $user_id The user who's going to get this score.
424
     * @param ?int    $course_id Default value of null means "get from context".
425
     * @param ?int    $session_id Default value of null means "get from context".
426
     * @param ?int    $learnpath_id (from c_lp table). Default value of null means "get from context".
427
     * @param ?int    $learnpath_item_id (from the c_lp_item table). Default value of null means "get from context".
428
     *
429
     * @return bool Result of the insert query
430
     * @throws Exception
431
     * @throws \Doctrine\DBAL\Exception
432
     */
433
    public static function saveQuestionAttempt(
434
        Exercise $exercise,
435
        float $score,
436
        string $answer,
437
        int $question_id,
438
        int $exe_id,
439
        int $position,
440
        ?int $exercise_id = 0,
441
        ?bool $updateResults = false,
442
        ?int $questionDuration = 0,
443
        ?string $fileName = null,
444
        ?int $user_id = null,
445
        ?int $course_id = null,
446
        ?int $session_id = null,
447
        ?int $learnpath_id = null,
448
        ?int $learnpath_item_id = null
449
    ) {
450
        global $debug;
451
        $questionDuration = (int) $questionDuration;
452
        $question_id = (int) $question_id;
453
        $exe_id = (int) $exe_id;
454
        $position = (int) $position;
455
        $course_id = (int) $course_id;
456
        $now = api_get_utc_datetime();
457
        $recordingLog = ('true' === api_get_setting('exercise.quiz_answer_extra_recording'));
458
459
        // check user_id or get from context
460
        if (empty($user_id)) {
461
            $user_id = api_get_user_id();
462
            // anonymous
463
            if (empty($user_id)) {
464
                $user_id = api_get_anonymous_id();
465
            }
466
        }
467
        // check course_id or get from context
468
        if (empty($course_id)) {
469
            $course_id = api_get_course_int_id();
470
        }
471
        // check session_id or get from context
472
        $session_id = (int) $session_id;
473
        if (empty($session_id)) {
474
            $session_id = api_get_session_id();
475
        }
476
        // check learnpath_id or get from context
477
        if (empty($learnpath_id)) {
478
            global $learnpath_id;
479
        }
480
        // check learnpath_item_id or get from context
481
        if (empty($learnpath_item_id)) {
482
            global $learnpath_item_id;
483
        }
484
485
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
486
487
        if ($debug) {
488
            error_log('----- entering saveQuestionAttempt() function ------');
489
            error_log("answer: $answer");
490
            error_log("score: $score");
491
            error_log("question_id : $question_id");
492
            error_log("position: $position");
493
        }
494
495
        //Validation in case of fraud with active control time
496
        if (!ExerciseLib::exercise_time_control_is_valid($exercise, $learnpath_id, $learnpath_item_id)) {
497
            if ($debug) {
498
                error_log("exercise_time_control_is_valid is false");
499
            }
500
            $score = 0;
501
            $answer = 0;
502
        }
503
504
        if (empty($question_id) || empty($exe_id) || empty($user_id)) {
505
            return false;
506
        }
507
508
        if (null === $answer) {
509
            $answer = '';
510
        }
511
        if (null === $score) {
512
            $score = 0;
513
        }
514
515
        $attempt = [
516
            'user_id' => $user_id,
517
            'question_id' => $question_id,
518
            'answer' => $answer,
519
            'marks' => $score,
520
            'position' => $position,
521
            'tms' => $now,
522
            'filename' => !empty($fileName) ? basename($fileName) : $fileName,
523
            'teacher_comment' => '',
524
            'seconds_spent' => $questionDuration,
525
        ];
526
527
        // Check if attempt exists.
528
        $sql = "SELECT exe_id FROM $TBL_TRACK_ATTEMPT
529
                WHERE
530
                    exe_id = $exe_id AND
531
                    user_id = $user_id AND
532
                    question_id = $question_id AND
533
                    position = $position";
534
        $result = Database::query($sql);
535
        $attemptData = [];
536
        if (Database::num_rows($result)) {
537
            $attemptData = Database::fetch_assoc($result);
538
            if (!$updateResults) {
539
                //The attempt already exist do not update use  update_event_exercise() instead
540
                return false;
541
            }
542
        } else {
543
            $attempt['exe_id'] = $exe_id;
544
        }
545
546
        if ($debug) {
547
            error_log("updateResults : $updateResults");
548
            error_log('Saving question attempt:');
549
            error_log($sql);
550
        }
551
552
        $em = Database::getManager();
553
        if (!$updateResults) {
554
            $attempt_id = Database::insert($TBL_TRACK_ATTEMPT, $attempt);
555
            $trackExercise = $em->find(TrackEExercise::class, $exe_id);
556
557
            if ($recordingLog) {
558
                $recording = new TrackEAttemptQualify();
559
                $recording
560
                    ->setTrackExercise($trackExercise)
561
                    ->setQuestionId($question_id)
562
                    ->setAnswer($answer)
563
                    ->setMarks((int) $score)
564
                    ->setAuthor(api_get_user_id())
565
                    ->setSessionId($session_id)
566
                ;
567
                $em->persist($recording);
568
                $em->flush();
569
            }
570
        } else {
571
            if ('true' === api_get_setting('exercise.allow_time_per_question')) {
572
                $attempt['seconds_spent'] = $questionDuration + (int) $attemptData['seconds_spent'];
573
            }
574
            Database::update(
575
                $TBL_TRACK_ATTEMPT,
576
                $attempt,
577
                [
578
                    'exe_id = ? AND question_id = ? AND user_id = ? ' => [
579
                        $exe_id,
580
                        $question_id,
581
                        $user_id,
582
                    ],
583
                ]
584
            );
585
586
            if ($recordingLog) {
587
                $repoTrackQualify = $em->getRepository(TrackEAttemptQualify::class);
588
                $trackQualify = $repoTrackQualify->findBy(
589
                    [
590
                        'exeId' => $exe_id,
591
                        'questionId' => $question_id,
592
                        'sessionId' => $session_id,
593
                    ]
594
                );
595
                $trackExercise = $em->find(TrackEExercise::class, $exe_id);
596
                /** @var TrackEAttemptQualify $trackQualify */
597
                $trackQualify
598
                    ->setTrackExercise($trackExercise)
599
                    ->setQuestionId($question_id)
600
                    ->setAnswer($answer)
601
                    ->setMarks((int) $score)
602
                    ->setAuthor(api_get_user_id())
603
                    ->setSessionId($session_id)
604
                ;
605
                $em->persist($trackQualify);
606
                $em->flush();
607
            }
608
            $attempt_id = $exe_id;
609
        }
610
611
        return $attempt_id;
612
    }
613
614
    /**
615
     * Record a hotspot spot for this attempt at answering a hotspot question.
616
     *
617
     * @param int    $exeId
618
     * @param int    $questionId Question ID
619
     * @param int    $answerId Answer ID
620
     * @param int    $correct
621
     * @param string $coords Coordinates of this point (e.g. 123;324)
622
     * @param bool   $updateResults
623
     * @param ?int   $exerciseId Deprecated param
624
     * @param ?int   $lpId
625
     * @param ?int   $lpItemId
626
     *
627
     * @return int Result of the insert query, or 0 on error
628
     *
629
     * @throws \Doctrine\DBAL\Exception
630
     * @uses \Course code and user_id from global scope $_cid and $_user
631
     */
632
    public static function saveExerciseAttemptHotspot(
633
        Exercise $exercise,
634
        int $exeId,
635
        int $questionId,
636
        int $answerId,
637
        int $correct,
638
        string $coords,
639
        bool $updateResults = false,
640
        ?int $exerciseId = 0,
641
        ?int $lpId = 0,
642
        ?int $lpItemId = 0
643
    ): int
644
    {
645
        $debug = false;
646
647
        if (false == $updateResults) {
648
            // Validation in case of fraud with activated control time
649
            if (!ExerciseLib::exercise_time_control_is_valid($exercise, $lpId, $lpItemId)) {
650
                if ($debug) {
651
                    error_log('Attempt is fraud');
652
                }
653
                $correct = 0;
654
            }
655
        }
656
657
        if (empty($exeId)) {
658
            if ($debug) {
659
                error_log('exe id is empty');
660
            }
661
662
            return 0;
663
        }
664
665
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
666
        if ($updateResults) {
667
            if ($debug) {
668
                error_log("Insert hotspot results: exeId: $exeId correct: $correct");
669
            }
670
            $params = [
671
                'hotspot_correct' => $correct,
672
                'hotspot_coordinate' => $coords,
673
            ];
674
            $res = Database::update(
675
                $table,
676
                $params,
677
                [
678
                    'hotspot_user_id = ? AND hotspot_exe_id = ? AND hotspot_question_id = ? AND hotspot_answer_id = ? ' => [
679
                        api_get_user_id(),
680
                        $exeId,
681
                        $questionId,
682
                        $answerId,
683
                    ],
684
                ]
685
            );
686
            if (!$res) {
687
                return 0;
688
            }
689
690
            return $res;
691
        } else {
692
            if ($debug) {
693
                error_log("Insert hotspot results: exeId: $exeId correct: $correct");
694
            }
695
696
            $res = Database::insert(
697
                $table,
698
                [
699
                    'hotspot_user_id' => api_get_user_id(),
700
                    'c_id' => api_get_course_int_id(),
701
                    'hotspot_exe_id' => $exeId,
702
                    'hotspot_question_id' => $questionId,
703
                    'hotspot_answer_id' => $answerId,
704
                    'hotspot_correct' => $correct,
705
                    'hotspot_coordinate' => $coords,
706
                ]
707
            );
708
            if (!$res) {
709
                return 0;
710
            }
711
712
            return $res;
713
        }
714
    }
715
716
    /**
717
     * Records information for common (or admin) events (in the track_e_default table).
718
     *
719
     * @param string  $event_type Type of event
720
     * @param string  $event_value_type Type of value
721
     * @param mixed   $event_value Value (string, or array in the case of user info)
722
     * @param ?string $datetime Datetime (UTC) (defaults to null)
723
     * @param ?int    $user_id User ID (defaults to null)
724
     * @param ?int    $course_id Course ID (defaults to null)
725
     * @param ?int    $sessionId Session ID
726
     *
727
     * @return bool
728
     * @assert ('','','') === false
729
     * @throws \Doctrine\ORM\Exception\ORMException
730
     * @throws Exception
731
     * @author Yannick Warnier <[email protected]>
732
     *
733
     */
734
    public static function addEvent(
735
        string $event_type,
736
        string $event_value_type,
737
        mixed $event_value,
738
        ?string $datetime = null,
739
        ?int $user_id = null,
740
        ?int $course_id = null,
741
        ?int $sessionId = 0
742
    ): bool
743
    {
744
        if (empty($event_type)) {
745
            return false;
746
        }
747
        if (empty($course_id)) {
748
            $course_id = api_get_course_int_id();
749
        }
750
        if (empty($sessionId)) {
751
            $sessionId = api_get_session_id();
752
        }
753
754
        // Clean the user_info
755
        if (LOG_USER_OBJECT == $event_value_type) {
756
            if (is_array($event_value)) {
757
                unset($event_value['complete_name']);
758
                unset($event_value['complete_name_with_username']);
759
                unset($event_value['firstName']);
760
                unset($event_value['lastName']);
761
                unset($event_value['avatar_small']);
762
                unset($event_value['avatar']);
763
                unset($event_value['mail']);
764
                unset($event_value['password']);
765
                unset($event_value['last_login']);
766
                unset($event_value['picture_uri']);
767
                $event_value = serialize($event_value);
768
            }
769
770
            if ($event_value instanceof User) {
771
                $event_value = serialize(
772
                    [
773
                        'id' => $event_value->getId(),
774
                        'username' => $event_value->getUsername(),
775
                        'firstname' => $event_value->getFirstName(),
776
                        'lastname' => $event_value->getLastname(),
777
                    ]
778
                );
779
            }
780
        }
781
        // If event is an array then the $event_value_type should finish with
782
        // the suffix _array for example LOG_WORK_DATA = work_data_array
783
        if (is_array($event_value)) {
784
            $event_value = serialize($event_value);
785
        }
786
787
        $sessionId = empty($sessionId) ? api_get_session_id() : $sessionId;
788
789
        if (!isset($datetime)) {
790
            $datetime = api_get_utc_datetime();
791
        }
792
793
        if (!isset($user_id)) {
794
            $user_id = api_get_user_id();
795
        }
796
797
        $track = (new TrackEDefault())
798
            ->setDefaultUserId($user_id)
799
            ->setCId($course_id)
800
            ->setDefaultDate(new DateTime($datetime, new DateTimeZone('UTC')))
801
            ->setDefaultEventType($event_type)
802
            ->setDefaultValueType($event_value_type)
803
            ->setDefaultValue($event_value)
804
            ->setSessionId($sessionId);
805
806
        $em = Database::getManager();
807
        $em->persist($track);
808
        $em->flush();
809
810
        return true;
811
    }
812
813
    /**
814
     * Gets the last attempt of an exercise based in the exe_id.
815
     *
816
     * @param int $exeId
817
     *
818
     * @return string
819
     * @throws \Doctrine\DBAL\Exception
820
     * @throws Exception
821
     */
822
    public static function getLastAttemptDateOfExercise(int $exeId): string
823
    {
824
        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
825
        $sql = "SELECT max(tms) as last_attempt_date
826
                FROM $track_attempts
827
                WHERE exe_id = $exeId";
828
        $rs_last_attempt = Database::query($sql);
829
        if (0 == Database::num_rows($rs_last_attempt)) {
830
            return '';
831
        }
832
        $row_last_attempt = Database::fetch_array($rs_last_attempt);
833
834
        return $row_last_attempt['last_attempt_date']; //Get the date of last attempt
835
    }
836
837
    /**
838
     * Gets the last attempt of an exercise based in the exe_id.
839
     *
840
     * @param int $exeId
841
     *
842
     * @return int
843
     * @throws \Doctrine\DBAL\Exception
844
     * @throws Exception
845
     */
846
    public static function getLatestQuestionIdFromAttempt(int $exeId): int
847
    {
848
        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
849
        $sql = "SELECT question_id FROM $track_attempts
850
                WHERE exe_id = $exeId
851
                ORDER BY tms DESC
852
                LIMIT 1";
853
        $result = Database::query($sql);
854
        if (Database::num_rows($result)) {
855
            $row = Database::fetch_array($result);
856
857
            return $row['question_id'];
858
        }
859
860
        return 0;
861
    }
862
863
    /**
864
     * Gets how many attempts exists by user, exercise, learning path.
865
     *
866
     * @param int $user_id
867
     * @param int $exerciseId
868
     * @param int $lp_id
869
     * @param int $lp_item_id
870
     * @param int $lp_item_view_id
871
     *
872
     * @return int
873
     * @throws \Doctrine\DBAL\Exception
874
     * @throws Exception
875
     */
876
    public static function get_attempt_count(
877
        int $user_id,
878
        int $exerciseId,
879
        int $lp_id,
880
        int $lp_item_id,
881
        int $lp_item_view_id
882
    ): int
883
    {
884
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
885
        $courseId = api_get_course_int_id();
886
        $sessionId = api_get_session_id();
887
        $sessionCondition = api_get_session_condition($sessionId);
888
        $sql = "SELECT count(*) as count
889
                FROM $table
890
                WHERE
891
                    exe_exo_id = $exerciseId AND
892
                    exe_user_id = $user_id AND
893
                    status != 'incomplete' AND
894
                    orig_lp_id = $lp_id AND
895
                    orig_lp_item_id = $lp_item_id AND
896
                    orig_lp_item_view_id = $lp_item_view_id AND
897
                    c_id = $courseId
898
                    $sessionCondition";
899
900
        $result = Database::query($sql);
901
        if (Database::num_rows($result) > 0) {
902
            $attempt = Database::fetch_assoc($result);
903
904
            return (int) $attempt['count'];
905
        }
906
907
        return 0;
908
    }
909
910
    /**
911
     * Find the order (not the count) of the given attempt in the queue of attempts
912
     * @param int $exeId The attempt ID from track_e_exercises
913
     * @param int $user_id
914
     * @param int $exerciseId
915
     * @param int $lp_id
916
     * @param int $lp_item_id
917
     * @param int $lp_item_view_id
918
     *
919
     * @return int
920
     * @throws \Doctrine\DBAL\Exception
921
     * @throws Exception
922
     */
923
    public static function getAttemptPosition(
924
        int $exeId,
925
        int $user_id,
926
        int $exerciseId,
927
        int $lp_id,
928
        int $lp_item_id,
929
        int $lp_item_view_id
930
    ): int
931
    {
932
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
933
        $courseId = api_get_course_int_id();
934
        $sessionId = api_get_session_id();
935
936
        $sessionCondition = api_get_session_condition($sessionId);
937
        // Select all matching attempts
938
        $sql = "SELECT exe_id
939
                FROM $table
940
                WHERE
941
                    exe_exo_id = $exerciseId AND
942
                    exe_user_id = $user_id AND
943
                    status = '' AND
944
                    orig_lp_id = $lp_id AND
945
                    orig_lp_item_id = $lp_item_id AND
946
                    orig_lp_item_view_id = $lp_item_view_id AND
947
                    c_id = $courseId
948
                    $sessionCondition
949
                ORDER by exe_id
950
                ";
951
952
        $result = Database::query($sql);
953
        // Scroll through them until we found ours, to locate its order in the queue
954
        if (Database::num_rows($result) > 0) {
955
            $position = 1;
956
            while ($row = Database::fetch_assoc($result)) {
957
                if ($row['exe_id'] === $exeId) {
958
                    break;
959
                }
960
                $position++;
961
            }
962
963
            return $position;
964
        }
965
966
        return 0;
967
    }
968
969
    /**
970
     * @param int   $user_id
971
     * @param int   $lp_id
972
     * @param array $course
973
     * @param int   $session_id
974
     * @param ?bool $disconnectExerciseResultsFromLp (Replace orig_lp_* variables to null)
975
     *
976
     * @return bool
977
     * @throws Exception
978
     * @throws \Doctrine\DBAL\Exception
979
     * @throws \Doctrine\ORM\Exception\ORMException
980
     */
981
    public static function delete_student_lp_events(
982
        int $user_id,
983
        int $lp_id,
984
        array $course,
985
        int $session_id,
986
        ?bool $disconnectExerciseResultsFromLp = false
987
    ): bool
988
    {
989
        $lp_view_table = Database::get_course_table(TABLE_LP_VIEW);
990
        $lp_item_view_table = Database::get_course_table(TABLE_LP_ITEM_VIEW);
991
        $lpInteraction = Database::get_course_table(TABLE_LP_IV_INTERACTION);
992
        $lpObjective = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
993
994
        if (empty($course) || empty($user_id)) {
995
            return false;
996
        }
997
998
        $course_id = $course['real_id'];
999
1000
        if (empty($course_id)) {
1001
            $course_id = api_get_course_int_id();
1002
        }
1003
1004
        $track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1005
        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1006
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1007
        $sessionCondition = api_get_session_condition($session_id);
1008
        // Make sure we have the exact lp_view_id
1009
        $sql = "SELECT iid FROM $lp_view_table
1010
                WHERE
1011
                    c_id = $course_id AND
1012
                    user_id = $user_id AND
1013
                    lp_id = $lp_id
1014
                    $sessionCondition";
1015
        $result = Database::query($sql);
1016
1017
        if (Database::num_rows($result)) {
1018
            $view = Database::fetch_assoc($result);
1019
            $lp_view_id = $view['iid'];
1020
1021
            $sql = "DELETE FROM $lp_item_view_table
1022
                    WHERE lp_view_id = $lp_view_id";
1023
            Database::query($sql);
1024
1025
            $sql = "DELETE FROM $lpInteraction
1026
                    WHERE c_id = $course_id AND lp_iv_id = $lp_view_id";
1027
            Database::query($sql);
1028
1029
            $sql = "DELETE FROM $lpObjective
1030
                    WHERE c_id = $course_id AND lp_iv_id = $lp_view_id";
1031
            Database::query($sql);
1032
        }
1033
1034
        if ('true' === api_get_setting('lp.lp_minimum_time')) {
1035
            $sql = "DELETE FROM track_e_access_complete
1036
                    WHERE
1037
                        tool = 'learnpath' AND
1038
                        c_id = $course_id AND
1039
                        tool_id = $lp_id AND
1040
                        user_id = $user_id AND
1041
                        session_id = $session_id
1042
                    ";
1043
            Database::query($sql);
1044
        }
1045
1046
        $sql = "SELECT exe_id FROM $track_e_exercises
1047
                WHERE
1048
                    exe_user_id = $user_id AND
1049
                    session_id = $session_id AND
1050
                    c_id = $course_id AND
1051
                    orig_lp_id = $lp_id";
1052
        $result = Database::query($sql);
1053
        $exeList = [];
1054
        while ($row = Database::fetch_assoc($result)) {
1055
            $exeList[] = $row['exe_id'];
1056
        }
1057
1058
        if (!empty($exeList) && count($exeList) > 0) {
1059
            $exeListString = implode(',', $exeList);
1060
            if ($disconnectExerciseResultsFromLp) {
1061
                $sql = "UPDATE $track_e_exercises
1062
                        SET orig_lp_id = null,
1063
                            orig_lp_item_id = null,
1064
                            orig_lp_item_view_id = null
1065
                        WHERE exe_id IN ($exeListString)";
1066
                Database::query($sql);
1067
            } else {
1068
                $sql = "DELETE FROM $track_e_exercises
1069
                    WHERE exe_id IN ($exeListString)";
1070
                Database::query($sql);
1071
1072
                $sql = "DELETE FROM $track_attempts
1073
                    WHERE exe_id IN ($exeListString)";
1074
                Database::query($sql);
1075
1076
                $sql = "DELETE FROM $tblTrackAttemptQualify
1077
                    WHERE exe_id IN ($exeListString)";
1078
                Database::query($sql);
1079
            }
1080
        }
1081
1082
        $sql = "DELETE FROM $lp_view_table
1083
                WHERE
1084
                    c_id = $course_id AND
1085
                    user_id = $user_id AND
1086
                    lp_id= $lp_id AND
1087
                    session_id = $session_id
1088
            ";
1089
        Database::query($sql);
1090
1091
        self::addEvent(
1092
            LOG_LP_ATTEMPT_DELETE,
1093
            LOG_LP_ID,
1094
            $lp_id,
1095
            null,
1096
            null,
1097
            $course_id,
1098
            $session_id
1099
        );
1100
1101
        return true;
1102
    }
1103
1104
    /**
1105
     * Delete all exercise attempts (included in LP or not).
1106
     *
1107
     * @param int  $user_id
1108
     * @param int  $exercise_id
1109
     * @param int  $course_id
1110
     * @param ?int $session_id
1111
     * @throws \Doctrine\ORM\Exception\ORMException
1112
     */
1113
    public static function delete_all_incomplete_attempts(
1114
        int $user_id,
1115
        int $exercise_id,
1116
        int $course_id,
1117
        ?int $session_id = 0
1118
    ): void
1119
    {
1120
        if (!empty($user_id) && !empty($exercise_id) && !empty($course_id)) {
1121
            $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1122
            $sessionCondition = api_get_session_condition($session_id);
1123
            $sql = "SELECT exe_id FROM $table
1124
                    WHERE
1125
                        exe_user_id = $user_id AND
1126
                        exe_exo_id = $exercise_id AND
1127
                        c_id = $course_id AND
1128
                        status = 'incomplete'
1129
                        $sessionCondition
1130
                        ";
1131
            $result = Database::query($sql);
1132
            $repo = Container::getTrackEExerciseRepository();
1133
            while ($row = Database::fetch_assoc($result)) {
1134
                $exeId = $row['exe_id'];
1135
                /** @var TrackEExercise $track */
1136
                $track = $repo->find($exeId);
1137
1138
                self::addEvent(
1139
                    LOG_EXERCISE_RESULT_DELETE,
1140
                    LOG_EXERCISE_AND_USER_ID,
1141
                    ($track->getQuiz()?->getIid()).'-'.$track->getUser()->getId(),
1142
                    null,
1143
                    null,
1144
                    $course_id,
1145
                    $session_id
1146
                );
1147
                $repo->delete($track);
1148
            }
1149
        }
1150
    }
1151
1152
    /**
1153
     * Gets all exercise results (NO Exercises in LPs ) from a given exercise id, course, session.
1154
     *
1155
     * @param int $exercise_id
1156
     * @param int $courseId
1157
     * @param ?int $session_id
1158
     * @param ?bool $load_question_list
1159
     * @param ?int $user_id
1160
     *
1161
     * @return array with the results
1162
     * @throws \Doctrine\DBAL\Exception
1163
     * @throws Exception
1164
     */
1165
    public static function get_all_exercise_results(
1166
        int $exercise_id,
1167
        int $courseId,
1168
        ?int $session_id = 0,
1169
        ?bool $load_question_list = true,
1170
        ?int $user_id = null
1171
    ): array
1172
    {
1173
        $TABLETRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1174
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1175
1176
        $user_condition = null;
1177
        if (!empty($user_id)) {
1178
            $user_condition = "AND exe_user_id = $user_id ";
1179
        }
1180
        $sessionCondition = api_get_session_condition($session_id);
1181
        $sql = "SELECT * FROM $TABLETRACK_EXERCISES
1182
                WHERE
1183
                    status = ''  AND
1184
                    c_id = $courseId AND
1185
                    exe_exo_id = $exercise_id AND
1186
                    orig_lp_id = 0 AND
1187
                    orig_lp_item_id = 0
1188
                    $user_condition
1189
                    $sessionCondition
1190
                ORDER BY exe_id";
1191
        $res = Database::query($sql);
1192
        $list = [];
1193
        while ($row = Database::fetch_assoc($res)) {
1194
            $list[$row['exe_id']] = $row;
1195
            if ($load_question_list) {
1196
                $sql = "SELECT * FROM $TBL_TRACK_ATTEMPT
1197
                        WHERE exe_id = {$row['exe_id']}";
1198
                $res_question = Database::query($sql);
1199
                while ($row_q = Database::fetch_assoc($res_question)) {
1200
                    $list[$row['exe_id']]['question_list'][$row_q['question_id']] = $row_q;
1201
                }
1202
            }
1203
        }
1204
1205
        return $list;
1206
    }
1207
1208
    /**
1209
     * Gets all exercise results (NO Exercises in LPs ) from a given exercise id, course, session.
1210
     *
1211
     * @param int  $courseId
1212
     * @param ?int  $session_id
1213
     * @param ?bool $get_count
1214
     *
1215
     * @return array with the results
1216
     * @throws \Doctrine\DBAL\Exception
1217
     */
1218
    public static function get_all_exercise_results_by_course(
1219
        int $courseId,
1220
        ?int $session_id = 0,
1221
        ?bool $get_count = true
1222
    ) {
1223
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1224
1225
        $select = '*';
1226
        if ($get_count) {
1227
            $select = 'count(*) as count';
1228
        }
1229
        $sessionCondition = api_get_session_condition($session_id);
1230
        $sql = "SELECT $select FROM $table_track_exercises
1231
                WHERE   status = ''  AND
1232
                        c_id = $courseId AND
1233
                        orig_lp_id = 0 AND
1234
                        orig_lp_item_id = 0
1235
                        $sessionCondition
1236
                ORDER BY exe_id";
1237
        $res = Database::query($sql);
1238
        if ($get_count) {
1239
            $row = Database::fetch_assoc($res);
1240
1241
            return $row['count'];
1242
        } else {
1243
            $list = [];
1244
            while ($row = Database::fetch_assoc($res)) {
1245
                $list[$row['exe_id']] = $row;
1246
            }
1247
1248
            return $list;
1249
        }
1250
    }
1251
1252
    /**
1253
     * Gets all exercise results (NO Exercises in LPs) from a given exercise id, course, session.
1254
     *
1255
     * @param int $user_id
1256
     * @param int $courseId
1257
     * @param ?int $session_id
1258
     *
1259
     * @return array with the results
1260
     * @throws \Doctrine\DBAL\Exception
1261
     */
1262
    public static function get_all_exercise_results_by_user(
1263
        int $user_id,
1264
        int $courseId,
1265
        ?int $session_id = 0
1266
    ): array
1267
    {
1268
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1269
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1270
1271
        $sessionCondition = api_get_session_condition($session_id);
1272
        $sql = "SELECT * FROM $table_track_exercises
1273
                WHERE
1274
                    status = '' AND
1275
                    exe_user_id = $user_id AND
1276
                    c_id = $courseId AND
1277
                    orig_lp_id = 0 AND
1278
                    orig_lp_item_id = 0
1279
                    $sessionCondition
1280
                ORDER by exe_id";
1281
1282
        $res = Database::query($sql);
1283
        $list = [];
1284
        while ($row = Database::fetch_assoc($res)) {
1285
            $list[$row['exe_id']] = $row;
1286
            $sql = "SELECT * FROM $table_track_attempt
1287
                    WHERE exe_id = {$row['exe_id']}";
1288
            $res_question = Database::query($sql);
1289
            while ($row_q = Database::fetch_assoc($res_question)) {
1290
                $list[$row['exe_id']]['question_list'][$row_q['question_id']] = $row_q;
1291
            }
1292
        }
1293
1294
        return $list;
1295
    }
1296
1297
    /**
1298
     * Gets exercise results (NO Exercises in LPs) from a given exercise id, course, session.
1299
     *
1300
     * @param int    $exe_id attempt id
1301
     * @param string $status
1302
     *
1303
     * @return array with the results
1304
     * @throws \Doctrine\DBAL\Exception
1305
     * @throws Exception
1306
     */
1307
    public static function get_exercise_results_by_attempt(int $exe_id, ?string $status = null): array
1308
    {
1309
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1310
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1311
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1312
1313
        $status = Database::escape_string($status);
1314
1315
        $sql = "SELECT * FROM $table_track_exercises
1316
                WHERE status = '$status' AND exe_id = $exe_id";
1317
1318
        $res = Database::query($sql);
1319
        $list = [];
1320
        if (Database::num_rows($res)) {
1321
            $row = Database::fetch_assoc($res);
1322
1323
            //Checking if this attempt was revised by a teacher
1324
            $sql_revised = "SELECT exe_id FROM $tblTrackAttemptQualify
1325
                            WHERE author != '' AND exe_id = $exe_id
1326
                            LIMIT 1";
1327
            $res_revised = Database::query($sql_revised);
1328
            $row['attempt_revised'] = 0;
1329
            if (Database::num_rows($res_revised) > 0) {
1330
                $row['attempt_revised'] = 1;
1331
            }
1332
            $list[$exe_id] = $row;
1333
            $sql = "SELECT * FROM $table_track_attempt
1334
                    WHERE exe_id = $exe_id
1335
                    ORDER BY tms ASC";
1336
            $res_question = Database::query($sql);
1337
            while ($row_q = Database::fetch_assoc($res_question)) {
1338
                $list[$exe_id]['question_list'][$row_q['question_id']] = $row_q;
1339
            }
1340
        }
1341
1342
        return $list;
1343
    }
1344
1345
    /**
1346
     * Gets exercise results (NO Exercises in LPs) from a given user, exercise id, course, session, lp_id, lp_item_id.
1347
     *
1348
     * @param int     $user_id
1349
     * @param int     $exercise_id
1350
     * @param int     $course_id
1351
     * @param ?int    $session_id
1352
     * @param ?int    $lp_id
1353
     * @param ?int    $lp_item_id
1354
     * @param ?string $order asc or desc
1355
     *
1356
     * @return array with the results
1357
     * @throws Exception
1358
     * @throws \Doctrine\DBAL\Exception
1359
     */
1360
    public static function getExerciseResultsByUser(
1361
        int $user_id,
1362
        int $exercise_id,
1363
        int $courseId,
1364
        ?int $session_id = 0,
1365
        ?int $lp_id = 0,
1366
        ?int $lp_item_id = 0,
1367
        ?string $order = null
1368
    ) {
1369
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1370
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1371
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1372
1373
        if (!in_array(strtolower($order), ['asc', 'desc'])) {
1374
            $order = 'asc';
1375
        }
1376
1377
        $sessionCondition = api_get_session_condition($session_id);
1378
1379
        $sql = "SELECT * FROM $table_track_exercises
1380
                WHERE
1381
                    status 			= '' AND
1382
                    exe_user_id 	= $user_id AND
1383
                    c_id 	        = $courseId AND
1384
                    exe_exo_id 		= $exercise_id AND
1385
                    orig_lp_id 		= $lp_id AND
1386
                    orig_lp_item_id = $lp_item_id
1387
                    $sessionCondition
1388
                ORDER by exe_id $order ";
1389
        $res = Database::query($sql);
1390
        $list = [];
1391
        while ($row = Database::fetch_assoc($res)) {
1392
            // Checking if this attempt was revised by a teacher
1393
            $exeId = $row['exe_id'];
1394
            $sql = "SELECT exe_id FROM $tblTrackAttemptQualify
1395
                    WHERE author != '' AND exe_id = $exeId
1396
                    LIMIT 1";
1397
            $res_revised = Database::query($sql);
1398
            $row['attempt_revised'] = 0;
1399
            if (Database::num_rows($res_revised) > 0) {
1400
                $row['attempt_revised'] = 1;
1401
            }
1402
            $row['total_percentage'] = $row['max_score'] > 0 ? ($row['score'] / $row['max_score']) * 100 : 0;
1403
            $list[$row['exe_id']] = $row;
1404
            $sql = "SELECT * FROM $table_track_attempt
1405
                    WHERE exe_id = $exeId";
1406
            $res_question = Database::query($sql);
1407
            while ($row_q = Database::fetch_assoc($res_question)) {
1408
                $list[$row['exe_id']]['question_list'][$row_q['question_id']][] = $row_q;
1409
            }
1410
        }
1411
1412
        return $list;
1413
    }
1414
1415
    /**
1416
     * Count exercise attempts (NO Exercises in LPs ) from a given exercise id, course, session.
1417
     *
1418
     * @param int $user_id
1419
     * @param int $exercise_id
1420
     * @param int $courseId
1421
     * @param int $session_id
1422
     *
1423
     * @return int with the results
1424
     * @throws \Doctrine\DBAL\Exception
1425
     * @throws Exception
1426
     */
1427
    public static function count_exercise_attempts_by_user(
1428
        int $user_id,
1429
        int $exercise_id,
1430
        int $courseId,
1431
        int $session_id = 0
1432
    ): int {
1433
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1434
        $sessionCondition = api_get_session_condition($session_id);
1435
        $sql = "SELECT count(*) as count
1436
                FROM $table
1437
                WHERE status = ''  AND
1438
                    exe_user_id = $user_id AND
1439
                    c_id = $courseId AND
1440
                    exe_exo_id = $exercise_id AND
1441
                    orig_lp_id = 0 AND
1442
                    orig_lp_item_id = 0
1443
                    $sessionCondition
1444
                ORDER BY exe_id";
1445
        $res = Database::query($sql);
1446
        $result = 0;
1447
        if (Database::num_rows($res) > 0) {
1448
            $row = Database::fetch_assoc($res);
1449
            $result = $row['count'];
1450
        }
1451
1452
        return $result;
1453
    }
1454
1455
    /**
1456
     * Gets all exercise BEST results attempts (NO Exercises in LPs)
1457
     * from a given exercise id, course, session per user.
1458
     *
1459
     * @param int  $exercise_id
1460
     * @param int  $courseId
1461
     * @param ?int $session_id
1462
     * @param ?int $userId
1463
     *
1464
     * @return array with the results
1465
     *
1466
     * @throws Exception
1467
     * @throws \Doctrine\DBAL\Exception
1468
     * @todo rename this function
1469
     */
1470
    public static function get_best_exercise_results_by_user(
1471
        int $exercise_id,
1472
        int $courseId,
1473
        ?int $session_id = 0,
1474
        ?int $userId = 0
1475
    ): array
1476
    {
1477
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1478
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1479
        $sessionCondition = api_get_session_condition($session_id);
1480
        $sql = "SELECT * FROM $table_track_exercises
1481
                WHERE
1482
                    status = '' AND
1483
                    c_id = $courseId AND
1484
                    exe_exo_id = $exercise_id AND
1485
                    orig_lp_id = 0 AND
1486
                    orig_lp_item_id = 0
1487
                    $sessionCondition
1488
                ";
1489
1490
        if (!empty($userId)) {
1491
            $sql .= " AND exe_user_id = $userId ";
1492
        }
1493
        $sql .= ' ORDER BY exe_id';
1494
1495
        $res = Database::query($sql);
1496
        $list = [];
1497
        while ($row = Database::fetch_assoc($res)) {
1498
            $list[$row['exe_id']] = $row;
1499
            $exeId = $row['exe_id'];
1500
            $sql = "SELECT * FROM $table_track_attempt
1501
                    WHERE exe_id = $exeId";
1502
            $res_question = Database::query($sql);
1503
            while ($row_q = Database::fetch_assoc($res_question)) {
1504
                $list[$exeId]['question_list'][$row_q['question_id']] = $row_q;
1505
            }
1506
        }
1507
1508
        // Getting the best results of every student
1509
        $best_score_return = [];
1510
        foreach ($list as $student_result) {
1511
            $user_id = $student_result['exe_user_id'];
1512
            $current_best_score[$user_id] = $student_result['score'];
1513
            if (!isset($best_score_return[$user_id]['score'])) {
1514
                $best_score_return[$user_id] = $student_result;
1515
            }
1516
1517
            if ($current_best_score[$user_id] > $best_score_return[$user_id]['score']) {
1518
                $best_score_return[$user_id] = $student_result;
1519
            }
1520
        }
1521
1522
        return $best_score_return;
1523
    }
1524
1525
    /**
1526
     * Get the last best result from all attempts in exercises per user (out of learning paths).
1527
     *
1528
     * @param int  $user_id
1529
     * @param int  $exercise_id
1530
     * @param int  $courseId
1531
     * @param ?int  $session_id
1532
     * @param ?bool $skipLpResults
1533
     *
1534
     * @return array
1535
     * @throws Exception
1536
     * @throws \Doctrine\DBAL\Exception
1537
     */
1538
    public static function get_best_attempt_exercise_results_per_user(
1539
        int $user_id,
1540
        int $exercise_id,
1541
        int $courseId,
1542
        ?int $session_id = 0,
1543
        ?bool $skipLpResults = true
1544
    ) {
1545
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1546
1547
        $sessionCondition = api_get_session_condition($session_id);
1548
        $sql = "SELECT * FROM $table
1549
                WHERE
1550
                    status = ''  AND
1551
                    c_id = $courseId AND
1552
                    exe_exo_id = $exercise_id AND
1553
                    exe_user_id = $user_id
1554
                    $sessionCondition
1555
                ";
1556
1557
        if ($skipLpResults) {
1558
            $sql .= ' AND
1559
                    orig_lp_id = 0 AND
1560
                orig_lp_item_id = 0 ';
1561
        }
1562
1563
        $sql .= ' ORDER BY exe_id ';
1564
1565
        $res = Database::query($sql);
1566
        $list = [];
1567
        while ($row = Database::fetch_assoc($res)) {
1568
            $list[$row['exe_id']] = $row;
1569
        }
1570
        //Getting the best results of every student
1571
        $best_score_return = [];
1572
        $best_score_return['score'] = 0;
1573
1574
        foreach ($list as $result) {
1575
            $current_best_score = $result;
1576
            if ($current_best_score['score'] > $best_score_return['score']) {
1577
                $best_score_return = $result;
1578
            }
1579
        }
1580
        if (!isset($best_score_return['max_score'])) {
1581
            $best_score_return = [];
1582
        }
1583
1584
        return $best_score_return;
1585
    }
1586
1587
    /**
1588
     * Gets all exercise events from a Learning Path within a Course    nd Session.
1589
     *
1590
     * @param int $exercise_id
1591
     * @param int $courseId
1592
     * @param ?int $session_id
1593
     *
1594
     * @return array
1595
     * @throws Exception
1596
     * @throws \Doctrine\DBAL\Exception
1597
     */
1598
    public static function get_all_exercise_event_from_lp(
1599
        int $exercise_id,
1600
        int $courseId,
1601
        ?int $session_id = 0
1602
    ): array
1603
    {
1604
        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1605
        $table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1606
        $sessionCondition = api_get_session_condition($session_id);
1607
1608
        $sql = "SELECT * FROM $table_track_exercises
1609
                WHERE
1610
                    status = '' AND
1611
                    c_id = $courseId AND
1612
                    exe_exo_id = $exercise_id AND
1613
                    orig_lp_id !=0 AND
1614
                    orig_lp_item_id != 0
1615
                    $sessionCondition
1616
                    ";
1617
1618
        $res = Database::query($sql);
1619
        $list = [];
1620
        while ($row = Database::fetch_assoc($res)) {
1621
            $exeId = $row['exe_id'];
1622
            $list[$exeId] = $row;
1623
            $sql = "SELECT * FROM $table_track_attempt
1624
                    WHERE exe_id = $exeId";
1625
            $res_question = Database::query($sql);
1626
            while ($row_q = Database::fetch_assoc($res_question)) {
1627
                $list[$exeId]['question_list'][$row_q['question_id']] = $row_q;
1628
            }
1629
        }
1630
1631
        return $list;
1632
    }
1633
1634
    /**
1635
     * Get a list of all the exercises in a given learning path.
1636
     *
1637
     * @param int $lp_id
1638
     *
1639
     * @return array
1640
     * @throws \Doctrine\DBAL\Exception
1641
     * @throws Exception
1642
     */
1643
    public static function get_all_exercises_from_lp(int $lp_id): array
1644
    {
1645
        $lp_item_table = Database::get_course_table(TABLE_LP_ITEM);
1646
        $sql = "SELECT * FROM $lp_item_table
1647
                WHERE
1648
                    lp_id = $lp_id AND
1649
                    item_type = 'quiz'
1650
                ORDER BY parent_item_id, display_order";
1651
        $res = Database::query($sql);
1652
1653
        $list = [];
1654
        while ($row = Database::fetch_assoc($res)) {
1655
            $list[] = $row;
1656
        }
1657
1658
        return $list;
1659
    }
1660
1661
    /**
1662
     * This function gets the comments of an exercise.
1663
     *
1664
     * @param int $exe_id
1665
     * @param int $question_id
1666
     *
1667
     * @return string the comment
1668
     * @throws \Doctrine\DBAL\Exception
1669
     * @throws Exception
1670
     */
1671
    public static function get_comments(int $exe_id, int $question_id): string
1672
    {
1673
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1674
        $sql = "SELECT teacher_comment
1675
                FROM $table
1676
                WHERE
1677
                    exe_id = $exe_id AND
1678
                    question_id = $question_id
1679
                ORDER by question_id";
1680
        $sqlres = Database::query($sql);
1681
        $comm = strval(Database::result($sqlres, 0, 'teacher_comment'));
1682
        $comm = trim($comm);
1683
1684
        return $comm;
1685
    }
1686
1687
    /**
1688
     * Get all the track_e_attempt records for a given
1689
     * track_e_exercises.exe_id (pk).
1690
     *
1691
     * @param int $exeId The exe_id from an exercise attempt record
1692
     *
1693
     * @return array The complete records from track_e_attempt that match the given exe_id
1694
     * @throws \Doctrine\DBAL\Exception
1695
     * @throws Exception
1696
     */
1697
    public static function getAllExerciseEventByExeId(int $exeId): array
1698
    {
1699
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1700
1701
        $sql = "SELECT * FROM $table
1702
                WHERE exe_id = $exeId
1703
                ORDER BY position";
1704
        $res_question = Database::query($sql);
1705
        $list = [];
1706
        if (Database::num_rows($res_question)) {
1707
            while ($row = Database::fetch_assoc($res_question)) {
1708
                $list[$row['question_id']][] = $row;
1709
            }
1710
        }
1711
1712
        return $list;
1713
    }
1714
1715
    /**
1716
     * Get a question attempt from track_e_attempt based on en exe_id and question_id
1717
     * @param int $exeId
1718
     * @param int $questionId
1719
     * @return array
1720
     * @throws \Doctrine\DBAL\Exception
1721
     * @throws Exception
1722
     */
1723
    public static function getQuestionAttemptByExeIdAndQuestion(int $exeId, int $questionId): array
1724
    {
1725
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1726
1727
        $sql = "SELECT * FROM $table
1728
                WHERE
1729
                    exe_id = $exeId AND
1730
                    question_id = $questionId
1731
                ORDER BY position";
1732
        $result = Database::query($sql);
1733
        $attempt = [];
1734
        if (Database::num_rows($result)) {
1735
            $attempt = Database::fetch_array($result, 'ASSOC');
1736
        }
1737
1738
        return $attempt;
1739
    }
1740
1741
    /**
1742
     * Delete one record from the track_e_attempt table (recorded quiz answer)
1743
     * and register the deletion event (LOG_QUESTION_RESULT_DELETE) in
1744
     * track_e_default.
1745
     *
1746
     * @param int $exeId The track_e_exercises.exe_id (primary key)
1747
     * @param int $user_id The user who answered (already contained in exe_id)
1748
     * @param int $courseId The course in which it happened (already contained in exe_id)
1749
     * @param int $session_id The session in which it happened (already contained in exe_id)
1750
     * @param int $question_id The c_quiz_question.iid
1751
     * @throws \Doctrine\ORM\Exception\ORMException
1752
     * @throws Exception
1753
     */
1754
    public static function delete_attempt(
1755
        int $exeId,
1756
        int $user_id,
1757
        int $courseId,
1758
        int $session_id,
1759
        int $question_id
1760
    ): void
1761
    {
1762
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1763
1764
        $sql = "DELETE FROM $table
1765
                WHERE
1766
                    exe_id = $exeId AND
1767
                    user_id = $user_id AND
1768
                    question_id = $question_id ";
1769
        Database::query($sql);
1770
1771
        self::addEvent(
1772
            LOG_QUESTION_RESULT_DELETE,
1773
            LOG_EXERCISE_ATTEMPT_QUESTION_ID,
1774
            $exeId.'-'.$question_id,
1775
            null,
1776
            null,
1777
            $courseId,
1778
            $session_id
1779
        );
1780
    }
1781
1782
    /**
1783
     * Delete one record from the track_e_hotspot table based on a given
1784
     * track_e_exercises.exe_id.
1785
     *
1786
     * @param int $exeId
1787
     * @param int $user_id
1788
     * @param int $courseId
1789
     * @param int $question_id
1790
     * @param ?int $sessionId
1791
     * @throws \Doctrine\ORM\Exception\ORMException
1792
     * @throws Exception
1793
     */
1794
    public static function delete_attempt_hotspot(
1795
        int $exeId,
1796
        int $user_id,
1797
        int $courseId,
1798
        int $question_id,
1799
        ?int $sessionId = null
1800
    ): void
1801
    {
1802
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
1803
1804
        if (!isset($sessionId)) {
1805
            $sessionId = api_get_session_id();
1806
        }
1807
1808
        $sql = "DELETE FROM $table
1809
                WHERE
1810
                    hotspot_exe_id = $exeId AND
1811
                    hotspot_user_id = $user_id AND
1812
                    c_id = $courseId AND
1813
                    hotspot_question_id = $question_id ";
1814
        Database::query($sql);
1815
        self::addEvent(
1816
            LOG_QUESTION_RESULT_DELETE,
1817
            LOG_EXERCISE_ATTEMPT_QUESTION_ID,
1818
            $exeId.'-'.$question_id,
1819
            null,
1820
            null,
1821
            $courseId,
1822
            $sessionId
1823
        );
1824
    }
1825
1826
    /**
1827
     * Registers in track_e_course_access when user logs in for the first time to a course.
1828
     *
1829
     * @param int $courseId ID of the course
1830
     * @param int $user_id ID of the user
1831
     * @param int $sessionId ID of the session (if any)
1832
     *
1833
     * @return bool
1834
     * @throws Exception
1835
     */
1836
    public static function eventCourseLogin(int $courseId, int $user_id, int $sessionId): bool
1837
    {
1838
        if (Session::read('login_as')) {
1839
            return false;
1840
        }
1841
1842
        $sessionId = (int) $sessionId;
1843
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
1844
            return false;
1845
        }
1846
1847
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
1848
        $loginDate = $logoutDate = api_get_utc_datetime();
1849
1850
        // $counter represents the number of time this record has been refreshed
1851
        $counter = 1;
1852
        $ip = Database::escape_string(api_get_real_ip());
1853
1854
        $sql = "INSERT INTO $table(c_id, user_ip, user_id, login_course_date, logout_course_date, counter, session_id)
1855
                VALUES($courseId, '$ip', $user_id, '$loginDate', '$logoutDate', $counter, $sessionId)";
1856
        $courseAccessId = Database::query($sql);
1857
1858
        if ($courseAccessId) {
1859
            // Course catalog stats modifications see #4191
1860
            CourseManager::update_course_ranking(
1861
                null,
1862
                0,
1863
                null,
1864
                null,
1865
                true,
1866
                false
1867
            );
1868
1869
            return true;
1870
        }
1871
1872
        return false;
1873
    }
1874
1875
    /**
1876
     * Updates the user - course - session every X minutes
1877
     *
1878
     * @param int $courseId
1879
     * @param int $userId
1880
     * @param int $sessionId
1881
     * @param ?int $minutes
1882
     *
1883
     * @return bool
1884
     * @throws \Doctrine\DBAL\Exception
1885
     * @throws Exception
1886
     */
1887
    public static function eventCourseLoginUpdate(
1888
        int $courseId,
1889
        int $userId,
1890
        int $sessionId,
1891
        ?int $minutes = 5
1892
    ): bool
1893
    {
1894
        if (Session::read('login_as')) {
1895
            return false;
1896
        }
1897
1898
        if (empty($courseId) || empty($userId)) {
1899
            return false;
1900
        }
1901
1902
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
1903
            return false;
1904
        }
1905
1906
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
1907
        $sql = "SELECT course_access_id, logout_course_date
1908
                FROM $table
1909
                WHERE
1910
                    c_id = $courseId AND
1911
                    session_id = $sessionId AND
1912
                    user_id = $userId
1913
                ORDER BY login_course_date DESC
1914
                LIMIT 1";
1915
1916
        $result = Database::query($sql);
1917
1918
        // Save every 5 minutes by default
1919
        $seconds = $minutes * 60;
1920
        $maxSeconds = 3600; // Only update if max diff is one hour
1921
        if (Database::num_rows($result)) {
1922
            $row = Database::fetch_array($result);
1923
            $id = $row['course_access_id'];
1924
            $logout = $row['logout_course_date'];
1925
            $now = time();
1926
            $logout = api_strtotime($logout, 'UTC');
1927
            if ($now - $logout > $seconds &&
1928
                $now - $logout < $maxSeconds
1929
            ) {
1930
                $now = api_get_utc_datetime();
1931
                $sql = "UPDATE $table SET
1932
                            logout_course_date = '$now',
1933
                            counter = counter + 1
1934
                        WHERE course_access_id = $id";
1935
                Database::query($sql);
1936
            }
1937
1938
            return true;
1939
        }
1940
1941
        return false;
1942
    }
1943
1944
    /**
1945
     * Register the logout of the course (usually when logging out of the platform)
1946
     * from the track_e_course_access table.
1947
     *
1948
     * @param array $logoutInfo Information stored by local.inc.php
1949
     *                          before new context ['uid'=> x, 'cid'=>y, 'sid'=>z]
1950
     *
1951
     * @return bool
1952
     * @throws \Doctrine\DBAL\Exception
1953
     * @throws Exception
1954
     */
1955
    public static function courseLogout(array $logoutInfo): bool
1956
    {
1957
        if (Session::read('login_as')) {
1958
            return false;
1959
        }
1960
1961
        if (empty($logoutInfo['uid']) || empty($logoutInfo['cid'])) {
1962
            return false;
1963
        }
1964
1965
        $sessionLifetime = api_get_configuration_value('session_lifetime');
1966
        /*
1967
         * When $_configuration['session_lifetime'] is larger than ~100 hours
1968
         * (in order to let users take exercises with no problems)
1969
         * the function Tracking::get_time_spent_on_the_course() returns larger values (200h) due the condition:
1970
         * login_course_date > now() - INTERVAL $session_lifetime SECOND
1971
         */
1972
        if (empty($sessionLifetime) || $sessionLifetime > 86400) {
1973
            $sessionLifetime = 3600; // 1 hour
1974
        }
1975
        if (!empty($logoutInfo) && !empty($logoutInfo['cid'])) {
1976
            $sessionId = 0;
1977
            if (!empty($logoutInfo['sid'])) {
1978
                $sessionId = (int) $logoutInfo['sid'];
1979
            }
1980
1981
            if (false === self::isSessionLogNeedToBeSave($sessionId)) {
1982
                return false;
1983
            }
1984
1985
            $tableCourseAccess = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
1986
            $userId = (int) $logoutInfo['uid'];
1987
            $courseId = (int) $logoutInfo['cid'];
1988
1989
            $currentDate = api_get_utc_datetime();
1990
            // UTC time
1991
            $diff = time() - $sessionLifetime;
1992
            $time = api_get_utc_datetime($diff);
1993
            $sql = "SELECT course_access_id, logout_course_date
1994
                    FROM $tableCourseAccess
1995
                    WHERE
1996
                        user_id = $userId AND
1997
                        c_id = $courseId  AND
1998
                        session_id = $sessionId AND
1999
                        login_course_date > '$time'
2000
                    ORDER BY login_course_date DESC
2001
                    LIMIT 1";
2002
            $result = Database::query($sql);
2003
            $insert = false;
2004
            if (Database::num_rows($result) > 0) {
2005
                $row = Database::fetch_assoc($result);
2006
                $courseAccessId = $row['course_access_id'];
2007
                $sql = "UPDATE $tableCourseAccess SET
2008
                                logout_course_date = '$currentDate',
2009
                                counter = counter + 1
2010
                            WHERE course_access_id = $courseAccessId";
2011
                Database::query($sql);
2012
            } else {
2013
                $insert = true;
2014
            }
2015
2016
            if ($insert) {
2017
                $ip = Database::escape_string(api_get_real_ip());
2018
                $sql = "INSERT INTO $tableCourseAccess (c_id, user_ip, user_id, login_course_date, logout_course_date, counter, session_id)
2019
                        VALUES ($courseId, '$ip', $userId, '$currentDate', '$currentDate', 1, $sessionId)";
2020
                Database::query($sql);
2021
            }
2022
2023
            return true;
2024
        }
2025
2026
        return false;
2027
    }
2028
2029
    /**
2030
     * Register a "fake" time spent on the platform, for example to match the
2031
     * estimated time he took to author an assignment/work, see configuration
2032
     * setting considered_working_time.
2033
     * This assumes there is already some connection of the student to the
2034
     * course, otherwise he wouldn't be able to upload an assignment.
2035
     * This works by creating a new record, copy of the current one, then
2036
     * updating the current one to be just the considered_working_time and
2037
     * end at the same second as the user connected to the course.
2038
     *
2039
     * @param int    $courseId The course in which to add the time
2040
     * @param int    $userId The user for whom to add the time
2041
     * @param int    $sessionId The session in which to add the time (if any)
2042
     * @param string $virtualTime The amount of time to be added,
2043
     *                            in a hh:mm:ss format. If int, we consider it is expressed in hours.
2044
     * @param int    $workId Student publication id result
2045
     *
2046
     * @return bool true on successful insertion, false otherwise
2047
     * @throws \Doctrine\DBAL\Exception
2048
     */
2049
    public static function eventAddVirtualCourseTime(
2050
        int $courseId,
2051
        int $userId,
2052
        int $sessionId,
2053
        string $virtualTime,
2054
        int $workId
2055
    ): bool
2056
    {
2057
        if (empty($virtualTime)) {
2058
            return false;
2059
        }
2060
2061
        $logoutDate = api_get_utc_datetime();
2062
        $loginDate = ChamiloApi::addOrSubTimeToDateTime(
2063
            $virtualTime,
2064
            $logoutDate,
2065
            false
2066
        );
2067
2068
        $ip = api_get_real_ip();
2069
        $params = [
2070
            'login_course_date' => $loginDate,
2071
            'logout_course_date' => $logoutDate,
2072
            'session_id' => $sessionId,
2073
            'user_id' => $userId,
2074
            'counter' => 0,
2075
            'c_id' => $courseId,
2076
            'user_ip' => $ip,
2077
        ];
2078
        $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
2079
        Database::insert($courseTrackingTable, $params);
2080
2081
        // Time should also be added to the track_e_login table to
2082
        // affect total time on the platform
2083
        $params = [
2084
            'login_user_id' => $userId,
2085
            'login_date' => $loginDate,
2086
            'user_ip' => $ip,
2087
            'logout_date' => $logoutDate,
2088
        ];
2089
        $platformTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
2090
        Database::insert($platformTrackingTable, $params);
2091
2092
        if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
2093
            $workId = (int) $workId;
2094
            $uniqueId = time();
2095
            $logInfo = [
2096
                'c_id' => $courseId,
2097
                'session_id' => $sessionId,
2098
                'tool' => TOOL_STUDENTPUBLICATION,
2099
                'date_reg' => $loginDate,
2100
                'action' => 'add_work_start_'.$workId,
2101
                'action_details' => $virtualTime,
2102
                'user_id' => $userId,
2103
                'current_id' => $uniqueId,
2104
            ];
2105
            self::registerLog($logInfo);
2106
2107
            $logInfo = [
2108
                'c_id' => $courseId,
2109
                'session_id' => $sessionId,
2110
                'tool' => TOOL_STUDENTPUBLICATION,
2111
                'date_reg' => $logoutDate,
2112
                'action' => 'add_work_end_'.$workId,
2113
                'action_details' => $virtualTime,
2114
                'user_id' => $userId,
2115
                'current_id' => $uniqueId,
2116
            ];
2117
            self::registerLog($logInfo);
2118
        }
2119
2120
        return true;
2121
    }
2122
2123
    /**
2124
     * Removes a "fake" time spent on the platform, for example to match the
2125
     * estimated time he took to author an assignment/work, see configuration
2126
     * setting considered_working_time.
2127
     * This method should be called when something that generated a fake
2128
     * time record is removed. Given the database link is weak (no real
2129
     * relationship kept between the deleted item and this record), this
2130
     * method just looks for the latest record that has the same time as the
2131
     * item's fake time, is in the past and in this course+session. If such a
2132
     * record cannot be found, it doesn't do anything.
2133
     * The IP address is not considered a useful filter here.
2134
     *
2135
     * @param int    $courseId The course in which to add the time
2136
     * @param int    $userId The user for whom to add the time
2137
     * @param int    $sessionId The session in which to add the time (if any)
2138
     * @param string $virtualTime The amount of time to be added, in a hh:mm:ss format. If int, we consider it is
2139
     *                            expressed in hours.
2140
     * @param int $workId
2141
     *
2142
     * @return bool true on successful removal, false otherwise
2143
     * @throws \Doctrine\DBAL\Exception
2144
     * @throws Exception
2145
     */
2146
    public static function eventRemoveVirtualCourseTime(
2147
        int $courseId,
2148
        int $userId,
2149
        int $sessionId,
2150
        string $virtualTime,
2151
        int $workId
2152
    ):bool
2153
    {
2154
        if (empty($virtualTime)) {
2155
            return false;
2156
        }
2157
2158
        $originalVirtualTime = Database::escape_string($virtualTime);
2159
2160
        $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
2161
        $platformTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
2162
2163
        // Change $virtualTime format from hh:mm:ss to hhmmss which is the
2164
        // format returned by SQL for a subtraction of two datetime values
2165
        // @todo make sure this is portable between DBMSes
2166
        // @todo make sure this is portable between DBMSes
2167
        if (preg_match('/:/', $virtualTime)) {
2168
            [$h, $m, $s] = preg_split('/:/', $virtualTime);
2169
            $virtualTime = (int) $h * 3600 + (int) $m * 60 + (int) $s;
2170
        } else {
2171
            $virtualTime = (int) $virtualTime * 3600;
2172
        }
2173
2174
        // Get the current latest course connection register. We need that
2175
        // record to re-use the data and create a new record.
2176
        $sql = "SELECT course_access_id
2177
                FROM $courseTrackingTable
2178
                WHERE
2179
                    user_id = $userId AND
2180
                    c_id = $courseId  AND
2181
                    session_id  = $sessionId AND
2182
                    counter = 0 AND
2183
                    (UNIX_TIMESTAMP(logout_course_date) - UNIX_TIMESTAMP(login_course_date)) = '$virtualTime'
2184
                ORDER BY login_course_date DESC LIMIT 0,1";
2185
        $result = Database::query($sql);
2186
2187
        // Ignore if we didn't find any course connection record in the last
2188
        // hour. In this case it wouldn't be right to add a "fake" time record.
2189
        if (Database::num_rows($result) > 0) {
2190
            // Found the latest connection
2191
            $row = Database::fetch_row($result);
2192
            $courseAccessId = $row[0];
2193
            $sql = "DELETE FROM $courseTrackingTable
2194
                    WHERE course_access_id = $courseAccessId";
2195
            Database::query($sql);
2196
        }
2197
        $sql = "SELECT login_id
2198
                FROM $platformTrackingTable
2199
                WHERE
2200
                    login_user_id = $userId AND
2201
                    (UNIX_TIMESTAMP(logout_date) - UNIX_TIMESTAMP(login_date)) = '$virtualTime'
2202
                ORDER BY login_date DESC LIMIT 0,1";
2203
        $result = Database::query($sql);
2204
        if (Database::num_rows($result) > 0) {
2205
            $row = Database::fetch_row($result);
2206
            $loginAccessId = $row[0];
2207
            $sql = "DELETE FROM $platformTrackingTable
2208
                    WHERE login_id = $loginAccessId";
2209
            Database::query($sql);
2210
        }
2211
2212
        if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
2213
            $sql = "SELECT id FROM track_e_access_complete
2214
                    WHERE
2215
                        tool = '".TOOL_STUDENTPUBLICATION."' AND
2216
                        c_id = $courseId AND
2217
                        session_id = $sessionId AND
2218
                        user_id = $userId AND
2219
                        action_details = '$originalVirtualTime' AND
2220
                        action = 'add_work_start_$workId' ";
2221
            $result = Database::query($sql);
2222
            $result = Database::fetch_array($result);
2223
            if ($result) {
2224
                $sql = 'DELETE FROM track_e_access_complete WHERE id = '.$result['id'];
2225
                Database::query($sql);
2226
            }
2227
2228
            $sql = "SELECT id FROM track_e_access_complete
2229
                    WHERE
2230
                        tool = '".TOOL_STUDENTPUBLICATION."' AND
2231
                        c_id = $courseId AND
2232
                        session_id = $sessionId AND
2233
                        user_id = $userId AND
2234
                        action_details = '$originalVirtualTime' AND
2235
                        action = 'add_work_end_$workId' ";
2236
            $result = Database::query($sql);
2237
            $result = Database::fetch_array($result);
2238
            if ($result) {
2239
                $sql = 'DELETE FROM track_e_access_complete WHERE id = '.$result['id'];
2240
                Database::query($sql);
2241
            }
2242
        }
2243
2244
        return false;
2245
    }
2246
2247
    /**
2248
     * Register the logout of the course (usually when logging out of the platform)
2249
     * from the track_e_access_complete table.
2250
     *
2251
     * @param array $logInfo Information stored by local.inc.php
2252
     *
2253
     * @return bool
2254
     * @throws \Doctrine\DBAL\Exception
2255
     */
2256
    public static function registerLog(array $logInfo): bool
2257
    {
2258
        $sessionId = api_get_session_id();
2259
        $courseId = api_get_course_int_id();
2260
2261
        if (isset($logInfo['c_id']) && !empty($logInfo['c_id'])) {
2262
            $courseId = $logInfo['c_id'];
2263
        }
2264
2265
        if (isset($logInfo['session_id']) && !empty($logInfo['session_id'])) {
2266
            $sessionId = $logInfo['session_id'];
2267
        }
2268
2269
        if (!Tracking::minimumTimeAvailable($sessionId, $courseId)) {
2270
            return false;
2271
        }
2272
2273
        if (false === self::isSessionLogNeedToBeSave($sessionId)) {
2274
            return false;
2275
        }
2276
2277
        $loginAs = true === (int) Session::read('login_as');
2278
2279
        $logInfo['user_id'] = isset($logInfo['user_id']) ? $logInfo['user_id'] : api_get_user_id();
2280
        $logInfo['date_reg'] = isset($logInfo['date_reg']) ? $logInfo['date_reg'] : api_get_utc_datetime();
2281
        $logInfo['tool'] = !empty($logInfo['tool']) ? $logInfo['tool'] : '';
2282
        $logInfo['tool_id'] = !empty($logInfo['tool_id']) ? (int) $logInfo['tool_id'] : 0;
2283
        $logInfo['tool_id_detail'] = !empty($logInfo['tool_id_detail']) ? (int) $logInfo['tool_id_detail'] : 0;
2284
        $logInfo['action'] = !empty($logInfo['action']) ? $logInfo['action'] : '';
2285
        $logInfo['action_details'] = !empty($logInfo['action_details']) ? $logInfo['action_details'] : '';
2286
        $logInfo['ip_user'] = api_get_real_ip();
2287
        $logInfo['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
2288
        $logInfo['session_id'] = $sessionId;
2289
        $logInfo['c_id'] = $courseId;
2290
        $logInfo['ch_sid'] = session_id();
2291
        $logInfo['login_as'] = $loginAs;
2292
        $logInfo['info'] = !empty($logInfo['info']) ? $logInfo['info'] : '';
2293
        $logInfo['url'] = $_SERVER['REQUEST_URI'];
2294
        $logInfo['current_id'] = isset($logInfo['current_id']) ? $logInfo['current_id'] : Session::read('last_id', 0);
2295
2296
        $id = Database::insert('track_e_access_complete', $logInfo);
2297
        if ($id && empty($logInfo['current_id'])) {
2298
            Session::write('last_id', $id);
2299
        }
2300
2301
        return true;
2302
    }
2303
2304
    /**
2305
     * Get the remaining time to answer a question when there is question-based timing in place ('time' field exists for question items)
2306
     * @param int $exeId
2307
     * @param int $questionId
2308
     * @return int
2309
     * @throws \Doctrine\DBAL\Exception
2310
     */
2311
    public static function getAttemptQuestionDuration(int $exeId, int $questionId): int
2312
    {
2313
        // Check current attempt.
2314
        $questionAttempt = self::getQuestionAttemptByExeIdAndQuestion($exeId, $questionId);
2315
        $alreadySpent = 0;
2316
        if (!empty($questionAttempt) && $questionAttempt['seconds_spent']) {
2317
            $alreadySpent = $questionAttempt['seconds_spent'];
2318
        }
2319
        $now = time();
2320
        $questionStart = Session::read('question_start', []);
2321
        if (!empty($questionStart) &&
2322
            isset($questionStart[$questionId]) && !empty($questionStart[$questionId])
2323
        ) {
2324
            $time = $questionStart[$questionId];
2325
        } else {
2326
            $diff = 0;
2327
            if (!empty($alreadySpent)) {
2328
                $diff = $alreadySpent;
2329
            }
2330
            $time = $questionStart[$questionId] = $now - $diff;
2331
            Session::write('question_start', $questionStart);
2332
        }
2333
2334
        return $now - $time;
2335
    }
2336
2337
    /**
2338
     * Wrapper to addEvent with event LOG_SUBSCRIBE_USER_TO_COURSE
2339
     * @param User         $subscribedUser
2340
     * @param CourseEntity $course
2341
     * @return void
2342
     * @throws \Doctrine\ORM\Exception\ORMException
2343
     */
2344
    public static function logSubscribedUserInCourse(
2345
        User $subscribedUser,
2346
        CourseEntity $course
2347
    ): void
2348
    {
2349
        $dateTime = api_get_utc_datetime();
2350
        $registrantId = api_get_user_id();
2351
2352
        self::addEvent(
2353
            LOG_SUBSCRIBE_USER_TO_COURSE,
2354
            LOG_COURSE_CODE,
2355
            $course->getCode(),
2356
            $dateTime,
2357
            $registrantId,
2358
            $course->getId()
2359
        );
2360
2361
        self::addEvent(
2362
            LOG_SUBSCRIBE_USER_TO_COURSE,
2363
            LOG_USER_OBJECT,
2364
            api_get_user_info($subscribedUser->getId()),
2365
            $dateTime,
2366
            $registrantId,
2367
            $course->getId()
2368
        );
2369
    }
2370
2371
    /**
2372
     * Wrapper to addEvent with event LOG_SESSION_ADD_USER_COURSE and LOG_SUBSCRIBE_USER_TO_COURSE
2373
     * @param User          $userSubscribed
2374
     * @param CourseEntity  $course
2375
     * @param SessionEntity $session
2376
     * @return void
2377
     * @throws \Doctrine\ORM\Exception\ORMException
2378
     */
2379
    public static function logUserSubscribedInCourseSession(
2380
        User $userSubscribed,
2381
        CourseEntity $course,
2382
        SessionEntity $session
2383
    ): void
2384
    {
2385
        $dateTime = api_get_utc_datetime();
2386
        $registrantId = api_get_user_id();
2387
2388
        self::addEvent(
2389
            LOG_SESSION_ADD_USER_COURSE,
2390
            LOG_USER_ID,
2391
            $userSubscribed,
2392
            $dateTime,
2393
            $registrantId,
2394
            $course->getId(),
2395
            $session->getId()
2396
        );
2397
        self::addEvent(
2398
            LOG_SUBSCRIBE_USER_TO_COURSE,
2399
            LOG_COURSE_CODE,
2400
            $course->getCode(),
2401
            $dateTime,
2402
            $registrantId,
2403
            $course->getId(),
2404
            $session->getId()
2405
        );
2406
        self::addEvent(
2407
            LOG_SUBSCRIBE_USER_TO_COURSE,
2408
            LOG_USER_OBJECT,
2409
            api_get_user_info($userSubscribed->getId()),
2410
            $dateTime,
2411
            $registrantId,
2412
            $course->getId(),
2413
            $session->getId()
2414
        );
2415
    }
2416
}
2417