Issues (2130)

main/inc/ajax/exercise.ajax.php (1 issue)

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
6
use Chamilo\CoreBundle\Entity\TrackEExercises;
7
use Chamilo\CourseBundle\Entity\CQuizQuestion;
8
use ChamiloSession as Session;
9
10
require_once __DIR__.'/../global.inc.php';
11
$current_course_tool = TOOL_QUIZ;
12
$debug = false;
13
14
ExerciseLib::logPingForCheckingConnection();
15
16
// Check if the user has access to the contextual course/session
17
api_protect_course_script(true);
18
19
$action = $_REQUEST['a'];
20
$course_id = api_get_course_int_id();
21
$session_id = isset($_REQUEST['session_id']) ? (int) $_REQUEST['session_id'] : api_get_session_id();
22
$course_code = isset($_REQUEST['cidReq']) ? $_REQUEST['cidReq'] : api_get_course_id();
23
$currentUserId = api_get_user_id();
24
$exeId = isset($_REQUEST['exe_id']) ? $_REQUEST['exe_id'] : 0;
25
26
switch ($action) {
27
    case 'get_exercise_by_course':
28
        $course_id = (isset($_GET['course_id']) && !empty($_GET['course_id'])) ? (int) $_GET['course_id'] : 0;
29
        $session_id = (!empty($_GET['session_id'])) ? (int) $_GET['session_id'] : 0;
30
        $data = [];
31
        $onlyActiveExercises = !(api_is_platform_admin(true) || api_is_course_admin());
32
        $results = ExerciseLib::get_all_exercises_for_course_id(
33
            null,
34
            $session_id,
35
            $course_id,
36
            $onlyActiveExercises
37
        );
38
39
        if (!empty($results)) {
40
            foreach ($results as $exercise) {
41
                $data[] = ['id' => $exercise['iid'], 'text' => html_entity_decode($exercise['title'])];
42
            }
43
        }
44
45
        echo json_encode($data);
46
        break;
47
    case 'update_duration':
48
        if (Session::read('login_as')) {
49
            if ($debug) {
50
                error_log("User is 'login as' don't update duration time.");
51
            }
52
            exit;
53
        }
54
55
        if (empty($exeId)) {
56
            if ($debug) {
57
                error_log('Exe id not provided.');
58
            }
59
            exit;
60
        }
61
62
        /** @var Exercise $exerciseInSession */
63
        $exerciseInSession = Session::read('objExercise');
64
65
        if (empty($exerciseInSession)) {
66
            if ($debug) {
67
                error_log('Exercise obj not provided.');
68
            }
69
            exit;
70
        }
71
72
        // If exercise was updated x seconds before, then don't updated duration.
73
        $onlyUpdateValue = 10;
74
75
        $em = Database::getManager();
76
        /** @var TrackEExercises $attempt */
77
        $attempt = $em->getRepository('ChamiloCoreBundle:TrackEExercises')->find($exeId);
78
79
        if (empty($attempt)) {
80
            if ($debug) {
81
                error_log("Attempt #$exeId doesn't exists.");
82
            }
83
            exit;
84
        }
85
86
        $nowObject = api_get_utc_datetime(null, false, true);
87
        $now = $nowObject->getTimestamp();
88
        $exerciseId = $attempt->getExeExoId();
89
        $userId = $attempt->getExeUserId();
90
91
        if ($userId != $currentUserId) {
92
            if ($debug) {
93
                error_log("User $currentUserId trying to change time for user $userId");
94
            }
95
            exit;
96
        }
97
98
        if ($exerciseInSession->iid != $exerciseId) {
99
            if ($debug) {
100
                error_log("Cannot update, exercise are different.");
101
            }
102
            exit;
103
        }
104
105
        if ($attempt->getStatus() != 'incomplete') {
106
            if ($debug) {
107
                error_log('Cannot update exercise is already completed.');
108
            }
109
            exit;
110
        }
111
112
        // Check if we are dealing with the same exercise.
113
        $timeWithOutUpdate = $now - $attempt->getExeDate()->getTimestamp();
114
        if ($timeWithOutUpdate > $onlyUpdateValue) {
115
            $key = ExerciseLib::get_time_control_key(
116
                $exerciseId,
117
                $attempt->getOrigLpId(),
118
                $attempt->getOrigLpItemId()
119
            );
120
            $durationFromObject = $attempt->getExeDuration();
121
            $previousTime = Session::read('duration_time_previous');
122
            if (isset($previousTime[$key]) &&
123
                !empty($previousTime[$key])
124
            ) {
125
                $sessionTime = $previousTime[$key];
126
                $duration = $sessionTime = $now - $sessionTime;
127
                if (!empty($durationFromObject)) {
128
                    $duration += $durationFromObject;
129
                }
130
                $duration = (int) $duration;
131
                if (!empty($duration)) {
132
                    if ($debug) {
133
                        error_log("Exe_id: #".$exeId);
134
                        error_log("Key: $key");
135
                        error_log("Exercise to update: #$exerciseId of user: #$userId");
136
                        error_log("Duration time found in DB before update: $durationFromObject");
137
                        error_log("Current spent time $sessionTime before an update");
138
                        error_log("Accumulate duration to save in DB: $duration");
139
                        error_log("End date (UTC) before update: ".$attempt->getExeDate()->format('Y-m-d H:i:s'));
140
                        error_log("End date (UTC) to be save in DB: ".$nowObject->format('Y-m-d H:i:s'));
141
                    }
142
                    $attempt
143
                        ->setExeDuration($duration)
144
                        ->setExeDate($nowObject);
145
                    $em->merge($attempt);
146
                    $em->flush();
147
                }
148
            } else {
149
                if ($debug) {
150
                    error_log("Nothing to update, 'duration_time_previous' session not set");
151
                    error_log("Key: $key");
152
                }
153
            }
154
        } else {
155
            if ($debug) {
156
                error_log("Can't update, time was already updated $timeWithOutUpdate seconds ago");
157
            }
158
        }
159
160
        break;
161
    case 'get_live_stats':
162
        if (!api_is_allowed_to_edit(null, true)) {
163
            break;
164
        }
165
166
        // 1. Setting variables needed by jqgrid
167
        $exercise_id = (int) $_GET['exercise_id'];
168
        $page = (int) $_REQUEST['page']; //page
169
        $limit = (int) $_REQUEST['rows']; //quantity of rows
170
        $sidx = $_REQUEST['sidx']; //index to filter
171
        $sord = $_REQUEST['sord']; //asc or desc
172
173
        if (!in_array($sidx, ['firstname', 'lastname', 'start_date'])) {
174
            $sidx = 1;
175
        }
176
177
        if (!in_array($sord, ['asc', 'desc'])) {
178
            $sord = 'desc';
179
        }
180
        // get index row - i.e. user click to sort $sord = $_GET['sord'];
181
        // get the direction
182
        if (!$sidx) {
183
            $sidx = 1;
184
        }
185
186
        $track_exercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
187
        $user_table = Database::get_main_table(TABLE_MAIN_USER);
188
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
189
190
        $minutes = (int) $_REQUEST['minutes'];
191
        $now = time() - 60 * $minutes;
192
        $now = api_get_utc_datetime($now);
193
194
        $where_condition = " orig_lp_id = 0 AND exe_exo_id = $exercise_id AND start_date > '$now' ";
195
        $sql = "SELECT COUNT(DISTINCT exe_id)
196
                FROM $track_exercise
197
                WHERE $where_condition ";
198
        $result = Database::query($sql);
199
        $count = Database::fetch_row($result);
200
        $count = $count[0];
201
202
        //3. Calculating first, end, etc
203
        $total_pages = 0;
204
        if ($count > 0) {
205
            if (!empty($limit)) {
206
                $total_pages = ceil($count / $limit);
207
            }
208
        }
209
210
        if ($page > $total_pages) {
211
            $page = $total_pages;
212
        }
213
214
        $start = $limit * $page - $limit;
215
        if ($start < 0) {
216
            $start = 0;
217
        }
218
219
        $sql = "SELECT
220
                    exe_id,
221
                    exe_user_id,
222
                    firstname,
223
                    lastname,
224
                    aa.status,
225
                    start_date,
226
                    exe_result,
227
                    exe_weighting,
228
                    exe_result/exe_weighting as score,
229
                    exe_duration,
230
                    questions_to_check,
231
                    orig_lp_id
232
                FROM $user_table u
233
                INNER JOIN (
234
                    SELECT
235
                        t.exe_id,
236
                        t.exe_user_id,
237
                        status,
238
                        start_date,
239
                        exe_result,
240
                        exe_weighting,
241
                        exe_result/exe_weighting as score,
242
                        exe_duration,
243
                        questions_to_check,
244
                        orig_lp_id
245
                    FROM  $track_exercise  t
246
                    LEFT JOIN $track_attempt a
247
                    ON (a.exe_id = t.exe_id AND t.exe_user_id = a.user_id)
248
                    WHERE t.status = 'incomplete' AND $where_condition
249
                    GROUP BY exe_user_id
250
                ) as aa
251
                ON aa.exe_user_id = user_id
252
                ORDER BY $sidx $sord
253
                LIMIT $start, $limit";
254
255
        $result = Database::query($sql);
256
        $results = [];
257
        while ($row = Database::fetch_array($result, 'ASSOC')) {
258
            $results[] = $row;
259
        }
260
261
        $oExe = new Exercise();
262
        $oExe->read($exercise_id);
263
264
        $response = new stdClass();
265
        $response->page = $page;
266
        $response->total = $total_pages;
267
        $response->records = $count;
268
        $i = 0;
269
270
        if (!empty($results)) {
271
            foreach ($results as $row) {
272
                $sql = "SELECT SUM(count_question_id) as count_question_id
273
                        FROM (
274
                            SELECT 1 as count_question_id
275
                            FROM $track_attempt a
276
                            WHERE
277
                              user_id = {$row['exe_user_id']} AND
278
                              exe_id = {$row['exe_id']}
279
                            GROUP by question_id
280
                        ) as count_table";
281
                $result_count = Database::query($sql);
282
                $count_questions = Database::fetch_array(
283
                    $result_count,
284
                    'ASSOC'
285
                );
286
                $count_questions = $count_questions['count_question_id'];
287
288
                $row['count_questions'] = $count_questions;
289
290
                $response->rows[$i]['id'] = $row['exe_id'];
291
                if (!empty($oExe->expired_time)) {
292
                    $remaining = strtotime($row['start_date']) +
293
                        ($oExe->expired_time * 60) -
294
                        strtotime(api_get_utc_datetime(time()));
295
                    $h = floor($remaining / 3600);
296
                    $m = floor(($remaining - ($h * 3600)) / 60);
297
                    $s = ($remaining - ($h * 3600) - ($m * 60));
298
                    $timeInfo = api_convert_and_format_date(
299
                            $row['start_date'],
300
                            DATE_TIME_FORMAT_LONG
301
                        ).' ['.($h > 0 ? $h.':' : '').sprintf("%02d", $m).':'.sprintf("%02d", $s).']';
302
                } else {
303
                    $timeInfo = api_convert_and_format_date(
304
                        $row['start_date'],
305
                        DATE_TIME_FORMAT_LONG
306
                    );
307
                }
308
                $array = [
309
                    $row['firstname'],
310
                    $row['lastname'],
311
                    $timeInfo,
312
                    $row['count_questions'],
313
                    round($row['score'] * 100).'%',
314
                ];
315
                $response->rows[$i]['cell'] = $array;
316
                $i++;
317
            }
318
        }
319
        echo json_encode($response);
320
        break;
321
    case 'update_exercise_list_order':
322
        if (api_is_allowed_to_edit(null, true)) {
323
            $new_list = $_REQUEST['exercise_list'];
324
            $table = Database::get_course_table(TABLE_QUIZ_ORDER);
325
            $counter = 1;
326
            //Drop all
327
            $sql = "DELETE FROM $table WHERE session_id = $session_id AND c_id = $course_id";
328
            Database::query($sql);
329
            // Insert all
330
            foreach ($new_list as $new_order_id) {
331
                Database::insert(
332
                    $table,
333
                    [
334
                        'exercise_order' => $counter,
335
                        'session_id' => $session_id,
336
                        'exercise_id' => (int) $new_order_id,
337
                        'c_id' => $course_id,
338
                    ]
339
                );
340
                $counter++;
341
            }
342
            echo Display::return_message(get_lang('Saved'), 'confirmation');
343
        }
344
        break;
345
    case 'update_question_order':
346
        $course_info = api_get_course_info_by_id($course_id);
347
        $course_id = $course_info['real_id'];
348
        $exercise_id = isset($_REQUEST['exercise_id']) ? (int) $_REQUEST['exercise_id'] : null;
349
350
        if (empty($exercise_id)) {
351
            return Display::return_message(get_lang('Error'), 'error');
352
        }
353
        if (api_is_allowed_to_edit(null, true)) {
354
            $new_question_list = $_POST['question_id_list'];
355
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
356
            $counter = 1;
357
            foreach ($new_question_list as $new_order_id) {
358
                Database::update(
359
                    $TBL_QUESTIONS,
360
                    ['question_order' => $counter],
361
                    [
362
                        'question_id = ? AND c_id = ? AND exercice_id = ? ' => [
363
                            (int) $new_order_id,
364
                            $course_id,
365
                            $exercise_id,
366
                        ],
367
                    ]
368
                )
369
                ;
370
                $counter++;
371
            }
372
            echo Display::return_message(get_lang('Saved'), 'confirmation');
373
        }
374
        break;
375
    case 'add_question_to_reminder':
376
        /** @var Exercise $objExercise */
377
        $objExercise = Session::read('objExercise');
378
379
        if (empty($objExercise) || empty($exeId)) {
380
            echo 0;
381
            exit;
382
        }
383
384
        $option = isset($_GET['option']) ? $_GET['option'] : '';
385
        switch ($option) {
386
            case 'add_all':
387
                $questionListInSession = Session::read('questionList');
388
                $objExercise->addAllQuestionToRemind($exeId, $questionListInSession);
389
                break;
390
            case 'remove_all':
391
                $objExercise->removeAllQuestionToRemind($exeId);
392
                break;
393
            default:
394
                $objExercise->editQuestionToRemind(
395
                    $exeId,
396
                    $_REQUEST['question_id'],
397
                    $_REQUEST['action']
398
                );
399
                break;
400
        }
401
402
        echo 1;
403
        exit;
404
405
        break;
406
    case 'check_answers':
407
        if (false === api_is_allowed_to_session_edit()) {
408
            echo 'error';
409
            exit;
410
        }
411
412
        /** @var Exercise $objExercise */
413
        $objExercise = Session::read('objExercise');
414
        $questionList = Session::read('questionList');
415
        $exeId = Session::read('exe_id');
416
417
        // If exercise or question is not set then exit.
418
        if (empty($questionList) || empty($objExercise)) {
419
            echo 'error';
420
            exit;
421
        }
422
423
        $statInfo = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
424
425
        echo Display::page_subheader(get_lang('VerificationOfAnsweredQuestions'));
426
        echo $objExercise->getReminderTable($questionList, $statInfo, true);
427
        break;
428
    case 'save_question_description':
429
        if (!api_get_configuration_value('allow_quick_question_description_popup')) {
430
            exit;
431
        }
432
        if (!api_is_allowed_to_edit(null, true)) {
433
            exit;
434
        }
435
436
        /** @var \Exercise $objExercise */
437
        $objExercise = Session::read('objExercise');
438
        if (empty($objExercise)) {
439
            exit;
440
        }
441
442
        $questionId = isset($_REQUEST['question_id']) ? (int) $_REQUEST['question_id'] : null;
443
        $image = isset($_REQUEST['image']) ? $_REQUEST['image'] : '';
444
445
        $questionList = $objExercise->getQuestionList();
446
447
        if (!in_array($questionId, $questionList)) {
448
            echo '0';
449
            exit;
450
        }
451
452
        $em = Database::getManager();
453
        $repo = $em->getRepository(CQuizQuestion::class);
454
        /** @var CQuizQuestion $question */
455
        $question = $repo->find($questionId);
456
        if (null !== $question) {
457
            $question->setDescription('<img src="'.$image.'" />');
458
            $em->persist($question);
459
            $em->flush();
460
            echo 1;
461
            exit;
462
        }
463
        echo 0;
464
        exit;
465
        break;
466
    case 'save_exercise_by_now':
467
        header('Content-Type: application/json');
468
469
        $course_info = api_get_course_info_by_id($course_id);
470
471
        if (empty($course_info)) {
472
            echo json_encode(['error' => true]);
473
            exit;
474
        }
475
476
        $course_id = $course_info['real_id'];
477
478
        // Use have permissions to edit exercises results now?
479
        if (false === api_is_allowed_to_session_edit()) {
480
            echo json_encode(['error' => true]);
481
            if ($debug) {
482
                error_log(
483
                    'Exercises attempt '.$exeId.': Failed saving question(s) in course/session '.
484
                    $course_id.'/'.$session_id.
485
                    ': The user ('.api_get_user_id().') does not have the permission to access this session now'
486
                );
487
            }
488
            exit;
489
        }
490
491
        // "all" or "simple" strings means that there's one or all questions exercise type
492
        $type = $_REQUEST['type'] ?? null;
493
494
        // Questions choices.
495
        $choice = $_REQUEST['choice'] ?? [];
496
497
        // certainty degree choice
498
        $choiceDegreeCertainty = $_REQUEST['choiceDegreeCertainty'] ?? [];
499
500
        // Hot spot coordinates from all questions.
501
        $hot_spot_coordinates = $_REQUEST['hotspot'] ?? [];
502
503
        // the filenames in upload answer type
504
        $uploadAnswerFileNames = $_REQUEST['uploadChoice'] ?? [];
505
506
        // There is a reminder?
507
        $remind_list = !empty($_REQUEST['remind_list']) ? array_keys($_REQUEST['remind_list']) : [];
508
509
        // Needed in manage_answer.
510
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
511
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
512
513
        if ($debug) {
514
            error_log("exe_id = $exeId");
515
            error_log("type = $type");
516
            error_log("choice = ".print_r($choice, 1)." ");
517
            error_log("hot_spot_coordinates = ".print_r($hot_spot_coordinates, 1));
518
            error_log("remind_list = ".print_r($remind_list, 1));
519
            error_log("uploadAnswerFileNames = ".print_r($uploadAnswerFileNames, 1));
520
            error_log("--------------------------------");
521
        }
522
523
        /** @var Exercise $objExercise */
524
        $objExercise = Session::read('objExercise');
525
526
        // Question info.
527
        $question_id = isset($_REQUEST['question_id']) ? (int) $_REQUEST['question_id'] : null;
528
        $question_list = Session::read('questionList');
529
530
        // If exercise or question is not set then exit.
531
        if (empty($question_list) || empty($objExercise)) {
532
            echo json_encode(['error' => true]);
533
            if ($debug) {
534
                if (empty($question_list)) {
535
                    error_log("question_list is empty");
536
                }
537
                if (empty($objExercise)) {
538
                    error_log("objExercise is empty");
539
                }
540
            }
541
            exit;
542
        }
543
544
        if (WhispeakAuthPlugin::questionRequireAuthentify($question_id)) {
545
            if ($objExercise->type == ONE_PER_PAGE) {
546
                echo json_encode(['type' => 'one_per_page']);
547
                exit;
548
            }
549
550
            echo json_encode(['ok' => true]);
551
            exit;
552
        } else {
553
            ChamiloSession::erase(WhispeakAuthPlugin::SESSION_QUIZ_QUESTION);
554
        }
555
556
        // Getting information of the current exercise.
557
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
558
        $exercise_id = $exercise_stat_info['exe_exo_id'];
559
        $attemptList = [];
560
        // First time here we create an attempt (getting the exe_id).
561
        if (!empty($exercise_stat_info)) {
562
            // We know the user we get the exe_id.
563
            $exeId = $exercise_stat_info['exe_id'];
564
            $total_score = $exercise_stat_info['exe_result'];
565
            // Getting the list of attempts
566
            $attemptList = Event::getAllExerciseEventByExeId($exeId);
567
        }
568
569
        // No exe id? Can't save answer.
570
        if (empty($exeId)) {
571
            // Fires an error.
572
            echo json_encode(['error' => true]);
573
            if ($debug) {
574
                error_log('exe_id is empty');
575
            }
576
            exit;
577
        }
578
579
        Session::write('exe_id', $exeId);
580
581
        // Updating Reminder algorithm.
582
        if ($objExercise->type == ONE_PER_PAGE) {
583
            $bd_reminder_list = explode(',', $exercise_stat_info['questions_to_check']);
584
            if (empty($remind_list)) {
585
                $remind_list = $bd_reminder_list;
586
                $new_list = [];
587
                foreach ($bd_reminder_list as $item) {
588
                    if ($item != $question_id) {
589
                        $new_list[] = $item;
590
                    }
591
                }
592
                $remind_list = $new_list;
593
            } else {
594
                if (isset($remind_list[0])) {
595
                    if (!in_array($remind_list[0], $bd_reminder_list)) {
596
                        array_push($bd_reminder_list, $remind_list[0]);
597
                    }
598
                    $remind_list = $bd_reminder_list;
599
                }
600
            }
601
        }
602
603
        // Getting the total weight if the request is simple.
604
        $total_weight = 0;
605
        if ($type === 'simple') {
606
            foreach ($question_list as $my_question_id) {
607
                $objQuestionTmp = Question::read($my_question_id, $objExercise->course);
608
                $total_weight += $objQuestionTmp->selectWeighting();
609
            }
610
        }
611
        unset($objQuestionTmp);
612
613
        if ($debug) {
614
            error_log('Starting questions loop in save_exercise_by_now');
615
        }
616
617
        $now = time();
618
        if ('all' === $type) {
619
            // Check we have at least one non-empty answer in the array
620
            // provided by the user's click on the "Finish test" button.
621
            $atLeastOneAnswer = false;
622
            foreach ($question_list as $my_question_id) {
623
                if (!empty($choice[$my_question_id])) {
624
                    $atLeastOneAnswer = true;
625
                    break;
626
                }
627
            }
628
629
            if (!$atLeastOneAnswer) {
630
                // Check if time is over.
631
                if ($objExercise->expired_time != 0) {
632
                    $clockExpiredTime = ExerciseLib::get_session_time_control_key(
633
                        $objExercise->iid,
634
                        $learnpath_id,
635
                        $learnpath_item_id
636
                    );
637
                    if (!empty($clockExpiredTime)) {
638
                        $timeLeft = api_strtotime($clockExpiredTime, 'UTC') - $now;
639
                        if ($timeLeft <= 0) {
640
                            // There's no time, but still no answers ...
641
                            echo json_encode(['ok' => true, 'savedAnswerMessage' => '']);
642
                            exit;
643
                        }
644
                    }
645
                }
646
647
                error_log(
648
                    'In '.__FILE__.'::action save_exercise_by_now,'.
649
                    ' from user '.api_get_user_id().
650
                    ' for track_e_exercises.exe_id = '.$exeId.
651
                    ', we received an empty set of answers.'.
652
                    'Preventing submission to avoid overwriting w/ null.'
653
                );
654
                echo json_encode(['error' => true]);
655
                exit;
656
            }
657
        }
658
659
        // Looping the question list from database (not from the user answer)
660
        foreach ($question_list as $my_question_id) {
661
            if ($type === 'simple' && $question_id != $my_question_id) {
662
                continue;
663
            }
664
            $my_choice = $choice[$my_question_id] ?? null;
665
            $objQuestionTmp = Question::read($my_question_id, $objExercise->course);
666
            $myChoiceDegreeCertainty = null;
667
            if ($objQuestionTmp->type === MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
668
                if (isset($choiceDegreeCertainty[$my_question_id])) {
669
                    $myChoiceDegreeCertainty = $choiceDegreeCertainty[$my_question_id];
670
                }
671
            }
672
            if ($objQuestionTmp->type === UPLOAD_ANSWER) {
673
                $my_choice = '';
674
                if (!empty($uploadAnswerFileNames)) {
675
                    // Clean user upload_answer folder
676
                    $userUploadAnswerSyspath = UserManager::getUserPathById(api_get_user_id(), 'system').'my_files'.'/upload_answer/'.$exeId.'/'.$my_question_id.'/*';
677
                    foreach (glob($userUploadAnswerSyspath) as $file) {
678
                        $filename = basename($file);
679
                        if (!in_array($filename, $uploadAnswerFileNames[$my_question_id])) {
680
                            unlink($file);
681
                        }
682
                    }
683
                    $my_choice = implode('|', $uploadAnswerFileNames[$my_question_id]);
684
                }
685
            }
686
            // Getting free choice data.
687
            if ('all' === $type && in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION])) {
688
                $my_choice = isset($_REQUEST['free_choice'][$my_question_id]) && !empty($_REQUEST['free_choice'][$my_question_id])
689
                    ? $_REQUEST['free_choice'][$my_question_id]
690
                    : null;
691
            }
