MoodleImport::updateLpItemPreviousId()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 2
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
        // This process will upload all question resource files
89
        $filesXml = @file_get_contents($destinationDir.'/files.xml');
90
        $mainFileModuleValues = $this->getAllQuestionFiles($filesXml);
91
        $currentResourceFilePath = $destinationDir.'/files/';
92
        $importedFiles = [];
93
        if ($debug) {
94
            error_log('loading files');
95
        }
96
97
        $_POST['moodle_import'] = true;
98
        $_POST['language'] = $courseInfo['language'];
99
100
        $modScormFileZips = [];
101
        $allFiles = [];
102
        foreach ($mainFileModuleValues as $fileInfo) {
103
            $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
104
            foreach (new RecursiveIteratorIterator($dirs) as $file) {
105
                if (!is_file($file) || false === strpos($file, $fileInfo['contenthash'])) {
106
                    continue;
107
                }
108
109
                if (isset($importedFiles[$fileInfo['filename']])) {
110
                    continue;
111
                }
112
113
                if ($debug) {
114
                    error_log($fileInfo['filename']);
115
                }
116
                $files = [];
117
                $files['file']['name'] = $fileInfo['filename'];
118
                $files['file']['tmp_name'] = $file->getPathname();
119
                $files['file']['type'] = $fileInfo['mimetype'];
120
                $files['file']['error'] = 0;
121
                $files['file']['size'] = $fileInfo['filesize'];
122
                $files['file']['from_file'] = true;
123
                $files['file']['move_file'] = true;
124
125
                if (isset($fileInfo['modscorm']) && true === $fileInfo['modscorm']) {
126
                    if ('application/zip' == $fileInfo['mimetype']) {
127
                        $modScormFileZips[$fileInfo['contenthash']] = $files;
128
                    }
129
                    continue;
130
                }
131
                $allFiles[$fileInfo['contextid']][] = $files;
132
            }
133
        }
134
135
        $xml = @file_get_contents($destinationDir.'/moodle_backup.xml');
136
        $doc = new DOMDocument();
137
        $res = @$doc->loadXML($xml);
138
139
        if (empty($res)) {
140
            removeDir($destinationDir);
141
            unlink($filePath);
142
143
            throw new Exception(get_lang('FailedToImportThisIsNotAMoodleFile'));
144
        }
145
146
        // It process the sections as learnpaths
147
        $sections = $this->readSections($xml, $destinationDir);
148
        $activities = $doc->getElementsByTagName('activity');
149
        $sectionLpValues = $this->processSections($sections, $activities);
150
151
        require_once api_get_path(SYS_CODE_PATH).'forum/forumfunction.inc.php';
152
153
        if ($debug) {
154
            error_log('Loading activities: '.count($activities));
155
        }
156
157
        $previousItemId = 0;
158
        $lpItemId = null;
159
        foreach ($activities as $activity) {
160
            if (empty($activity->childNodes->length)) {
161
                continue;
162
            }
163
164
            $currentItem = [];
165
            foreach ($activity->childNodes as $item) {
166
                $currentItem[$item->nodeName] = $item->nodeValue;
167
            }
168
169
            $moduleName = isset($currentItem['modulename']) ? $currentItem['modulename'] : false;
170
            if ($debug) {
171
                error_log('moduleName: '.$moduleName);
172
            }
173
174
            switch ($moduleName) {
175
                case 'lesson':
176
                    $moduleDir = $currentItem['directory'];
177
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
178
                    $moduleValues = $this->readLessonModule($moduleXml);
179
                    $this->processLesson($moduleValues, $allFiles);
180
                    break;
181
                case 'assign':
182
                    $moduleDir = $currentItem['directory'];
183
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
184
                    $moduleValues = $this->readAssignModule($moduleXml);
185
                    $sectionPath = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionPath'] : '';
186
                    $assignId = $this->processAssignment($moduleValues, $allFiles, $sectionPath);
187
188
                    // It is added as item in Learnpath
189
                    if (!empty($currentItem['sectionid']) && !empty($assignId)) {
190
                        $lpItemId = $this->processSectionItem($sectionLpValues[$currentItem['sectionid']]['lpId'], 'student_publication', $assignId, $moduleValues['name'], $previousItemId);
191
                    }
192
                    break;
193
                case 'scorm':
194
                    $moduleDir = $currentItem['directory'];
195
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
196
                    $moduleValues = $this->readScormModule($moduleXml);
197
                    $this->processScorm($moduleValues, $modScormFileZips);
198
                    break;
199
                case 'glossary':
200
                    $moduleDir = $currentItem['directory'];
201
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
202
                    $moduleValues = $this->readGlossaryModule($moduleXml, $currentItem['moduleid']);
203
                    $this->processGlossary($moduleValues, $currentItem['moduleid'], $allFiles, '');
204
                    break;
205
                case 'label':
206
                    $moduleDir = $currentItem['directory'];
207
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
208
                    $moduleValues = $this->readHtmlModule($moduleXml, $moduleName);
209
                    $sectionPath = isset($currentItem['sectionid']) ? '/'.$sectionLpValues[$currentItem['sectionid']]['sectionPath'].'/' : '/';
210
                    $contextId = $moduleValues['attributes']['contextid'];
211
                    if (isset($currentItem['sectionid'])) {
212
                        $sectionLp = $sectionLpValues[$currentItem['sectionid']];
213
                        $lpId = $sectionLp['lpId'];
214
                        $chapterTitle = $moduleValues['name'] ?? 'Capítulo sin título';
215
                        if (!empty($lpId)) {
216
                            $lp = new \learnpath(
217
                                api_get_course_id(),
218
                                $lpId,
219
                                api_get_user_id()
220
                            );
221
                            $lpItemId = $lp->add_item(
222
                                0,
223
                                $previousItemId,
224
                                'dir',
225
                                0,
226
                                $chapterTitle,
227
                                '',
228
                                0,
229
                                0,
230
                                0,
231
                                0
232
                            );
233
                        }
234
                    } else {
235
                        if (isset($allFiles[$contextId])) {
236
                            $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
237
                        }
238
                        $documentId = $this->processHtmlDocument($moduleValues, $moduleName, $importedFiles, $sectionPath);
239
                        if (!empty($currentItem['sectionid']) && !empty($documentId)) {
240
                            $lpItemId = $this->processSectionItem($sectionLpValues[$currentItem['sectionid']]['lpId'], 'document', $documentId, $moduleValues['name'], $previousItemId);
241
                        }
242
                    }
243
                    break;
244
                case 'page':
245
                    $moduleDir = $currentItem['directory'];
246
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
247
                    $moduleValues = $this->readHtmlModule($moduleXml, $moduleName);
248
                    $sectionPath = isset($currentItem['sectionid']) ? '/'.$sectionLpValues[$currentItem['sectionid']]['sectionPath'].'/' : '/';
249
                    $contextId = $moduleValues['attributes']['contextid'];
250
                    if (isset($allFiles[$contextId])) {
251
                        $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
252
                    }
253
                    $documentId = $this->processHtmlDocument($moduleValues, $moduleName, $importedFiles, $sectionPath);
254
255
                    // It is added as item in Learnpath
256
                    if (!empty($currentItem['sectionid']) && !empty($documentId)) {
257
                        $lpItemId = $this->processSectionItem($sectionLpValues[$currentItem['sectionid']]['lpId'], 'document', $documentId, $moduleValues['name'], $previousItemId);
258
                    }
259
                    break;
260
                case 'forum':
261
                    $catForumValues = [];
262
                    // Read the current forum module xml.
263
                    $moduleDir = $currentItem['directory'];
264
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
265
                    $moduleValues = $this->readForumModule($moduleXml);
266
                    $sectionPath = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionPath'] : '';
267
                    $contextId = $moduleValues['attributes']['contextid'];
268
                    if (isset($allFiles[$contextId])) {
269
                        $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
270
                    }
271
272
                    // Create a Forum category based on Moodle forum type.
273
                    $catForumValues['forum_category_title'] = $moduleValues['type'];
274
                    $catForumValues['forum_category_comment'] = '';
275
                    $catId = store_forumcategory(
276
                        $catForumValues,
277
                        $courseInfo,
278
                        false
279
                    );
280
                    $forumValues = [];
281
                    $forumValues['forum_title'] = $moduleValues['name'];
282
                    $forumValues['forum_image'] = '';
283
                    $moduleValues['intro'] = $this->replaceMoodleChamiloCoursePath($moduleValues['intro'], $sectionPath);
284
                    if ($importedFiles) {
285
                        $this->fixPathInText($importedFiles, $moduleValues['intro']);
286
                    }
287
                    $forumValues['forum_comment'] = $moduleValues['intro'];
288
                    $forumValues['forum_category'] = $catId;
289
                    $forumValues['moderated'] = 0;
290
291
                    $forumId = store_forum($forumValues, $courseInfo, true);
292
                    if (!empty($moduleValues['discussions'])) {
293
                        $forum = get_forums($forumId);
294
                        foreach ($moduleValues['discussions'] as $discussion) {
295
                            $moduleValues['intro'] = $this->replaceMoodleChamiloCoursePath($moduleValues['intro'], $sectionPath);
296
                            if ($importedFiles) {
297
                                $this->fixPathInText($importedFiles, $moduleValues['intro']);
298
                            }
299
                            $postText = '';
300
                            if (!empty($discussion['posts'])) {
301
                                $postText = $discussion['posts'][0]['message'];
302
                                $postText = $this->replaceMoodleChamiloCoursePath($postText, $sectionPath);
303
                                if ($importedFiles) {
304
                                    $this->fixPathInText($importedFiles, $postText);
305
                                }
306
                            }
307
                            store_thread(
308
                                $forum,
309
                                [
310
                                    'forum_id' => $forumId,
311
                                    'thread_id' => 0,
312
                                    'gradebook' => 0,
313
                                    'post_title' => $discussion['name'],
314
                                    'post_text' => $postText,
315
                                    'category_id' => 1,
316
                                    'numeric_calification' => 0,
317
                                    'calification_notebook_title' => 0,
318
                                    'weight_calification' => 0.00,
319
                                    'thread_peer_qualify' => 0,
320
                                    'lp_item_id' => 0,
321
                                ],
322
                                [],
323
                                false
324
                            );
325
                        }
326
                    }
327
                    // It is added as item in Learnpath
328
                    if (!empty($currentItem['sectionid']) && !empty($forumId)) {
329
                        $lpItemId = $this->processSectionItem($sectionLpValues[$currentItem['sectionid']]['lpId'], 'forum', $forumId, $moduleValues['name'], $previousItemId);
330
                    }
331
                    break;
332
                case 'quiz':
333
                    // Read the current quiz module xml.
334
                    // The quiz case is the very complicate process of all the import.
335
                    // Please if you want to review the script, try to see the readingXML functions.
336
                    // The readingXML functions in this clases do all the mayor work here.
337
338
                    $moduleDir = $currentItem['directory'];
339
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
340
                    $questionsXml = @file_get_contents($destinationDir.'/questions.xml');
341
342
                    // Detect version of Moodle.
343
                    $version = $this->detectMoodleVersion($destinationDir);
344
                    if ($version === 4) {
345
                        // If version is 4, use the V4 method to read quiz and questions.
346
                        $moduleValues = $this->readQuizModuleV4($moduleXml);
347
                        $questionValuesList = $this->readMainQuestionsXmlV4($questionsXml, $moduleXml);
348
                    } else {
349
                        // Otherwise use the standard method for older versions.
350
                        $moduleValues = $this->readQuizModule($moduleXml);
351
                        $questionValuesList = [];
352
                        foreach ($moduleValues['question_instances'] as $index => $question) {
353
                            $questionValuesList[] = $this->readMainQuestionsXml($questionsXml, $question['questionid']);
354
                        }
355
                    }
356
357
                    $sectionPath = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionPath'] : '';
358
                    $contextId = $moduleValues['attributes']['contextid'];
359
                    if (isset($allFiles[$contextId])) {
360
                        $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
361
                    }
362
                    // At this point we got all the prepared resources from Moodle file
363
                    // $moduleValues variable contains all the necesary info to the quiz import
364
                    // var_dump($moduleValues); // <-- uncomment this to see the final array
365
366
                    // Create exercise (quiz)
367
                    $exercise = new Exercise($courseInfo['real_id']);
368
                    if ($debug) {
369
                        error_log('quiz:'.$moduleValues['name']);
370
                    }
371
372
                    $title = Exercise::format_title_variable($moduleValues['name']);
373
                    $exercise->updateTitle($title);
374
                    $introText = $this->replaceMoodleChamiloCoursePath($moduleValues['intro'], $sectionPath);
375
                    $exercise->updateDescription($introText);
376
                    $exercise->updateAttempts($moduleValues['attempts_number']);
377
378
                    // Set feedback type depending on behavior
379
                    $feedbackType = 2;
380
                    if (in_array($moduleValues['preferredbehaviour'], ['adaptive', 'adaptivenopenalty'])) {
381
                        $feedbackType = 1;
382
                    } elseif (in_array($moduleValues['preferredbehaviour'], ['immediatefeedback', 'immediatecbm'])) {
383
                        $feedbackType = 3;
384
                    } elseif ('deferredfeedback' === $moduleValues['preferredbehaviour']) {
385
                        $feedbackType = 0;
386
                    }
387
                    $exercise->updateFeedbackType($feedbackType);
388
389
                    // Shuffle questions and answers
390
                    $exercise->setRandom((int) $moduleValues['shufflequestions'] === 1 ? -1 : 0);
391
                    $exercise->updateRandomAnswers(!empty($moduleValues['shuffleanswers']));
392
393
                    // Set time limit and grading
394
                    $limeLimit = !empty($moduleValues['timelimit']) ? round($moduleValues['timelimit'] / 60) : 0;
395
                    $exercise->updateExpiredTime((int) $limeLimit);
396
397
                    $gradesXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/grades.xml');
398
                    $gradeQuizValues = $this->readQuizGradeModule($gradesXml, $moduleValues['quiz_id']);
399
                    $exercise->pass_percentage = !empty($gradeQuizValues['grademax']) ? round(($gradeQuizValues['gradepass'] * 100) / $gradeQuizValues['grademax']) : 0;
400
401
                    // Set type (single question per page or all in one)
402
                    $exercise->updateType((int) $moduleValues['questionsperpage'] === 1 ? 2 : 1);
403
404
                    // Set start and end times
405
                    $exercise->start_time = !empty($moduleValues['timeopen']) ? api_get_utc_datetime($moduleValues['timeopen']) : null;
406
                    $exercise->end_time = !empty($moduleValues['timeclose']) ? api_get_utc_datetime($moduleValues['timeclose']) : null;
407
408
                    // Save the quiz
409
                    $exercise->save();
410
411
                    // Process and import questions (shared code for both versions)
412
                    foreach ($questionValuesList as $index => $questionValues) {
413
                        // Set Question Type from Moodle XML element <qtype>
414
                        $qType = $questionValues['qtype'];
415
                        $questionType = $this->matchMoodleChamiloQuestionTypes($questionValues);
416
                        $questionInstance = Question::getInstance($questionType);
417
                        if (empty($questionInstance)) {
418
                            continue;
419
                        }
420
                        if ($debug) {
421
                            error_log('question: '.$questionValues['questionid']);
422
                        }
423
424
                        $questionInstance->updateTitle($questionValues['name']);
425
                        $questionText = $this->replaceMoodleChamiloCoursePath($questionValues['questiontext'], $sectionPath);
426
427
                        if ($importedFiles) {
428
                            $this->fixPathInText($importedFiles, $questionText);
429
                        }
430
431
                        $questionInstance->updateDescription($questionText);
432
                        $questionInstance->updateLevel(1);
433
                        $questionInstance->updateCategory(0);
434
435
                        //Save normal question if NOT media
436
                        if ($questionInstance->type != MEDIA_QUESTION) {
437
                            $questionInstance->save($exercise);
438
                            // modify the exercise
439
                            $exercise->addToList($questionInstance->iid);
440
                            $exercise->update_question_positions();
441
                        }
442
443
                        // Now process the answers for the question
444
                        $questionList = $questionValues['plugin_qtype_'.$qType.'_question'];
445
                        $this->processAnswers($exercise, $questionList, $qType, $questionInstance, $questionValues, $importedFiles, $sectionPath);
446
                    }
447
448
                    // Add to learnpath if applicable
449
                    if (!empty($currentItem['sectionid']) && !empty($exercise->iid)) {
450
                        $lpItemId = $this->processSectionItem($sectionLpValues[$currentItem['sectionid']]['lpId'], 'quiz', $exercise->iid, $title, $previousItemId);
451
                    }
452
                    break;
453
                case 'folder':
454
                    $moduleDir = $currentItem['directory'];
455
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
456
                    $filesXml = @file_get_contents($destinationDir.'/files.xml');
457
                    $moduleValues = $this->readFolderModule($moduleXml);
458
                    $mainFileModuleValues = $this->readFolderModuleFilesXml(
459
                        $filesXml,
460
                        $moduleValues['contextid']
461
                    );
462
                    $resourcesFiles = [];
463
                    $currentResourceFilePath = $destinationDir.'/files/';
464
                    $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
465
                    foreach (new RecursiveIteratorIterator($dirs) as $file) {
466
                        foreach ($mainFileModuleValues['files'] as $info) {
467
                            if (!is_file($file) || false === strpos($file, $info['contenthash'])) {
468
                                continue;
469
                            }
470
                            $files = [];
471
                            $files['file']['name'] = $info['filename'];
472
                            $files['file']['tmp_name'] = $file->getPathname();
473
                            $files['file']['type'] = $info['mimetype'];
474
                            $files['file']['error'] = 0;
475
                            $files['file']['size'] = $info['filesize'];
476
                            $files['file']['from_file'] = true;
477
                            $files['file']['move_file'] = true;
478
                            $files['file']['filepath'] = $info['filepath'];
479
                            $resourcesFiles[] = $files;
480
                        }
481
                    }
482
                    $sectionPath = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionPath'] : '';
483
                    $lpId = (int) $sectionLpValues[$currentItem['sectionid']]['lpId'];
484
                    $this->processSectionFolderModule($mainFileModuleValues, $sectionPath, $moduleValues['name'], $resourcesFiles, $lpId, $previousItemId);
485
486
                    break;
487
                case 'resource':
488
                    // Read the current resource module xml.
489
                    $moduleDir = $currentItem['directory'];
490
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
491
                    $filesXml = @file_get_contents($destinationDir.'/files.xml');
492
                    $moduleValues = $this->readResourceModule($moduleXml);
493
                    $sectionPath = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionPath'] : '';
494
                    $mainFileModuleValues = $this->readMainFilesXml(
495
                        $filesXml,
496
                        $moduleValues['contextid']
497
                    );
498
                    $resourcesFiles = [];
499
                    $fileInfo = array_merge($moduleValues, $mainFileModuleValues, $currentItem);
500
                    $currentResourceFilePath = $destinationDir.'/files/';
501
                    $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
502
                    foreach (new RecursiveIteratorIterator($dirs) as $file) {
503
                        if (!is_file($file) || false === strpos($file, $fileInfo['contenthash'])) {
504
                            continue;
505
                        }
506
507
                        $files = [];
508
                        $files['file']['name'] = $fileInfo['filename'];
509
                        $files['file']['tmp_name'] = $file->getPathname();
510
                        $files['file']['type'] = $fileInfo['mimetype'];
511
                        $files['file']['error'] = 0;
512
                        $files['file']['size'] = $fileInfo['filesize'];
513
                        $files['file']['from_file'] = true;
514
                        $files['file']['move_file'] = true;
515
                        $_POST['language'] = $courseInfo['language'];
516
                        $_POST['moodle_import'] = true;
517
518
                        $resourcesFiles[] = $files;
519
                    }
520
                    if (!empty($resourcesFiles)) {
521
                        $lpId = 0;
522
                        if (!empty($currentItem['sectionid'])) {
523
                            $lpId = $sectionLpValues[$currentItem['sectionid']]['lpId'];
524
                        }
525
                        $importedFiles = $this->processSectionMultimedia($resourcesFiles, $sectionPath, $lpId, $previousItemId);
526
                    }
527
528
                    break;
529
                case 'url':
530
                    // Read the current url module xml.
531
                    $moduleDir = $currentItem['directory'];
532
                    $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
533
                    $moduleValues = $this->readUrlModule($moduleXml);
534
                    $sectionPath = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionPath'] : '';
535
                    $sectionName = isset($sectionLpValues[$currentItem['sectionid']]) ? $sectionLpValues[$currentItem['sectionid']]['sectionName'] : '';
536
                    $categoryId = 0;
537
                    if (!empty($sectionName)) {
538
                        $category = Link::getCategoryByName($sectionName);
539
                        if (!empty($category)) {
540
                            $categoryId = $category['iid'];
541
                        } else {
542
                            $_POST['category_title'] = $sectionName;
543
                            $_POST['description'] = '';
544
                            $categoryId = Link::addlinkcategory('category');
545
                        }
546
                    }
547
                    $contextId = $moduleValues['attributes']['contextid'];
548
                    if (isset($allFiles[$contextId])) {
549
                        $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
550
                    }
551
                    $_POST['title'] = $moduleValues['name'];
552
                    $_POST['url'] = $moduleValues['externalurl'];
553
                    $moduleValues['intro'] = $this->replaceMoodleChamiloCoursePath($moduleValues['intro'], $sectionPath);
554
                    if ($importedFiles) {
555
                        $this->fixPathInText($importedFiles, $moduleValues['intro']);
556
                    }
557
                    $_POST['description'] = strip_tags($moduleValues['intro']);
558
                    $_POST['category_id'] = $categoryId;
559
                    $_POST['target'] = '_blank';
560
561
                    $linkId = Link::addlinkcategory('link');
562
                    // It is added as item in Learnpath
563
                    if (!empty($currentItem['sectionid']) && !empty($linkId)) {
564
                        $lpItemId = $this->processSectionItem($sectionLpValues[$currentItem['sectionid']]['lpId'], 'link', $linkId, $moduleValues['name'], $previousItemId);
565
                    }
566
                    break;
567
            }
568
569
            if (!empty($previousItemId)) {
570
                $this->updateLpItemNextId($previousItemId, $lpItemId);
571
                $this->updateLpItemPreviousId($lpItemId, $previousItemId);
572
            }
573
            $previousItemId = $lpItemId;
574
        }
