Completed
Push — master ( 27e209...a08afa )
by Julito
186:04 queued 150:53
created

Question::search_engine_edit()   F

Complexity

Conditions 26
Paths 1123

Size

Total Lines 133
Code Lines 76

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 76
nc 1123
nop 3
dl 0
loc 133
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CourseBundle\Entity\CQuizAnswer;
5
6
/**
7
 * Class Question
8
 *
9
 * This class allows to instantiate an object of type Question
10
 *
11
 * @author Olivier Brouckaert, original author
12
 * @author Patrick Cool, LaTeX support
13
 * @author Julio Montoya <[email protected]> lot of bug fixes
14
 * @author [email protected] - add question categories
15
 * @package chamilo.exercise
16
 */
17
abstract class Question
18
{
19
    public $id;
20
    public $question;
21
    public $description;
22
    public $weighting;
23
    public $position;
24
    public $type;
25
    public $level;
26
    public $picture;
27
    public $exerciseList; // array with the list of exercises which this question is in
28
    public $category_list;
29
    public $parent_id;
30
    public $category;
31
    public $isContent;
32
    public $course;
33
    public $feedback;
34
    public static $typePicture = 'new_question.png';
35
    public static $explanationLangVar = '';
36
    public $question_table_class = 'table table-striped';
37
    public $questionTypeWithFeedback;
38
    public $extra;
39
    public $export = false;
40
    public static $questionTypes = [
41
        UNIQUE_ANSWER => ['unique_answer.class.php', 'UniqueAnswer'],
42
        MULTIPLE_ANSWER => ['multiple_answer.class.php', 'MultipleAnswer'],
43
        FILL_IN_BLANKS => ['fill_blanks.class.php', 'FillBlanks'],
44
        MATCHING => ['matching.class.php', 'Matching'],
45
        FREE_ANSWER => ['freeanswer.class.php', 'FreeAnswer'],
46
        ORAL_EXPRESSION => ['oral_expression.class.php', 'OralExpression'],
47
        HOT_SPOT => ['hotspot.class.php', 'HotSpot'],
48
        HOT_SPOT_DELINEATION => ['hotspot.class.php', 'HotspotDelineation'],
49
        MULTIPLE_ANSWER_COMBINATION => ['multiple_answer_combination.class.php', 'MultipleAnswerCombination'],
50
        UNIQUE_ANSWER_NO_OPTION => ['unique_answer_no_option.class.php', 'UniqueAnswerNoOption'],
51
        MULTIPLE_ANSWER_TRUE_FALSE => ['multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'],
52
        MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => [
53
            'multiple_answer_combination_true_false.class.php',
54
            'MultipleAnswerCombinationTrueFalse'
55
        ],
56
        GLOBAL_MULTIPLE_ANSWER => ['global_multiple_answer.class.php', 'GlobalMultipleAnswer'],
57
        CALCULATED_ANSWER => ['calculated_answer.class.php', 'CalculatedAnswer'],
58
        UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
59
        DRAGGABLE => ['Draggable.php', 'Draggable'],
60
        MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'],
61
        //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion')
62
        ANNOTATION => ['Annotation.php', 'Annotation'],
63
        READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension']
64
    ];
65
66
    /**
67
     * constructor of the class
68
     *
69
     * @author Olivier Brouckaert
70
     */
71
    public function __construct()
72
    {
73
        $this->id = 0;
74
        $this->question = '';
75
        $this->description = '';
76
        $this->weighting = 0;
77
        $this->position = 1;
78
        $this->picture = '';
79
        $this->level = 1;
80
        $this->category = 0;
81
        // This variable is used when loading an exercise like an scenario with
82
        // an special hotspot: final_overlap, final_missing, final_excess
83
        $this->extra = '';
84
        $this->exerciseList = [];
85
        $this->course = api_get_course_info();
86
        $this->category_list = [];
87
        $this->parent_id = 0;
88
        // See BT#12611
89
        $this->questionTypeWithFeedback = [
90
            MATCHING,
91
            MATCHING_DRAGGABLE,
92
            DRAGGABLE,
93
            FILL_IN_BLANKS,
94
            FREE_ANSWER,
95
            ORAL_EXPRESSION,
96
            CALCULATED_ANSWER,
97
            ANNOTATION
98
        ];
99
    }
100
101
    /**
102
     * @return int|null
103
     */
104
    public function getIsContent()
105
    {
106
        $isContent = null;
107
        if (isset($_REQUEST['isContent'])) {
108
            $isContent = intval($_REQUEST['isContent']);
109
        }
110
111
        return $this->isContent = $isContent;
112
    }
113
114
    /**
115
     * Reads question information from the data base
116
     *
117
     * @param int $id - question ID
118
     * @param int $course_id
119
     *
120
     * @return Question
121
     *
122
     * @author Olivier Brouckaert
123
     */
124
    public static function read($id, $course_id = null)
125
    {
126
        $id = intval($id);
127
        if (!empty($course_id)) {
128
            $course_info = api_get_course_info_by_id($course_id);
129
        } else {
130
            $course_info = api_get_course_info();
131
        }
132
133
        $course_id = $course_info['real_id'];
134
135
        if (empty($course_id) || $course_id == -1) {
136
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type Question.
Loading history...
137
        }
138
139
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
140
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
141
142
        $sql = "SELECT *
143
                FROM $TBL_QUESTIONS
144
                WHERE c_id = $course_id AND id = $id ";
145
        $result = Database::query($sql);
146
147
        // if the question has been found
148
        if ($object = Database::fetch_object($result)) {
149
            $objQuestion = self::getInstance($object->type);
150
            if (!empty($objQuestion)) {
151
                $objQuestion->id = (int) $id;
152
                $objQuestion->question = $object->question;
153
                $objQuestion->description = $object->description;
154
                $objQuestion->weighting = $object->ponderation;
155
                $objQuestion->position = $object->position;
156
                $objQuestion->type = (int) $object->type;
157
                $objQuestion->picture = $object->picture;
158
                $objQuestion->level = (int) $object->level;
159
                $objQuestion->extra = $object->extra;
160
                $objQuestion->course = $course_info;
161
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
162
                $objQuestion->category = TestCategory::getCategoryForQuestion($id, $course_id);
163
164
                $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
165
                $sql = "SELECT DISTINCT q.exercice_id
166
                        FROM $TBL_EXERCISE_QUESTION q
167
                        INNER JOIN $tblQuiz e
168
                        ON e.c_id = q.c_id AND e.id = q.exercice_id
169
                        WHERE
170
                            q.c_id = $course_id AND
171
                            q.question_id = $id AND
172
                            e.active >= 0";
173
174
                $result = Database::query($sql);
175
176
                // fills the array with the exercises which this question is in
177
                if ($result) {
178
                    while ($obj = Database::fetch_object($result)) {
179
                        $objQuestion->exerciseList[] = $obj->exercice_id;
180
                    }
181
                }
182
183
                return $objQuestion;
184
            }
185
        }
186
187
        // question not found
188
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type Question.
Loading history...
189
    }
190
191
    /**
192
     * returns the question ID
193
     *
194
     * @author Olivier Brouckaert
195
     *
196
     * @return integer - question ID
197
     */
198
    public function selectId()
199
    {
200
        return $this->id;
201
    }
202
203
    /**
204
     * returns the question title
205
     *
206
     * @author Olivier Brouckaert
207
     * @return string - question title
208
     */
209
    public function selectTitle()
210
    {
211
        if (!api_get_configuration_value('save_titles_as_html')) {
212
            return $this->question;
213
        }
214
215
        return Display::div($this->question, ['style' => 'display: inline-block;']);
216
    }
217
218
    /**
219
     * @param int $itemNumber
220
     * @return string
221
     */
222
    public function getTitleToDisplay($itemNumber)
223
    {
224
        $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
225
226
        $title = $showQuestionTitleHtml ? '' : '<strong>';
227
        $title .= $itemNumber.'. '.$this->selectTitle();
228
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
229
230
        return Display::div(
231
            $title,
232
            ['class' => 'question_title']
233
        );
234
    }
235
236
    /**
237
     * returns the question description
238
     *
239
     * @author Olivier Brouckaert
240
     * @return string - question description
241
     */
242
    public function selectDescription()
243
    {
244
        return $this->description;
245
    }
246
247
    /**
248
     * returns the question weighting
249
     *
250
     * @author Olivier Brouckaert
251
     * @return integer - question weighting
252
     */
253
    public function selectWeighting()
254
    {
255
        return $this->weighting;
256
    }
257
258
    /**
259
     * returns the question position
260
     *
261
     * @author Olivier Brouckaert
262
     * @return integer - question position
263
     */
264
    public function selectPosition()
265
    {
266
        return $this->position;
267
    }
268
269
    /**
270
     * returns the answer type
271
     *
272
     * @author Olivier Brouckaert
273
     * @return integer - answer type
274
     */
275
    public function selectType()
276
    {
277
        return $this->type;
278
    }
279
280
    /**
281
     * returns the level of the question
282
     *
283
     * @author Nicolas Raynaud
284
     * @return integer - level of the question, 0 by default.
285
     */
286
    public function getLevel()
287
    {
288
        return $this->level;
289
    }
290
291
    /**
292
     * returns the picture name
293
     *
294
     * @author Olivier Brouckaert
295
     * @return string - picture name
296
     */
297
    public function selectPicture()
298
    {
299
        return $this->picture;
300
    }
301
302
    /**
303
     * @return string
304
     */
305
    public function selectPicturePath()
306
    {
307
        if (!empty($this->picture)) {
308
            return api_get_path(WEB_COURSE_PATH).$this->course['directory'].'/document/images/'.$this->getPictureFilename();
309
        }
310
311
        return '';
312
    }
313
314
    /**
315
     * @return int|string
316
     */
317
    public function getPictureId()
318
    {
319
        // for backward compatibility
320
        // when in field picture we had the filename not the document id
321
        if (preg_match("/quiz-.*/", $this->picture)) {
322
            return DocumentManager::get_document_id(
323
                $this->course,
324
                $this->selectPicturePath(),
325
                api_get_session_id()
326
            );
327
        }
328
329
        return $this->picture;
330
    }
331
332
    /**
333
     * @param int $courseId
334
     * @param int $sessionId
335
     * @return string
336
     */
337
    public function getPictureFilename($courseId = 0, $sessionId = 0)
338
    {
339
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
340
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
341
342
        if (empty($courseId)) {
343
            return '';
344
        }
345
        // for backward compatibility
346
        // when in field picture we had the filename not the document id
347
        if (preg_match("/quiz-.*/", $this->picture)) {
348
            return $this->picture;
349
        }
350
351
        $pictureId = $this->getPictureId();
352
        $courseInfo = $this->course;
353
        $documentInfo = DocumentManager::get_document_data_by_id(
354
            $pictureId,
0 ignored issues
show
Bug introduced by
It seems like $pictureId can also be of type string; however, parameter $id of DocumentManager::get_document_data_by_id() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

354
            /** @scrutinizer ignore-type */ $pictureId,
Loading history...
355
            $courseInfo['code'],
356
            false,
357
            $sessionId
358
        );
359
        $documentFilename = '';
360
        if ($documentInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $documentInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
361
            // document in document/images folder
362
            $documentFilename = pathinfo(
363
                $documentInfo['path'],
364
                PATHINFO_BASENAME
365
            );
366
        }
367
368
        return $documentFilename;
369
    }
370
371
    /**
372
     * returns the array with the exercise ID list
373
     *
374
     * @author Olivier Brouckaert
375
     * @return array - list of exercise ID which the question is in
376
     */
377
    public function selectExerciseList()
378
    {
379
        return $this->exerciseList;
380
    }
381
382
    /**
383
     * returns the number of exercises which this question is in
384
     *
385
     * @author Olivier Brouckaert
386
     * @return integer - number of exercises
387
     */
388
    public function selectNbrExercises()
389
    {
390
        return sizeof($this->exerciseList);
0 ignored issues
show
Bug introduced by
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

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

390
        return /** @scrutinizer ignore-call */ sizeof($this->exerciseList);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
391
    }
392
393
    /**
394
     * changes the question title
395
     *
396
     * @param string $title - question title
397
     *
398
     * @author Olivier Brouckaert
399
     */
400
    public function updateTitle($title)
401
    {
402
        $this->question = $title;
403
    }
404
405
    /**
406
     * @param int $id
407
     */
408
    public function updateParentId($id)
409
    {
410
        $this->parent_id = intval($id);
411
    }
412
413
    /**
414
     * changes the question description
415
     *
416
     * @param string $description - question description
417
     *
418
     * @author Olivier Brouckaert
419
     *
420
     */
421
    public function updateDescription($description)
422
    {
423
        $this->description = $description;
424
    }
425
426
    /**
427
     * changes the question weighting
428
     *
429
     * @param integer $weighting - question weighting
430
     *
431
     * @author Olivier Brouckaert
432
     */
433
    public function updateWeighting($weighting)
434
    {
435
        $this->weighting = $weighting;
436
    }
437
438
    /**
439
     * @param array $category
440
     *
441
     * @author Hubert Borderiou 12-10-2011
442
     *
443
     */
444
    public function updateCategory($category)
445
    {
446
        $this->category = $category;
447
    }
448
449
    /**
450
     * @param int $value
451
     *
452
     * @author Hubert Borderiou 12-10-2011
453
     */
454
    public function updateScoreAlwaysPositive($value)
455
    {
456
        $this->scoreAlwaysPositive = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property scoreAlwaysPositive does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
457
    }
458
459
    /**
460
     * @param int $value
461
     *
462
     * @author Hubert Borderiou 12-10-2011
463
     */
464
    public function updateUncheckedMayScore($value)
465
    {
466
        $this->uncheckedMayScore = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property uncheckedMayScore does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
467
    }
468
469
    /**
470
     * Save category of a question
471
     *
472
     * A question can have n categories if category is empty,
473
     * then question has no category then delete the category entry
474
     *
475
     * @param array $category_list
476
     *
477
     * @author Julio Montoya - Adding multiple cat support
478
     */
479
    public function saveCategories($category_list)
480
    {
481
        if (!empty($category_list)) {
482
            $this->deleteCategory();
483
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
484
485
            // update or add category for a question
486
            foreach ($category_list as $category_id) {
487
                $category_id = intval($category_id);
488
                $question_id = intval($this->id);
489
                $sql = "SELECT count(*) AS nb
490
                        FROM $table
491
                        WHERE
492
                            category_id = $category_id
493
                            AND question_id = $question_id
494
                            AND c_id=".api_get_course_int_id();
495
                $res = Database::query($sql);
496
                $row = Database::fetch_array($res);
497
                if ($row['nb'] > 0) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
498
                    // DO nothing
499
                } else {
500
                    $sql = "INSERT INTO $table (c_id, question_id, category_id)
501
                            VALUES (".api_get_course_int_id().", $question_id, $category_id)";
502
                    Database::query($sql);
503
                }
504
            }
505
        }
506
    }
