Passed
Push — master ( f9e6b3...5e8c5a )
by Julito
09:39
created

MoodleImport::processMultipleAnswer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 16
nc 2
nop 5
dl 0
loc 27
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
/**
6
 * Class MoodleImport.
7
 *
8
 * @author  José Loguercio <[email protected]>,
9
 * @author  Julio Montoya <[email protected]>
10
 */
11
class MoodleImport
12
{
13
    /**
14
     * Import moodle file.
15
     *
16
     * @param resource $uploadedFile *.* mbz file moodle course backup
17
     *
18
     * @throws Exception
19
     *
20
     * @return bool
21
     */
22
    public function import($uploadedFile)
23
    {
24
        $debug = false;
25
        if (UPLOAD_ERR_OK !== $uploadedFile['error']) {
26
            throw new Exception(get_lang('UploadError'));
27
        }
28
29
        $cachePath = api_get_path(SYS_ARCHIVE_PATH);
30
        $tempPath = $uploadedFile['tmp_name'];
31
        $nameParts = explode('.', $uploadedFile['name']);
32
        $extension = array_pop($nameParts);
33
        $name = basename($tempPath).".$extension";
34
35
        if (!move_uploaded_file($tempPath, api_get_path(SYS_ARCHIVE_PATH).$name)) {
36
            throw new Exception(get_lang('UploadError'));
37
        }
38
39
        $filePath = $cachePath.$name;
40
        if (!is_readable($filePath)) {
41
            throw new Exception(get_lang('UploadError'));
42
        }
43
44
        $mimeType = mime_content_type($filePath);
45
        $folder = api_get_unique_id();
46
        $destinationDir = api_get_path(SYS_ARCHIVE_PATH).$folder;
47
48
        mkdir($destinationDir, api_get_permissions_for_new_directories(), true);
49
50
        switch ($mimeType) {
51
            case 'application/gzip':
52
            case 'application/x-gzip':
53
                $backUpFile = new PharData($filePath);
54
55
                if (false === $backUpFile->extractTo($destinationDir)) {
56
                    throw new Exception(get_lang('ErrorImportingFile'));
57
                }
58
59
                if (!file_exists($destinationDir.'/moodle_backup.xml')) {
60
                    throw new Exception(get_lang('FailedToImportThisIsNotAMoodleFile'));
61
                }
62
63
                break;
64
            case 'application/zip':
65
                $package = new PclZip($filePath);
66
                $mainFileKey = 0;
67
                $packageContent = $package->listContent();
68
69
                if (!empty($packageContent)) {
70
                    foreach ($packageContent as $index => $value) {
71
                        if ($value['filename'] === 'moodle_backup.xml') {
72
                            $mainFileKey = $index;
73
                            break;
74
                        }
75
                    }
76
                }
77
78
                if (!$mainFileKey) {
79
                    throw new Exception(get_lang('FailedToImportThisIsNotAMoodleFile'));
80
                }
81
82
                $package->extract(PCLZIP_OPT_PATH, $destinationDir);
83
84
                break;
85
        }
86
87
        $courseInfo = api_get_course_info();
88
        $sessionId = api_get_session_id();
89
        $groupId = api_get_group_id();
90
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
91
92
        create_unexisting_directory(
93
            $courseInfo,
94
            api_get_user_id(),
95
            $sessionId,
96
            $groupId,
97
            null,
98
            $documentPath,
99
            '/moodle',
100
            'Moodle Docs',
101
            0
102
        );
103
104
        // This process will upload all question resource files
105
        $filesXml = @file_get_contents($destinationDir.'/files.xml');
106
        $mainFileModuleValues = $this->getAllQuestionFiles($filesXml);
107
        $currentResourceFilePath = $destinationDir.'/files/';
108
        $importedFiles = [];
109
        if ($debug) {
110
            error_log('loading files');
111
        }
112
113
        $_POST['moodle_import'] = true;
114
        $_POST['language'] = $courseInfo['language'];
115
116
        foreach ($mainFileModuleValues as $fileInfo) {
117
            $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
118
            foreach (new RecursiveIteratorIterator($dirs) as $file) {
119
                if (!is_file($file) || false === strpos($file, $fileInfo['contenthash'])) {
120
                    continue;
121
                }
122
123
                if (isset($importedFiles[$fileInfo['filename']])) {
124
                    continue;
125
                }
126
127
                if ($debug) {
128
                    error_log($fileInfo['filename']);
129
                }
130
                $files = [];
131
                $files['file']['name'] = $fileInfo['filename'];
132
                $files['file']['tmp_name'] = $file->getPathname();
133
                $files['file']['type'] = $fileInfo['mimetype'];
134
                $files['file']['error'] = 0;
135
                $files['file']['size'] = $fileInfo['filesize'];
136
                $files['file']['from_file'] = true;
137
                $files['file']['move_file'] = true;
138
139
                $title = isset($fileInfo['title']) ? $fileInfo['title'] : pathinfo($fileInfo['filename'], PATHINFO_FILENAME);
140
141
                $data = DocumentManager::upload_document(
142
                    $files,
143
                    '/moodle',
144
                    $title,
145
                    '',
146
                    null,
147
                    'overwrite',
148
                    true,
149
                    true,
150
                    'file',
151
                    false
152
                );
153
154
                if ($data) {
155
                    $importedFiles[$fileInfo['filename']] = basename($data['path']);
156
                }
157
            }
158
        }
159
160
        $xml = @file_get_contents($destinationDir.'/moodle_backup.xml');
161
        $doc = new DOMDocument();
162
        $res = @$doc->loadXML($xml);
163
164
        if (empty($res)) {
165
            removeDir($destinationDir);
166
            unlink($filePath);
167
168
            throw new Exception(get_lang('FailedToImportThisIsNotAMoodleFile'));
169
        }
170
        $activities = $doc->getElementsByTagName('activity');
171
172
        require_once api_get_path(SYS_CODE_PATH).'forum/forumfunction.inc.php';
173
174
        if ($debug) {
175
            error_log('Loading activities: '.count($activities));
176
        }
177
178
        foreach ($activities as $activity) {
179
            if (empty($activity->childNodes->length)) {
180
                continue;
181
            }
182
183
            $currentItem = [];
184
            foreach ($activity->childNodes as $item) {
185
                $currentItem[$item->nodeName] = $item->nodeValue;
186
            }
187
188
            $moduleName = isset($currentItem['modulename']) ? $currentItem['modulename'] : false;
189
            if ($debug) {
190
                error_log('moduleName: '.$moduleName);
191
            }
192
193
            switch ($moduleName) {
194
                case 'forum':
195
                    $catForumValues = [];
196
                    // Read the current forum module xml.
197
                    $moduleDir = $currentItem['directory'];
198
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
199
                    $moduleValues = $this->readForumModule($moduleXml);
200
201
                    // Create a Forum category based on Moodle forum type.
202
                    $catForumValues['forum_category_title'] = $moduleValues['type'];
203
                    $catForumValues['forum_category_comment'] = '';
204
                    $catId = store_forumcategory(
205
                        $catForumValues,
206
                        $courseInfo,
207
                        false
208
                    );
209
                    $forumValues = [];
210
                    $forumValues['forum_title'] = $moduleValues['name'];
211
                    $forumValues['forum_image'] = '';
212
                    $forumValues['forum_comment'] = $moduleValues['intro'];
213
                    $forumValues['forum_category'] = $catId;
214
                    $forumValues['moderated'] = 0;
215
216
                    store_forum($forumValues, $courseInfo);
217
                    break;
218
                case 'quiz':
219
                    // Read the current quiz module xml.
220
                    // The quiz case is the very complicate process of all the import.
221
                    // Please if you want to review the script, try to see the readingXML functions.
222
                    // The readingXML functions in this clases do all the mayor work here.
223
224
                    $moduleDir = $currentItem['directory'];
225
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
226
                    $questionsXml = @file_get_contents($destinationDir.'/questions.xml');
227
                    $moduleValues = $this->readQuizModule($moduleXml);
228
229
                    // At this point we got all the prepared resources from Moodle file
230
                    // $moduleValues variable contains all the necesary info to the quiz import
231
                    // var_dump($moduleValues); // <-- uncomment this to see the final array
232
233
                    $exercise = new Exercise($courseInfo['real_id']);
234
                    if ($debug) {
235
                        error_log('quiz:'.$moduleValues['name']);
236
                    }
237
238
                    $title = Exercise::format_title_variable($moduleValues['name']);
239
                    $exercise->updateTitle($title);
240
                    $exercise->updateDescription($moduleValues['intro']);
241
                    $exercise->updateAttempts($moduleValues['attempts_number']);
242
                    $exercise->updateFeedbackType(0);
243
244
                    // Match shuffle question with chamilo
245
                    if (isset($moduleValues['shufflequestions']) &&
246
                        (int) $moduleValues['shufflequestions'] === 1
247
                    ) {
248
                        $exercise->setRandom(-1);
249
                    } else {
250
                        $exercise->setRandom(0);
251
                    }
252
                    $exercise->updateRandomAnswers(!empty($moduleValues['shuffleanswers']));
253
                    // @todo divide to minutes
254
                    $exercise->updateExpiredTime((int) $moduleValues['timelimit']);
255
256
                    if ($moduleValues['questionsperpage'] == 1) {
257
                        $exercise->updateType(2);
258
                    } else {
259
                        $exercise->updateType(1);
260
                    }
261
262
                    // Create the new Quiz
263
                    $exercise->save();
264
265
                    // Ok, we got the Quiz and create it, now its time to add the Questions
266
                    foreach ($moduleValues['question_instances'] as $index => $question) {
267
                        $questionsValues = $this->readMainQuestionsXml($questionsXml, $question['questionid']);
268
                        $moduleValues['question_instances'][$index] = $questionsValues;
269
                        // Set Question Type from Moodle XML element <qtype>
270
                        $qType = $questionsValues['qtype'];
271
                        $questionType = $this->matchMoodleChamiloQuestionTypes($questionsValues);
272
                        $questionInstance = Question::getInstance($questionType);
273
274
                        if (empty($questionInstance)) {
275
                            continue;
276
                        }
277
                        if ($debug) {
278
                            error_log('question: '.$question['questionid']);
279
                        }
280
281
                        $questionInstance->updateTitle($questionsValues['name']);
282
                        $questionText = $questionsValues['questiontext'];
283
284
                        // Replace the path from @@PLUGINFILE@@ to a correct chamilo path
285
                        $questionText = str_replace(
286
                            '@@PLUGINFILE@@',
287
                            '/courses/'.$courseInfo['path'].'/document/moodle',
288
                            $questionText
289
                        );
290
291
                        if ($importedFiles) {
292
                            $this->fixPathInText($importedFiles, $questionText);
293
                        }
294
295
                        $questionInstance->updateDescription($questionText);
296
                        $questionInstance->updateLevel(1);
297
                        $questionInstance->updateCategory(0);
298
299
                        //Save normal question if NOT media
300
                        if ($questionInstance->type != MEDIA_QUESTION) {
301
                            $questionInstance->save($exercise);
302
                            // modify the exercise
303
                            $exercise->addToList($questionInstance->id);
304
                            $exercise->update_question_positions();
305
                        }
306
307
                        $questionList = $moduleValues['question_instances'][$index]['plugin_qtype_'.$qType.'_question'];
308
                        $currentQuestion = $moduleValues['question_instances'][$index];
309
310
                        $this->processAnswers(
311
                            $exercise,
312
                            $questionList,
313
                            $qType,
314
                            $questionInstance,
315
                            $currentQuestion,
316
                            $importedFiles
317
                        );
318
                    }
319
                    break;
320
                case 'resource':
321
                    // Read the current resource module xml.
322
                    $moduleDir = $currentItem['directory'];
323
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
324
                    $filesXml = @file_get_contents($destinationDir.'/files.xml');
325
                    $moduleValues = $this->readResourceModule($moduleXml);
326
                    $mainFileModuleValues = $this->readMainFilesXml(
327
                        $filesXml,
328
                        $moduleValues['contextid']
329
                    );
330
                    $fileInfo = array_merge($moduleValues, $mainFileModuleValues, $currentItem);
331
                    $currentResourceFilePath = $destinationDir.'/files/';
332
                    $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
333
                    foreach (new RecursiveIteratorIterator($dirs) as $file) {
334
                        if (!is_file($file) || false === strpos($file, $fileInfo['contenthash'])) {
335
                            continue;
336
                        }
337
338
                        $files = [];
339
                        $files['file']['name'] = $fileInfo['filename'];
340
                        $files['file']['tmp_name'] = $file->getPathname();
341
                        $files['file']['type'] = $fileInfo['mimetype'];
342
                        $files['file']['error'] = 0;
343
                        $files['file']['size'] = $fileInfo['filesize'];
344
                        $files['file']['from_file'] = true;
345
                        $files['file']['move_file'] = true;
346
                        $_POST['language'] = $courseInfo['language'];
347
                        $_POST['moodle_import'] = true;
348
349
                        DocumentManager::upload_document(
350
                            $files,
351
                            '/moodle',
352
                            $fileInfo['title'],
353
                            '',
354
                            null,
355
                            null,
356
                            true,
357
                            true
358
                        );
359
                    }
360
361
                    break;
362
                case 'url':
363
                    // Read the current url module xml.
364
                    $moduleDir = $currentItem['directory'];
365
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
366
                    $moduleValues = $this->readUrlModule($moduleXml);
367
                    $_POST['title'] = $moduleValues['name'];
368
                    $_POST['url'] = $moduleValues['externalurl'];
369
                    $_POST['description'] = $moduleValues['intro'];
370
                    $_POST['category_id'] = 0;
371
                    $_POST['target'] = '_blank';
372
373
                    Link::addlinkcategory('link');
374
                    break;
375
            }
376
        }
377
378
        if ($debug) {
379
            error_log('Finish');
380
        }
381
382
        removeDir($destinationDir);
383
        unlink($filePath);
384
385
        return true;
386
    }
387
388
    /**
389
     * Read and validate the forum module XML.
390
     *
391
     * @param resource $moduleXml XML file
392
     *
393
     * @return mixed | array if is a valid xml file, false otherwise
394
     */
395
    public function readForumModule($moduleXml)
396
    {
397
        $moduleDoc = new DOMDocument();
398
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
399
        if (empty($moduleRes)) {
400
            return false;
401
        }
402
        $activities = $moduleDoc->getElementsByTagName('forum');
403
        $currentItem = [];
404
        foreach ($activities as $activity) {
405
            if ($activity->childNodes->length) {
406
                foreach ($activity->childNodes as $item) {
407
                    $currentItem[$item->nodeName] = $item->nodeValue;
408
                }
409
            }
410
        }
411
412
        return $currentItem;
413
    }
414
415
    /**
416
     * Read and validate the resource module XML.
417
     *
418
     * @param resource $moduleXml XML file
419
     *
420
     * @return mixed | array if is a valid xml file, false otherwise
421
     */
422
    public function readResourceModule($moduleXml)
423
    {
424
        $moduleDoc = new DOMDocument();
425
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
426
        if (empty($moduleRes)) {
427
            return false;
428
        }
429
        $activities = $moduleDoc->getElementsByTagName('resource');
430
        $mainActivity = $moduleDoc->getElementsByTagName('activity');
431
        $contextId = $mainActivity->item(0)->getAttribute('contextid');
432
        $currentItem = [];
433
        foreach ($activities as $activity) {
434
            if ($activity->childNodes->length) {
435
                foreach ($activity->childNodes as $item) {
436
                    $currentItem[$item->nodeName] = $item->nodeValue;
437
                }
438
            }
439
        }
440
441
        $currentItem['contextid'] = $contextId;
442
443
        return $currentItem;
444
    }
445
446
    /**
447
     * Read and validate the url module XML.
448
     *
449
     * @param resource $moduleXml XML file
450
     *
451
     * @return mixed | array if is a valid xml file, false otherwise
452
     */
453
    public function readUrlModule($moduleXml)
454
    {
455
        $moduleDoc = new DOMDocument();
456
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
457
        if (empty($moduleRes)) {
458
            return false;
459
        }
460
        $activities = $moduleDoc->getElementsByTagName('url');
461
        $currentItem = [];
462
        foreach ($activities as $activity) {
463
            if ($activity->childNodes->length) {
464
                foreach ($activity->childNodes as $item) {
465
                    $currentItem[$item->nodeName] = $item->nodeValue;
466
                }
467
            }
468
        }
469
470
        return $currentItem;
471
    }
472
473
    /**
474
     * Read and validate the quiz module XML.
475
     *
476
     * @param resource $moduleXml XML file
477
     *
478
     * @return mixed | array if is a valid xml file, false otherwise
479
     */
480
    public function readQuizModule($moduleXml)
481
    {
482
        $moduleDoc = new DOMDocument();
483
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
484
        if (empty($moduleRes)) {
485
            return false;
486
        }
487
        $activities = $moduleDoc->getElementsByTagName('quiz');
488
        $currentItem = [];
489
        foreach ($activities as $activity) {
490
            if ($activity->childNodes->length) {
491
                foreach ($activity->childNodes as $item) {
492
                    $currentItem[$item->nodeName] = $item->nodeValue;
493
                }
494
            }
495
        }
496
497
        $questions = $moduleDoc->getElementsByTagName('question_instance');
498
499
        $questionList = [];
500
        $counter = 0;
501
        foreach ($questions as $question) {
502
            if ($question->childNodes->length) {
503
                foreach ($question->childNodes as $item) {
504
                    $questionList[$counter][$item->nodeName] = $item->nodeValue;
505
                }
506
                $counter++;
507
            }
508
        }
509
        $currentItem['question_instances'] = $questionList;
510
511
        return $currentItem;
512
    }
513
514
    /**
515
     * Search the current file resource in main Files XML.
516
     *
517
     * @param resource $filesXml  XML file
518
     * @param int      $contextId
519
     *
520
     * @return mixed | array if is a valid xml file, false otherwise
521
     */
522
    public function readMainFilesXml($filesXml, $contextId)
523
    {
524
        $moduleDoc = new DOMDocument();
525
        $moduleRes = @$moduleDoc->loadXML($filesXml);
526
527
        if (empty($moduleRes)) {
528
            return false;
529
        }
530
531
        $activities = $moduleDoc->getElementsByTagName('file');
532
        $currentItem = [];
533
        foreach ($activities as $activity) {
534
            if (empty($activity->childNodes->length)) {
535
                continue;
536
            }
537
            $isThisItemThatIWant = false;
538
            foreach ($activity->childNodes as $item) {
539
                if (!$isThisItemThatIWant && $item->nodeName == 'contenthash') {
540
                    $currentItem['contenthash'] = $item->nodeValue;
541
                }
542
                if ($item->nodeName == 'contextid' &&
543
                    (int) $item->nodeValue == (int) $contextId &&
544
                    !$isThisItemThatIWant
545
                ) {
546
                    $isThisItemThatIWant = true;
547
                    continue;
548
                }
549
550
                if ($isThisItemThatIWant && $item->nodeName == 'filename') {
551
                    $currentItem['filename'] = $item->nodeValue;
552
                }
553
554
                if ($isThisItemThatIWant && $item->nodeName == 'filesize') {
555
                    $currentItem['filesize'] = $item->nodeValue;
556
                }
557
558
                if ($isThisItemThatIWant && $item->nodeName == 'mimetype' &&
559
                    $item->nodeValue == 'document/unknown'
560
                ) {
561
                    break;
562
                }
563
564
                if ($isThisItemThatIWant && $item->nodeName == 'mimetype' &&
565
                    $item->nodeValue !== 'document/unknown'
566
                ) {
567
                    $currentItem['mimetype'] = $item->nodeValue;
568
                    break 2;
569
                }
570
            }
571
        }
572
573
        return $currentItem;
574
    }
575
576
    /**
577
     * Search the current question resource in main Questions XML.
578
     *
579
     * @param resource $questionsXml XML file
580
     * @param int      $questionId
581
     *
582
     * @return mixed | array if is a valid xml file, false otherwise
583
     */
584
    public function readMainQuestionsXml($questionsXml, $questionId)
585
    {
586
        $moduleDoc = new DOMDocument();
587
        $moduleRes = @$moduleDoc->loadXML($questionsXml);
588
        if (empty($moduleRes)) {
589
            return false;
590
        }
591
592
        $questions = $moduleDoc->getElementsByTagName('question');
593
        $currentItem = [];
594
        foreach ($questions as $question) {
595
            if ((int) $question->getAttribute('id') !== (int) $questionId) {
596
                continue;
597
            }
598
599
            if (empty($question->childNodes->length)) {
600
                continue;
601
            }
602
603
            $currentItem['questionid'] = $questionId;
604
            $questionType = '';
605
            foreach ($question->childNodes as $item) {
606
                $currentItem[$item->nodeName] = $item->nodeValue;
607
                if ('qtype' === $item->nodeName) {
608
                    $questionType = $item->nodeValue;
609
                }
610
611
                if ($item->nodeName != 'plugin_qtype_'.$questionType.'_question') {
612
                    continue;
613
                }
614
615
                $answer = $item->getElementsByTagName($this->getQuestionTypeAnswersTag($questionType));
616
                $currentItem['plugin_qtype_'.$questionType.'_question'] = [];
617
                for ($i = 0; $i <= $answer->length - 1; $i++) {
618
                    $label = 'plugin_qtype_'.$questionType.'_question';
619
                    $currentItem[$label][$i]['answerid'] = $answer->item($i)->getAttribute('id');
620
                    foreach ($answer->item($i)->childNodes as $properties) {
621
                        $currentItem[$label][$i][$properties->nodeName] = $properties->nodeValue;
622
                    }
623
                }
624
625
                $typeValues = $item->getElementsByTagName($this->getQuestionTypeOptionsTag($questionType));
626
                for ($i = 0; $i <= $typeValues->length - 1; $i++) {
627
                    foreach ($typeValues->item($i)->childNodes as $properties) {
628
                        $currentItem[$questionType.'_values'][$properties->nodeName] = $properties->nodeValue;
629
630
                        if ($properties->nodeName !== 'sequence') {
631
                            continue;
632
                        }
633
634
                        $sequence = $properties->nodeValue;
635
                        $sequenceIds = explode(',', $sequence);
636
                        foreach ($sequenceIds as $qId) {
637
                            $questionMatch = $this->readMainQuestionsXml($questionsXml, $qId);
638
                            $currentItem['plugin_qtype_'.$questionType.'_question'][] = $questionMatch;
639
                        }
640
                    }
641
                }
642
            }
643
        }
644
645
        $this->traverseArray($currentItem, ['#text', 'question_hints', 'tags']);
646
647
        return $currentItem;
648
    }
649
650
    /**
651
     * return the correct question type options tag.
652
     *
653
     * @param string $questionType name
654
     *
655
     * @return string question type tag
656
     */
657
    public function getQuestionTypeOptionsTag($questionType)
658
    {
659
        switch ($questionType) {
660
            case 'match':
661
            case 'ddmatch':
662
                return 'matchoptions';
663
            default:
664
                return $questionType;
665
        }
666
    }
667
668
    /**
669
     * return the correct question type answers tag.
670
     *
671
     * @param string $questionType name
672
     *
673
     * @return string question type tag
674
     */
675
    public function getQuestionTypeAnswersTag($questionType)
676
    {
677
        switch ($questionType) {
678
            case 'match':
679
            case 'ddmatch':
680
                return 'match';
681
            default:
682
                return 'answer';
683
        }
684
    }
685
686
    /**
687
     * @param array Result of readMainQuestionsXml
688
     *
689
     * @return int Chamilo question type
690
     */
691
    public function matchMoodleChamiloQuestionTypes($questionsValues)
692
    {
693
        $moodleQuestionType = $questionsValues['qtype'];
694
        $questionOptions = $moodleQuestionType.'_values';
695
        // Check <single> located in <plugin_qtype_multichoice_question><multichoice><single><single>
696
        if (
697
            'multichoice' === $moodleQuestionType &&
698
            isset($questionsValues[$questionOptions]) &&
699
            isset($questionsValues[$questionOptions]['single']) &&
700
            1 === (int) $questionsValues[$questionOptions]['single']
701
        ) {
702
            return UNIQUE_ANSWER;
703
        }
704
        switch ($moodleQuestionType) {
705
            case 'multichoice':
706
                return MULTIPLE_ANSWER;
707
            case 'multianswer':
708
            case 'shortanswer':
709
            case 'match':
710
                return FILL_IN_BLANKS;
711
            case 'essay':
712
                return FREE_ANSWER;
713
            case 'truefalse':
714
                return UNIQUE_ANSWER_NO_OPTION;
715
        }
716
    }
717
718
    /**
719
     * Fix moodle files that contains spaces.
720
     *
721
     * @param array  $importedFiles
722
     * @param string $text
723
     *
724
     * @return mixed
725
     */
726
    public function fixPathInText($importedFiles, &$text)
727
    {
728
        if ($importedFiles) {
729
            foreach ($importedFiles as $old => $new) {
730
                // Ofaj fix moodle file names
731
                // In some questions moodle text contains file with name like:
732
                // Bild%20Check-In-Formular%20Ausfu%CC%88llen.jpg"
733
                // rawurlencode function transforms '' (whitespace) to %20 and so on
734
                $text = str_replace(rawurlencode($old), $new, $text);
735
            }
736
        }
737
738
        return $text;
739
    }
740
741
    /**
742
     * Process Moodle Answers to Chamilo.
743
     *
744
     * @param Exercise $exercise
745
     * @param array    $questionList
746
     * @param string   $questionType
747
     * @param Question $questionInstance Question/Answer instance
748
     * @param array    $currentQuestion
749
     * @param array    $importedFiles
750
     *
751
     * @return int db response
752
     */
753
    public function processAnswers(
754
        $exercise,
755
        $questionList,
756
        $questionType,
757
        $questionInstance,
758
        $currentQuestion,
759
        $importedFiles
760
    ) {
761
        switch ($questionType) {
762
            case 'multichoice':
763
                $objAnswer = new Answer($questionInstance->id);
764
                $questionWeighting = 0;
765
                foreach ($questionList as $slot => $answer) {
766
                    $this->processMultipleAnswer(
767
                        $objAnswer,
768
                        $answer,
769
                        $slot + 1,
770
                        $questionWeighting,
771
                        $importedFiles
772
                    );
773
                }
774
775
                // saves the answers into the data base
776
                $objAnswer->save();
777
                // sets the total weighting of the question
778
                $questionInstance->updateWeighting($questionWeighting);
779
                $questionInstance->save($exercise);
780
781
                return true;
782
            case 'multianswer':
783
                $objAnswer = new Answer($questionInstance->id);
784
                $coursePath = api_get_course_path();
785
                $placeholder = str_replace(
786
                    '@@PLUGINFILE@@',
787
                    '/courses/'.$coursePath.'/document/moodle',
788
                    $currentQuestion['questiontext']
789
                );
790
                $optionsValues = [];
791
                foreach ($questionList as $slot => $subQuestion) {
792
                    $qtype = $subQuestion['qtype'];
793
                    $optionsValues[] = $this->processFillBlanks(
794
                        $objAnswer,
795
                        $qtype,
796
                        $subQuestion['plugin_qtype_'.$qtype.'_question'],
797
                        $placeholder,
798
                        $slot + 1,
799
                        $importedFiles
800
                    );
801
                }
802
803
                $answerOptionsWeight = '::';
804
                $answerOptionsSize = '';
805
                $questionWeighting = 0;
806
                foreach ($optionsValues as $index => $value) {
807
                    $questionWeighting += $value['weight'];
808
                    $answerOptionsWeight .= $value['weight'].',';
809
                    $answerOptionsSize .= $value['size'].',';
810
                }
811
812
                $answerOptionsWeight = substr($answerOptionsWeight, 0, -1);
813
                $answerOptionsSize = substr($answerOptionsSize, 0, -1);
814
                $answerOptions = $answerOptionsWeight.':'.$answerOptionsSize.':0@';
815
                $placeholder = $placeholder.PHP_EOL.$answerOptions;
816
817
                // This is a minor trick to clean the question description that in a multianswer is the main placeholder
818
                $questionInstance->updateDescription('');
819
                // sets the total weighting of the question
820
                $questionInstance->updateWeighting($questionWeighting);
821
                $questionInstance->save($exercise);
822
                $this->fixPathInText($importedFiles, $placeholder);
823
824
                // saves the answers into the data base
825
                $objAnswer->createAnswer($placeholder, 0, '', 0, 1);
826
                $objAnswer->save();
827
828
                return true;
829
            case 'match':
830
                $objAnswer = new Answer($questionInstance->id);
831
                $placeholder = '';
832
833
                $optionsValues = $this->processFillBlanks(
834
                    $objAnswer,
835
                    'match',
836
                    $questionList,
837
                    $placeholder,
838
                    0,
839
                    $importedFiles
840
                );
841
842
                $answerOptionsWeight = '::';
843
                $answerOptionsSize = '';
844
                $questionWeighting = 0;
845
                foreach ($optionsValues as $index => $value) {
846
                    $questionWeighting += $value['weight'];
847
                    $answerOptionsWeight .= $value['weight'].',';
848
                    $answerOptionsSize .= $value['size'].',';
849
                }
850
851
                $answerOptionsWeight = substr($answerOptionsWeight, 0, -1);
852
                $answerOptionsSize = substr($answerOptionsSize, 0, -1);
853
                $answerOptions = $answerOptionsWeight.':'.$answerOptionsSize.':0@';
854
                $placeholder = $placeholder.PHP_EOL.$answerOptions;
855
856
                // sets the total weighting of the question
857
                $questionInstance->updateWeighting($questionWeighting);
858
                $questionInstance->save($exercise);
859
                // saves the answers into the database
860
                $this->fixPathInText($importedFiles, $placeholder);
861
                $objAnswer->createAnswer($placeholder, 0, '', 0, 1);
862
                $objAnswer->save();
863
864
                return true;
865
            case 'shortanswer':
866
            case 'ddmatch':
867
                $questionWeighting = $currentQuestion['defaultmark'];
868
                $questionInstance->updateWeighting($questionWeighting);
869
                $questionInstance->updateDescription(get_lang('ThisQuestionIsNotSupportedYet'));
870
                $questionInstance->save($exercise);
871
872
                return false;
873
            case 'essay':
874
                $questionWeighting = $currentQuestion['defaultmark'];
875
                $questionInstance->updateWeighting($questionWeighting);
876
                $questionInstance->save($exercise);
877
878
                return true;
879
            case 'truefalse':
880
                $objAnswer = new Answer($questionInstance->id);
881
                $questionWeighting = 0;
882
                foreach ($questionList as $slot => $answer) {
883
                    $this->processTrueFalse(
884
                        $objAnswer,
885
                        $answer,
886
                        $slot + 1,
887
                        $questionWeighting,
888
                        $importedFiles
889
                    );
890
                }
891
892
                // saves the answers into the data base
893
                $objAnswer->save();
894
                // sets the total weighting of the question
895
                $questionInstance->updateWeighting($questionWeighting);
896
                $questionInstance->save($exercise);
897
898
                return false;
899
            default:
900
                return false;
901
        }
902
    }
903
904
    /**
905
     * Process Chamilo Unique Answer.
906
     *
907
     * @param object $objAnswer
908
     * @param array  $answerValues
909
     * @param int    $position
910
     * @param int    $questionWeighting
911
     * @param array  $importedFiles
912
     *
913
     * @return int db response
914
     */
915
    public function processUniqueAnswer(
916
        $objAnswer,
917
        $answerValues,
918
        $position,
919
        &$questionWeighting,
920
        $importedFiles
921
    ) {
922
        $correct = (int) $answerValues['fraction'] ? (int) $answerValues['fraction'] : 0;
923
        $answer = $answerValues['answertext'];
924
        $comment = $answerValues['feedback'];
925
        $weighting = $answerValues['fraction'];
926
        $weighting = abs($weighting);
927
        if ($weighting > 0) {
928
            $questionWeighting += $weighting;
929
        }
930
        $goodAnswer = $correct ? true : false;
931
        $this->fixPathInText($importedFiles, $answer);
932
933
        $objAnswer->createAnswer(
934
            $answer,
935
            $goodAnswer,
936
            $comment,
937
            $weighting,
938
            $position,
939
            null,
940
            null,
941
            ''
942
        );
943
    }
944
945
    public function processMultipleAnswer(
946
        Answer $objAnswer,
947
        $answerValues,
948
        $position,
949
        &$questionWeighting,
950
        $importedFiles
951
    ) {
952
        $answer = $answerValues['answertext'];
953
        $comment = $answerValues['feedback'];
954
        $weighting = $answerValues['fraction'];
955
        //$weighting = abs($weighting);
956
        if ($weighting > 0) {
957
            $questionWeighting += $weighting;
958
        }
959
        $goodAnswer = $weighting > 0;
960
961
        $this->fixPathInText($importedFiles, $answer);
962
963
        $objAnswer->createAnswer(
964
            $answer,
965
            $goodAnswer,
966
            $comment,
967
            $weighting,
968
            $position,
969
            null,
970
            null,
971
            ''
972
        );
973
    }
974
975
    /**
976
     * Process Chamilo True False.
977
     *
978
     * @param Answer $objAnswer
979
     * @param array  $answerValues
980
     * @param int    $position
981
     * @param int    $questionWeighting
982
     * @param array  $importedFiles
983
     *
984
     * @return int db response
985
     */
986
    public function processTrueFalse(
987
        $objAnswer,
988
        $answerValues,
989
        $position,
990
        &$questionWeighting,
991
        $importedFiles
992
    ) {
993
        $correct = (int) $answerValues['fraction'] ? (int) $answerValues['fraction'] : 0;
994
        $answer = $answerValues['answertext'];
995
        $comment = $answerValues['feedback'];
996
        $weighting = $answerValues['fraction'];
997
        $weighting = abs($weighting);
998
        if ($weighting > 0) {
999
            $questionWeighting += $weighting;
1000
        }
1001
        $goodAnswer = $correct ? true : false;
1002
1003
        $this->fixPathInText($importedFiles, $answer);
1004
1005
        $objAnswer->createAnswer(
1006
            $answer,
1007
            $goodAnswer,
1008
            $comment,
1009
            $weighting,
1010
            $position,
1011
            null,
1012
            null,
1013
            ''
1014
        );
1015
    }
1016
1017
    /**
1018
     * Process Chamilo FillBlanks.
1019
     *
1020
     * @param object $objAnswer
1021
     * @param array  $questionType
1022
     * @param array  $answerValues
1023
     * @param string $placeholder
1024
     * @param int    $position
1025
     * @param array  $importedFiles
1026
     *
1027
     * @return int db response
1028
     */
1029
    public function processFillBlanks(
1030
        $objAnswer,
1031
        $questionType,
1032
        $answerValues,
1033
        &$placeholder,
1034
        $position,
1035
        $importedFiles
1036
    ) {
1037
        $coursePath = api_get_course_path();
1038
1039
        switch ($questionType) {
1040
            case 'multichoice':
1041
                $optionsValues = [];
1042
                $correctAnswer = '';
1043
                $othersAnswer = '';
1044
                foreach ($answerValues as $answer) {
1045
                    $correct = (int) $answer['fraction'];
1046
                    if ($correct) {
1047
                        $correctAnswer .= $answer['answertext'].'|';
1048
                        $optionsValues['weight'] = $answer['fraction'];
1049
                        $optionsValues['size'] = '200';
1050
                    } else {
1051
                        $othersAnswer .= $answer['answertext'].'|';
1052
                    }
1053
                }
1054
                $currentAnswers = $correctAnswer.$othersAnswer;
1055
                $currentAnswers = '['.substr($currentAnswers, 0, -1).']';
1056
                $placeholder = str_replace("{#$position}", $currentAnswers, $placeholder);
1057
1058
                return $optionsValues;
1059
            case 'shortanswer':
1060
                $optionsValues = [];
1061
                $correctAnswer = '';
1062
                foreach ($answerValues as $answer) {
1063
                    $correct = (int) $answer['fraction'];
1064
                    if ($correct) {
1065
                        $correctAnswer .= $answer['answertext'];
1066
                        $optionsValues['weight'] = $answer['fraction'];
1067
                        $optionsValues['size'] = '200';
1068
                    }
1069
                }
1070
1071
                $currentAnswers = '['.$correctAnswer.']';
1072
                $placeholder = str_replace("{#$position}", $currentAnswers, $placeholder);
1073
1074
                return $optionsValues;
1075
            case 'match':
1076
                $answers = [];
1077
                // Here first we need to extract all the possible answers
1078
                foreach ($answerValues as $slot => $answer) {
1079
                    $answers[$slot] = $answer['answertext'];
1080
                }
1081
1082
                // Now we set the order of the values matching the correct answer and set it to the first element
1083
                $optionsValues = [];
1084
                foreach ($answerValues as $slot => $answer) {
1085
                    $correctAnswer = '';
1086
                    $othersAnswers = '';
1087
                    $correctAnswer .= $answer['answertext'].'|';
1088
1089
                    foreach ($answers as $other) {
1090
                        if ($other !== $answer['answertext']) {
1091
                            $othersAnswers .= $other.'|';
1092
                        }
1093
                    }
1094
1095
                    $optionsValues[$slot]['weight'] = 1;
1096
                    $optionsValues[$slot]['size'] = '200';
1097
1098
                    $currentAnswers = htmlentities($correctAnswer.$othersAnswers);
1099
                    $currentAnswers = '['.substr($currentAnswers, 0, -1).'] ';
1100
                    $answer['questiontext'] = str_replace(
1101
                        '@@PLUGINFILE@@',
1102
                        '/courses/'.$coursePath.'/document/moodle',
1103
                        $answer['questiontext']
1104
                    );
1105
1106
                    $placeholder .= '<p> '.strip_tags($answer['questiontext']).' '.$currentAnswers.' </p>';
1107
                }
1108
1109
                return $optionsValues;
1110
            default:
1111
                return false;
1112
        }
1113
    }
1114
1115
    /**
1116
     * get All files associated with a question.
1117
     *
1118
     * @param $filesXml
1119
     *
1120
     * @return array
1121
     */
1122
    public function getAllQuestionFiles($filesXml)
1123
    {
1124
        $moduleDoc = new DOMDocument();
1125
        $moduleRes = @$moduleDoc->loadXML($filesXml);
1126
1127
        if (empty($moduleRes)) {
1128
            return [];
1129
        }
1130
1131
        $allFiles = [];
1132
        $activities = $moduleDoc->getElementsByTagName('file');
1133
        foreach ($activities as $activity) {
1134
            $currentItem = [];
1135
            $thisIsAnInvalidItem = false;
1136
1137
            if ($activity->childNodes->length) {
1138
                foreach ($activity->childNodes as $item) {
1139
                    if ($item->nodeName == 'component' && $item->nodeValue == 'mod_resource') {
1140
                        $thisIsAnInvalidItem = true;
1141
                    }
1142
1143
                    if ($item->nodeName == 'contenthash') {
1144
                        $currentItem['contenthash'] = $item->nodeValue;
1145
                    }
1146
1147
                    if ($item->nodeName == 'filename') {
1148
                        $currentItem['filename'] = $item->nodeValue;
1149
                    }
1150
1151
                    if ($item->nodeName == 'filesize') {
1152
                        $currentItem['filesize'] = $item->nodeValue;
1153
                    }
1154
1155
                    if ($item->nodeName == 'mimetype' && $item->nodeValue == 'document/unknown') {
1156
                        $thisIsAnInvalidItem = true;
1157
                    }
1158
1159
                    if ($item->nodeName == 'mimetype' && $item->nodeValue !== 'document/unknown') {
1160
                        $currentItem['mimetype'] = $item->nodeValue;
1161
                    }
1162
                }
1163
            }
1164
1165
            if (!$thisIsAnInvalidItem) {
1166
                $allFiles[] = $currentItem;
1167
            }
1168
        }
1169
1170
        return $allFiles;
1171
    }
1172
1173
    /**
1174
     * Litle utility to delete the unuseful tags.
1175
     *
1176
     * @param $array
1177
     * @param $keys
1178
     */
1179
    public function traverseArray(&$array, $keys)
1180
    {
1181
        foreach ($array as $key => &$value) {
1182
            if (is_array($value)) {
1183
                $this->traverseArray($value, $keys);
1184
            } else {
1185
                if (in_array($key, $keys)) {
1186
                    unset($array[$key]);
1187
                }
1188
            }
1189
        }
1190
    }
1191
}
1192