575
576
        if (!empty($sectionLpValues)) {
577
            foreach ($sectionLpValues as $section) {
578
                if (!empty($section['sectionPath'])) {
579
                    $documentPath = '/'.$section['sectionPath'];
580
                    $baseWorkDir = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
581
                    $checkDir = $baseWorkDir.$documentPath;
582
                    if ($this->isEmptyDir($checkDir)) {
583
                        $document = DocumentManager::getDocumentByPathInCourse($courseInfo, $documentPath);
584
                        my_delete($checkDir);
585
                        // Hard delete.
586
                        DocumentManager::deleteDocumentFromDb(
587
                            $document[0]['iid'],
588
                            $courseInfo,
589
                            api_get_session_id(),
590
                            true
591
                        );
592
                    }
593
                }
594
            }
595
        }
596
597
        if ($debug) {
598
            error_log('Finish');
599
        }
600
601
        removeDir($destinationDir);
602
        unlink($filePath);
603
604
        return true;
605
    }
606
607
    /**
608
     * Detects the Moodle version from the backup XML file.
609
     *
610
     * @param string $destinationDir The directory where the moodle_backup.xml is located.
611
     *
612
     * @return int Returns 4 if the Moodle version is 4 or higher, otherwise returns 3.
613
     */
614
    public function detectMoodleVersion($destinationDir): int
615
    {
616
        $moodleBackupXmlPath = $destinationDir.'/moodle_backup.xml';
617
        $xml = file_get_contents($moodleBackupXmlPath);
618
619
        $doc = new DOMDocument();
620
        $doc->loadXML($xml);
621
        $backupRelease = $doc->getElementsByTagName('backup_release');
622
        $version = null;
623
        foreach ($backupRelease as $release) {
624
            $version = (float) $release->nodeValue;
625
        }
626
627
        return $version >= 4 ? 4 : 3;
628
    }
629
630
    /**
631
     * Replace the path from @@PLUGINFILE@@ to a correct chamilo path.
632
     *
633
     * @param $text
634
     * @param string $sectionPath
635
     *
636
     * @return string
637
     */
638
    public function replaceMoodleChamiloCoursePath($text, $sectionPath = '')
639
    {
640
        if (!empty($sectionPath)) {
641
            $sectionPath = '/'.$sectionPath;
642
        }
643
        $multimediaPath = $sectionPath.'/Multimedia';
644
        $coursePath = api_get_course_path();
645
        $text = str_replace(
646
            '@@PLUGINFILE@@',
647
            '/courses/'.$coursePath.'/document'.$multimediaPath,
648
            $text
649
        );
650
651
        return $text;
652
    }
653
654
    /**
655
     * Adds an item to a learning path section and processes its relationships.
656
     *
657
     * @param int      $lpId           The ID of the learning path.
658
     * @param string   $itemType       The type of the item (e.g., quiz, document).
659
     * @param int      $itemId         The ID of the item to be added.
660
     * @param string   $itemTitle      The title of the item.
661
     * @param int|null $previousItemId The ID of the previous item (optional).
662
     *
663
     * @return int The ID of the newly added learning path item.
664
     */
665
    public function processSectionItem($lpId, $itemType, $itemId, $itemTitle, $previousItemId = null): int
666
    {
667
        $lp = new \learnpath(
668
            api_get_course_id(),
669
            $lpId,
670
            api_get_user_id()
671
        );
672
673
        $lpItemId = $lp->add_item(
674
            0,
675
            $previousItemId,
676
            $itemType,
677
            $itemId,
678
            $itemTitle,
679
            '',
680
            0,
681
            0,
682
            0,
683
            0
684
        );
685
686
        return $lpItemId;
687
    }
688
689
    /**
690
     * It adds the section module as learnpath.
691
     *
692
     * @param $sections
693
     *
694
     * @return array|false
695
     */