507
508
    /**
509
     * in this version, a question can only have 1 category
510
     * if category is 0, then question has no category then delete the category entry
511
     * @param int $categoryId
512
     * @return bool
513
     *
514
     * @author Hubert Borderiou 12-10-2011
515
     */
516
    public function saveCategory($categoryId)
517
    {
518
        $courseId = api_get_course_int_id();
519
        if (empty($courseId)) {
520
            return false;
521
        }
522
        if ($categoryId <= 0) {
523
            $this->deleteCategory();
524
        } else {
525
            // update or add category for a question
526
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
527
            $categoryId = intval($categoryId);
528
            $question_id = intval($this->id);
529
            $sql = "SELECT count(*) AS nb FROM $table
530
                    WHERE
531
                        question_id = $question_id AND
532
                        c_id = ".$courseId;
533
            $res = Database::query($sql);
534
            $row = Database::fetch_array($res);
535
            if ($row['nb'] > 0) {
536
                $sql = "UPDATE $table
537
                        SET category_id = $categoryId
538
                        WHERE
539
                            question_id = $question_id AND
540
                            c_id = ".$courseId;
541
                Database::query($sql);
542
            } else {
543
                $sql = "INSERT INTO $table (c_id, question_id, category_id)
544
                        VALUES (".$courseId.", $question_id, $categoryId)";
545
                Database::query($sql);
546
            }
547
548
            return true;
549
        }
550
    }
551
552
    /**
553
     * @author hubert borderiou 12-10-2011
554
     * delete any category entry for question id
555
     * delete the category for question
556
     */
557
    public function deleteCategory()
558
    {
559
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
560
        $question_id = intval($this->id);
561
        $sql = "DELETE FROM $table
562
                WHERE
563
                    question_id = $question_id AND
564
                    c_id = ".api_get_course_int_id();
565
        Database::query($sql);
566
    }
567
568
    /**
569
     * changes the question position
570
     *
571
     * @param integer $position - question position
572
     *
573
     * @author Olivier Brouckaert
574
     */
575
    public function updatePosition($position)
576
    {
577
        $this->position = $position;
578
    }
579
580
    /**
581
     * changes the question level
582
     *
583
     * @param integer $level - question level
584
     *
585
     * @author Nicolas Raynaud
586
     */
587
    public function updateLevel($level)
588
    {
589
        $this->level = $level;
590
    }
591
592
    /**
593
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
594
     * (or conversely) answers are not deleted, otherwise yes
595
     *
596
     * @param integer $type - answer type
597
     *
598
     * @author Olivier Brouckaert
599
     */
600
    public function updateType($type)
601
    {
602
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
603
        $course_id = $this->course['real_id'];
604
605
        if (empty($course_id)) {
606
            $course_id = api_get_course_int_id();
607
        }
608
        // if we really change the type
609
        if ($type != $this->type) {
610
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
611
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
612
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
613
            ) {
614
                // removes old answers
615
                $sql = "DELETE FROM $TBL_REPONSES
616
                        WHERE c_id = $course_id AND question_id = ".intval($this->id);
617
                Database::query($sql);
618
            }
619
620
            $this->type = $type;
621
        }
622
    }