692
693
            if ($type === 'all') {
694
                // If saving the whole exercise (not only one question),
695
                // record the sum of individual max scores (called
696
                // "exe_weighting" in track_e_exercises)
697
                $total_weight += $objQuestionTmp->selectWeighting();
698
            }
699
700
            // This variable came from exercise_submit_modal.php.
701
            $hotspot_delineation_result = null;
702
            if (isset($_SESSION['hotspot_delineation_result'][$objExercise->selectId()])
703
            ) {
704
                $hotspot_delineation_result = $_SESSION['hotspot_delineation_result'][$objExercise->selectId()][$my_question_id];
705
            }
706
707
            if ('simple' === $type) {
708
                // Getting old attempt in order to decrease the total score.
709
                $old_result = $objExercise->manage_answer(
710
                    $exeId,
711
                    $my_question_id,
712
                    null,
713
                    'exercise_show',
714
                    [],
715
                    false,
716
                    true,
717
                    false,
718
                    $objExercise->selectPropagateNeg()
719
                );
720
                // Removing old score.
721
                $total_score = $total_score - $old_result['score'];
722
            }
723
724
            $questionDuration = 0;
725
            if (api_get_configuration_value('allow_time_per_question')) {
726
                $extraFieldValue = new ExtraFieldValue('question');
727
                $value = $extraFieldValue->get_values_by_handler_and_field_variable($objQuestionTmp->iid, 'time');
728
                if (!empty($value) && isset($value['value']) && !empty($value['value'])) {
729
                    $questionDuration = Event::getAttemptQuestionDuration($exeId, $objQuestionTmp->iid);
730
                    if (empty($questionDuration)) {
731
                        echo json_encode(['error' => true]);
732
                        if ($debug) {
733
                            error_log("Question duration = 0, in exeId: $exeId, question_id: $my_question_id");
734
                        }
735
                        exit;
736
                    }
737
                }
738
            }