696
    public function processSections($sections, $activities)
697
    {
698
        if (empty($sections)) {
699
            return false;
700
        }
701
702
        $courseInfo = api_get_course_info();
703
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
704
        $i = 1;
705
        $lpAdded = [];
706
        foreach ($sections as $sectionId => $section) {
707
            $countSectionActivities = $this->countSectionActivities($activities, $sectionId);
708
            if ($countSectionActivities > 0) {
709
                $lpName = $section['name'];
710
                if ('$@NULL@$' == $lpName) {
711
                    $lpName = get_lang('Topic').' '.$i;
712
                }
713
                $lpDescription = $section['summary'];
714
                $lpId = learnpath::add_lp(
715
                    api_get_course_id(),
716
                    $lpName,
717
                    $lpDescription,
718
                    'chamilo',
719
                    'manual'
720
                );
721
                $dirName = api_replace_dangerous_char($lpName);
722
                create_unexisting_directory(
723
                    $courseInfo,
724
                    api_get_user_id(),
725
                    api_get_session_id(),
726
                    api_get_group_id(),
727
                    null,
728
                    $documentPath,
729
                    '/'.$dirName,
730
                    $lpName,
731
                    0
732
                );
733
                $lpAdded[$sectionId] = [
734
                    'lpId' => $lpId,
735
                    'sectionPath' => $dirName,
736
                    'sectionName' => $lpName,
737
                ];
738
                $i++;
739
            }
740
        }
741
742
        return $lpAdded;
743
    }
744
745
    /**
746
     * It counts the activities inside a section module.
747
     *
748
     * @param $activities
749
     * @param $sectionId
750
     *
751
     * @return int|void
752
     */
753
    public function countSectionActivities($activities, $sectionId)
754
    {
755
        $sectionActivities = [];
756
        $modulesLpTypes = ['url', 'resource', 'quiz', 'forum', 'page', 'label', 'assign'];
757
        $i = 0;
758
        foreach ($activities as $activity) {
759
            if (empty($activity->childNodes->length)) {
760
                continue;
761
            }
762
            $currentItem = [];
763
            foreach ($activity->childNodes as $item) {
764
                $currentItem[$item->nodeName] = $item->nodeValue;
765
            }
766
            if (!empty($currentItem['sectionid']) && in_array($currentItem['modulename'], $modulesLpTypes)) {
767
                $sectionActivities[$currentItem['sectionid']][$i] = $currentItem;
768
                $i++;
769
            }
770
        }
771
772
        $countActivities = 0;
773
        if (isset($sectionActivities[$sectionId])) {
774
            $countActivities = count($sectionActivities[$sectionId]);
775
        }
776
777
        return $countActivities;
778
    }
779
780
    /**
781
     * It gets the sections from xml module.
782
     *
783
     * @param $xml
784
     * @param $destinationDir
785
     *
786
     * @return array|false
787
     */
788
    public function readSections($xml, $destinationDir)
789
    {
790
        $doc = new DOMDocument();
791
        $res = @$doc->loadXML($xml);
792
        if (empty($res)) {
793
            return false;
794
        }
795
796
        $sections = [];
797
        $sectionNodes = $doc->getElementsByTagName('section');
798
        foreach ($sectionNodes as $section) {
799
            if (empty($section->childNodes->length)) {
800
                continue;
801
            }
802
            $currentItem = [];
803
            foreach ($section->childNodes as $item) {
804
                $currentItem[$item->nodeName] = $item->nodeValue;
805
            }
806
            if (!empty($currentItem['directory'])) {
807
                $sectionDir = $destinationDir.'/'.$currentItem['directory'];
808
                $sectionInfoXml = @file_get_contents($sectionDir.'/section.xml');
809
                $sections[$currentItem['sectionid']] = $this->readSectionModule($sectionInfoXml);
810
            }
811
        }
812
813
        return $sections;
814
    }
815
816
    /**
817
     * It reads module xml to get section info.
818
     *
819
     * @param $sectionInfoXml
820
     *
821
     * @return array|false
822
     */
823
    public function readSectionModule($sectionInfoXml)
824
    {
825
        $doc = new DOMDocument();
826
        $res = @$doc->loadXML($sectionInfoXml);
827
        if (empty($res)) {
828
            return false;
829
        }
830
831
        $sectionInfo = [];
832
        $sectionNode = $doc->getElementsByTagName('section');
833
        foreach ($sectionNode as $section) {
834
            if (empty($section->childNodes->length)) {
835
                continue;
836
            }
837
            foreach ($section->childNodes as $item) {
838
                $sectionInfo[$item->nodeName] = $item->nodeValue;
839
            }
840
        }
841
842
        return $sectionInfo;
843
    }
844
845
    /**
846
     * It gets lesson information from module xml.
847
     *
848
     * @param $moduleXml
849
     *
850
     * @return array|false
851
     */
852
    public function readLessonModule($moduleXml)
853
    {
854
        $doc = new DOMDocument();
855
        $res = @$doc->loadXML($moduleXml);
856
        if (empty($res)) {
857
            return false;
858
        }
859
860
        $activities = $doc->getElementsByTagName('lesson');
861
        $currentItem = [];
862
        foreach ($activities as $activity) {
863
            if ($activity->childNodes->length) {
864
                foreach ($activity->childNodes as $item) {
865
                    $currentItem[$item->nodeName] = $item->nodeValue;
866
                }
867
            }
868
        }
869
870
        $pages = $doc->getElementsByTagName('page');
871
872
        $pagesList = [];
873
        $counter = 0;
874
        foreach ($pages as $page) {
875
            if ($page->childNodes->length) {
876
                foreach ($page->childNodes as $item) {
877
                    $pagesList[$counter][$item->nodeName] = $item->nodeValue;
878
                }
879
                $i = 0;
880
                $answerNodes = $page->getElementsByTagName("answer");
881
                $answers = [];
882
                foreach ($answerNodes as $answer) {
883
                    foreach ($answer->childNodes as $n) {
884
                        $answers[$i][$n->nodeName] = $n->nodeValue;
885
                    }
886
                    $i++;
887
                }
888
                $pagesList[$counter]['answers'] = $answers;
889
                $counter++;
890
            }
891
        }
892
        $currentItem['pages'] = $pagesList;
893
        $attributes = $this->getDocActivityAttributes($doc);
894
        $currentItem['attributes'] = $attributes;
895
896
        return $currentItem;
897
    }
898
899
    /**
900
     * It gets assignment information from module xml.
901
     *
902
     * @param $moduleXml
903
     *
904
     * @return array|false
905
     */
906
    public function readAssignModule($moduleXml)
907
    {
908
        $doc = new DOMDocument();
909
        $res = @$doc->loadXML($moduleXml);
910
        if (empty($res)) {
911
            return false;
912
        }
913
914
        $info = [];
915
        $entries = $doc->getElementsByTagName('assign');
916
        foreach ($entries as $entry) {
917
            if (empty($entry->childNodes->length)) {
918
                continue;
919
            }
920
            foreach ($entry->childNodes as $item) {
921
                if (in_array($item->nodeName, ['name', 'intro', 'duedate', 'cutoffdate', 'grade'])) {
922
                    $info[$item->nodeName] = $item->nodeValue;
923
                }
924
            }
925
        }
926
927
        $configOpts = $doc->getElementsByTagName('plugin_config');
928
        $config = [];
929
        $counter = 0;
930
        foreach ($configOpts as $opt) {
931
            if (empty($opt->childNodes->length)) {
932
                continue;
933
            }
934
            $pluginName = '';
935
            foreach ($opt->childNodes as $item) {
936
                if ('#text' == $item->nodeName) {
937
                    continue;
938
                }
939
                if ('plugin' == $item->nodeName && !in_array($item->nodeValue, ['onlinetext', 'file'])) {
940
                    break;
941
                }
942
                if ('subtype' == $item->nodeName && 'assignsubmission' != $item->nodeValue) {
943
                    break;
944
                }
945
                if ('name' == $item->nodeName && 'enabled' != $item->nodeValue) {
946
                    break;
947
                }
948
                if ('plugin' == $item->nodeName) {
949
                    $pluginName = $item->nodeValue;
950
                }
951
                if ('value' == $item->nodeName) {
952
                    $config[$pluginName]['enabled'] = (int) $item->nodeValue;
953
                }
954
            }
955
            $counter++;
956
        }
957
        $info['config'] = $config;
958
        $attributes = $this->getDocActivityAttributes($doc);
959
        $info['attributes'] = $attributes;
960
961
        return $info;
962
    }
963
964
    /**
965
     * It gets the attributes of each activity from module xml.
966
     *
967
     * @param $doc
968
     *
969
     * @return array
970
     */
971
    public function getDocActivityAttributes($doc)
972
    {
973
        $activityAttr = [];
974
        $searchActivity = $doc->getElementsByTagName('activity');
975
        foreach ($searchActivity as $searchNode) {
976
            $activityAttr['contextid'] = $searchNode->getAttribute('contextid');
977
            $activityAttr['modulename'] = $searchNode->getAttribute('modulename');
978
            $activityAttr['moduleid'] = $searchNode->getAttribute('moduleid');
979
        }
980
981
        return $activityAttr;
982
    }
983
984
    /**
985
     * It gest scorm information from module xml.
986
     *
987
     * @param $moduleXml
988
     *
989
     * @return array|false
990
     */
991
    public function readScormModule($moduleXml)
992
    {
993
        $doc = new DOMDocument();
994
        $res = @$doc->loadXML($moduleXml);
995
        if (empty($res)) {
996
            return false;
997
        }
998
999
        $info = [];
1000
        $entries = $doc->getElementsByTagName('scorm');
1001
        $i = 0;
1002
        foreach ($entries as $entry) {
1003
            if (empty($entry->childNodes->length)) {
1004
                continue;
1005
            }
1006
            foreach ($entry->childNodes as $item) {
1007
                if (in_array($item->nodeName, ['name', 'reference', 'sha1hash', 'scormtype'])) {
1008
                    $info[$i][$item->nodeName] = $item->nodeValue;
1009
                }
1010
            }
1011
            $i++;
1012
        }
1013
1014
        return $info;
1015
    }
1016
1017
    /**
1018
     * Get glossary information from module xml.
1019
     *
1020
     * @param $moduleXml
1021
     * @param $moduleId
1022
     *
1023
     * @return array|false
1024
     */
1025
    public function readGlossaryModule($moduleXml, $moduleId)
1026
    {
1027
        $doc = new DOMDocument();
1028
        $res = @$doc->loadXML($moduleXml);
1029
        if (empty($res)) {
1030
            return false;
1031
        }
1032
1033
        $glossaryInfo = [];
1034
        $entries = $doc->getElementsByTagName('entry');
1035
        $i = 0;
1036
        foreach ($entries as $entry) {
1037
            if (empty($entry->childNodes->length)) {
1038
                continue;
1039
            }
1040
            foreach ($entry->childNodes as $item) {
1041
                $glossaryInfo[$moduleId][$i][$item->nodeName] = $item->nodeValue;
1042
            }
1043
            $i++;
1044
        }
1045
        $attributes = $this->getDocActivityAttributes($doc);
1046
        $glossaryInfo['attributes'] = $attributes;
1047
1048
        return $glossaryInfo;
1049
    }
1050
1051
    /**
1052
     * It reads item html from module to documents.
1053
     *
1054
     * @param $moduleXml
1055
     * @param $moduleName
1056
     *
1057
     * @return array|false
1058
     */
1059
    public function readHtmlModule($moduleXml, $moduleName)
1060
    {
1061
        $moduleDoc = new DOMDocument();
1062
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
1063
        if (empty($moduleRes)) {
1064
            return false;
1065
        }
1066
        $activities = $moduleDoc->getElementsByTagName($moduleName);
1067
        $currentItem = [];
1068
        foreach ($activities as $activity) {
1069
            if ($activity->childNodes->length) {
1070
                foreach ($activity->childNodes as $item) {
1071
                    $currentItem[$item->nodeName] = $item->nodeValue;
1072
                }
1073
            }
1074
        }
1075
        $attributes = $this->getDocActivityAttributes($moduleDoc);
1076
        $currentItem['attributes'] = $attributes;
1077
1078
        return $currentItem;
1079
    }
1080
1081
    /**
1082
     * It addes the files from a resources type folder.
1083
     *
1084
     * @param     $files
1085
     * @param     $mainFolderName
1086
     * @param     $sectionPath
1087
     * @param int $lpId
1088
     * @param int $n
1089
     */
1090
    public function processSectionFolderModule($mainFileModuleValues, $sectionPath, $mainFolderName, $resourcesFiles, $lpId = 0, $dspOrder = 0)