623
624
    /**
625
     * Get default hot spot folder in documents
626
     * @return string
627
     */
628
    public function getHotSpotFolderInCourse()
629
    {
630
        if (empty($this->course) || empty($this->course['directory'])) {
631
            // Stop everything if course is not set.
632
            api_not_allowed();
633
        }
634
635
        $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$this->course['directory'].'/document/images/';
636
        $picturePath = basename($pictureAbsolutePath);
637
638
        if (!is_dir($picturePath)) {
639
            create_unexisting_directory(
640
                $this->course,
641
                api_get_user_id(),
642
                0,
643
                0,
644
                0,
645
                dirname($pictureAbsolutePath),
646
                '/'.$picturePath,
647
                $picturePath
648
            );
649
        }
650
651
        return $pictureAbsolutePath;
652
    }
653
654
    /**
655
     * adds a picture to the question
656
     *
657
     * @param string $picture - temporary path of the picture to upload
658
     *
659
     * @return boolean - true if uploaded, otherwise false
660
     *
661
     * @author Olivier Brouckaert
662
     */
663
    public function uploadPicture($picture)
664
    {
665
        $picturePath = $this->getHotSpotFolderInCourse();
666
667
        // if the question has got an ID
668
        if ($this->id) {
669
            $pictureFilename = self::generatePictureName();
0 ignored issues
show
Bug Best Practice introduced by
The method Question::generatePictureName() is not static, but was called statically. ( Ignorable by Annotation )

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

669
            /** @scrutinizer ignore-call */ 
670
            $pictureFilename = self::generatePictureName();
Loading history...
670
            $img = new Image($picture);
671
            $img->send_image($picturePath.'/'.$pictureFilename, -1, 'jpg');
672
            $document_id = add_document(
673
                $this->course,
674
                '/images/'.$pictureFilename,
675
                'file',
676
                filesize($picturePath.'/'.$pictureFilename),
677
                $pictureFilename
678
            );
679
680
            if ($document_id) {
681
                $this->picture = $document_id;
682
683
                if (!file_exists($picturePath.'/'.$pictureFilename)) {
684
                    return false;
685
                }
686
687
                api_item_property_update(
688
                    $this->course,
689
                    TOOL_DOCUMENT,
690
                    $document_id,
691
                    'DocumentAdded',
692
                    api_get_user_id()
693
                );
694
695
                $this->resizePicture('width', 800);
696
697
                return true;
698
            }
699
        }
700
701
        return false;
702
    }
703
704
    /**
705
     * return the name for image use in hotspot question
706
     * to be unique, name is quiz-[utc unix timestamp].jpg
707
     * @param string $prefix
708
     * @param string $extension
709
     * @return string
710
     */
711
    public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
712
    {
713
        // image name is quiz-xxx.jpg in folder images/
714
        $utcTime = time();
715
        return $prefix.$utcTime.'.'.$extension;
716
    }
717
718
    /**
719
     * Resizes a picture || Warning!: can only be called after uploadPicture,
720
     * or if picture is already available in object.
721
     * @param string $Dimension - Resizing happens proportional according to given dimension: height|width|any
722
     * @param integer $Max - Maximum size
723
     *
724
     * @return boolean|null - true if success, false if failed
725
     *
726
     * @author Toon Keppens
727
     */
728
    private function resizePicture($Dimension, $Max)
729
    {
730
        // if the question has an ID
731
        if (!$this->id) {
732
            return false;
733
        }
734
735
        $picturePath = $this->getHotSpotFolderInCourse().'/'.$this->getPictureFilename();
736
737
        // Get dimensions from current image.
738
        $my_image = new Image($picturePath);
739
740
        $current_image_size = $my_image->get_image_size();
741
        $current_width = $current_image_size['width'];
742
        $current_height = $current_image_size['height'];
743
744
        if ($current_width < $Max && $current_height < $Max) {
745
            return true;
746
        } elseif ($current_height == '') {
747
            return false;
748
        }
749
750
        // Resize according to height.
751
        if ($Dimension == "height") {
752
            $resize_scale = $current_height / $Max;
753
            $new_width = ceil($current_width / $resize_scale);
754
        }
755
756
        // Resize according to width
757
        if ($Dimension == "width") {
758
            $new_width = $Max;
759
        }
760
761
        // Resize according to height or width, both should not be larger than $Max after resizing.
762
        if ($Dimension == "any") {
763
            if ($current_height > $current_width || $current_height == $current_width) {
764
                $resize_scale = $current_height / $Max;
765
                $new_width = ceil($current_width / $resize_scale);
766
            }
767
            if ($current_height < $current_width) {
768
                $new_width = $Max;
769
            }
770
        }
771
772
        $my_image->resize($new_width);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $new_width does not seem to be defined for all execution paths leading up to this point.
Loading history...
773
        $result = $my_image->send_image($picturePath);
774
775
        if ($result) {
776
            return true;
777
        }
778
779
        return false;
780
    }
781
782
    /**
783
     * deletes the picture
784
     *
785
     * @author Olivier Brouckaert
786
     * @return boolean - true if removed, otherwise false
787
     */
788
    public function removePicture()
789
    {
790
        $picturePath = $this->getHotSpotFolderInCourse();
791
792
        // if the question has got an ID and if the picture exists
793
        if ($this->id) {
794
            $picture = $this->picture;
795
            $this->picture = '';
796
797
            return @unlink($picturePath.'/'.$picture) ? true : false;
798
        }
799
800
        return false;
801
    }
802
803
    /**
804
     * Exports a picture to another question
805
     *
806
     * @author Olivier Brouckaert
807
     * @param integer $questionId - ID of the target question
808
     * @param array $courseInfo
809
     * @return boolean - true if copied, otherwise false
810
     */
811
    public function exportPicture($questionId, $courseInfo)
812
    {
813
        if (empty($questionId) || empty($courseInfo)) {
814
            return false;
815
        }
816
817
        $course_id = $courseInfo['real_id'];
818
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
819
        $destination_path = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/images';
820
        $source_path = $this->getHotSpotFolderInCourse();
821
822
        // if the question has got an ID and if the picture exists
823
        if (!$this->id || empty($this->picture)) {
824
            return false;
825
        }
826
827
        $picture = $this->generatePictureName();
828
829
        if (file_exists($source_path.'/'.$this->picture)) {
830
            // for backward compatibility
831
            $result = @copy(
832
                $source_path.'/'.$this->picture,
833
                $destination_path.'/'.$picture
834
            );
835
        } else {
836
            $imageInfo = DocumentManager::get_document_data_by_id(
837
                $this->picture,
838
                $courseInfo['code']
839
            );
840
            if (file_exists($imageInfo['absolute_path'])) {
841
                $result = @copy(
842
                    $imageInfo['absolute_path'],
843
                    $destination_path.'/'.$picture
844
                );
845
            }
846
        }
847
848
        // If copy was correct then add to the database
849
        if (!$result) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
850
            return false;
851
        }
852
853
        $sql = "UPDATE $TBL_QUESTIONS SET
854
                picture = '".Database::escape_string($picture)."'
855
                WHERE c_id = $course_id AND id='".intval($questionId)."'";
856
        Database::query($sql);
857
858
        $documentId = add_document(
859
            $courseInfo,
860
            '/images/'.$picture,
861
            'file',
862
            filesize($destination_path.'/'.$picture),
863
            $picture
864
        );
865
866
        if (!$documentId) {
867
            return false;
868
        }
869
870
        return api_item_property_update(
871
            $courseInfo,
872
            TOOL_DOCUMENT,
873
            $documentId,
874
            'DocumentAdded',
875
            api_get_user_id()
876
        );
877
    }
878
879
    /**
880
     * Saves the picture coming from POST into a temporary file
881
     * Temporary pictures are used when we don't want to save a picture right after a form submission.
882
     * For example, if we first show a confirmation box.
883
     *
884
     * @author Olivier Brouckaert
885
     * @param string $picture - temporary path of the picture to move
886
     * @param string $pictureName - Name of the picture
887
     */
888
    public function setTmpPicture($picture, $pictureName)
889
    {
890
        $picturePath = $this->getHotSpotFolderInCourse();
891
        $pictureName = explode('.', $pictureName);
892
        $Extension = $pictureName[sizeof($pictureName) - 1];
0 ignored issues
show
Bug introduced by
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

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

892
        $Extension = $pictureName[/** @scrutinizer ignore-call */ sizeof($pictureName) - 1];

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
893
894
        // saves the picture into a temporary file
895
        @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for move_uploaded_file(). 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

895
        /** @scrutinizer ignore-unhandled */ @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);

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...
896
    }
897
898
    /**
899
     * Set title
900
     * @param string $title
901
     */
902
    public function setTitle($title)
903
    {
904
        $this->question = $title;
905
    }
906
907
    /**
908
     * Sets extra info
909
     * @param string $extra
910
     */
911
    public function setExtra($extra)
912
    {
913
        $this->extra = $extra;
914
    }
915
916
    /**
917
     * updates the question in the data base
918
     * if an exercise ID is provided, we add that exercise ID into the exercise list
919
     *
920
     * @author Olivier Brouckaert
921
     * @param Exercise $exercise
922
     */