739
740
            // Deleting old attempt.
741
            if (isset($attemptList) && !empty($attemptList[$my_question_id])) {
742
                if ($debug) {
743
                    error_log("delete_attempt exe_id : $exeId, my_question_id: $my_question_id");
744
                }
745
                Event::delete_attempt(
746
                    $exeId,
747
                    api_get_user_id(),
748
                    $course_id,
749
                    $session_id,
750
                    $my_question_id
751
                );
752
                if (in_array($objQuestionTmp->type, [HOT_SPOT, HOT_SPOT_COMBINATION])) {
753
                    Event::delete_attempt_hotspot(
754
                        $exeId,
755
                        api_get_user_id(),
756
                        $course_id,
757
                        $session_id,
758
                        $my_question_id
759
                    );
760
                }
761
762
                if (isset($attemptList[$my_question_id]) &&
763
                    isset($attemptList[$my_question_id]['marks'])
764
                ) {
765
                    $total_score -= $attemptList[$my_question_id]['marks'];
766
                }
767
            }
768
769
            // We're inside *one* question. Go through each possible answer for this question
770
            if ($objQuestionTmp->type === MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
771
                $myChoiceTmp = [];
772
                $myChoiceTmp['choice'] = $my_choice;
773
                $myChoiceTmp['choiceDegreeCertainty'] = $myChoiceDegreeCertainty;
774
                $result = $objExercise->manage_answer(
775
                    $exeId,
776
                    $my_question_id,
777
                    $myChoiceTmp,
778
                    'exercise_result',
779
                    $hot_spot_coordinates,
780
                    true,
781
                    false,
782
                    false,
783
                    $objExercise->selectPropagateNeg(),
784
                    $hotspot_delineation_result,
785
                    true,
786
                    false,
787
                    false,
788
                    $questionDuration
789
                );
790
            } else {
791
                $result = $objExercise->manage_answer(
792
                    $exeId,
793
                    $my_question_id,
794
                    $my_choice,
795
                    'exercise_result',
796
                    $hot_spot_coordinates,
797
                    true,
798
                    false,
799
                    false,
800
                    $objExercise->selectPropagateNeg(),
801
                    $hotspot_delineation_result,
802
                    true,
803
                    false,
804
                    false,
805
                    $questionDuration
806
                );
807
            }