1091
    {
1092
        if (!empty($mainFileModuleValues['folder'])) {
1093
            $courseInfo = api_get_course_info();
1094
            $chapters = [];
1095
            $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
1096
            // It creates the first lp chapter (module folder name)
1097
            $safeMainFolderName = api_replace_dangerous_char($mainFolderName);
1098
            $documentData = create_unexisting_directory(
1099
                $courseInfo,
1100
                api_get_user_id(),
1101
                api_get_session_id(),
1102
                api_get_group_id(),
1103
                null,
1104
                $documentPath,
1105
                '/'.$sectionPath.'/'.$safeMainFolderName,
1106
                $mainFolderName,
1107
                0
1108
            );
1109
            if (!empty($lpId) && !empty($documentData['iid'])) {
1110
                $lp = new \learnpath(
1111
                    api_get_course_id(),
1112
                    $lpId,
1113
                    api_get_user_id()
1114
                );
1115
                $lpItemId = $lp->add_item(
1116
                    0,
1117
                    0,
1118
                    'dir',
1119
                    $documentData['iid'],
1120
                    $mainFolderName,
1121
                    '',
1122
                    0,
1123
                    0,
1124
                    0,
1125
                    $dspOrder
1126
                );
1127
                $chapters['/'] = $lpItemId;
1128
            }
1129
            // It checks the subfolder for second level.
1130
            foreach ($mainFileModuleValues['folder'] as $folder) {
1131
                if ('/' == $folder['filepath']) {
1132
                    continue;
1133
                }
1134
                $folder['filepath'] = trim($folder['filepath'], '/');
1135
                $arrFolderPath = explode('/', $folder['filepath']);
1136
                if (1 == count($arrFolderPath)) {
1137
                    $folderName = $arrFolderPath[0];
1138
                    $safeFolderName = api_replace_dangerous_char($folderName);
1139
                    $documentSubData = create_unexisting_directory(
1140
                        $courseInfo,
1141
                        api_get_user_id(),
1142
                        api_get_session_id(),
1143
                        api_get_group_id(),
1144
                        null,
1145
                        $documentPath,
1146
                        '/'.$sectionPath.'/'.$safeMainFolderName.'/'.$safeFolderName,
1147
                        $folderName,
1148
                        0
1149
                    );
1150
                    if (!empty($lpId) && !empty($documentSubData['iid'])) {
1151
                        $lp = new \learnpath(
1152
                            api_get_course_id(),
1153
                            $lpId,
1154
                            api_get_user_id()
1155
                        );
1156
                        $lpItemId = $lp->add_item(
1157
                            $chapters['/'],
1158
                            0,
1159
                            'dir',
1160
                            $documentSubData['iid'],
1161
                            $folderName,
1162
                            '',
1163
                            0,
1164
                            0,
1165
                            0
1166
                        );
1167
                        $chapters["/$folderName/"] = $lpItemId;
1168
                    }
1169
                }
1170
            }
1171
            if (!empty($resourcesFiles)) {
1172
                foreach ($resourcesFiles as $file) {
1173
                    $title = pathinfo($file['file']['name'], PATHINFO_FILENAME);
1174
                    $path = $file['file']['filepath'];
1175
                    if (1 == count(explode('/', trim($path, '/')))) {
1176
                        $safePath = api_replace_dangerous_char($path);
1177
                        $newSafePath = !empty($safePath) ? '/'.$sectionPath.'/'.$safeMainFolderName.'/'.$safePath : '/'.$sectionPath.'/'.$safeMainFolderName;
1178
                        $data = DocumentManager::upload_document(
1179
                            $file,
1180
                            $newSafePath,
1181
                            $title,
1182
                            '',
1183
                            null,
1184
                            'overwrite',
1185
                            true,
1186
                            true,
1187
                            'file',
1188
                            false
1189
                        );
1190
                        if (!empty($lpId) && !empty($data['iid'])) {
1191
                            // It is added as item in Learnpath
1192
                            $lp = new \learnpath(
1193
                                api_get_course_id(),
1194
                                $lpId,
1195
                                api_get_user_id()
1196
                            );
1197
                            $lpItemId = $lp->add_item(
1198
                                $chapters[$path],
1199
                                0,
1200
                                'document',
1201
                                $data['iid'],
1202
                                $title,
1203
                                '',
1204
                                0,
1205
                                0,
1206
                                0
1207
                            );
1208
                        }
1209
                    }
1210
                }
1211
            }
1212
        }
1213
    }
1214
1215
    /**
1216
     * It reorganizes files imported to documents.
1217
     *
1218
     * @param $files
1219
     * @param $sectionPath
1220
     *
1221
     * @return array
1222
     */
1223
    public function processSectionMultimedia($files, $sectionPath, $lpId = 0, $previousItemId = 0)
1224
    {
1225
        $importedFiles = [];
1226
        if (!empty($files)) {
1227
            $courseInfo = api_get_course_info();
1228
            if (!empty($sectionPath)) {
1229
                $sectionPath = '/'.trim($sectionPath, '/\\');
1230
            }
1231
            $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
1232
            $multimediaPath = $documentPath.$sectionPath.'/Multimedia';
1233
            if (!is_dir($multimediaPath)) {
1234
                create_unexisting_directory(
1235
                    $courseInfo,
1236
                    api_get_user_id(),
1237
                    api_get_session_id(),
1238
                    api_get_group_id(),
1239
                    null,
1240
                    $documentPath,
1241
                    $sectionPath.'/Multimedia',
1242
                    'Multimedia',
1243
                    0
1244
                );
1245
            }
1246
            foreach ($files as $file) {
1247
                $title = pathinfo($file['file']['name'], PATHINFO_FILENAME);
1248
                $path = !empty($lpId) ? $sectionPath : $sectionPath.'/Multimedia';
1249
                $data = DocumentManager::upload_document(
1250
                    $file,
1251
                    $path,
1252
                    $title,
1253
                    '',
1254
                    null,
1255
                    'overwrite',
1256
                    true,
1257
                    true,
1258
                    'file',
1259
                    false
1260
                );
1261
                if ($data) {
1262
                    $importedFiles[$file['file']['name']] = basename($data['path']);
1263
                    // It is added as item in Learnpath
1264
                    if (!empty($lpId) && !empty($data['iid'])) {
1265
                        $currentItemId = $this->processSectionItem($lpId, 'document', $data['iid'], $title, $previousItemId);
1266
                        if (!empty($previousItemId)) {
1267
                            $this->updateLpItemNextId($previousItemId, $currentItemId);
1268
                            $this->updateLpItemPreviousId($currentItemId, $previousItemId);
1269
                        }
1270
1271
                        $previousItemId = $currentItemId;
1272
                    }
1273
                }
1274
            }
1275
        }
1276
1277
        return $importedFiles;
1278
    }
1279
1280
    /**
1281
     * It saves a learnpath from module xml.
1282
     *
1283
     * @param $moduleValues
1284
     *
1285
     * @return false
1286
     */
1287
    public function processLesson($moduleValues, $allFiles = [])
1288
    {
1289
        if (!empty($moduleValues['pages'])) {
1290
            $qtypes = [
1291
                20 => 'page',
1292
                10 => 'essay',
1293
                5 => 'matching',
1294
                3 => 'multichoice',
1295
                1 => 'shortanswer',
1296
                2 => 'truefalse',
1297
            ];
1298
            $items = $moduleValues['pages'];
1299
            $lpName = $moduleValues['name'];
1300
            $lpDescription = api_utf8_decode($moduleValues['intro']);
1301
            $lpId = learnpath::add_lp(
1302
                api_get_course_id(),
1303
                $lpName,
1304
                $lpDescription,
1305
                'chamilo',
1306
                'manual'
1307
            );
1308
1309
            $dirName = api_replace_dangerous_char($lpName);
1310
            $courseInfo = api_get_course_info();
1311
            $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
1312
            create_unexisting_directory(
1313
                $courseInfo,
1314
                api_get_user_id(),
1315
                api_get_session_id(),
1316
                api_get_group_id(),
1317
                null,
1318
                $documentPath,
1319
                '/'.$dirName,
1320
                $lpName,
1321
                0
1322
            );
1323
1324
            $importedFiles = [];
1325
            $contextId = $moduleValues['attributes']['contextid'];
1326
            if (isset($allFiles[$contextId])) {
1327
                $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $dirName);
1328
            }
1329
1330
            $questionList = [];
1331
            foreach ($items as $item) {
1332
                if (in_array($item['qtype'], array_keys($qtypes))) {
1333
                    $qTypeName = $qtypes[$item['qtype']];
1334
                    switch ($qTypeName) {
1335
                        case 'page':
1336
                            $pageValues = [];
1337
                            $pageValues['name'] = $item['title'];
1338
                            $pageValues['content'] = $item['contents'];
1339
                            $sectionPath = '/'.$dirName.'/';
1340
                            $documentId = $this->processHtmlDocument($pageValues, 'page', $importedFiles, $sectionPath);
1341
                            $lpItemId = $this->processSectionItem($lpId, 'document', $documentId, $pageValues['name']);
1342
                            break;
1343
                        case 'essay':
1344
                        case 'match':
1345
                        case 'multichoice':
1346
                        case 'shortanswer':
1347
                        case 'truefalse':
1348
                            $qType = $qtypes[$item['qtype']];
1349
                            $question = [];
1350
                            $question['qtype'] = $qType;
1351
                            $question['name'] = $item['title'];
1352
                            $question['questiontext'] = api_utf8_decode($item['contents']);
1353
                            $question[$qType.'_values']['single'] = 1;
1354
                            $question['questionType'] = $this->matchMoodleChamiloQuestionTypes($question);
1355
                            $answers = [];
1356
                            if (!empty($item['answers'])) {
1357
                                $defaultmark = 0;
1358
                                foreach ($item['answers'] as $answer) {
1359
                                    $answerValue = [];
1360
                                    $answerValue['answertext'] = api_utf8_decode($answer['answer_text']);
1361
                                    $answerValue['feedback'] = api_utf8_decode($answer['response']);
1362
                                    $answerValue['fraction'] = $answer['score'];
1363
                                    $defaultmark += $answer['score'];
1364
                                    $answers[] = $answerValue;
1365
                                }
1366
                                $question['defaultmark'] = $defaultmark;
1367
                            }
1368
                            $question['answers'] = $answers;
1369
                            $questionList[] = $question;
1370
                            break;
1371
                    }
1372
                }
1373
            }
1374
1375
            if (!empty($questionList)) {
1376
                $courseInfo = api_get_course_info();
1377
                // It creates a quiz for those questions.
1378
                $exercise = new Exercise($courseInfo['real_id']);
1379
                $quizLpName = $lpName.' - '.get_lang('Quiz');
1380
                $title = Exercise::format_title_variable($quizLpName);
1381
                $exercise->updateTitle($title);
1382
                $moduleValues['intro'] = $this->replaceMoodleChamiloCoursePath($moduleValues['intro'], $dirName);
1383
                $exercise->updateDescription(api_utf8_decode($moduleValues['intro']));
1384
                $exercise->updateAttempts(0);
1385
                $exercise->updateFeedbackType(0);
1386
                $exercise->setRandom(0);
1387
                $exercise->updateRandomAnswers(false);
1388
                $exercise->updateExpiredTime(0);
1389
                $exercise->updateType(2);
1390
                $exercise->updateResultsDisabled(0);
1391
1392
                // Create the new Quiz
1393
                $exercise->save();
1394
1395
                $lpItemId = $this->processSectionItem($lpId, 'quiz', $exercise->iid, $quizLpName);
1396
1397
                // Ok, we got the Quiz and create it, now its time to add the Questions
1398
                foreach ($questionList as $question) {
1399
                    $questionInstance = Question::getInstance($question['questionType']);
1400
                    if (empty($questionInstance)) {
1401
                        continue;
1402
                    }
1403
                    $questionInstance->updateTitle($question['name']);
1404
                    $questionText = $question['questiontext'];
1405
                    $questionText = $this->replaceMoodleChamiloCoursePath($questionText, $dirName);
1406
                    $questionInstance->updateDescription($questionText);
1407
                    $questionInstance->updateLevel(1);
1408
                    $questionInstance->updateCategory(0);
1409
1410
                    //Save normal question if NOT media
1411
                    if ($questionInstance->type != MEDIA_QUESTION) {
1412
                        $questionInstance->save($exercise);
1413
                        // modify the exercise
1414
                        $exercise->addToList($questionInstance->iid);
1415
                        $exercise->update_question_positions();
1416
                    }
1417
                    $this->processAnswers(
1418
                        $exercise,
1419
                        $question['answers'],
1420
                        $question['qtype'],
1421
                        $questionInstance,
1422
                        $question,
1423
                        $importedFiles,
1424
                        $dirName
1425
                    );
1426
                }
1427
            }
1428
        }
1429
1430
        return false;
1431
    }
1432
1433
    /**
1434
     * It saves a student publication from module xml.
1435
     *
1436
     * @param $assign
1437
     *
1438
     * @return bool|int
1439
     */
1440
    public function processAssignment($assign, $allFiles = [], $sectionPath = '')