923
    public function save($exercise)
924
    {
925
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
926
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
927
        $em = Database::getManager();
928
        $exerciseId = $exercise->id;
929
930
        $id = $this->id;
931
        $question = $this->question;
932
        $description = $this->description;
933
        $weighting = $this->weighting;
934
        $position = $this->position;
935
        $type = $this->type;
936
        $picture = $this->picture;
937
        $level = $this->level;
938
        $extra = $this->extra;
939
        $c_id = $this->course['real_id'];
940
        $categoryId = $this->category;
941
942
        // question already exists
943
        if (!empty($id)) {
944
            $params = [
945
                'question' => $question,
946
                'description' => $description,
947
                'ponderation' => $weighting,
948
                'position' => $position,
949
                'type' => $type,
950
                'picture' => $picture,
951
                'extra' => $extra,
952
                'level' => $level,
953
            ];
954
            if ($exercise->questionFeedbackEnabled) {
955
                $params['feedback'] = $this->feedback;
956
            }
957
            Database::update(
958
                $TBL_QUESTIONS,
959
                $params,
960
                ['c_id = ? AND id = ?' => [$c_id, $id]]
961
            );
962
            $this->saveCategory($categoryId);
963
964
            if (!empty($exerciseId)) {
965
                api_item_property_update(
966
                    $this->course,
967
                    TOOL_QUIZ,
968
                    $id,
969
                    'QuizQuestionUpdated',
970
                    api_get_user_id()
971
                );
972
            }
973
            if (api_get_setting('search_enabled') == 'true') {
974
                if ($exerciseId != 0) {
975
                    $this->search_engine_edit($exerciseId);
976
                } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
977
                    /**
978
                     * actually there is *not* an user interface for
979
                     * creating questions without a relation with an exercise
980
                     */
981
                }
982
            }
983
        } else {
984
            // creates a new question
985
            $sql = "SELECT max(position)
986
                    FROM $TBL_QUESTIONS as question,
987
                    $TBL_EXERCISE_QUESTION as test_question
988
                    WHERE
989
                        question.id = test_question.question_id AND
990
                        test_question.exercice_id = ".intval($exerciseId)." AND
991
                        question.c_id = $c_id AND
992
                        test_question.c_id = $c_id ";
993
            $result = Database::query($sql);
994
            $current_position = Database::result($result, 0, 0);
995
            $this->updatePosition($current_position + 1);
996
            $position = $this->position;
997
998
            $params = [
999
                'c_id' => $c_id,
1000
                'question' => $question,
1001
                'description' => $description,
1002
                'ponderation' => $weighting,
1003
                'position' => $position,
1004
                'type' => $type,
1005
                'picture'  => $picture,
1006
                'extra' => $extra,
1007
                'level' => $level
1008
            ];
1009
1010
            if ($exercise->questionFeedbackEnabled) {
1011
                $params['feedback'] = $this->feedback;
1012
            }
1013
            $this->id = Database::insert($TBL_QUESTIONS, $params);
1014
1015
            if ($this->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->id of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1016
                $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
1017
                Database::query($sql);
1018
1019
                api_item_property_update(
1020
                    $this->course,
1021
                    TOOL_QUIZ,
1022
                    $this->id,
1023
                    'QuizQuestionAdded',
1024
                    api_get_user_id()
1025
                );
1026
1027
                // If hotspot, create first answer
1028
                if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
1029
                    $quizAnswer = new CQuizAnswer();
1030
                    $quizAnswer
1031
                        ->setCId($c_id)
1032
                        ->setQuestionId($this->id)
1033
                        ->setAnswer('')
1034
                        ->setPonderation(10)
1035
                        ->setPosition(1)
1036
                        ->setHotspotCoordinates('0;0|0|0')
1037
                        ->setHotspotType('square');
1038
1039
                    $em->persist($quizAnswer);
1040
                    $em->flush();
1041
1042
                    $id = $quizAnswer->getIid();
1043
1044
                    if ($id) {
1045
                        $quizAnswer
1046
                            ->setId($id)
1047
                            ->setIdAuto($id);
1048
1049
                        $em->merge($quizAnswer);
1050
                        $em->flush();
1051
                    }
1052
                }
1053
1054
                if ($type == HOT_SPOT_DELINEATION) {
1055
                    $quizAnswer = new CQuizAnswer();
1056
                    $quizAnswer
1057
                        ->setCId($c_id)
1058
                        ->setQuestionId($this->id)
1059
                        ->setAnswer('')
1060
                        ->setPonderation(10)
1061
                        ->setPosition(1)
1062
                        ->setHotspotCoordinates('0;0|0|0')
1063
                        ->setHotspotType('delineation');
1064
1065
                    $em->persist($quizAnswer);
1066
                    $em->flush();
1067
1068
                    $id = $quizAnswer->getIid();
1069
1070
                    if ($id) {
1071
                        $quizAnswer
1072
                            ->setId($id)
1073
                            ->setIdAuto($id);
1074
1075
                        $em->merge($quizAnswer);
1076
                        $em->flush();
1077
                    }
1078
                }
1079
1080
                if (api_get_setting('search_enabled') == 'true') {
1081
                    if ($exerciseId != 0) {
1082
                        $this->search_engine_edit($exerciseId, true);
1083
                    } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
1084
                        /**
1085
                         * actually there is *not* an user interface for
1086
                         * creating questions without a relation with an exercise
1087
                         */
1088
                    }
1089
                }
1090
            }
1091
        }
1092
1093
        // if the question is created in an exercise
1094
        if ($exerciseId) {
1095
            // adds the exercise into the exercise list of this question
1096
            $this->addToList($exerciseId, true);
1097
        }
1098
    }
1099
1100
    public function search_engine_edit(
1101
        $exerciseId,
1102
        $addQs = false,
1103
        $rmQs = false
1104
    ) {
1105
        // update search engine and its values table if enabled
1106
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1107
            $course_id = api_get_course_id();
1108
            // get search_did
1109
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
1110
            if ($addQs || $rmQs) {
1111
                //there's only one row per question on normal db and one document per question on search engine db
1112
                $sql = 'SELECT * FROM %s
1113
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
1114
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1115
            } else {
1116
                $sql = 'SELECT * FROM %s
1117
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
1118
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
1119
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1120
            }
1121
            $res = Database::query($sql);
1122
1123
            if (Database::num_rows($res) > 0 || $addQs) {
1124
                $di = new ChamiloIndexer();
1125
                if ($addQs) {
1126
                    $question_exercises = [(int) $exerciseId];
1127
                } else {
1128
                    $question_exercises = [];
1129
                }
1130
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
1131
                $di->connectDb(null, null, $lang);
1132
1133
                // retrieve others exercise ids
1134
                $se_ref = Database::fetch_array($res);
1135
                $se_doc = $di->get_document((int) $se_ref['search_did']);
1136
                if ($se_doc !== false) {
1137
                    if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
1138
                        $se_doc_data = unserialize($se_doc_data);
1139
                        if (isset($se_doc_data[SE_DATA]['type']) &&
1140
                            $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
1141
                        ) {
1142
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
1143
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
1144
                            ) {
1145
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
1146
                                    if (!in_array($old_value, $question_exercises)) {
1147
                                        $question_exercises[] = $old_value;
1148
                                    }
1149
                                }
1150
                            }
1151
                        }
1152
                    }
1153
                }
1154
                if ($rmQs) {
1155
                    while (($key = array_search($exerciseId, $question_exercises)) !== false) {
1156
                        unset($question_exercises[$key]);
1157
                    }
1158
                }
1159
1160
                // build the chunk to index
1161
                $ic_slide = new IndexableChunk();
1162
                $ic_slide->addValue("title", $this->question);
1163
                $ic_slide->addCourseId($course_id);
1164
                $ic_slide->addToolId(TOOL_QUIZ);
1165
                $xapian_data = [
1166
                    SE_COURSE_ID => $course_id,
1167
                    SE_TOOL_ID => TOOL_QUIZ,
1168
                    SE_DATA => [
1169
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
1170
                        'exercise_ids' => $question_exercises,
1171
                        'question_id' => (int) $this->id
1172
                    ],
1173
                    SE_USER => (int) api_get_user_id(),
1174
                ];
1175
                $ic_slide->xapian_data = serialize($xapian_data);
1176
                $ic_slide->addValue("content", $this->description);
1177
1178
                //TODO: index answers, see also form validation on question_admin.inc.php
1179
1180
                $di->remove_document($se_ref['search_did']);
1181
                $di->addChunk($ic_slide);
1182
1183
                //index and return search engine document id
1184
                if (!empty($question_exercises)) { // if empty there is nothing to index
1185
                    $did = $di->index();
1186
                    unset($di);
1187
                }
1188
                if ($did || $rmQs) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $did does not seem to be defined for all execution paths leading up to this point.
Loading history...
1189
                    // save it to db
1190
                    if ($addQs || $rmQs) {
1191
                        $sql = "DELETE FROM %s
1192
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
1193
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1194
                    } else {
1195
                        $sql = "DELETE FROM %S
1196
                            WHERE
1197
                                course_code = '%s'
1198
                                AND tool_id = '%s'
1199
                                AND tool_id = '%s'
1200
                                AND ref_id_high_level = '%s'
1201
                                AND ref_id_second_level = '%s'";
1202
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1203
                    }