808
809
            // Adding the new score.
810
            $total_score += $result['score'];
811
812
            if ($debug) {
813
                error_log("total_score: $total_score ");
814
                error_log("total_weight: $total_weight ");
815
            }
816
817
            $duration = 0;
818
            if ($type === 'all') {
819
                $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
820
            }
821
822
            $key = ExerciseLib::get_time_control_key(
823
                $exercise_id,
824
                $exercise_stat_info['orig_lp_id'],
825
                $exercise_stat_info['orig_lp_item_id']
826
            );
827
828
            $durationTime = Session::read('duration_time');
829
            if (isset($durationTime[$key]) && !empty($durationTime[$key])) {
830
                if ($debug) {
831
                    error_log('Session time: '.$durationTime[$key]);
832
                }
833
                $duration = $now - $durationTime[$key];
834
                if (!empty($exercise_stat_info['exe_duration'])) {
835
                    $duration += $exercise_stat_info['exe_duration'];
836
                }
837
                $duration = (int) $duration;
838
            } else {
839
                if (!empty($exercise_stat_info['exe_duration'])) {
840
                    $duration = $exercise_stat_info['exe_duration'];
841
                }
842
            }
843
844
            if ($debug) {
845
                error_log('duration to save in DB:'.$duration);
846
            }