1441
    {
1442
        if (!empty($assign)) {
1443
            require_once api_get_path(SYS_CODE_PATH).'work/work.lib.php';
1444
1445
            $importedFiles = [];
1446
            $contextId = $assign['attributes']['contextid'];
1447
            if (isset($allFiles[$contextId])) {
1448
                $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
1449
            }
1450
1451
            $values = [];
1452
            $values['new_dir'] = $assign['name'];
1453
            if (!empty($assign['cutoffdate'])) {
1454
                $values['enableEndDate'] = 1;
1455
                $values['ends_on'] = date('Y-m-d H:i', $assign['cutoffdate']);
1456
            }
1457
            if (!empty($assign['duedate'])) {
1458
                $values['enableExpiryDate'] = 1;
1459
                $values['expires_on'] = date('Y-m-d H:i', $assign['duedate']);
1460
            }
1461
            $values['work_title'] = $assign['name'];
1462
            $assign['intro'] = $this->replaceMoodleChamiloCoursePath($assign['intro'], $sectionPath);
1463
            if ($importedFiles) {
1464
                $this->fixPathInText($importedFiles, $assign['intro']);
1465
            }
1466
1467
            $values['description'] = api_utf8_decode($assign['intro']);
1468
            $values['qualification'] = (int) $assign['grade'];
1469
            $values['weight'] = (int) $assign['grade'];
1470
            $values['allow_text_assignment'] = 2;
1471
            if (!empty($assign['config'])) {
1472
                if (1 == (int) $assign['config']['onlinetext']['enabled'] && 1 == (int) $assign['config']['file']['enabled']) {
1473
                    $values['allow_text_assignment'] = 0;
1474
                } elseif (1 == (int) $assign['config']['onlinetext']['enabled'] && empty($assign['config']['file']['enabled'])) {
1475
                    $values['allow_text_assignment'] = 1;
1476
                } elseif (empty($assign['config']['onlinetext']['enabled']) && 1 == (int) $assign['config']['file']['enabled']) {
1477
                    $values['allow_text_assignment'] = 2;
1478
                }
1479
            }
1480
1481
            $assignId = addDir(
1482
                $values,
1483
                api_get_user_id(),
1484
                api_get_course_info(),
1485
                api_get_group_id(),
1486
                api_get_session_id()
1487
            );
1488
1489
            return $assignId;
1490
        }
1491
1492
        return false;
1493
    }
1494
1495
    /**
1496
     * It saves a scorm from module xml.
1497
     *
1498
     * @param $moduleValues
1499
     * @param $modScormFileZips
1500
     *
1501
     * @return bool
1502
     */
1503
    public function processScorm($moduleValues, $modScormFileZips)
1504
    {
1505
        if (!empty($moduleValues)) {
1506
            foreach ($moduleValues as $info) {
1507
                $sha1hash = $info['sha1hash'];
1508
                if (!empty($modScormFileZips[$sha1hash])) {
1509
                    $scormFile = $modScormFileZips[$sha1hash];
1510
                    $oScorm = new scorm();
1511
                    $manifest = $oScorm->import_package(
1512
                        $scormFile['file']
1513
                    );
1514
                    if (!empty($manifest)) {
1515
                        $oScorm->parse_manifest($manifest);
1516
                        $oScorm->import_manifest(
1517
                            api_get_course_id(),
1518
                            1,
1519
                            0,
1520
                            0,
1521
                            $info['name']
1522
                        );
1523
                    }
1524
                    $oScorm->set_proximity($info['scormtype']);
1525
                    $oScorm->set_maker('Scorm');
1526
                    $oScorm->set_jslib('scorm_api.php');
1527
                }
1528
            }
1529
1530
            return true;
1531
        }
1532
1533
        return false;
1534
    }
1535
1536
    /**
1537
     * It saves glossary terms from module xml.
1538
     *
1539
     * @param $moduleValues
1540
     * @param $moduleId
1541
     *
1542
     * @return bool
1543
     */
1544
    public function processGlossary($moduleValues, $moduleId, $allFiles = [], $sectionPath = '/')
1545
    {
1546
        $importedFiles = [];
1547
        $contextId = $moduleValues['attributes']['contextid'];
1548
        if (isset($allFiles[$contextId])) {
1549
            $importedFiles = $this->processSectionMultimedia($allFiles[$contextId], $sectionPath);
1550
        }
1551
1552
        if (!empty($moduleValues[$moduleId])) {
1553
            foreach ($moduleValues[$moduleId] as $entry) {
1554
                $values = [];
1555
                $values['name'] = $entry['concept'];
1556
                $entry['definition'] = $this->replaceMoodleChamiloCoursePath($entry['definition'], $sectionPath);
1557
                if ($importedFiles) {
1558
                    $this->fixPathInText($importedFiles, $entry['definition']);
1559
                }
1560
                $values['description'] = $entry['definition'];
1561
                GlossaryManager::save_glossary($values);
1562
            }
1563
1564
            return true;
1565
        }
1566
1567
        return false;
1568
    }
1569
1570
    /**
1571
     * It process the module as document html.
1572
     *
1573
     * @param $moduleValues
1574
     * @param $moduleName
1575
     *
1576
     * @return false|int
1577
     */
1578
    public function processHtmlDocument($moduleValues, $moduleName, $importedFiles = [], $sectionPath = '/')
1579
    {
1580
        $dir = $sectionPath;
1581
        $filepath = api_get_path(SYS_COURSE_PATH).api_get_course_path().'/document'.$dir;
1582
        $title = trim($moduleValues['name']);
1583
        if ('$@NULL@$' == $title) {
1584
            $title = get_lang('Tag');
1585
        }
1586
1587
        // Setting the filename
1588
        $filename = $title;
1589
        $filename = addslashes(trim($filename));
1590
        $filename = Security::remove_XSS($filename);
1591
        $filename = api_replace_dangerous_char($filename);
1592
        $filename = disable_dangerous_file($filename);
1593
        $filename .= DocumentManager::getDocumentSuffix(
1594
            api_get_course_info(),
1595
            api_get_session_id(),
1596
            api_get_group_id()
1597
        );
1598
1599
        $extension = 'html';
1600
        $content = ('page' == $moduleName ? $moduleValues['content'] : $moduleValues['intro']);
1601
        $content = api_html_entity_decode($content);
1602
        $cleanSectionPath = trim($sectionPath, '/\\');
1603
        $content = $this->replaceMoodleChamiloCoursePath($content, $cleanSectionPath);
1604
1605
        if ($importedFiles) {
1606
            $this->fixPathInText($importedFiles, $content);
1607
        }
1608
1609
        if ($fp = @fopen($filepath.$filename.'.'.$extension, 'w')) {
1610
            $content = str_replace(
1611
                api_get_path(WEB_COURSE_PATH),
1612
                api_get_configuration_value('url_append').api_get_path(REL_COURSE_PATH),
1613
                $content
1614
            );
1615
1616
            fputs($fp, $content);
1617
            fclose($fp);
1618
            chmod($filepath.$filename.'.'.$extension, api_get_permissions_for_new_files());
1619
            $fileSize = filesize($filepath.$filename.'.'.$extension);
1620
            $saveFilePath = $dir.$filename.'.'.$extension;
1621
1622
            $documentId = add_document(
1623
                api_get_course_info(),
1624
                $saveFilePath,
1625
                'file',
1626
                $fileSize,
1627
                $title
1628
            );
1629
1630
            if ($documentId) {
1631
                api_item_property_update(
1632
                    api_get_course_info(),
1633
                    TOOL_DOCUMENT,
1634
                    $documentId,
1635
                    'DocumentAdded',
1636
                    api_get_user_id(),
1637
                    [],
1638
                    null,
1639
                    null,
1640
                    null,
1641
                    api_get_session_id()
1642
                );
1643
                // Update parent folders
1644
                item_property_update_on_folder(api_get_course_info(), $dir, api_get_user_id());
1645
            }
1646
1647
            return $documentId;
1648
        }
1649
1650
        return false;
1651
    }
1652
1653
    /**
1654
     * Read and validate the forum module XML.
1655
     *
1656
     * @param resource $moduleXml XML file
1657
     *
1658
     * @return mixed|array if is a valid xml file, false otherwise
1659
     */
1660
    public function readForumModule($moduleXml)
1661
    {
1662
        $moduleDoc = new DOMDocument();
1663
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
1664
        if (empty($moduleRes)) {
1665
            return false;
1666
        }
1667
        $activities = $moduleDoc->getElementsByTagName('forum');
1668
        $currentItem = [];
1669
        foreach ($activities as $activity) {
1670
            if ($activity->childNodes->length) {
1671
                foreach ($activity->childNodes as $item) {
1672
                    if (in_array($item->nodeName, ['type', 'name', 'intro', 'grade_forum'])) {
1673
                        $currentItem[$item->nodeName] = $item->nodeValue;
1674
                    }
1675
                }
1676
            }
1677
        }
1678
1679
        $discussions = $moduleDoc->getElementsByTagName('discussion');
1680
1681
        $discussionsList = [];
1682
        $counter = 0;
1683
        foreach ($discussions as $discussion) {
1684
            if ($discussion->childNodes->length) {
1685
                foreach ($discussion->childNodes as $item) {
1686
                    $discussionsList[$counter][$item->nodeName] = $item->nodeValue;
1687
                }
1688
                $i = 0;
1689
                $postsNodes = $discussion->getElementsByTagName("post");
1690
                $posts = [];
1691
                foreach ($postsNodes as $post) {
1692
                    foreach ($post->childNodes as $n) {
1693
                        $posts[$i][$n->nodeName] = $n->nodeValue;
1694
                    }
1695
                    $i++;
1696
                }
1697
                $discussionsList[$counter]['posts'] = $posts;
1698
                $counter++;
1699
            }
1700
        }
1701
        $currentItem['discussions'] = $discussionsList;
1702
        $attributes = $this->getDocActivityAttributes($moduleDoc);
1703
        $currentItem['attributes'] = $attributes;
1704
1705
        return $currentItem;
1706
    }
1707
1708
    /**
1709
     * Read and validate the folder module XML.
1710
     *
1711
     * @param resource $moduleXml XML file
1712
     *
1713
     * @return mixed|array if is a valid xml file, false otherwise
1714
     */
1715
    public function readFolderModule($moduleXml)
1716
    {
1717
        $moduleDoc = new DOMDocument();
1718
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
1719
        if (empty($moduleRes)) {
1720
            return false;
1721
        }
1722
        $activities = $moduleDoc->getElementsByTagName('folder');
1723
        $mainActivity = $moduleDoc->getElementsByTagName('activity');
1724
        $contextId = $mainActivity->item(0)->getAttribute('contextid');
1725
        $currentItem = [];
1726
        foreach ($activities as $activity) {
1727
            if ($activity->childNodes->length) {
1728
                foreach ($activity->childNodes as $item) {
1729
                    $currentItem[$item->nodeName] = $item->nodeValue;
1730
                }
1731
            }
1732
        }
1733
1734
        $currentItem['contextid'] = $contextId;
1735
        $attributes = $this->getDocActivityAttributes($moduleDoc);
1736
        $currentItem['attributes'] = $attributes;
1737
1738
        return $currentItem;
1739
    }
1740
1741
    /**
1742
     * Read and validate the resource module XML.
1743
     *
1744
     * @param resource $moduleXml XML file
1745
     *
1746
     * @return mixed|array if is a valid xml file, false otherwise
1747
     */
1748
    public function readResourceModule($moduleXml)
1749
    {
1750
        $moduleDoc = new DOMDocument();
1751
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
1752
        if (empty($moduleRes)) {
1753
            return false;
1754
        }
1755
        $activities = $moduleDoc->getElementsByTagName('resource');
1756
        $mainActivity = $moduleDoc->getElementsByTagName('activity');
1757
        $contextId = $mainActivity->item(0)->getAttribute('contextid');
1758
        $currentItem = [];
1759
        foreach ($activities as $activity) {
1760
            if ($activity->childNodes->length) {
1761
                foreach ($activity->childNodes as $item) {
1762
                    $currentItem[$item->nodeName] = $item->nodeValue;
1763
                }
1764
            }
1765
        }
1766
1767
        $currentItem['contextid'] = $contextId;
1768
        $attributes = $this->getDocActivityAttributes($moduleDoc);
1769
        $currentItem['attributes'] = $attributes;
1770
1771
        return $currentItem;
1772
    }
1773
1774
    /**
1775
     * Read and validate the url module XML.
1776
     *
1777
     * @param resource $moduleXml XML file
1778
     *
1779
     * @return mixed|array if is a valid xml file, false otherwise
1780
     */
1781
    public function readUrlModule($moduleXml)
1782
    {
1783
        $moduleDoc = new DOMDocument();
1784
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
1785
        if (empty($moduleRes)) {
1786
            return false;
1787
        }
1788
        $activities = $moduleDoc->getElementsByTagName('url');
1789
        $currentItem = [];
1790
        foreach ($activities as $activity) {
1791
            if ($activity->childNodes->length) {
1792
                foreach ($activity->childNodes as $item) {
1793
                    $currentItem[$item->nodeName] = $item->nodeValue;
1794
                }
1795
            }
1796
        }
1797
        $attributes = $this->getDocActivityAttributes($moduleDoc);
1798
        $currentItem['attributes'] = $attributes;
1799
1800
        return $currentItem;
1801
    }
1802
1803
    /**
1804
     * It gets the grade values about a quiz imported.
1805
     *
1806
     * @param $gradeXml
1807
     * @param $quizId
1808
     *
1809
     * @return false|mixed
1810
     */
1811
    public function readQuizGradeModule($gradeXml, $quizId)