1204
                    Database::query($sql);
1205
                    if ($rmQs) {
1206
                        if (!empty($question_exercises)) {
1207
                            $sql = "INSERT INTO %s (
1208
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1209
                                )
1210
                                VALUES (
1211
                                    NULL, '%s', '%s', %s, %s, %s
1212
                                )";
1213
                            $sql = sprintf(
1214
                                $sql,
1215
                                $tbl_se_ref,
1216
                                $course_id,
1217
                                TOOL_QUIZ,
1218
                                array_shift($question_exercises),
1219
                                $this->id,
1220
                                $did
1221
                            );
1222
                            Database::query($sql);
1223
                        }
1224
                    } else {
1225
                        $sql = "INSERT INTO %s (
1226
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1227
                            )
1228
                            VALUES (
1229
                                NULL , '%s', '%s', %s, %s, %s
1230
                            )";
1231
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
1232
                        Database::query($sql);
1233
                    }
1234
                }
1235
            }
1236
        }
1237
    }
1238
1239
    /**
1240
     * adds an exercise into the exercise list
1241
     *
1242
     * @author Olivier Brouckaert
1243
     * @param integer $exerciseId - exercise ID
1244
     * @param boolean $fromSave - from $this->save() or not
1245
     */
1246
    public function addToList($exerciseId, $fromSave = false)
1247
    {
1248
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1249
        $id = $this->id;
1250
        // checks if the exercise ID is not in the list
1251
        if (!in_array($exerciseId, $this->exerciseList)) {
1252
            $this->exerciseList[] = $exerciseId;
1253
            $new_exercise = new Exercise();
1254
            $new_exercise->read($exerciseId);
1255
            $count = $new_exercise->selectNbrQuestions();
1256
            $count++;
1257
            $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
1258
                    VALUES ({$this->course['real_id']}, ".intval($id).", ".intval($exerciseId).", '$count')";
1259
            Database::query($sql);
1260
1261
            // we do not want to reindex if we had just saved adnd indexed the question
1262
            if (!$fromSave) {
1263
                $this->search_engine_edit($exerciseId, true);
1264
            }
1265
        }
1266
    }
1267
1268
    /**
1269
     * removes an exercise from the exercise list
1270
     *
1271
     * @author Olivier Brouckaert
1272
     * @param integer $exerciseId - exercise ID
1273
     * @return boolean - true if removed, otherwise false
1274
     */
1275
    public function removeFromList($exerciseId)
1276
    {
1277
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1278
        $id = $this->id;
1279
1280
        // searches the position of the exercise ID in the list
1281
        $pos = array_search($exerciseId, $this->exerciseList);
1282
        $course_id = api_get_course_int_id();
1283
1284
        // exercise not found
1285
        if ($pos === false) {
1286
            return false;
1287
        } else {
1288
            // deletes the position in the array containing the wanted exercise ID
1289
            unset($this->exerciseList[$pos]);
1290
            //update order of other elements
1291
            $sql = "SELECT question_order
1292
                    FROM $TBL_EXERCISE_QUESTION
1293
                    WHERE
1294
                        c_id = $course_id
1295
                        AND question_id = ".intval($id)."
1296
                        AND exercice_id = " . intval($exerciseId);
1297
            $res = Database::query($sql);
1298
            if (Database::num_rows($res) > 0) {
1299
                $row = Database::fetch_array($res);
1300
                if (!empty($row['question_order'])) {
1301
                    $sql = "UPDATE $TBL_EXERCISE_QUESTION
1302
                        SET question_order = question_order-1
1303
                        WHERE
1304
                            c_id = $course_id
1305
                            AND exercice_id = ".intval($exerciseId)."
1306
                            AND question_order > " . $row['question_order'];
1307
                    Database::query($sql);
1308
                }
1309
            }
1310
1311
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1312
                    WHERE
1313
                        c_id = $course_id
1314
                        AND question_id = ".intval($id)."
1315
                        AND exercice_id = " . intval($exerciseId);
1316
            Database::query($sql);
1317
1318
            return true;
1319
        }
1320
    }
1321
1322
    /**
1323
     * Deletes a question from the database
1324
     * the parameter tells if the question is removed from all exercises (value = 0),
1325
     * or just from one exercise (value = exercise ID)
1326
     *
1327
     * @author Olivier Brouckaert
1328
     * @param integer $deleteFromEx - exercise ID if the question is only removed from one exercise
1329
     */
1330
    public function delete($deleteFromEx = 0)
1331
    {
1332
        $course_id = api_get_course_int_id();
1333
1334
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1335
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1336
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
1337
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1338
1339
        $id = intval($this->id);
1340
1341
        // if the question must be removed from all exercises
1342
        if (!$deleteFromEx) {
1343
            //update the question_order of each question to avoid inconsistencies
1344
            $sql = "SELECT exercice_id, question_order 
1345
                    FROM $TBL_EXERCISE_QUESTION
1346
                    WHERE c_id = $course_id AND question_id = ".intval($id)."";
1347
1348
            $res = Database::query($sql);
1349
            if (Database::num_rows($res) > 0) {
1350
                while ($row = Database::fetch_array($res)) {
1351
                    if (!empty($row['question_order'])) {
1352
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
1353
                                SET question_order = question_order-1
1354
                                WHERE
1355
                                    c_id = $course_id AND 
1356
                                    exercice_id = ".intval($row['exercice_id'])." AND 
1357
                                    question_order > " . $row['question_order'];
1358
                        Database::query($sql);
1359
                    }
1360
                }
1361
            }
1362
1363
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1364
                    WHERE c_id = $course_id AND question_id = ".$id;
1365
            Database::query($sql);
1366
1367
            $sql = "DELETE FROM $TBL_QUESTIONS
1368
                    WHERE c_id = $course_id AND id = ".$id;
1369
            Database::query($sql);
1370
1371
            $sql = "DELETE FROM $TBL_REPONSES
1372
                    WHERE c_id = $course_id AND question_id = ".$id;
1373
            Database::query($sql);
1374
1375
            // remove the category of this question in the question_rel_category table
1376
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
1377
                    WHERE 
1378
                        c_id = $course_id AND 
1379
                        question_id = ".$id;
1380
            Database::query($sql);
1381
1382
            api_item_property_update(
1383
                $this->course,
1384
                TOOL_QUIZ,
1385
                $id,
1386
                'QuizQuestionDeleted',
1387
                api_get_user_id()
1388
            );
1389
            $this->removePicture();
1390
        } else {
1391
            // just removes the exercise from the list
1392
            $this->removeFromList($deleteFromEx);
1393
            if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1394
                // disassociate question with this exercise
1395
                $this->search_engine_edit($deleteFromEx, false, true);
1396
            }
1397
1398
            api_item_property_update(
1399
                $this->course,
1400
                TOOL_QUIZ,
1401
                $id,
1402
                'QuizQuestionDeleted',
1403
                api_get_user_id()
1404
            );
1405
        }
1406
    }
1407
1408
    /**
1409
     * Duplicates the question
1410
     *
1411
     * @author Olivier Brouckaert
1412
     * @param  array   $course_info Course info of the destination course
1413
     * @return false|string     ID of the new question
1414
     */
1415
    public function duplicate($course_info = [])
1416
    {
1417
        if (empty($course_info)) {
1418
            $course_info = $this->course;
1419
        } else {
1420
            $course_info = $course_info;
1421
        }
1422
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1423
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1424
1425
        $question = $this->question;
1426
        $description = $this->description;
1427
        $weighting = $this->weighting;
1428
        $position = $this->position;
1429
        $type = $this->type;
1430
        $level = intval($this->level);
1431
        $extra = $this->extra;
1432
1433
        // Using the same method used in the course copy to transform URLs
1434
        if ($this->course['id'] != $course_info['id']) {
1435
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1436
                $description,
1437
                $this->course['code'],
1438
                $course_info['id']
1439
            );
1440
            $question = DocumentManager::replaceUrlWithNewCourseCode(
1441
                $question,
1442
                $this->course['code'],
1443
                $course_info['id']
1444
            );
1445
        }
1446
1447
        $course_id = $course_info['real_id'];
1448
1449
        // Read the source options
1450
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1451
1452
        // Inserting in the new course db / or the same course db
1453
        $params = [
1454
            'c_id' => $course_id,
1455
            'question' => $question,
1456
            'description' => $description,
1457
            'ponderation' => $weighting,
1458
            'position' => $position,
1459
            'type' => $type,
1460
            'level' => $level,
1461
            'extra' => $extra
1462
        ];
1463
        $newQuestionId = Database::insert($TBL_QUESTIONS, $params);