847
            Session::write('duration_time', [$key => $now]);
848
            Event::updateEventExercise(
849
                $exeId,
850
                $objExercise->selectId(),
851
                $total_score,
852
                $total_weight,
853
                $session_id,
854
                $exercise_stat_info['orig_lp_id'],
855
                $exercise_stat_info['orig_lp_item_id'],
856
                $exercise_stat_info['orig_lp_item_view_id'],
857
                $duration,
858
                $question_list,
859
                'incomplete',
860
                $remind_list
861
            );
862
863
            if (api_get_configuration_value('allow_time_per_question')) {
864
                $questionStart = Session::read('question_start', []);
865
                if (!empty($questionStart)) {
866
                    if (isset($questionStart[$my_question_id])) {
867
                        unset($questionStart[$my_question_id]);
868
                    }
869
                    array_filter($questionStart);
870
                    Session::write('question_start', $questionStart);
871
                }
872
            }
873
874
            HookQuizQuestionAnswered::create()
875
                ->setEventData(
876
                    [
877
                        'exe_id' => (int) $exeId,
878
                        'quiz' => [
879
                            'id' => (int) $objExercise->iid,
880
                            'title' => $objExercise->selectTitle(true),
881
                        ],
882
                        'question' => [
883
                            'id' => (int) $my_question_id,
884
                            'weight' => (float) $result['weight'],
885
                        ],
886
                    ]
887
                )