1812
    {
1813
        $doc = new DOMDocument();
1814
        $gradeRes = @$doc->loadXML($gradeXml);
1815
        if (empty($gradeRes)) {
1816
            return false;
1817
        }
1818
        $entries = $doc->getElementsByTagName('grade_item');
1819
        $info = [];
1820
        $i = 0;
1821
        foreach ($entries as $entry) {
1822
            if (empty($entry->childNodes->length)) {
1823
                continue;
1824
            }
1825
            foreach ($entry->childNodes as $item) {
1826
                if (in_array($item->nodeName, ['iteminstance', 'grademax', 'grademin', 'gradepass'])) {
1827
                    $info[$i][$item->nodeName] = $item->nodeValue;
1828
                }
1829
            }
1830
            $i++;
1831
        }
1832
        $grades = [];
1833
        if (!empty($info)) {
1834
            foreach ($info as $res) {
1835
                $grades[$res['iteminstance']] = $res;
1836
            }
1837
1838
            return $grades[$quizId];
1839
        }
1840
1841
        return false;
1842
    }
1843
1844
    /**
1845
     * Read and validate the quiz module XML.
1846
     *
1847
     * @param resource $moduleXml XML file
1848
     *
1849
     * @return mixed|array if is a valid xml file, false otherwise
1850
     */
1851
    public function readQuizModule($moduleXml)
1852
    {
1853
        $moduleDoc = new DOMDocument();
1854
        $moduleRes = @$moduleDoc->loadXML($moduleXml);
1855
        if (empty($moduleRes)) {
1856
            return false;
1857
        }
1858
        $activities = $moduleDoc->getElementsByTagName('quiz');
1859
        $currentItem = [];
1860
        foreach ($activities as $activity) {
1861
            if ($activity->childNodes->length) {
1862
                foreach ($activity->childNodes as $item) {
1863
                    $currentItem[$item->nodeName] = $item->nodeValue;
1864
                }
1865
                $currentItem['quiz_id'] = $activity->getAttribute('id');
1866
            }
1867
        }
1868
1869
        $questions = $moduleDoc->getElementsByTagName('question_instance');
1870
1871
        $questionList = [];
1872
        $counter = 0;
1873
        foreach ($questions as $question) {
1874
            if ($question->childNodes->length) {
1875
                foreach ($question->childNodes as $item) {
1876
                    $questionList[$counter][$item->nodeName] = $item->nodeValue;
1877
                }
1878
                $counter++;
1879
            }
1880
        }
1881
        $currentItem['question_instances'] = $questionList;
1882
        $attributes = $this->getDocActivityAttributes($moduleDoc);
1883
        $currentItem['attributes'] = $attributes;
1884
1885
        return $currentItem;
1886
    }
1887
1888
    /**
1889
     * Reads and parses the quiz module XML for Moodle version 4.
1890
     *
1891
     * @param string $moduleXml The XML content of the quiz module.
1892
     *
1893
     * @return array Returns an array with quiz data and question instances.
1894
     */
1895
    public function readQuizModuleV4($moduleXml)
1896
    {
1897
        $moduleDoc = new DOMDocument();
1898
        @$moduleDoc->loadXML($moduleXml);
1899
1900
        $quizData = [];
1901
        $quizNodes = $moduleDoc->getElementsByTagName('quiz');
1902
        foreach ($quizNodes as $quizNode) {
1903
            foreach ($quizNode->childNodes as $child) {
1904
                $quizData[$child->nodeName] = $child->nodeValue;
1905
            }
1906
            $quizData['quiz_id'] = $quizNode->getAttribute('id');
1907
        }
1908
1909
        $quizData['question_instances'] = [];
1910
        $questionInstances = $moduleDoc->getElementsByTagName('question_instance');
1911
        foreach ($questionInstances as $questionInstance) {
1912
            $questionId = $questionInstance->getAttribute('id');
1913
            foreach ($questionInstance->childNodes as $child) {
1914
                $quizData['question_instances'][] = [
1915
                    'questionid' => $questionId,
1916
                    $child->nodeName => $child->nodeValue,
1917
                ];
1918
            }
1919
        }
1920
1921
        return $quizData;
1922
    }
1923
1924
    /**
1925
     * Reads and processes quiz questions for Moodle version 4.
1926
     *
1927
     * @param string $questionsXml The XML content containing question data.
1928
     * @param string $quizXml      The XML content containing quiz data.
1929
     *
1930
     * @return array Returns an array of processed questions, either by category or by question bank reference.
1931
     */
1932
    public function readMainQuestionsXmlV4($questionsXml, $quizXml)
1933
    {
1934
        $questionsDoc = new DOMDocument();
1935
        @$questionsDoc->loadXML($questionsXml);
1936
1937
        $quizDoc = new DOMDocument();
1938
        @$quizDoc->loadXML($quizXml);
1939
1940
        $isByCategory = $quizDoc->getElementsByTagName('question_set_reference')->length > 0;
1941
        $isByBankReference = $quizDoc->getElementsByTagName('question_reference')->length > 0;
1942
1943
        if ($isByCategory) {
1944
            return $this->processByCategory($questionsDoc, $quizDoc);
1945
        } elseif ($isByBankReference) {
1946
            return $this->processByBankReference($questionsDoc, $quizDoc);
1947
        }
1948
1949
        return [];
1950
    }
1951
1952
    /**
1953
     * Search the files of a resource type folder in main Files XML.
1954
     *
1955
     * @param resource $filesXml  XML file
1956
     * @param int      $contextId
1957
     *
1958
     * @return mixed|array if is a valid xml file, false otherwise
1959
     */
1960
    public function readFolderModuleFilesXml($filesXml, $contextId = null)
1961
    {
1962
        $moduleDoc = new DOMDocument();
1963
        $moduleRes = @$moduleDoc->loadXML($filesXml);
1964
1965
        if (empty($moduleRes)) {
1966
            return false;
1967
        }
1968
        $activities = $moduleDoc->getElementsByTagName('file');
1969
        $filesInfo = [];
1970
        $i = 0;
1971
        foreach ($activities as $activity) {
1972
            if (empty($activity->childNodes->length)) {
1973
                continue;
1974
            }
1975
            foreach ($activity->childNodes as $item) {
1976
                if (in_array($item->nodeName, ['filename', 'filesize', 'contenthash', 'contextid', 'filepath', 'filesize', 'mimetype'])) {
1977
                    $filesInfo[$i][$item->nodeName] = $item->nodeValue;
1978
                }
1979
            }
1980
            $i++;
1981
        }
1982
        $currentItem = [];
1983
        if (!empty($filesInfo)) {
1984
            foreach ($filesInfo as $info) {
1985
                if (!empty($info['filesize'])) {
1986
                    $currentItem[$info['contextid']]['files'][] = $info;
1987
                } else {
1988
                    $currentItem[$info['contextid']]['folder'][] = $info;
1989
                }
1990
            }
1991
        }
1992
        $files = isset($contextId) ? $currentItem[$contextId] : $currentItem;
1993
1994
        return $files;
1995
    }
1996
1997
    /**
1998
     * Search the current file resource in main Files XML.
1999
     *
2000
     * @param resource $filesXml  XML file
2001
     * @param int      $contextId
2002
     *
2003
     * @return mixed|array if is a valid xml file, false otherwise
2004
     */
2005
    public function readMainFilesXml($filesXml, $contextId)
2006
    {
2007
        $moduleDoc = new DOMDocument();
2008
        $moduleRes = @$moduleDoc->loadXML($filesXml);
2009
2010
        if (empty($moduleRes)) {
2011
            return false;
2012
        }
2013
2014
        $activities = $moduleDoc->getElementsByTagName('file');
2015
        $filesInfo = [];
2016
        $i = 0;
2017
        foreach ($activities as $activity) {
2018
            if (empty($activity->childNodes->length)) {
2019
                continue;
2020
            }
2021
            foreach ($activity->childNodes as $item) {
2022
                if (in_array($item->nodeName, ['filename', 'filesize', 'contenthash', 'contextid', 'filesize', 'mimetype'])) {
2023
                    $filesInfo[$i][$item->nodeName] = $item->nodeValue;
2024
                }
2025
            }
2026
            $i++;
2027
        }
2028
        $currentItem = [];
2029
        if (!empty($filesInfo)) {
2030
            foreach ($filesInfo as $info) {
2031
                if (!empty($info['filesize'])) {
2032
                    $currentItem[$info['contextid']] = $info;
2033
                }
2034
            }
2035
        }
2036
2037
        return $currentItem[$contextId];
2038
    }
2039
2040
    /**
2041
     * Search the current question resource in main Questions XML.
2042
     *
2043
     * @param resource $questionsXml XML file
2044
     * @param int      $questionId
2045
     *
2046
     * @return mixed|array if is a valid xml file, false otherwise
2047
     */
2048
    public function readMainQuestionsXml($questionsXml, $questionId)
2049
    {
2050
        $moduleDoc = new DOMDocument();
2051
        $moduleRes = @$moduleDoc->loadXML($questionsXml);
2052
        if (empty($moduleRes)) {
2053
            return false;
2054
        }
2055
2056
        $questions = $moduleDoc->getElementsByTagName('question');
2057
        $currentItem = [];
2058
        foreach ($questions as $question) {
2059
            if ((int) $question->getAttribute('id') !== (int) $questionId) {
2060
                continue;
2061
            }
2062
2063
            if (empty($question->childNodes->length)) {
2064
                continue;
2065
            }
2066
2067
            $currentItem['questionid'] = $questionId;
2068
            $questionType = '';
2069
            foreach ($question->childNodes as $item) {
2070
                $currentItem[$item->nodeName] = $item->nodeValue;
2071
                if ('qtype' === $item->nodeName) {
2072
                    $questionType = $item->nodeValue;
2073
                }
2074
2075
                if ($item->nodeName != 'plugin_qtype_'.$questionType.'_question') {
2076
                    continue;
2077
                }
2078
2079
                $answer = $item->getElementsByTagName($this->getQuestionTypeAnswersTag($questionType));
2080
                $currentItem['plugin_qtype_'.$questionType.'_question'] = [];
2081
                for ($i = 0; $i <= $answer->length - 1; $i++) {
2082
                    $label = 'plugin_qtype_'.$questionType.'_question';
2083
                    $currentItem[$label][$i]['answerid'] = $answer->item($i)->getAttribute('id');
2084
                    foreach ($answer->item($i)->childNodes as $properties) {
2085
                        $currentItem[$label][$i][$properties->nodeName] = $properties->nodeValue;
2086
                    }
2087
                }
2088
2089
                $typeValues = $item->getElementsByTagName($this->getQuestionTypeOptionsTag($questionType));
2090
                for ($i = 0; $i <= $typeValues->length - 1; $i++) {
2091
                    foreach ($typeValues->item($i)->childNodes as $properties) {
2092
                        $currentItem[$questionType.'_values'][$properties->nodeName] = $properties->nodeValue;
2093
                        if ($properties->nodeName !== 'sequence') {
2094
                            continue;
2095
                        }
2096
2097
                        $sequence = $properties->nodeValue;
2098
                        $sequenceIds = explode(',', $sequence);
2099
                        foreach ($sequenceIds as $qId) {
2100
                            $questionMatch = $this->readMainQuestionsXml($questionsXml, $qId);
2101
                            $currentItem['plugin_qtype_'.$questionType.'_question'][] = $questionMatch;
2102
                        }
2103
                    }
2104
                }
2105
            }
2106
        }
2107
2108
        $this->traverseArray($currentItem, ['#text', 'question_hints', 'tags']);
2109
2110
        return $currentItem;
2111
    }
2112
2113
    /**
2114
     * return the correct question type options tag.
2115
     *
2116
     * @param string $questionType name
2117
     *
2118
     * @return string question type tag
2119
     */
2120
    public function getQuestionTypeOptionsTag($questionType)
2121
    {
2122
        switch ($questionType) {
2123
            case 'match':
2124
            case 'ddmatch':
2125
                return 'matchoptions';
2126
            default:
2127
                return $questionType;
2128
        }
2129
    }
2130
2131
    /**
2132
     * return the correct question type answers tag.
2133
     *
2134
     * @param string $questionType name
2135
     *
2136
     * @return string question type tag
2137
     */
2138
    public function getQuestionTypeAnswersTag($questionType)
2139
    {
2140
        switch ($questionType) {
2141
            case 'match':
2142
            case 'ddmatch':
2143
                return 'match';
2144
            default:
2145
                return 'answer';
2146
        }
2147
    }
2148
2149
    /**
2150
     * @param array Result of readMainQuestionsXml
2151
     *
2152
     * @return int Chamilo question type
2153
     */
2154
    public function matchMoodleChamiloQuestionTypes($questionsValues)