1464
1465
        if ($newQuestionId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newQuestionId of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1466
            $sql = "UPDATE $TBL_QUESTIONS 
1467
                    SET id = iid
1468
                    WHERE iid = $newQuestionId";
1469
            Database::query($sql);
1470
1471
            if (!empty($options)) {
1472
                // Saving the quiz_options
1473
                foreach ($options as $item) {
1474
                    $item['question_id'] = $newQuestionId;
1475
                    $item['c_id'] = $course_id;
1476
                    unset($item['id']);
1477
                    unset($item['iid']);
1478
                    $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
1479
                    if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1480
                        $sql = "UPDATE $TBL_QUESTION_OPTIONS 
1481
                                SET id = iid
1482
                                WHERE iid = $id";
1483
                        Database::query($sql);
1484
                    }
1485
                }
1486
            }
1487
1488
            // Duplicates the picture of the hotspot
1489
            $this->exportPicture($newQuestionId, $course_info);
1490
        }
1491
1492
        return $newQuestionId;
1493
    }
1494
1495
    /**
1496
     * @return string
1497
     */
1498
    public function get_question_type_name()
1499
    {
1500
        $key = self::$questionTypes[$this->type];
1501
1502
        return get_lang($key[1]);
1503
    }
1504
1505
    /**
1506
     * @param string $type
1507
     * @return null
1508
     */
1509
    public static function get_question_type($type)
1510
    {
1511
        if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
1512
            return null;
1513
        }
1514
        return self::$questionTypes[$type];
1515
    }
1516
1517
    /**
1518
     * @return array
1519
     */
1520
    public static function get_question_type_list()
1521
    {
1522
        if (api_get_setting('enable_record_audio') !== 'true') {
1523
            self::$questionTypes[ORAL_EXPRESSION] = null;
1524
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1525
        }
1526
        if (api_get_setting('enable_quiz_scenario') !== 'true') {
1527
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1528
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1529
        }
1530
        return self::$questionTypes;
1531
    }
1532
1533
    /**
1534
     * Returns an instance of the class corresponding to the type
1535
     * @param integer $type the type of the question
1536
     * @return $this instance of a Question subclass (or of Questionc class by default)
1537
     */
1538
    public static function getInstance($type)
1539
    {
1540
        if (!is_null($type)) {
1541
            list($file_name, $class_name) = self::get_question_type($type);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to list($file_name, $class_name) is correct as self::get_question_type($type) targeting Question::get_question_type() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1542
            if (!empty($file_name)) {
1543
                if (class_exists($class_name)) {
1544
                    return new $class_name();
1545
                } else {
1546
                    echo 'Can\'t instanciate class '.$class_name.' of type '.$type;
1547
                }
1548
            }
1549
        }
1550
1551
        return null;
1552
    }
1553
1554
    /**
1555
     * Creates the form to create / edit a question
1556
     * A subclass can redefine this function to add fields...
1557
     * @param FormValidator $form
1558
     * @param Exercise $exercise
1559
     */
1560
    public function createForm(&$form, $exercise)
1561
    {
1562
        echo '<style>
1563
                .media { display:none;}
1564
            </style>';
1565
1566
        // question name
1567
        if (api_get_configuration_value('save_titles_as_html')) {
1568
            $editorConfig = ['ToolbarSet' => 'Minimal'];
1569
            $form->addHtmlEditor(
1570
                'questionName',
1571
                get_lang('Question'),
1572
                false,
1573
                false,
1574
                $editorConfig,
1575
                true
1576
            );
1577
        } else {
1578
            $form->addElement('text', 'questionName', get_lang('Question'));
1579
        }
1580
1581
        $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
1582
1583
        // default content
1584
        $isContent = isset($_REQUEST['isContent']) ? intval($_REQUEST['isContent']) : null;
1585
1586
        // Question type
1587
        $answerType = isset($_REQUEST['answerType']) ? intval($_REQUEST['answerType']) : null;
1588
        $form->addElement('hidden', 'answerType', $answerType);
1589
1590
        // html editor
1591
        $editorConfig = [
1592
            'ToolbarSet' => 'TestQuestionDescription',
1593
            'Height' => '150'
1594
        ];
1595
1596
        if (!api_is_allowed_to_edit(null, true)) {
1597
            $editorConfig['UserStatus'] = 'student';
1598
        }
1599
1600
        $form->addButtonAdvancedSettings('advanced_params');
1601
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1602
        $form->addHtmlEditor(
1603
            'questionDescription',
1604
            get_lang('QuestionDescription'),
1605
            false,
1606
            false,
1607
            $editorConfig
1608
        );
1609
1610
        // hidden values
1611
        $my_id = isset($_REQUEST['myid']) ? intval($_REQUEST['myid']) : null;
1612
        $form->addElement('hidden', 'myid', $my_id);
1613
1614
        if ($this->type != MEDIA_QUESTION) {
1615
            // Advanced parameters
1616
            $select_level = self::get_default_levels();
1617
            $form->addElement(
1618
                'select',
1619
                'questionLevel',
1620
                get_lang('Difficulty'),
1621
                $select_level
1622
            );
1623
1624
            // Categories
1625
            $tabCat = TestCategory::getCategoriesIdAndName();
1626
            $form->addElement(
1627
                'select',
1628
                'questionCategory',
1629
                get_lang('Category'),
1630
                $tabCat
1631
            );
1632
1633
            global $text;
1634
1635
            switch ($this->type) {
1636
                case UNIQUE_ANSWER:
1637
                    $buttonGroup = [];
1638
                    $buttonGroup[] = $form->addButtonSave(
1639
                        $text,
1640
                        'submitQuestion',
1641
                        true
1642
                    );
1643
                    $buttonGroup[] = $form->addButton(
1644
                        'convertAnswer',
1645
                        get_lang('ConvertToMultipleAnswer'),
1646
                        'dot-circle-o',
1647
                        'default',
1648
                        null,
1649
                        null,
1650
                        null,
1651
                        true
1652
                    );
1653
                    $form->addGroup($buttonGroup);
1654
                    break;
1655
                case MULTIPLE_ANSWER:
1656
                    $buttonGroup = [];
1657
                    $buttonGroup[] = $form->addButtonSave(
1658
                        $text,
1659
                        'submitQuestion',
1660
                        true
1661
                    );
1662
                    $buttonGroup[] = $form->addButton(
1663
                        'convertAnswer',
1664
                        get_lang('ConvertToUniqueAnswer'),
1665
                        'check-square-o',
1666
                        'default',
1667
                        null,
1668
                        null,
1669
                        null,
1670
                        true
1671
                    );
1672
                    $form->addGroup($buttonGroup);
1673
                    break;
1674
            }
1675
1676
            //Medias
1677
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1678
            //$form->addElement('select', 'parent_id', get_lang('AttachToMedia'), $course_medias);
1679
        }
1680
1681
        $form->addElement('html', '</div>');
1682
1683
        if (!isset($_GET['fromExercise'])) {
1684
            switch ($answerType) {
1685
                case 1:
1686
                    $this->question = get_lang('DefaultUniqueQuestion');
1687
                    break;
1688
                case 2:
1689
                    $this->question = get_lang('DefaultMultipleQuestion');
1690
                    break;
1691
                case 3:
1692
                    $this->question = get_lang('DefaultFillBlankQuestion');
1693
                    break;
1694
                case 4:
1695
                    $this->question = get_lang('DefaultMathingQuestion');
1696
                    break;
1697
                case 5:
1698
                    $this->question = get_lang('DefaultOpenQuestion');
1699
                    break;
1700
                case 9:
1701
                    $this->question = get_lang('DefaultMultipleQuestion');
1702
                    break;
1703
            }
1704
        }
1705
1706
        if (!is_null($exercise)) {
1707
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1708
                $form->addTextarea('feedback', get_lang('FeedbackIfNotCorrect'));
1709
            }
1710
        }
1711
1712
1713
        // default values
1714
        $defaults = [];
1715
        $defaults['questionName'] = $this->question;
1716
        $defaults['questionDescription'] = $this->description;
1717
        $defaults['questionLevel'] = $this->level;
1718
        $defaults['questionCategory'] = $this->category;
1719
        $defaults['feedback'] = $this->feedback;
1720
1721
        // Came from he question pool
1722
        if (isset($_GET['fromExercise'])) {
1723
            $form->setDefaults($defaults);
1724
        }
1725
1726
        if (!empty($_REQUEST['myid'])) {
1727
            $form->setDefaults($defaults);
1728
        } else {
1729
            if ($isContent == 1) {
1730
                $form->setDefaults($defaults);
1731
            }
1732
        }
1733
    }
1734
1735
    /**
1736
     * function which process the creation of questions
1737
     * @param FormValidator $form
1738
     * @param Exercise $exercise
1739
     */
1740
    public function processCreation($form, $exercise)