888
                ->notifyQuizQuestionAnswered();
889
890
            // Destruction of the Question object
891
            unset($objQuestionTmp);
892
            if ($debug) {
893
                error_log("---------- end question ------------");
894
            }
895
        }
896
        if ($debug) {
897
            error_log('Finished questions loop in save_exercise_by_now');
898
        }
899
900
        $questionsCount = count(explode(',', $exercise_stat_info['data_tracking']));
901
        $savedAnswersCount = $objExercise->countUserAnswersSavedInExercise($exeId);
902
903
        if ($savedAnswersCount !== $questionsCount) {
904
            $savedQuestionsMessage = Display::span(
905
                sprintf(get_lang('XAnswersSavedByUsersFromXTotal'), $savedAnswersCount, $questionsCount),
906
                ['class' => 'text-warning']
907
            );
908
        } else {
909
            $savedQuestionsMessage = Display::span(
910
                sprintf(get_lang('XAnswersSavedByUsersFromXTotal'), $savedAnswersCount, $questionsCount),
911
                ['class' => 'text-success']
912
            );
913
        }
914
915
        if ($type === 'all') {
916
            if ($debug) {
917
                error_log("result: ok - all");
918
                error_log(" ------ end ajax call ------- ");
919
            }
920
            echo json_encode(['ok' => true, 'savedAnswerMessage' => $savedQuestionsMessage]);
921
            exit;
922
        }
923
924
        if ($objExercise->type == ONE_PER_PAGE) {
925
            if ($debug) {
926
                error_log("result: one_per_page");
927
                error_log(" ------ end ajax call ------- ");
928
            }
929
            echo json_encode(['type' => 'one_per_page', 'savedAnswerMessage' => $savedQuestionsMessage]);
930
            exit;
931
        }
932
        if ($debug) {
933
            error_log("result: ok");
934
            error_log(" ------ end ajax call ------- ");
935
        }
936
        echo json_encode(['ok' => true, 'savedAnswerMessage' => $savedQuestionsMessage]);
937
        break;
938
    case 'show_question_attempt':
939
        $isAllowedToEdit = api_is_allowed_to_edit(null, true, false, false);
940
941
        if (!$isAllowedToEdit) {
942
            api_not_allowed(true);
943
            exit;
944
        }
945
946
        $questionId = isset($_GET['question']) ? (int) $_GET['question'] : 0;
947
        $exerciseId = isset($_REQUEST['exercise']) ? (int) $_REQUEST['exercise'] : 0;
948
949
        if (!$questionId || !$exerciseId) {
950
            break;
951
        }
952
953
        $objExercise = new Exercise();
954
        $objExercise->read($exerciseId);
955
        $objQuestion = Question::read($questionId);
956
        $id = '';