2155
    {
2156
        $moodleQuestionType = $questionsValues['qtype'];
2157
        $questionOptions = $moodleQuestionType.'_values';
2158
        // Check <single> located in <plugin_qtype_multichoice_question><multichoice><single><single>
2159
        if (
2160
            'multichoice' === $moodleQuestionType &&
2161
            isset($questionsValues[$questionOptions]) &&
2162
            isset($questionsValues[$questionOptions]['single']) &&
2163
            1 === (int) $questionsValues[$questionOptions]['single']
2164
        ) {
2165
            return UNIQUE_ANSWER;
2166
        }
2167
2168
        switch ($moodleQuestionType) {
2169
            case 'multichoice':
2170
                return MULTIPLE_ANSWER;
2171
            case 'multianswer':
2172
            case 'match':
2173
                return FILL_IN_BLANKS;
2174
            case 'essay':
2175
            case 'shortanswer':
2176
                return FREE_ANSWER;
2177
            case 'truefalse':
2178
                return UNIQUE_ANSWER;
2179
        }
2180
    }
2181
2182
    /**
2183
     * Fix moodle files that contains spaces.
2184
     *
2185
     * @param array  $importedFiles
2186
     * @param string $text
2187
     *
2188
     * @return mixed
2189
     */
2190
    public function fixPathInText($importedFiles, &$text)
2191
    {
2192
        if ($importedFiles) {
2193
            foreach ($importedFiles as $old => $new) {
2194
                // Ofaj fix moodle file names
2195
                // In some questions moodle text contains file with name like:
2196
                // Bild%20Check-In-Formular%20Ausfu%CC%88llen.jpg"
2197
                // rawurlencode function transforms '' (whitespace) to %20 and so on
2198
                $text = str_replace(rawurlencode($old), $new, $text);
2199
            }
2200
        }
2201
2202
        return $text;
2203
    }
2204
2205
    /**
2206
     * Process Moodle Answers to Chamilo.
2207
     *
2208
     * @param Exercise $exercise
2209
     * @param array    $questionList
2210
     * @param string   $questionType
2211
     * @param Question $questionInstance Question/Answer instance
2212
     * @param array    $currentQuestion
2213
     * @param array    $importedFiles
2214
     *
2215
     * @return int db response
2216
     */
2217
    public function processAnswers(
2218
        $exercise,
2219
        $questionList,
2220
        $questionType,
2221
        $questionInstance,
2222
        $currentQuestion,
2223
        $importedFiles,
2224
        $sectionPath = ''
2225
    ) {
2226
        switch ($questionType) {
2227
            case 'multichoice':
2228
                $objAnswer = new Answer($questionInstance->iid);
2229
                $questionWeighting = 0;
2230
                foreach ($questionList as $slot => $answer) {
2231
                    $this->processMultipleAnswer(
2232
                        $objAnswer,
2233
                        $answer,
2234
                        $slot + 1,
2235
                        $questionWeighting,
2236
                        $importedFiles,
2237
                        $sectionPath
2238
                    );
2239
                }
2240
2241
                // saves the answers into the data base
2242
                $objAnswer->save();
2243
                // sets the total weighting of the question
2244
                $questionInstance->updateWeighting($questionWeighting);
2245
                $questionInstance->save($exercise);
2246
2247
                return true;
2248
            case 'multianswer':
2249
                $objAnswer = new Answer($questionInstance->iid);
2250
                $coursePath = api_get_course_path();
2251
                $placeholder = $this->replaceMoodleChamiloCoursePath($currentQuestion['questiontext'], $sectionPath);
2252
                $optionsValues = [];
2253
                foreach ($questionList as $slot => $subQuestion) {
2254
                    $qtype = $subQuestion['qtype'];
2255
                    $optionsValues[] = $this->processFillBlanks(
2256
                        $objAnswer,
2257
                        $qtype,
2258
                        $subQuestion['plugin_qtype_'.$qtype.'_question'],
2259
                        $placeholder,
2260
                        $slot + 1
2261
                    );
2262
                }
2263
2264
                $answerOptionsWeight = '::';
2265
                $answerOptionsSize = '';
2266
                $questionWeighting = 0;
2267
                foreach ($optionsValues as $index => $value) {
2268
                    $questionWeighting += $value['weight'];
2269
                    $answerOptionsWeight .= $value['weight'].',';
2270
                    $answerOptionsSize .= $value['size'].',';
2271
                }
2272
2273
                $answerOptionsWeight = substr($answerOptionsWeight, 0, -1);
2274
                $answerOptionsSize = substr($answerOptionsSize, 0, -1);
2275
                $answerOptions = $answerOptionsWeight.':'.$answerOptionsSize.':0@';
2276
                $placeholder = $placeholder.PHP_EOL.$answerOptions;
2277
2278
                // This is a minor trick to clean the question description that in a multianswer is the main placeholder
2279
                $questionInstance->updateDescription('');
2280
                // sets the total weighting of the question
2281
                $questionInstance->updateWeighting($questionWeighting);
2282
                $questionInstance->save($exercise);
2283
                $this->fixPathInText($importedFiles, $placeholder);
2284
2285
                // saves the answers into the data base
2286
                $objAnswer->createAnswer($placeholder, 0, '', 0, 1);
2287
                $objAnswer->save();
2288
2289
                return true;
2290
            case 'match':
2291
                $objAnswer = new Answer($questionInstance->iid);
2292
                $placeholder = '';
2293
2294
                $optionsValues = $this->processFillBlanks(
2295
                    $objAnswer,
2296
                    'match',
2297
                    $questionList,
2298
                    $placeholder,
2299
                    0,
2300
                    $sectionPath
2301
                );
2302
2303
                $answerOptionsWeight = '::';
2304
                $answerOptionsSize = '';
2305
                $questionWeighting = 0;
2306
                foreach ($optionsValues as $index => $value) {
2307
                    $questionWeighting += $value['weight'];
2308
                    $answerOptionsWeight .= $value['weight'].',';
2309
                    $answerOptionsSize .= $value['size'].',';
2310
                }
2311
2312
                $answerOptionsWeight = substr($answerOptionsWeight, 0, -1);
2313
                $answerOptionsSize = substr($answerOptionsSize, 0, -1);
2314
                $answerOptions = $answerOptionsWeight.':'.$answerOptionsSize.':0@';
2315
                $placeholder = $placeholder.PHP_EOL.$answerOptions;
2316
2317
                // sets the total weighting of the question
2318
                $questionInstance->updateWeighting($questionWeighting);
2319
                $questionInstance->save($exercise);
2320
                // saves the answers into the database
2321
                $this->fixPathInText($importedFiles, $placeholder);
2322
                $objAnswer->createAnswer($placeholder, 0, '', 0, 1);
2323
                $objAnswer->save();
2324
2325
                return true;
2326
            case 'ddmatch':
2327
                $questionWeighting = $currentQuestion['defaultmark'];
2328
                $questionInstance->updateWeighting($questionWeighting);
2329
                $questionInstance->updateDescription(get_lang('ThisQuestionIsNotSupportedYet'));
2330
                $questionInstance->save($exercise);
2331
2332
                return false;
2333
            case 'shortanswer':
2334
            case 'essay':
2335
                $questionWeighting = $currentQuestion['defaultmark'];
2336
                $questionInstance->updateWeighting($questionWeighting);
2337
                $questionInstance->save($exercise);
2338
2339
                return true;
2340
            case 'truefalse':
2341
                $objAnswer = new Answer($questionInstance->iid);
2342
                $questionWeighting = 0;
2343
                foreach ($questionList as $slot => $answer) {
2344
                    $this->processTrueFalse(
2345
                        $objAnswer,
2346
                        $answer,
2347
                        $slot + 1,
2348
                        $questionWeighting,
2349
                        $importedFiles,
2350
                        $sectionPath
2351
                    );
2352
                }
2353
2354
                // saves the answers into the data base
2355
                $objAnswer->save();
2356
                // sets the total weighting of the question
2357
                $questionInstance->updateWeighting($questionWeighting);
2358
                $questionInstance->save($exercise);
2359
2360
                return false;
2361
            default:
2362
                return false;
2363
        }
2364
    }
2365
2366
    /**
2367
     * Process Chamilo Unique Answer.
2368
     *
2369
     * @param object $objAnswer
2370
     * @param array  $answerValues
2371
     * @param int    $position
2372
     * @param int    $questionWeighting
2373
     * @param array  $importedFiles
2374
     *
2375
     * @return int db response
2376
     */
2377
    public function processUniqueAnswer(
2378
        $objAnswer,
2379
        $answerValues,
2380
        $position,
2381
        &$questionWeighting,
2382
        $importedFiles,
2383
        $sectionPath = ''
2384
    ) {
2385
        $correct = (int) $answerValues['fraction'] ? (int) $answerValues['fraction'] : 0;
2386
        $answer = $answerValues['answertext'];
2387
        $comment = $answerValues['feedback'];
2388
        $weighting = $answerValues['fraction'];
2389
        $weighting = abs($weighting);
2390
        if ($weighting > 0) {
2391
            $questionWeighting += $weighting;
2392
        }
2393
        $goodAnswer = $correct ? true : false;
2394
2395
        $this->fixPathInText($importedFiles, $answer);
2396
        $answer = $this->replaceMoodleChamiloCoursePath($answer, $sectionPath);
2397
        $comment = $this->replaceMoodleChamiloCoursePath($comment, $sectionPath);
2398
2399
        $objAnswer->createAnswer(
2400
            $answer,
2401
            $goodAnswer,
2402
            $comment,
2403
            $weighting,
2404
            $position,
2405
            null,
2406
            null,
2407
            ''
2408
        );
2409
    }
2410
2411
    public function processMultipleAnswer(
2412
        Answer $objAnswer,
2413
        $answerValues,
2414
        $position,
2415
        &$questionWeighting,
2416
        $importedFiles,
2417
        $sectionPath = ''
2418
    ) {
2419
        $answer = $answerValues['answertext'];
2420
        $comment = $answerValues['feedback'];
2421
        $weighting = $answerValues['fraction'];
2422
        //$weighting = abs($weighting);
2423
        if ($weighting > 0) {
2424
            $questionWeighting += $weighting;
2425
        }
2426
        $goodAnswer = $weighting > 0;
2427
2428
        $this->fixPathInText($importedFiles, $answer);
2429
        $answer = $this->replaceMoodleChamiloCoursePath($answer, $sectionPath);
2430
        $comment = $this->replaceMoodleChamiloCoursePath($comment, $sectionPath);
2431
2432
        $objAnswer->createAnswer(
2433
            $answer,
2434
            $goodAnswer,
2435
            $comment,
2436
            $weighting,
2437
            $position,
2438
            null,
2439
            null,
2440
            ''
2441
        );
2442
    }
2443
2444
    /**
2445
     * Process Chamilo True False.
2446
     *
2447
     * @param Answer $objAnswer
2448
     * @param array  $answerValues
2449
     * @param int    $position
2450
     * @param int    $questionWeighting
2451
     * @param array  $importedFiles
2452
     *
2453
     * @return int db response
2454
     */
2455
    public function processTrueFalse(
2456
        $objAnswer,
2457
        $answerValues,
2458
        $position,
2459
        &$questionWeighting,
2460
        $importedFiles,
2461
        $sectionPath = ''
2462
    ) {
2463
        $correct = isset($answerValues['fraction']) ? (int) $answerValues['fraction'] : 0;
2464
        $answer = $answerValues['answertext'];
2465
        $comment = $answerValues['feedback'];
2466
        $weighting = $answerValues['fraction'];
2467
        $weighting = abs($weighting);
2468
        if ($weighting > 0) {
2469
            $questionWeighting += $weighting;
2470
        }
2471
        $goodAnswer = $correct ? true : false;
2472
2473
        $this->fixPathInText($importedFiles, $answer);
2474
        $answer = $this->replaceMoodleChamiloCoursePath($answer, $sectionPath);
2475
        $comment = $this->replaceMoodleChamiloCoursePath($comment, $sectionPath);
2476
2477
        $objAnswer->createAnswer(
2478
            $answer,
2479
            $goodAnswer,
2480
            $comment,
2481
            $weighting,
2482
            $position
2483
        );
2484
    }
2485
2486
    /**
2487
     * Process Chamilo FillBlanks.
2488
     *
2489
     * @param object $objAnswer
2490
     * @param array  $questionType
2491
     * @param array  $answerValues
2492
     * @param string $placeholder
2493
     * @param int    $position
2494
     *
2495
     * @return int db response
2496
     */