1741
    {
1742
        $this->updateTitle($form->getSubmitValue('questionName'));
0 ignored issues
show
Bug introduced by
It seems like $form->getSubmitValue('questionName') can also be of type array and array and array and array; however, parameter $title of Question::updateTitle() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1742
        $this->updateTitle(/** @scrutinizer ignore-type */ $form->getSubmitValue('questionName'));
Loading history...
1743
        $this->updateDescription($form->getSubmitValue('questionDescription'));
0 ignored issues
show
Bug introduced by
It seems like $form->getSubmitValue('questionDescription') can also be of type array and array and array and array; however, parameter $description of Question::updateDescription() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1743
        $this->updateDescription(/** @scrutinizer ignore-type */ $form->getSubmitValue('questionDescription'));
Loading history...
1744
        $this->updateLevel($form->getSubmitValue('questionLevel'));
0 ignored issues
show
Bug introduced by
It seems like $form->getSubmitValue('questionLevel') can also be of type array and array and array and array; however, parameter $level of Question::updateLevel() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1744
        $this->updateLevel(/** @scrutinizer ignore-type */ $form->getSubmitValue('questionLevel'));
Loading history...
1745
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1746
        $this->setFeedback($form->getSubmitValue('feedback'));
0 ignored issues
show
Bug introduced by
It seems like $form->getSubmitValue('feedback') can also be of type array and array and array and array; however, parameter $value of Question::setFeedback() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1746
        $this->setFeedback(/** @scrutinizer ignore-type */ $form->getSubmitValue('feedback'));
Loading history...
1747
1748
        //Save normal question if NOT media
1749
        if ($this->type != MEDIA_QUESTION) {
1750
            $this->save($exercise);
1751
1752
            // modify the exercise
1753
            $exercise->addToList($this->id);
0 ignored issues
show
Bug introduced by
It seems like $this->id can also be of type false; however, parameter $questionId of Exercise::addToList() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1753
            $exercise->addToList(/** @scrutinizer ignore-type */ $this->id);
Loading history...
1754
            $exercise->update_question_positions();
1755
        }
1756
    }
1757
1758
    /**
1759
     * abstract function which creates the form to create / edit the answers of the question
1760
     * @param FormValidator $form
1761
     */
1762
    abstract public function createAnswersForm($form);
1763
1764
    /**
1765
     * abstract function which process the creation of answers
1766
     * @param FormValidator $form
1767
     * @param Exercise $exercise
1768
     */
1769
    abstract public function processAnswersCreation($form, $exercise);
1770
1771
    /**
1772
     * Displays the menu of question types
1773
     *
1774
     * @param Exercise $objExercise
1775
     */
1776
    public static function display_type_menu($objExercise)
1777
    {
1778
        $feedback_type = $objExercise->feedback_type;
1779
        $exerciseId = $objExercise->id;
1780
1781
        // 1. by default we show all the question types
1782
        $question_type_custom_list = self::get_question_type_list();
1783
1784
        if (!isset($feedback_type)) {
1785
            $feedback_type = 0;
1786
        }
1787
1788
        if ($feedback_type == 1) {
1789
            //2. but if it is a feedback DIRECT we only show the UNIQUE_ANSWER type that is currently available
1790
            $question_type_custom_list = [
1791
                UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1792
                HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION]
1793
            ];
1794
        } else {
1795
            unset($question_type_custom_list[HOT_SPOT_DELINEATION]);
1796
        }
1797
1798
        echo '<div class="well">';
1799
        echo '<ul class="question_menu">';
1800
1801
        foreach ($question_type_custom_list as $i => $a_type) {
1802
            // include the class of the type
1803
            require_once $a_type[0];
1804
            // get the picture of the type and the langvar which describes it
1805
            $img = $explanation = '';
1806
            eval('$img = '.$a_type[1].'::$typePicture;');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
1807
            eval('$explanation = get_lang('.$a_type[1].'::$explanationLangVar);');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
1808
            echo '<li>';
1809
            echo '<div class="icon-image">';
1810
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
1811
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1812
1813
            if ($objExercise->force_edit_exercise_in_lp === false) {
1814
                if ($objExercise->exercise_was_added_in_lp == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1815
                    $img = pathinfo($img);
1816
                    $img = $img['filename'].'_na.'.$img['extension'];
1817
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1818
                }
1819
            }
1820
1821
            echo $icon;
1822
            echo '</div>';
1823
            echo '</li>';
1824
        }
1825
1826
        echo '<li>';
1827
        echo '<div class="icon_image_content">';
1828
        if ($objExercise->exercise_was_added_in_lp == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1829
            echo Display::return_icon(
1830
                'database_na.png',
1831
                get_lang('GetExistingQuestion'),
1832
                null,
1833
                ICON_SIZE_BIG
1834
            );
1835
        } else {
1836
            if ($feedback_type == 1) {
1837
                echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
1838
            } else {
1839
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
1840
            }
1841
            echo Display::return_icon(
1842
                'database.png',
1843
                get_lang('GetExistingQuestion'),
1844
                null,
1845
                ICON_SIZE_BIG
1846
            );
1847
        }
1848
        echo '</a>';
1849
        echo '</div></li>';
1850
        echo '</ul>';
1851
        echo '</div>';
1852
    }
1853
1854
    /**
1855
     * @param int $question_id
1856
     * @param string $name
1857
     * @param int $course_id
1858
     * @param int $position
1859
     * @return false|string
1860
     */
1861
    public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
1862
    {
1863
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1864
        $params['question_id'] = intval($question_id);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
1865
        $params['name'] = $name;
1866
        $params['position'] = $position;
1867
        $params['c_id'] = $course_id;
1868
        $result = self::readQuestionOption($question_id, $course_id);
1869
        $last_id = Database::insert($table, $params);
1870
        if ($last_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $last_id of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1871
            $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
1872
            Database::query($sql);
1873
        }
1874
1875
        return $last_id;
1876
    }
1877
1878
    /**
1879
     * @param int $question_id
1880
     * @param int $course_id
1881
     */
1882
    public static function deleteAllQuestionOptions($question_id, $course_id)
1883
    {
1884
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1885
        Database::delete(
1886
            $table,
1887
            [
1888
                'c_id = ? AND question_id = ?' => [
1889
                    $course_id,
1890
                    $question_id
1891
                ]
1892
            ]
1893
        );
1894
    }
1895
1896
    /**
1897
     * @param int $id
1898
     * @param array $params
1899
     * @param int $course_id
1900
     * @return bool|int
1901
     */
1902
    public static function updateQuestionOption($id, $params, $course_id)
1903
    {
1904
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1905
        $result = Database::update(
1906
            $table,
1907
            $params,
1908
            ['c_id = ? AND id = ?' => [$course_id, $id]]
1909
        );
1910
        return $result;
1911
    }
1912
1913
    /**
1914
     * @param int $question_id
1915
     * @param int $course_id
1916
     * @return array
1917
     */
1918
    public static function readQuestionOption($question_id, $course_id)
1919
    {
1920
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1921
        $result = Database::select(
1922
            '*',
1923
            $table,
1924
            [
1925
                'where' => [
1926
                    'c_id = ? AND question_id = ?' => [
1927
                        $course_id,
1928
                        $question_id
1929
                    ]
1930
                ],
1931
                'order' => 'id ASC'
1932
            ]
1933
        );
1934
1935
        return $result;
1936
    }
1937
1938
    /**
1939
     * Shows question title an description
1940
     *
1941
     * @param Exercise $exercise
1942
     * @param int $counter
1943
     * @param array $score
1944
     * @return string HTML string with the header of the question (before the answers table)
1945
     */
1946
    public function return_header($exercise, $counter = null, $score = [])