957
        if (api_get_configuration_value('show_question_id')) {
958
            $id = '<h4>#'.$objQuestion->course['code'].'-'.$objQuestion->iid.'</h4>';
959
        }
960
        echo $id;
961
        echo '<p class="lead">'.$objQuestion->get_question_type_name().'</p>';
962
        if (in_array($objQuestion->type, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
963
            echo '<script>
964
                $(function() {
965
                    $(".selectpicker").selectpicker({});
966
                });
967
            </script>';
968
        }
969
970
        // Allows render MathJax elements in a ajax call
971
        if (api_get_setting('include_asciimathml_script') === 'true') {
972
            echo '<script> MathJax.Hub.Queue(["Typeset",MathJax.Hub]);</script>';
973
        }
974
975
        if (in_array($objQuestion->type, [HOT_SPOT, HOT_SPOT_COMBINATION])) {
976
            echo '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
977
        }
978
979
        $attemptList = [];
980
        if (!empty($exeId)) {
981
            $attemptList = Event::getAllExerciseEventByExeId($exeId);
982
        }
983
984
        $userChoice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
985
986
        ExerciseLib::showQuestion(
987
            $objExercise,
988
            $questionId,
989
            false,
990
            null,
991
            null,
992
            false,
993
            true,
994
            $userChoice,
995
            true
996
        );
997
        break;
998
    case 'show_question':
999
        $isAllowedToEdit = api_is_allowed_to_edit(null, true, false, false);
1000
1001
        if (!$isAllowedToEdit) {
1002
            api_not_allowed(true);
1003
            exit;
1004
        }
1005
1006
        $questionId = isset($_GET['question']) ? (int) $_GET['question'] : 0;
1007
        $exerciseId = isset($_REQUEST['exercise']) ? (int) $_REQUEST['exercise'] : 0;
1008
1009
        if (!$questionId || !$exerciseId) {
1010
            break;
1011
        }
1012
1013
        $objExercise = new Exercise();
1014
        $objExercise->read($exerciseId);
1015
        $objQuestion = Question::read($questionId);
1016
        $id = '';
1017
        if (api_get_configuration_value('show_question_id')) {
1018
            $id = '<h4>#'.$objQuestion->course['code'].'-'.$objQuestion->iid.'</h4>';
1019
        }
1020
        echo $id;
1021
        echo '<p class="lead">'.$objQuestion->get_question_type_name().'</p>';
1022
        if (in_array($objQuestion->type, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
1023
            echo '<script>
1024
                $(function() {
1025
                    $(".selectpicker").selectpicker({});
1026
                });
1027
            </script>';
1028
        }
1029
1030
        // Allows render MathJax elements in a ajax call
1031
        if (api_get_setting('include_asciimathml_script') === 'true') {
1032
            echo '<script> MathJax.Hub.Queue(["Typeset",MathJax.Hub]);</script>';
1033
        }
1034
1035
        ExerciseLib::showQuestion(
1036
            $objExercise,
1037
            $questionId,
1038
            false,
1039
            null,
1040
            null,
1041
            false,
1042
            true,
1043
            false,
1044
            true,
1045
            true
1046
        );
1047
        break;
1048
    case 'get_quiz_embeddable':
1049
        $exercises = ExerciseLib::get_all_exercises_for_course_id(
1050
            api_get_course_info(),
1051
            api_get_session_id(),
1052
            api_get_course_int_id(),
1053
            false
1054
        );
1055
1056
        $exercises = array_filter(
1057
            $exercises,
1058
            function (array $exercise) {
1059
                return ExerciseLib::isQuizEmbeddable($exercise);
1060
            }
1061
        );
1062
1063
        $result = [];
1064
1065
        $codePath = api_get_path(WEB_CODE_PATH);
1066
1067
        foreach ($exercises as $exercise) {
1068
            $title = Security::remove_XSS(api_html_entity_decode($exercise['title']));
1069
1070
            $result[] = [
1071
                'id' => $exercise['iid'],
1072
                'title' => strip_tags($title),
1073
            ];
1074
        }
1075
1076
        header('Content-Type: application/json');
1077
        echo json_encode($result);
1078
        break;
1079
    case 'browser_test':
1080
        $quizCheckButtonEnabled = api_get_configuration_value('quiz_check_button_enable');
1081
1082
        if ($quizCheckButtonEnabled) {
1083
            if (isset($_POST['sleep'])) {
1084
                sleep(2);
1085
            }
1086
1087
            echo 'ok';
1088
        }
1089
1090
        break;
1091
    case 'quiz_confirm_saved_answers':
1092
        if (false === api_get_configuration_value('quiz_confirm_saved_answers')) {
1093
            break;
1094
        }
1095
1096
        $trackConfirmationId = isset($_POST['tc_id']) ? (int) $_POST['tc_id'] : 0;
1097
        $cId = api_get_course_int_id();
1098
        $sessionId = api_get_session_id();
1099
        $userId = api_get_user_id();
1100
        $confirmed = !empty($_POST['quiz_confirm_saved_answers_check']);
1101
1102
        $em = Database::getManager();
1103
        $repo = $em->getRepository('ChamiloCoreBundle:TrackEExerciseConfirmation');
1104
1105
        try {
1106
            if (!$trackConfirmationId) {
1107
                throw new Exception(get_lang('ErrorOccurred'));
1108
            }
1109
1110
            /** @var TrackEExerciseConfirmation $trackConfirmation */
1111
            $trackConfirmation = $repo->findOneBy(
1112
                [
1113
                    'id' => $trackConfirmationId,
1114
                    'userId' => $userId,
1115
                    'courseId' => $cId,
1116
                    'sessionId' => $sessionId,
1117
                ],
1118
                ['createdAt' => 'DESC']
1119
            );
1120
1121
            if (!$trackConfirmation) {
1122
                throw new Exception(get_lang('NotFound'));
1123
            }
1124
1125
            $trackConfirmation
1126
                ->setConfirmed($confirmed)
1127
                ->setUpdatedAt(api_get_utc_datetime(null, false, true));
1128
1129
            $em->persist($trackConfirmation);
1130
            $em->flush();
1131
1132
            http_response_code(200);
1133
        } catch (Exception $exception) {
1134
            http_response_code(500);
1135
1136
            echo Display::return_message($exception->getMessage(), 'error');
1137
        }
1138
1139
        break;
1140
    case 'sign_attempt':
1141
        api_block_anonymous_users();
1142
        if ('true' !== api_get_plugin_setting('exercise_signature', 'tool_enable')) {
1143
            exit;
1144
        }
1145
1146
        $file = isset($_REQUEST['file']) ? $_REQUEST['file'] : '';
1147
        if (empty($exeId) || empty($file)) {
1148
            echo 0;
1149
            exit;
1150
        }
1151
1152
        $file = str_replace(' ', '+', $file);
1153
        $track = ExerciseLib::get_exercise_track_exercise_info($exeId);
1154
        if ($track) {
1155
            $result = ExerciseSignaturePlugin::saveSignature($currentUserId, $track, $file);
1156
            if ($result) {
1157
                echo 1;
1158
                exit;
1159
            }
1160
        }
1161
        echo 0;
1162
        break;
1163
    case 'upload_answer':
1164
        api_block_anonymous_users();
1165
1166
        if (isset($_REQUEST['chunkAction']) && 'send' === $_REQUEST['chunkAction']) {
1167
            // It uploads the files in chunks
1168
            if (!empty($_FILES)) {
1169
                $tempDirectory = api_get_path(SYS_ARCHIVE_PATH);
1170
                $files = $_FILES['files'];
1171
                $fileList = [];
1172
                foreach ($files as $name => $array) {
1173
                    $counter = 0;
1174
                    foreach ($array as $data) {
1175
                        $fileList[$counter][$name] = $data;
1176
                        $counter++;
1177
                    }
1178
                }
1179
                if (!empty($fileList)) {
1180
                    foreach ($fileList as $n => $file) {
1181
                        $tmpFile = disable_dangerous_file(
1182
                            api_replace_dangerous_char($file['name'])
1183
                        );
1184
1185
                        file_put_contents(
1186
                            $tempDirectory.$tmpFile,
1187
                            fopen($file['tmp_name'], 'r'),
1188
                            FILE_APPEND
1189
                        );
1190
                    }
1191
                }
1192
            }
1193
            echo json_encode([
1194
                'files' => $_FILES,
1195
                'errorStatus' => 0,
1196
            ]);
1197
            exit;
1198
        } else {
1199
            if (!empty($_FILES)) {
1200
                $currentDirectory = Security::remove_XSS($_REQUEST['curdirpath']);
1201
                $userId = api_get_user_id();
1202
1203
                // Upload answer path is created inside user personal folder my_files/upload_answer/[exe_id]/[question_id]
1204
                $syspath = UserManager::getUserPathById($userId, 'system').'my_files'.$currentDirectory;
1205
                @mkdir($syspath, api_get_permissions_for_new_directories(), true);
1206
                $webpath = UserManager::getUserPathById($userId, 'web').'my_files'.$currentDirectory;
1207
1208
                $files = $_FILES['files'];
1209
                $fileList = [];
1210
                foreach ($files as $name => $array) {
1211
                    $counter = 0;
1212
                    foreach ($array as $data) {
1213
                        $fileList[$counter][$name] = $data;
1214
                        $counter++;
1215
                    }
1216
                }
1217
                $resultList = [];
1218
                foreach ($fileList as $file) {
1219
                    $json = [];
1220
1221
                    if (isset($_REQUEST['chunkAction']) && 'done' === $_REQUEST['chunkAction']) {
1222
                        // to rename and move the finished file
1223
                        $chunkedFile = api_get_path(SYS_ARCHIVE_PATH).$file['name'];
1224
                        $file['tmp_name'] = $chunkedFile;
1225
                        $file['size'] = filesize($chunkedFile);
1226
                        $file['copy_file'] = true;
1227
                    }
1228
1229
                    $filename = api_replace_dangerous_char($file['name']);
1230
                    $filename = disable_dangerous_file($filename);
1231
1232
                    if (isset($file['copy_file']) && $file['copy_file']) {
1233
                        $uploaded = copy($file['tmp_name'], $syspath.$filename);
1234
                        @unlink($file['tmp_name']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1234
                        /** @scrutinizer ignore-unhandled */ @unlink($file['tmp_name']);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1235
                    } else {
1236
                        $uploaded = move_uploaded_file($file['tmp_name'], $syspath.$filename);
1237
                    }
1238
1239
                    if ($uploaded) {
1240
                        $title = $filename;
1241
                        $url = $webpath.$filename;
1242
                        $json['name'] = api_htmlentities($title);
1243
                        $json['link'] = Display::url(
1244
                            api_htmlentities($title),
1245
                            api_htmlentities($url),
1246
                            ['target' => '_blank']
1247
                        );
1248
                        $json['url'] = $url;
1249
                        $json['size'] = format_file_size($file['size']);
1250
                        $json['type'] = api_htmlentities($file['type']);
1251
                        $json['result'] = Display::return_icon(
1252
                            'accept.png',
1253
                            get_lang('Uploaded')
1254
                        );
1255
                    } else {
1256
                        $json['name'] = isset($file['name']) ? $filename : get_lang('Unknown');
1257
                        $json['url'] = '';
1258
                        $json['error'] = get_lang('Error');
1259
                    }
1260
                    $resultList[] = $json;
1261
                }
1262
                echo json_encode(['files' => $resultList]);
1263
                exit;
1264
            }
1265
        }
1266
        break;
1267
    default:
1268
        echo '';
1269
}
1270
exit;
1271