2497
    public function processFillBlanks(
2498
        $objAnswer,
2499
        $questionType,
2500
        $answerValues,
2501
        &$placeholder,
2502
        $position,
2503
        $sectionPath = ''
2504
    ) {
2505
        switch ($questionType) {
2506
            case 'multichoice':
2507
                $optionsValues = [];
2508
                $correctAnswer = '';
2509
                $othersAnswer = '';
2510
                foreach ($answerValues as $answer) {
2511
                    $correct = (int) $answer['fraction'];
2512
                    if ($correct) {
2513
                        $correctAnswer .= $answer['answertext'].'|';
2514
                        $optionsValues['weight'] = $answer['fraction'];
2515
                        $optionsValues['size'] = '200';
2516
                    } else {
2517
                        $othersAnswer .= $answer['answertext'].'|';
2518
                    }
2519
                }
2520
                $currentAnswers = $correctAnswer.$othersAnswer;
2521
                $currentAnswers = '['.substr($currentAnswers, 0, -1).']';
2522
                $placeholder = str_replace("{#$position}", $currentAnswers, $placeholder);
2523
2524
                return $optionsValues;
2525
            case 'shortanswer':
2526
                $optionsValues = [];
2527
                $correctAnswer = '';
2528
                foreach ($answerValues as $answer) {
2529
                    $correct = (int) $answer['fraction'];
2530
                    if ($correct) {
2531
                        $correctAnswer .= $answer['answertext'];
2532
                        $optionsValues['weight'] = $answer['fraction'];
2533
                        $optionsValues['size'] = '200';
2534
                    }
2535
                }
2536
2537
                $currentAnswers = '['.$correctAnswer.']';
2538
                $placeholder = str_replace("{#$position}", $currentAnswers, $placeholder);
2539
2540
                return $optionsValues;
2541
            case 'match':
2542
                $answers = [];
2543
                // Here first we need to extract all the possible answers
2544
                foreach ($answerValues as $slot => $answer) {
2545
                    $answers[$slot] = $answer['answertext'];
2546
                }
2547
2548
                // Now we set the order of the values matching the correct answer and set it to the first element
2549
                $optionsValues = [];
2550
                foreach ($answerValues as $slot => $answer) {
2551
                    $correctAnswer = '';
2552
                    $othersAnswers = '';
2553
                    $correctAnswer .= $answer['answertext'].'|';
2554
2555
                    foreach ($answers as $other) {
2556
                        if ($other !== $answer['answertext']) {
2557
                            $othersAnswers .= $other.'|';
2558
                        }
2559
                    }
2560
2561
                    $optionsValues[$slot]['weight'] = 1;
2562
                    $optionsValues[$slot]['size'] = '200';
2563
2564
                    $currentAnswers = htmlentities($correctAnswer.$othersAnswers);
2565
                    $currentAnswers = '['.substr($currentAnswers, 0, -1).'] ';
2566
                    $answer['questiontext'] = $this->replaceMoodleChamiloCoursePath($answer['questiontext'], $sectionPath);
2567
2568
                    $placeholder .= '<p> '.strip_tags($answer['questiontext']).' '.$currentAnswers.' </p>';
2569
                }
2570
2571
                return $optionsValues;
2572
            default:
2573
                return false;
2574
        }
2575
    }
2576
2577
    /**
2578
     * Updates the previous item ID for a given learning path item.
2579
     *
2580
     * @param int $currentItemId  The ID of the current item.
2581
     * @param int $previousItemId The ID of the previous item to be set.
2582
     */
2583
    public function updateLpItemPreviousId($currentItemId, $previousItemId)
2584
    {
2585
        $table = Database::get_course_table(TABLE_LP_ITEM);
2586
        $sql = "UPDATE $table SET previous_item_id = $previousItemId WHERE id = $currentItemId";
2587
        Database::query($sql);
2588
    }
2589
2590
    /**
2591
     * Updates the next item ID for a given learning path item.
2592
     *
2593
     * @param int $previousItemId The ID of the previous item.
2594
     * @param int $currentItemId  The ID of the current item to be set as next.
2595
     */
2596
    public function updateLpItemNextId($previousItemId, $currentItemId)
2597
    {
2598
        $table = Database::get_course_table(TABLE_LP_ITEM);
2599
        $sql = "UPDATE $table SET next_item_id = $currentItemId WHERE id = $previousItemId";
2600
        Database::query($sql);
2601
    }
2602
2603
    /**
2604
     * get All files associated with a question.
2605
     *
2606
     * @param $filesXml
2607
     *
2608
     * @return array
2609
     */
2610
    public function getAllQuestionFiles($filesXml)
2611
    {
2612
        $moduleDoc = new DOMDocument();
2613
        $moduleRes = @$moduleDoc->loadXML($filesXml);
2614
2615
        if (empty($moduleRes)) {
2616
            return [];
2617
        }
2618
2619
        $allFiles = [];
2620
        $activities = $moduleDoc->getElementsByTagName('file');
2621
        foreach ($activities as $activity) {
2622
            $currentItem = [];
2623
            $thisIsAnInvalidItem = false;
2624
2625
            if ($activity->childNodes->length) {
2626
                foreach ($activity->childNodes as $item) {
2627
                    if ($item->nodeName == 'component' && $item->nodeValue == 'mod_resource') {
2628
                        $thisIsAnInvalidItem = true;
2629
                    }
2630
2631
                    if ($item->nodeName == 'component' && $item->nodeValue == 'mod_scorm') {
2632
                        $currentItem['modscorm'] = true;
2633
                    }
2634
2635
                    if ($item->nodeName == 'contenthash') {
2636
                        $currentItem['contenthash'] = $item->nodeValue;
2637
                    }
2638
2639
                    if ($item->nodeName == 'filename') {
2640
                        $currentItem['filename'] = $item->nodeValue;
2641
                    }
2642
2643
                    if ($item->nodeName == 'filesize') {
2644
                        $currentItem['filesize'] = $item->nodeValue;
2645
                    }
2646
2647
                    if ($item->nodeName == 'contextid') {
2648
                        $currentItem['contextid'] = $item->nodeValue;
2649
                    }
2650
2651
                    if ($item->nodeName == 'mimetype' && $item->nodeValue == 'document/unknown') {
2652
                        $thisIsAnInvalidItem = true;
2653
                    }
2654
2655
                    if ($item->nodeName == 'mimetype' && $item->nodeValue !== 'document/unknown') {
2656
                        $currentItem['mimetype'] = $item->nodeValue;
2657
                    }
2658
                }
2659
            }
2660
2661
            if (!$thisIsAnInvalidItem) {
2662
                $allFiles[] = $currentItem;
2663
            }
2664
        }
2665
2666
        return $allFiles;
2667
    }
2668
2669
    /**
2670
     * Litle utility to delete the unuseful tags.
2671
     *
2672
     * @param $array
2673
     * @param $keys
2674
     */
2675
    public function traverseArray(&$array, $keys)
2676
    {
2677
        foreach ($array as $key => &$value) {
2678
            if (is_array($value)) {
2679
                $this->traverseArray($value, $keys);
2680
            } else {
2681
                if (in_array($key, $keys)) {
2682
                    unset($array[$key]);
2683
                }
2684
            }
2685
        }
2686
    }
2687
2688
    /**
2689
     * Processes quiz questions organized by category.
2690
     *
2691
     * @param DOMDocument $questionsDoc The document containing the questions XML.
2692
     * @param DOMDocument $quizDoc      The document containing the quiz XML.
2693
     *
2694
     * @return array Returns an array of processed questions filtered by category.
2695
     */
2696
    protected function processByCategory($questionsDoc, $quizDoc)
2697
    {
2698
        $questionInstances = $quizDoc->getElementsByTagName('question_instance');
2699
        $questionSetReferences = [];
2700
2701
        foreach ($questionInstances as $instance) {
2702
            $questionSetReference = $instance->getElementsByTagName('question_set_reference')->item(0);
2703
            if ($questionSetReference) {
2704
                $filterCondition = $questionSetReference->getElementsByTagName('filtercondition')->item(0)->nodeValue;
2705
                $filterCondition = json_decode($filterCondition, true);
2706
2707
                if (isset($filterCondition['questioncategoryid'])) {
2708
                    $categoryId = $filterCondition['questioncategoryid'];
2709
                    $questionSetReferences[] = $categoryId;
2710
                }
2711
            }
2712
        }
2713
2714
        if (empty($questionSetReferences)) {
2715
            return [];
2716
        }
2717
2718
        $processedQuestions = [];
2719
        $questionsCount = 0;
2720
2721
        foreach ($questionSetReferences as $categoryId) {
2722
            $xpath = new DOMXPath($questionsDoc);
2723
            $query = "//question_bank_entry[questioncategoryid='$categoryId']/question_version/question_versions/questions/question";
2724
            $entries = $xpath->query($query);
2725
2726
            foreach ($entries as $question) {
2727
                if ($questionsCount >= count($questionInstances)) {
2728
                    break;
2729
                }
2730
2731
                $currentItem = $this->processQuestionV4($question);
2732
                if (!empty($currentItem)) {
2733
                    $processedQuestions[] = $currentItem;
2734
                    $questionsCount++;
2735
                }
2736
            }
2737
        }
2738
2739
        return $processedQuestions;
2740
    }
2741
2742
    /**
2743
     * Processes quiz questions based on question bank references.
2744
     *
2745
     * @param DOMDocument $questionsDoc The document containing the questions XML.
2746
     * @param DOMDocument $quizDoc      The document containing the quiz XML.
2747
     *
2748
     * @return array Returns an array of processed questions from the question bank.
2749
     */
2750
    protected function processByBankReference($questionsDoc, $quizDoc)
2751
    {
2752
        $questionInstances = $quizDoc->getElementsByTagName('question_instance');
2753
        $questionBankEntries = [];
2754
2755
        foreach ($questionInstances as $instance) {
2756
            $questionReference = $instance->getElementsByTagName('question_reference')->item(0);
2757
            if ($questionReference) {
2758
                $questionBankEntryId = $questionReference->getElementsByTagName('questionbankentryid')->item(0)->nodeValue;
2759
                $questionBankEntries[] = $questionBankEntryId;
2760
            }
2761
        }
2762
2763
        if (empty($questionBankEntries)) {
2764
            return [];
2765
        }
2766
2767
        $processedQuestions = [];
2768
        $xpath = new DOMXPath($questionsDoc);
2769
2770
        foreach ($questionBankEntries as $entryId) {
2771
            $query = "//question_bank_entry[@id='$entryId']/question_version/question_versions/questions/question";
2772
            $entries = $xpath->query($query);
2773
2774
            foreach ($entries as $question) {
2775
                $processedQuestions[] = $this->processQuestionV4($question);
2776
            }
2777
        }
2778
2779
        return $processedQuestions;
2780
    }
2781
2782
    /**
2783
     * Processes individual quiz question data for Moodle version 4.
2784
     *
2785
     * @param DOMElement $question The DOM element representing a question in the XML.
2786
     *
2787
     * @return array Returns an associative array containing question data, including type, text, and answers.
2788
     */
2789
    protected function processQuestionV4($question)
2790
    {
2791
        $currentItem = [
2792
            "questionid" => $question->getAttribute('id'),
2793
            "name" => '',
2794
            "questiontext" => '',
2795
            "generalfeedback" => '',
2796
            "defaultmark" => "1.0000000",
2797
            "penalty" => "0.3333333",
2798
            "qtype" => '',
2799
            "plugin_qtype_question" => [],
2800
        ];
2801
2802
        $questionType = '';
2803
        foreach ($question->childNodes as $item) {
2804
            switch ($item->nodeName) {
2805
                case 'name':
2806
                    $currentItem['name'] = htmlspecialchars_decode($item->nodeValue);
2807
                    break;
2808
                case 'questiontext':
2809
                    $currentItem['questiontext'] = htmlspecialchars_decode($item->nodeValue);
2810
                    break;
2811
                case 'generalfeedback':
2812
                    $currentItem['generalfeedback'] = htmlspecialchars_decode($item->nodeValue);
2813
                    break;
2814
                case 'qtype':
2815
                    $questionType = $item->nodeValue;
2816
                    $currentItem['qtype'] = $questionType;
2817
                    break;
2818
                case 'plugin_qtype_'.$questionType.'_question':
2819
                    $currentItem['plugin_qtype_'.$questionType.'_question'] = $this->processAnswersV4($item);
2820
                    break;
2821
            }
2822
        }
2823
2824
        return !empty($currentItem['name']) ? $currentItem : [];
2825
    }
2826
2827
    /**
2828
     * Processes the answers for a given quiz question in Moodle version 4.
2829
     *
2830
     * @param DOMElement $item The DOM element containing answer data.
2831
     *
2832
     * @return array Returns an array of processed answers, each containing details like answer text and fraction.
2833
     */
2834
    protected function processAnswersV4($item)
2835
    {
2836
        $answersList = [];
2837
        $answers = $item->getElementsByTagName('answer');
2838
2839
        foreach ($answers as $i => $answer) {
2840
            $answerData = [
2841
                'answerid' => $answer->getAttribute('id'),
2842
                'answertext' => '',
2843
                'fraction' => '0.0000000',
2844
            ];
2845
2846
            foreach ($answer->childNodes as $properties) {
2847
                switch ($properties->nodeName) {
2848
                    case 'answertext':
2849
                        $answerData['answertext'] = htmlspecialchars_decode($properties->nodeValue);
2850
                        break;
2851
                    case 'fraction':
2852
                        $answerData['fraction'] = $properties->nodeValue;
2853
                        break;
2854
                    case 'feedback':
2855
                        $answerData['feedback'] = htmlspecialchars_decode($properties->nodeValue);
2856
                        break;
2857
                }
2858
            }
2859
2860
            $answersList[$i] = $answerData;
2861
        }
2862
2863
        return $answersList;
2864
    }
2865
2866
    /**
2867
     * Check if folder is empty or not.
2868
     *
2869
     * @param $dir
2870
     *
2871
     * @return bool
2872
     */
2873
    private function isEmptyDir($dir)
2874
    {
2875
        $iterator = new FilesystemIterator($dir);
2876
        $isDirEmpty = !$iterator->valid();
2877
2878
        return $isDirEmpty;
2879
    }
2880
}
2881