1947
    {
1948
        $counter_label = '';
1949
        if (!empty($counter)) {
1950
            $counter_label = intval($counter);
1951
        }
1952
        $score_label = get_lang('Wrong');
1953
        $class = 'error';
1954
        if ($score['pass'] == true) {
1955
            $score_label = get_lang('Correct');
1956
            $class = 'success';
1957
        }
1958
1959
        if (in_array($this->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
1960
            $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
1961
            if ($score['revised'] == true) {
1962
                $score_label = get_lang('Revised');
1963
                $class = '';
1964
            } else {
1965
                $score_label = get_lang('NotRevised');
1966
                $class = 'warning';
1967
                $weight = float_format($score['weight'], 1);
1968
                $score['result'] = " ? / ".$weight;
1969
                $model = ExerciseLib::getCourseScoreModel();
1970
                if (!empty($model)) {
1971
                    $score['result'] = " ? ";
1972
                }
1973
1974
                $hide = api_get_configuration_value('hide_free_question_score');
1975
                if ($hide === true) {
1976
                    $score['result'] = '-';
1977
                }
1978
            }
1979
        }
1980
1981
        // display question category, if any
1982
        $header = TestCategory::returnCategoryAndTitle($this->id);
1983
        $show_media = null;
1984
        if ($show_media) {
1985
            $header .= $this->show_media_content();
1986
        }
1987
1988
        $header .= Display::page_subheader2($counter_label.". ".$this->question);
1989
        $header .= ExerciseLib::getQuestionRibbon($class, $score_label, $score['result']);
1990
        if ($this->type != READING_COMPREHENSION) {
1991
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
1992
            $header .= Display::div(
1993
                $this->description,
1994
                ['class' => 'question_description']
1995
            );
1996
        } else {
1997
            if ($score['pass'] == true) {
1998
                $message = Display::div(
1999
                    sprintf(
2000
                        get_lang('ReadingQuestionCongratsSpeedXReachedForYWords'),
2001
                        ReadingComprehension::$speeds[$this->level],
2002
                        $this->getWordsCount()
0 ignored issues
show
Bug introduced by
The method getWordsCount() does not exist on Question. It seems like you code against a sub-type of Question such as ReadingComprehension. ( Ignorable by Annotation )

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

2002
                        $this->/** @scrutinizer ignore-call */ 
2003
                               getWordsCount()
Loading history...
2003
                    )
2004
                );
2005
            } else {
2006
                $message = Display::div(
2007
                    sprintf(
2008
                        get_lang('ReadingQuestionCongratsSpeedXNotReachedForYWords'),
2009
                        ReadingComprehension::$speeds[$this->level],
2010
                        $this->getWordsCount()
2011
                    )
2012
                );
2013
            }
2014
            $header .= $message.'<br />';
2015
        }
2016
2017
        if (isset($score['pass']) && $score['pass'] === false) {
2018
            if ($this->showFeedback($exercise)) {
2019
                $header .= $this->returnFormatFeedback();
2020
            }
2021
        }
2022
2023
        return $header;
2024
    }
2025
2026
    /**
2027
     * Create a question from a set of parameters
2028
     * @param   int     Quiz ID
2029
     * @param   string  Question name
2030
     * @param   int     Maximum result for the question
2031
     * @param   int     Type of question (see constants at beginning of question.class.php)
2032
     * @param   int     Question level/category
2033
     * @param string $quiz_id
2034
     */
2035
    public function create_question(
2036
        $quiz_id,
2037
        $question_name,
2038
        $question_description = '',
2039
        $max_score = 0,
2040
        $type = 1,
2041
        $level = 1
2042
    ) {
2043
        $course_id = api_get_course_int_id();
2044
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2045
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2046
2047
        $quiz_id = intval($quiz_id);
2048
        $max_score = (float) $max_score;
2049
        $type = intval($type);
2050
        $level = intval($level);
2051
2052
        // Get the max position
2053
        $sql = "SELECT max(position) as max_position
2054
                FROM $tbl_quiz_question q 
2055
                INNER JOIN $tbl_quiz_rel_question r
2056
                ON
2057
                    q.id = r.question_id AND
2058
                    exercice_id = $quiz_id AND
2059
                    q.c_id = $course_id AND
2060
                    r.c_id = $course_id";
2061
        $rs_max = Database::query($sql);
2062
        $row_max = Database::fetch_object($rs_max);
2063
        $max_position = $row_max->max_position + 1;
2064
2065
        $params = [
2066
            'c_id' => $course_id,
2067
            'question' => $question_name,
2068
            'description' => $question_description,
2069
            'ponderation' => $max_score,
2070
            'position' => $max_position,
2071
            'type' => $type,
2072
            'level' => $level,
2073
        ];
2074
        $question_id = Database::insert($tbl_quiz_question, $params);
2075
2076
        if ($question_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $question_id of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2077
            $sql = "UPDATE $tbl_quiz_question  
2078
                    SET id = iid WHERE iid = $question_id";
2079
            Database::query($sql);
2080
2081
            // Get the max question_order
2082
            $sql = "SELECT max(question_order) as max_order
2083
                    FROM $tbl_quiz_rel_question
2084
                    WHERE c_id = $course_id AND exercice_id = $quiz_id ";
2085
            $rs_max_order = Database::query($sql);
2086
            $row_max_order = Database::fetch_object($rs_max_order);
2087
            $max_order = $row_max_order->max_order + 1;
2088
            // Attach questions to quiz
2089
            $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
2090
                    VALUES($course_id, $question_id, $quiz_id, $max_order)";
2091
            Database::query($sql);
2092
        }
2093
2094
        return $question_id;
2095
    }
2096
2097
    /**
2098
     * @return array the image filename of the question type
2099
     */
2100
    public function get_type_icon_html()
2101
    {
2102
        $type = $this->selectType();
2103
        $tabQuestionList = self::get_question_type_list(); // [0]=file to include [1]=type name
2104
2105
        require_once $tabQuestionList[$type][0];
2106
2107
        $img = $explanation = null;
2108
        eval('$img = '.$tabQuestionList[$type][1].'::$typePicture;');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
2109
        eval('$explanation = get_lang('.$tabQuestionList[$type][1].'::$explanationLangVar);');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
2110
        return [$img, $explanation];
2111
    }
2112
2113
    /**
2114
     * Get course medias
2115
     * @param int course id
2116
     * @param integer $course_id
2117
     *
2118
     * @return array
2119
     */
2120
    public static function get_course_medias(
2121
        $course_id,
2122
        $start = 0,
2123
        $limit = 100,
2124
        $sidx = "question",
2125
        $sord = "ASC",
2126
        $where_condition = []
2127
    ) {
2128
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2129
        $default_where = [
2130
            'c_id = ? AND parent_id = 0 AND type = ?' => [
2131
                $course_id,
2132
                MEDIA_QUESTION
2133
            ]
2134
        ];
2135
        $result = Database::select(
2136
            '*',
2137
            $table_question,
2138
            [
2139
                'limit' => " $start, $limit",
2140
                'where' => $default_where,
2141
                'order' => "$sidx $sord"
2142
            ]
2143
        );
2144
2145
        return $result;
2146
    }
2147
2148
    /**
2149
     * Get count course medias
2150
     * @param int course id
2151
     *
2152
     * @return int
2153
     */
2154
    public static function get_count_course_medias($course_id)
2155
    {
2156
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2157
        $result = Database::select(
2158
            'count(*) as count',
2159
            $table_question,
2160
            [
2161
                'where' => [
2162
                    'c_id = ? AND parent_id = 0 AND type = ?' => [
2163
                        $course_id,
2164
                        MEDIA_QUESTION,
2165
                    ],
2166
                ]
2167
            ],
2168
            'first'
2169
        );
2170
2171
        if ($result && isset($result['count'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2172
            return $result['count'];
2173
        }
2174
        return 0;
2175
    }
2176
2177
    /**
2178
     * @param int $course_id
2179
     * @return array
2180
     */
2181
    public static function prepare_course_media_select($course_id)
2182
    {
2183
        $medias = self::get_course_medias($course_id);
2184
        $media_list = [];
2185
        $media_list[0] = get_lang('NoMedia');
2186
2187
        if (!empty($medias)) {
2188
            foreach ($medias as $media) {
2189
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
2190
            }
2191
        }
2192
        return $media_list;
2193
    }
2194
2195
    /**
2196
     * @return integer[]
2197
     */
2198
    public static function get_default_levels()
2199
    {
2200
        $select_level = [
2201
            1 => 1,
2202
            2 => 2,
2203
            3 => 3,
2204
            4 => 4,
2205
            5 => 5
2206
        ];
2207
2208
        return $select_level;
2209
    }
2210
2211
    /**
2212
     * @return string
2213
     */
2214
    public function show_media_content()
2215
    {
2216
        $html = '';
2217
        if ($this->parent_id != 0) {
2218
            $parent_question = self::read($this->parent_id);
2219
            $html = $parent_question->show_media_content();
2220
        } else {
2221
            $html .= Display::page_subheader($this->selectTitle());
2222
            $html .= $this->selectDescription();
2223
        }
2224
        return $html;
2225
    }
2226
2227
    /**
2228
     * Swap between unique and multiple type answers
2229
     * @return UniqueAnswer|MultipleAnswer
2230
     */
2231
    public function swapSimpleAnswerTypes()
2232
    {
2233
        $oppositeAnswers = [
2234
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2235
            MULTIPLE_ANSWER => UNIQUE_ANSWER
2236
        ];
2237
        $this->type = $oppositeAnswers[$this->type];
2238
        Database::update(
2239
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2240
            ['type' => $this->type],
2241
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2242
        );
2243
        $answerClasses = [
2244
            UNIQUE_ANSWER => 'UniqueAnswer',
2245
            MULTIPLE_ANSWER => 'MultipleAnswer'
2246
        ];
2247
        $swappedAnswer = new $answerClasses[$this->type];
2248
        foreach ($this as $key => $value) {
2249
            $swappedAnswer->$key = $value;
2250
        }
2251
        return $swappedAnswer;
2252
    }
2253
2254
    /**
2255
     * @param array $score
2256
     * @return bool
2257
     */
2258
    public function isQuestionWaitingReview($score)
2259
    {
2260
        $isReview = false;
2261
        if (!empty($score)) {
2262
            if (!empty($score['comments']) || $score['score'] > 0) {
2263
                $isReview = true;
2264
            }
2265
        }
2266
2267
        return $isReview;
2268
    }
2269
2270
    /**
2271
     * @param string $value
2272
     */
2273
    public function setFeedback($value)
2274
    {
2275
        $this->feedback = $value;
2276
    }
2277
2278
    /**
2279
     * @param Exercise $exercise
2280
     * @return bool
2281
     */
2282
    public function showFeedback($exercise)
2283
    {
2284
        return
2285
            in_array($this->type, $this->questionTypeWithFeedback) &&
2286
            $exercise->feedback_type != EXERCISE_FEEDBACK_TYPE_EXAM;
2287
    }
2288
2289
    /**
2290
     * @return string
2291
     */
2292
    public function returnFormatFeedback()
2293
    {
2294
        return Display::return_message($this->feedback, 'normal', false);
2295
    }
2296
}
2297