Completed
Push — 1.10.x ( 039d05...2fbd07 )
by Yannick
45:46
created

exercise_import.inc.php ➔ qtiProcessManifest()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 44
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
eloc 34
nc 9
nop 1
dl 0
loc 44
rs 6.7272
c 1
b 0
f 1
1
<?php
2
/* For licensing terms, see /license.txt */
3
/**
4
 * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
5
 * @package chamilo.exercise
6
 * @author claro team <[email protected]>
7
 * @author Guillaume Lederer <[email protected]>
8
 * @author Yannick Warnier <[email protected]>
9
 */
10
11
/**
12
 * Unzip the exercise in the temp folder
13
 * @param string The path of the temporary directory where the exercise was uploaded and unzipped
14
 * @param string
15
 * @param string $baseWorkDir
16
 * @param string $uploadPath
17
 * @return bool
18
 */
19
function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
0 ignored issues
show
Best Practice introduced by
The function get_and_unzip_uploaded_exercise() has been defined more than once; this definition is ignored, only the first definition in main/exercice/export/aiken/aiken_import.inc.php (L62-81) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
20
{
21
    $_course = api_get_course_info();
22
    $_user = api_get_user_info();
23
24
    //Check if the file is valid (not to big and exists)
25 View Code Duplication
    if (!isset($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
26
        // upload failed
27
        return false;
28
    }
29
30
    if (preg_match('/.zip$/i', $_FILES['userFile']['name']) &&
31
        handle_uploaded_document(
32
            $_course,
33
            $_FILES['userFile'],
34
            $baseWorkDir,
35
            $uploadPath,
36
            $_user['user_id'],
37
            0,
38
            null,
39
            1
40
        )
41
    ) {
42
        return true;
43
    }
44
    return false;
45
}
46
47
/**
48
 * Imports an exercise in QTI format if the XML structure can be found in it
49
 * @param array $file
50
 * @return string|array as a backlog of what was really imported, and error or debug messages to display
51
 */
52
function import_exercise($file)
53
{
54
    global $exercise_info;
55
    global $element_pile;
56
    global $non_HTML_tag_to_avoid;
57
    global $record_item_body;
58
    // used to specify the question directory where files could be found in relation in any question
59
    global $questionTempDir;
60
    global $resourcesLinks;
61
62
    $baseWorkDir = api_get_path(SYS_ARCHIVE_PATH) . 'qti2';
63
64
    if (!is_dir($baseWorkDir)) {
65
        mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true);
66
    }
67
68
    $uploadPath = '/';
69
70
    // set some default values for the new exercise
71
    $exercise_info = array();
72
    $exercise_info['name'] = preg_replace('/.zip$/i', '', $file);
73
    $exercise_info['question'] = array();
74
    $element_pile = array();
75
76
    // create parser and array to retrieve info from manifest
77
    $element_pile = array(); //pile to known the depth in which we are
78
    //$module_info = array (); //array to store the info we need
79
80
    // if file is not a .zip, then we cancel all
81
82
    if (!preg_match('/.zip$/i', $file)) {
83
84
        return 'UplZipCorrupt';
85
    }
86
87
    // unzip the uploaded file in a tmp directory
88
    if (!get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)) {
89
90
        return 'UplZipCorrupt';
91
    }
92
93
    // find the different manifests for each question and parse them.
94
    $exerciseHandle = opendir($baseWorkDir);
95
    //$question_number = 0;
96
    $file_found = false;
97
    $operation = false;
98
    $result = false;
99
    $filePath = null;
100
    $resourcesLinks = array();
101
102
    // parse every subdirectory to search xml question files
103
    while (false !== ($file = readdir($exerciseHandle))) {
104
        if (is_dir($baseWorkDir . '/' . $file) && $file != "." && $file != "..") {
105
            // Find each manifest for each question repository found
106
            $questionHandle = opendir($baseWorkDir . '/' . $file);
107
            // Only analyse one level of subdirectory - no recursivity here
108
            while (false !== ($questionFile = readdir($questionHandle))) {
109
                if (preg_match('/.xml$/i', $questionFile)) {
110
                    $isQti = isQtiQuestionBank($baseWorkDir . '/' . $file . '/' . $questionFile);
111
                    if ($isQti) {
112
                        $result = qti_parse_file($baseWorkDir, $file, $questionFile);
113
                        $filePath = $baseWorkDir . $file;
114
                        $file_found = true;
115
                    } else {
116
                        $isManifest = isQtiManifest($baseWorkDir . '/' . $file . '/' . $questionFile);
117
                        if ($isManifest) {
118
                            $resourcesLinks = qtiProcessManifest($baseWorkDir . '/' . $file . '/' . $questionFile);
119
                        }
120
                    }
121
                }
122
            }
123
        } elseif (preg_match('/.xml$/i', $file)) {
124
            $isQti = isQtiQuestionBank($baseWorkDir . '/' . $file);
125
            if ($isQti) {
126
                $result = qti_parse_file($baseWorkDir, '', $file);
127
                $filePath = $baseWorkDir . '/' . $file;
128
                $file_found = true;
129
            } else {
130
                $isManifest = isQtiManifest($baseWorkDir . '/' . $file);
131
                if ($isManifest) {
132
                    $resourcesLinks = qtiProcessManifest($baseWorkDir . '/' . $file);
133
                }
134
            }
135
136
        }
137
    }
138
139
    if (!$file_found) {
140
141
        return 'NoXMLFileFoundInTheZip';
142
    }
143
144
    if ($result == false) {
145
146
        return false;
147
    }
148
149
    $doc = new DOMDocument();
150
    $doc->load($filePath);
151
152
    // 1. Create exercise.
153
    $exercise = new Exercise();
154
    $exercise->exercise = $exercise_info['name'];
155
156
    $exercise->save();
157
    $last_exercise_id = $exercise->selectId();
158
    if (!empty($last_exercise_id)) {
159
        // For each question found...
160
        foreach ($exercise_info['question'] as $question_array) {
161
            //2. Create question
162
            $question = new Ims2Question();
163
            $question->type = $question_array['type'];
164
            $question->setAnswer();
165
            if (strlen($question_array['title']) < 50) {
166
                $question->updateTitle(formatText(strip_tags($question_array['title'])) . '...');
167
            } else {
168
                $question->updateTitle(formatText(substr(strip_tags($question_array['title']), 0, 50)));
169
                $question->updateDescription($question_array['title']);
170
            }
171
            //$question->updateDescription($question_array['title']);
172
            $question->save($last_exercise_id);
173
            $last_question_id = $question->selectId();
174
            //3. Create answer
175
            $answer = new Answer($last_question_id);
176
            $answer->new_nbrAnswers = count($question_array['answer']);
177
            $totalCorrectWeight = 0;
178
            $j = 1;
179
            $matchAnswerIds = array();
180
            foreach ($question_array['answer'] as $key => $answers) {
181
                if (preg_match('/_/', $key)) {
182
                    $split = explode('_', $key);
183
                    $i = $split[1];
184
                } else {
185
                    $i = $j;
186
                    $j++;
187
                    $matchAnswerIds[$key] = $j;
188
                }
189
                // Answer
190
                $answer->new_answer[$i] = formatText($answers['value']);
191
                // Comment
192
                $answer->new_comment[$i] = isset($answers['feedback']) ? formatText($answers['feedback']) : null;
193
                // Position
194
                $answer->new_position[$i] = $i;
195
                // Correct answers
196
                if (in_array($key, $question_array['correct_answers'])) {
197
                    $answer->new_correct[$i] = 1;
198
                } else {
199
                    $answer->new_correct[$i] = 0;
200
                }
201
                $answer->new_weighting[$i] = $question_array['weighting'][$key];
202
                if ($answer->new_correct[$i]) {
203
                    $totalCorrectWeight += $answer->new_weighting[$i];
204
                }
205
            }
206
            $question->updateWeighting($totalCorrectWeight);
207
            $question->save($last_exercise_id);
208
            $answer->save();
209
        }
210
211
        // delete the temp dir where the exercise was unzipped
212
        my_delete($baseWorkDir . $uploadPath);
213
        return $last_exercise_id;
214
    }
215
216
    return false;
217
}
218
219
/**
220
 * We assume the file charset is UTF8
221
 **/
222
function formatText($text)
223
{
224
    return api_html_entity_decode($text);
225
}
226
227
/**
228
 * Parses a given XML file and fills global arrays with the elements
229
 * @param string $exercisePath
230
 * @param string $file
231
 * @param string $questionFile
232
 * @return bool
233
 */
234
function qti_parse_file($exercisePath, $file, $questionFile)
235
{
236
    global $non_HTML_tag_to_avoid;
237
    global $record_item_body;
238
    global $questionTempDir;
239
240
    $questionTempDir = $exercisePath . '/' . $file . '/';
241
    $questionFilePath = $questionTempDir . $questionFile;
242
243
    if (!($fp = fopen($questionFilePath, 'r'))) {
244
        Display::addFlash(Display::return_message(get_lang('Error opening question\'s XML file'), 'error'));
245
246
        return false;
247
    } else {
248
        $data = fread($fp, filesize($questionFilePath));
249
    }
250
251
    //parse XML question file
252
    //$data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
253
    $data = stripGivenTags($data, array('p', 'front'));
254
    $qtiVersion = array();
255
    $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data, $qtiVersion);
256
    $qtiMainVersion = 2; //by default, assume QTI version 2
257
    if ($match) {
258
        $qtiMainVersion = $qtiVersion[1];
259
    }
260
261
    //used global variable start values declaration :
262
263
    $record_item_body = false;
264
    $non_HTML_tag_to_avoid = array(
265
        "SIMPLECHOICE",
266
        "CHOICEINTERACTION",
267
        "INLINECHOICEINTERACTION",
268
        "INLINECHOICE",
269
        "SIMPLEMATCHSET",
270
        "SIMPLEASSOCIABLECHOICE",
271
        "TEXTENTRYINTERACTION",
272
        "FEEDBACKINLINE",
273
        "MATCHINTERACTION",
274
        "ITEMBODY",
275
        "BR",
276
        "IMG"
277
    );
278
279
    $question_format_supported = true;
280
281
    $xml_parser = xml_parser_create();
282
    xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, false);
283
    if ($qtiMainVersion == 1) {
284
        xml_set_element_handler($xml_parser, 'startElementQti1', 'endElementQti1');
285
        xml_set_character_data_handler($xml_parser, 'elementDataQti1');
286
    } else {
287
        xml_set_element_handler($xml_parser, 'startElementQti2', 'endElementQti2');
288
        xml_set_character_data_handler($xml_parser, 'elementDataQti2');
289
    }
290
    if (!xml_parse($xml_parser, $data, feof($fp))) {
291
        // if reading of the xml file in not successful :
292
        // set errorFound, set error msg, break while statement
293
        $error = xml_get_error_code();
294
        Display::addFlash(
295
            Display::return_message(
296
                get_lang('Error reading XML file') . sprintf('[%d:%d]', xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser)),
297
                'error'
298
            )
299
        );
300
301
        return false;
302
    }
303
304
    //close file
305
    fclose($fp);
306
    if (!$question_format_supported) {
307
        Display::addFlash(
308
            Display::return_message(
309
                get_lang(
310
                    'Unknown question format in file %file',
311
                    array(
312
                        '%file' => $questionFile,
313
                    )
314
                ),
315
                'error'
316
            )
317
        );
318
319
        return false;
320
    }
321
    return true;
322
}
323
324
/**
325
 * Function used by the SAX xml parser when the parser meets a opening tag
326
 *
327
 * @param object $parser xml parser created with "xml_parser_create()"
328
 * @param string $name name of the element
329
 * @param array $attributes
330
 */
331
function startElementQti2($parser, $name, $attributes)
332
{
333
    global $element_pile;
334
    global $exercise_info;
335
    global $current_question_ident;
336
    global $current_answer_id;
337
    global $current_match_set;
338
    global $currentAssociableChoice;
339
    global $current_question_item_body;
340
    global $record_item_body;
341
    global $non_HTML_tag_to_avoid;
342
    global $current_inlinechoice_id;
343
    global $cardinality;
344
    global $questionTempDir;
345
346
    array_push($element_pile, $name);
347
    $current_element = end($element_pile);
348 View Code Duplication
    if (sizeof($element_pile) >= 2) {
349
        $parent_element = $element_pile[sizeof($element_pile) - 2];
350
    } else {
351
        $parent_element = "";
352
    }
353 View Code Duplication
    if (sizeof($element_pile) >= 3) {
354
        $grant_parent_element = $element_pile[sizeof($element_pile) - 3];
355
    } else {
356
        $grant_parent_element = "";
357
    }
358
359 View Code Duplication
    if ($record_item_body) {
360
361
        if ((!in_array($current_element, $non_HTML_tag_to_avoid))) {
362
            $current_question_item_body .= "<" . $name;
363
            foreach ($attributes as $attribute_name => $attribute_value) {
364
                $current_question_item_body .= " " . $attribute_name . "=\"" . $attribute_value . "\"";
365
            }
366
            $current_question_item_body .= ">";
367
        } else {
368
            //in case of FIB question, we replace the IMS-QTI tag b y the correct answer between "[" "]",
369
            //we first save with claroline tags ,then when the answer will be parsed, the claroline tags will be replaced
370
371
            if ($current_element == 'INLINECHOICEINTERACTION') {
372
                $current_question_item_body .= "**claroline_start**" . $attributes['RESPONSEIDENTIFIER'] . "**claroline_end**";
373
            }
374
            if ($current_element == 'TEXTENTRYINTERACTION') {
375
                $correct_answer_value = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
376
                $current_question_item_body .= "[" . $correct_answer_value . "]";
377
378
            }
379
            if ($current_element == 'BR') {
380
                $current_question_item_body .= "<br />";
381
            }
382
        }
383
    }
384
385
    switch ($current_element) {
386
        case 'ASSESSMENTITEM':
387
            //retrieve current question
388
            $current_question_ident = $attributes['IDENTIFIER'];
389
            $exercise_info['question'][$current_question_ident] = array();
390
            $exercise_info['question'][$current_question_ident]['answer'] = array();
391
            $exercise_info['question'][$current_question_ident]['correct_answers'] = array();
392
            $exercise_info['question'][$current_question_ident]['title'] = $attributes['TITLE'];
393
            $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir;
394
            break;
395
        case 'SECTION':
396
            //retrieve exercise name
397
            if (isset($attributes['TITLE']) && !empty($attributes['TITLE'])) {
398
                $exercise_info['name'] = $attributes['TITLE'];
399
            }
400
            break;
401
        case 'RESPONSEDECLARATION':
402
            // Retrieve question type
403 View Code Duplication
            if ("multiple" == $attributes['CARDINALITY']) {
404
                $exercise_info['question'][$current_question_ident]['type'] = MCMA;
405
                $cardinality = 'multiple';
406
            }
407 View Code Duplication
            if ("single" == $attributes['CARDINALITY']) {
408
                $exercise_info['question'][$current_question_ident]['type'] = MCUA;
409
                $cardinality = 'single';
410
            }
411
            //needed for FIB
412
            $current_answer_id = $attributes['IDENTIFIER'];
413
            break;
414
        case 'INLINECHOICEINTERACTION':
415
            $exercise_info['question'][$current_question_ident]['type'] = FIB;
416
            $exercise_info['question'][$current_question_ident]['subtype'] = 'LISTBOX_FILL';
417
            $current_answer_id = $attributes['RESPONSEIDENTIFIER'];
418
            break;
419
        case 'INLINECHOICE':
420
            $current_inlinechoice_id = $attributes['IDENTIFIER'];
421
            break;
422
        case 'TEXTENTRYINTERACTION':
423
            $exercise_info['question'][$current_question_ident]['type'] = FIB;
424
            $exercise_info['question'][$current_question_ident]['subtype'] = 'TEXTFIELD_FILL';
425
            $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
426
            //replace claroline tags
427
            break;
428
        case 'MATCHINTERACTION':
429
            $exercise_info['question'][$current_question_ident]['type'] = MATCHING;
430
            break;
431
        case 'SIMPLEMATCHSET':
432
            if (!isset($current_match_set)) {
433
                $current_match_set = 1;
434
            } else {
435
                $current_match_set++;
436
            }
437
            $exercise_info['question'][$current_question_ident]['answer'][$current_match_set] = array();
438
            break;
439
        case 'SIMPLEASSOCIABLECHOICE':
440
            $currentAssociableChoice = $attributes['IDENTIFIER'];
441
            break;
442
        //retrieve answers id for MCUA and MCMA questions
443
        case 'SIMPLECHOICE':
444
            $current_answer_id = $attributes['IDENTIFIER'];
445 View Code Duplication
            if (!isset($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id])) {
446
                $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id] = array();
447
            }
448
            break;
449
        case 'MAPENTRY':
450
            if ($parent_element == "MAPPING") {
451
                $answer_id = $attributes['MAPKEY'];
452 View Code Duplication
                if (!isset ($exercise_info['question'][$current_question_ident]['weighting'])) {
453
                    $exercise_info['question'][$current_question_ident]['weighting'] = array();
454
                }
455
                $exercise_info['question'][$current_question_ident]['weighting'][$answer_id] = $attributes['MAPPEDVALUE'];
456
            }
457
            break;
458
        case 'MAPPING':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
459
            if (isset ($attributes['DEFAULTVALUE'])) {
460
                $exercise_info['question'][$current_question_ident]['default_weighting'] = $attributes['DEFAULTVALUE'];
461
            }
462
        case 'ITEMBODY':
463
            $record_item_body = true;
464
            $current_question_item_body = '';
465
            break;
466
        case 'IMG':
467
            $exercise_info['question'][$current_question_ident]['attached_file_url'] = $attributes['SRC'];
468
            break;
469
    }
470
}
471
472
/**
473
 * Function used by the SAX xml parser when the parser meets a closing tag
474
 *
475
 * @param $parser xml parser created with "xml_parser_create()"
476
 * @param $name name of the element
477
 */
478
function endElementQti2($parser, $name)
479
{
480
    global $element_pile;
481
    global $exercise_info;
482
    global $current_question_ident;
483
    global $record_item_body;
484
    global $current_question_item_body;
485
    global $non_HTML_tag_to_avoid;
486
    global $cardinality;
487
488
    $current_element = end($element_pile);
489
490
    //treat the record of the full content of itembody tag :
491
492
    if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
493
        $current_question_item_body .= "</" . $name . ">";
494
    }
495
496
    switch ($name) {
497
        case 'ITEMBODY':
498
            $record_item_body = false;
499
            if ($exercise_info['question'][$current_question_ident]['type'] == FIB) {
500
                $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
501
            } else {
502
                $exercise_info['question'][$current_question_ident]['statement'] = $current_question_item_body;
503
            }
504
            break;
505
    }
506
    array_pop($element_pile);
507
}
508
509
/**
510
 * @param $parser
511
 * @param $data
512
 */
513
function elementDataQti2($parser, $data)
514
{
515
    global $element_pile;
516
    global $exercise_info;
517
    global $current_question_ident;
518
    global $current_answer_id;
519
    global $current_match_set;
520
    global $currentAssociableChoice;
521
    global $current_question_item_body;
522
    global $record_item_body;
523
    global $non_HTML_tag_to_avoid;
524
    global $current_inlinechoice_id;
525
    global $cardinality;
526
    global $resourcesLinks;
527
528
    $current_element = end($element_pile);
529 View Code Duplication
    if (sizeof($element_pile) >= 2) {
530
        $parent_element = $element_pile[sizeof($element_pile) - 2];
531
    } else {
532
        $parent_element = "";
533
    }
534 View Code Duplication
    if (sizeof($element_pile) >= 3) {
535
        $grant_parent_element = $element_pile[sizeof($element_pile) - 3];
536
    } else {
537
        $grant_parent_element = "";
538
    }
539
540
    //treat the record of the full content of itembody tag (needed for question statment and/or FIB text:
541
542
    if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
543
        $current_question_item_body .= $data;
544
    }
545
546
    switch ($current_element) {
547 View Code Duplication
        case 'SIMPLECHOICE':
548
            if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'])) {
549
                $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] = trim($data);
550
            } else {
551
                $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] .= '' . trim($data);
552
            }
553
            break;
554 View Code Duplication
        case 'FEEDBACKINLINE':
555
            if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'])) {
556
                $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] = trim($data);
557
            } else {
558
                $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] .= ' ' . trim($data);
559
            }
560
            break;
561
        case 'SIMPLEASSOCIABLECHOICE':
562
            $exercise_info['question'][$current_question_ident]['answer'][$current_match_set][$currentAssociableChoice] = trim($data);
563
            break;
564
        case 'VALUE':
565
            if ($parent_element == "CORRECTRESPONSE") {
566
                if ($cardinality == "single") {
567
                    $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id] = $data;
568
                } else {
569
                    $exercise_info['question'][$current_question_ident]['correct_answers'][] = $data;
570
                }
571
            }
572
            break;
573
        case 'ITEMBODY':
574 View Code Duplication
            foreach ($resourcesLinks['manifest'] as $key=>$value) {
575
                $data = preg_replace('|' . $value . '|', $resourcesLinks['web'][$key], $data);
576
            }
577
            $current_question_item_body .= $data;
578
            break;
579
        case 'INLINECHOICE':
580
            // if this is the right answer, then we must replace the claroline tags in the FIB text bye the answer between "[" and "]" :
581
            $answer_identifier = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
582
            if ($current_inlinechoice_id == $answer_identifier) {
583
                $current_question_item_body = str_replace(
584
                    "**claroline_start**" . $current_answer_id . "**claroline_end**",
585
                    "[" . $data . "]",
586
                    $current_question_item_body
587
                );
588
            } else {
589 View Code Duplication
                if (!isset ($exercise_info['question'][$current_question_ident]['wrong_answers'])) {
590
                    $exercise_info['question'][$current_question_ident]['wrong_answers'] = array();
591
                }
592
                $exercise_info['question'][$current_question_ident]['wrong_answers'][] = $data;
593
            }
594
            break;
595
    }
596
}
597
598
/**
599
 * Function used by the SAX xml parser when the parser meets a opening tag for QTI1
600
 *
601
 * @param object $parser xml parser created with "xml_parser_create()"
602
 * @param string $name name of the element
603
 * @param array $attributes
604
 */
605
function startElementQti1($parser, $name, $attributes)
606
{
607
    global $element_pile;
608
    global $exercise_info;
609
    global $current_question_ident;
610
    global $current_answer_id;
611
    global $current_match_set;
612
    global $currentAssociableChoice;
613
    global $current_question_item_body;
614
    global $record_item_body;
615
    global $non_HTML_tag_to_avoid;
616
    global $current_inlinechoice_id;
617
    global $cardinality;
618
    global $questionTempDir;
619
    global $lastLabelFieldName;
620
    global $lastLabelFieldValue;
621
622
    array_push($element_pile, $name);
623
    $current_element = end($element_pile);
624 View Code Duplication
    if (sizeof($element_pile) >= 2) {
625
        $parent_element = $element_pile[sizeof($element_pile) - 2];
626
    } else {
627
        $parent_element = "";
628
    }
629 View Code Duplication
    if (sizeof($element_pile) >= 3) {
630
        $grand_parent_element = $element_pile[sizeof($element_pile) - 3];
631
    } else {
632
        $grand_parent_element = "";
633
    }
634 View Code Duplication
    if (sizeof($element_pile) >= 4) {
635
        $great_grand_parent_element = $element_pile[sizeof($element_pile) - 4];
636
    } else {
637
        $great_grand_parent_element = "";
638
    }
639
640
641 View Code Duplication
    if ($record_item_body) {
642
643
        if ((!in_array($current_element, $non_HTML_tag_to_avoid))) {
644
            $current_question_item_body .= "<" . $name;
645
            foreach ($attributes as $attribute_name => $attribute_value) {
646
                $current_question_item_body .= " " . $attribute_name . "=\"" . $attribute_value . "\"";
647
            }
648
            $current_question_item_body .= ">";
649
        } else {
650
            //in case of FIB question, we replace the IMS-QTI tag b y the correct answer between "[" "]",
651
            //we first save with claroline tags ,then when the answer will be parsed, the claroline tags will be replaced
652
653
            if ($current_element == 'INLINECHOICEINTERACTION') {
654
                $current_question_item_body .= "**claroline_start**" . $attributes['RESPONSEIDENTIFIER'] . "**claroline_end**";
655
            }
656
            if ($current_element == 'TEXTENTRYINTERACTION') {
657
                $correct_answer_value = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
658
                $current_question_item_body .= "[" . $correct_answer_value . "]";
659
660
            }
661
            if ($current_element == 'BR') {
662
                $current_question_item_body .= "<br />";
663
            }
664
        }
665
    }
666
667
    switch ($current_element) {
668
        case 'ASSESSMENT':
669
            // This is the assessment element: we don't care, we just want questions
670
            if (!empty($attributes['TITLE'])) {
671
                $exercise_info['name'] = $attributes['TITLE'];
672
            }
673
            break;
674
        case 'ITEM':
675
            //retrieve current question
676
            $current_question_ident = $attributes['IDENT'];
677
            $exercise_info['question'][$current_question_ident] = array();
678
            $exercise_info['question'][$current_question_ident]['answer'] = array();
679
            $exercise_info['question'][$current_question_ident]['correct_answers'] = array();
680
            $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir;
681
            break;
682
        case 'SECTION':
683
            break;
684
        case 'RESPONSE_LID':
685
            // Retrieve question type
686
            if ("multiple" == strtolower($attributes['RCARDINALITY'])) {
687
                $cardinality = 'multiple';
688
            }
689
            if ("single" == strtolower($attributes['RCARDINALITY'])) {
690
                $cardinality = 'single';
691
            }
692
            //needed for FIB
693
            $current_answer_id = $attributes['IDENT'];
694
            $current_question_item_body = '';
695
            break;
696
        case 'RENDER_CHOICE';
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
697
            break;
698
        case 'RESPONSE_LABEL':
699 View Code Duplication
            if (!empty($attributes['IDENT'])) {
700
                $current_answer_id = $attributes['IDENT'];
701
                //set the placeholder for the answer to come (in endElementQti1)
702
                $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id] = '';
703
            }
704
            break;
705
        case 'DECVAR':
706
            if ($parent_element == 'OUTCOMES' && $grand_parent_element == 'RESPROCESSING') {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

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

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

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

could be turned into

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

This is much more concise to read.

Loading history...
707
                // The following attributes are available
708
                //$attributes['VARTYPE'];
709
                //$attributes['DEFAULTVAL'];
710
                //$attributes['MINVALUE'];
711
                //$attributes['MAXVALUE'];
712
            }
713
            break;
714
        case 'VAREQUAL':
715
            if ($parent_element == 'CONDITIONVAR' && $grand_parent_element == 'RESPCONDITION') {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

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

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

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

could be turned into

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

This is much more concise to read.

Loading history...
716
                // The following attributes are available
717
                //$attributes['RESPIDENT']
718
            }
719
            break;
720
        case 'SETVAR':
721
            if ($parent_element == 'RESPCONDITION') {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

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

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

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

could be turned into

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

This is much more concise to read.

Loading history...
722
                // The following attributes are available
723
                //$attributes['ACTION']
724
            }
725
            break;
726
        case 'IMG':
727
            break;
728
        case 'MATTEXT':
729
            if ($parent_element == 'MATERIAL') {
730
                if ($grand_parent_element == 'PRESENTATION') {
731
                    $exercise_info['question'][$current_question_ident]['title'] = $current_question_item_body;
732
                }
733
            }
734
    }
735
}
736
737
/**
738
 * Function used by the SAX xml parser when the parser meets a closing tag for QTI1
739
 *
740
 * @param object $parser xml parser created with "xml_parser_create()"
741
 * @param string $name name of the element
742
 * @param array $attributes The element attributes
743
 */
744
function endElementQti1($parser, $name, $attributes)
745
{
746
    global $element_pile;
747
    global $exercise_info;
748
    global $current_question_ident;
749
    global $record_item_body;
750
    global $current_question_item_body;
751
    global $non_HTML_tag_to_avoid;
752
    global $cardinality;
753
    global $lastLabelFieldName;
754
    global $lastLabelFieldValue;
755
    global $resourcesLinks;
756
757
    $current_element = end($element_pile);
758 View Code Duplication
    if (sizeof($element_pile) >= 2) {
759
        $parent_element = $element_pile[sizeof($element_pile) - 2];
760
    } else {
761
        $parent_element = "";
762
    }
763 View Code Duplication
    if (sizeof($element_pile) >= 3) {
764
        $grand_parent_element = $element_pile[sizeof($element_pile) - 3];
765
    } else {
766
        $grand_parent_element = "";
767
    }
768 View Code Duplication
    if (sizeof($element_pile) >= 4) {
769
        $great_grand_parent_element = $element_pile[sizeof($element_pile) - 4];
770
    } else {
771
        $great_grand_parent_element = "";
772
    }
773
774
    //treat the record of the full content of itembody tag :
775
776
    if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
777
        $current_question_item_body .= "</" . $name . ">";
778
    }
779
780
    switch ($name) {
781
        case 'MATTEXT':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
782
            if ($parent_element == 'MATERIAL') {
783
                if ($grand_parent_element == 'PRESENTATION' OR $grand_parent_element == 'ITEM') {
784
                    $exercise_info['question'][$current_question_ident]['title'] = $current_question_item_body;
785
                    $current_question_item_body = '';
786
                } elseif ($grand_parent_element == 'RESPONSE_LABEL') {
787
                    $last = '';
788
                    foreach ($exercise_info['question'][$current_question_ident]['answer'] as $key => $value) {
789
                        $last = $key;
790
                    }
791
                    $exercise_info['question'][$current_question_ident]['answer'][$last]['value'] = $current_question_item_body;
792
                    $current_question_item_body = '';
793
                }
794
            }
795
        case 'RESPONSE_LID':
796
            // Retrieve question type
797
            if (!isset($exercise_info['question'][$current_question_ident]['type'])) {
798 View Code Duplication
                if ("multiple" == strtolower($attributes['RCARDINALITY'])) {
799
                    $exercise_info['question'][$current_question_ident]['type'] = MCMA;
800
                }
801 View Code Duplication
                if ("single" == strtolower($attributes['RCARDINALITY'])) {
802
                    $exercise_info['question'][$current_question_ident]['type'] = MCUA;
803
                }
804
            }
805
            $current_question_item_body = '';
806
            //needed for FIB
807
            $current_answer_id = $attributes['IDENT'];
808
            break;
809
        case 'ITEMMETADATA':
810
            $current_question_item_body = '';
811
            break;
812
    }
813
    array_pop($element_pile);
814
}
815
816
/**
817
 * QTI1 element parser
818
 * @param $parser
819
 * @param $data
820
 */
821
function elementDataQti1($parser, $data)
822
{
823
    global $element_pile;
824
    global $exercise_info;
825
    global $current_question_ident;
826
    global $current_answer_id;
827
    global $current_match_set;
828
    global $currentAssociableChoice;
829
    global $current_question_item_body;
830
    global $record_item_body;
831
    global $non_HTML_tag_to_avoid;
832
    global $current_inlinechoice_id;
833
    global $cardinality;
834
    global $lastLabelFieldName;
835
    global $lastLabelFieldValue;
836
    global $resourcesLinks;
837
838
    $current_element = end($element_pile);
839 View Code Duplication
    if (sizeof($element_pile) >= 2) {
840
        $parent_element = $element_pile[sizeof($element_pile) - 2];
841
    } else {
842
        $parent_element = "";
843
    }
844
    //treat the record of the full content of itembody tag (needed for question statment and/or FIB text:
845
846
    if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
847
        $current_question_item_body .= $data;
848
    }
849
850
    switch ($current_element) {
851
        case 'FIELDLABEL':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
852
            if (!empty($data)) {
853
                $lastLabelFieldName = $current_element;
854
                $lastLabelFieldValue = $data;
855
            }
856
        case 'FIELDENTRY':
857
            $current_question_item_body = $data;
858
            switch ($lastLabelFieldValue) {
859
                case 'cc_profile':
860
                    // The following values might be proprietary in MATRIX software. No specific reference
861
                    // in QTI doc: http://www.imsglobal.org/question/qtiv1p2/imsqti_asi_infov1p2.html#1415855
862
                    switch ($data) {
863
                        case 'cc.true_false.v0p1':
864
                            //this is a true-false question (translated to multiple choice in Chamilo because true-false comes with "I don't know")
865
                            $exercise_info['question'][$current_question_ident]['type'] = MCUA;
866
                            break;
867
                        case 'cc.multiple_choice.v0p1':
868
                            //this is a multiple choice (unique answer) question
869
                            $exercise_info['question'][$current_question_ident]['type'] = MCUA;
870
                            break;
871
                        case 'cc.multiple_response.v0p1':
872
                            //this is a multiple choice (unique answer) question
873
                            $exercise_info['question'][$current_question_ident]['type'] = MCMA;
874
                            break;
875
                    }
876
                    break;
877
                case 'cc_weighting':
878
                    //defines the total weight of the question
879
                    $exercise_info['question'][$current_question_ident]['default_weighting'] = $lastLabelFieldValue;
880
                    break;
881
                case 'assessment_question_identifierref':
882
                    //placeholder - not used yet
883
                    // Possible values are not defined by qti v1.2
884
                    break;
885
            }
886
            break;
887
        case 'MATTEXT':
888 View Code Duplication
            foreach ($resourcesLinks['manifest'] as $key=>$value) {
889
                $data = preg_replace('|' . $value . '|', $resourcesLinks['web'][$key], $data);
890
            }
891
            if (!empty($current_question_item_body)) {
892
                $current_question_item_body .= $data;
893
            } else {
894
                $current_question_item_body = $data;
895
            }
896
            break;
897
        case 'VAREQUAL':
898
            $lastLabelFieldName = 'VAREQUAL';
899
            $lastLabelFieldValue = $data;
900
            break;
901
        case 'SETVAR':
902
            if ($parent_element == 'RESPCONDITION') {
903
                // The following attributes are available
904
                //$attributes['ACTION']
905
                $exercise_info['question'][$current_question_ident]['correct_answers'][] = $lastLabelFieldValue;
906
                $exercise_info['question'][$current_question_ident]['weighting'][$lastLabelFieldValue] = $data;
907
            }
908
            break;
909
910
    }
911
}
912
913
/**
914
 * Check if a given file is an IMS/QTI question bank file
915
 * @param string $filePath The absolute filepath
916
 * @return bool Whether it is an IMS/QTI question bank or not
917
 */
918 View Code Duplication
function isQtiQuestionBank($filePath)
919
{
920
    $data = file_get_contents($filePath);
921
    if (!empty($data)) {
922
        $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data);
923
        if ($match) {
924
            return true;
925
        }
926
    }
927
928
    return false;
929
}
930
931
/**
932
 * Check if a given file is an IMS/QTI manifest file (listing of extra files)
933
 * @param string $filePath The absolute filepath
934
 * @return bool Whether it is an IMS/QTI manifest file or not
935
 */
936 View Code Duplication
function isQtiManifest($filePath)
937
{
938
    $data = file_get_contents($filePath);
939
    if (!empty($data)) {
940
        $match = preg_match('/imsccv(\d)p(\d)/', $data);
941
        if ($match) {
942
            return true;
943
        }
944
    }
945
946
    return false;
947
}
948
949
/**
950
 * Processes an IMS/QTI manifest file: store links to new files to be able to transform them into questions text
951
 * @param string $filePath The absolute filepath
952
 * @param array $links List of filepaths changes
0 ignored issues
show
Bug introduced by
There is no parameter named $links. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
953
 * @return bool
954
 */
955
function qtiProcessManifest($filePath)
956
{
957
    $xml = simplexml_load_file($filePath);
958
    $course = api_get_course_info();
959
    $sessionId = api_get_session_id();
960
    $courseDir = $course['path'];
961
    $sysPath = api_get_path(SYS_COURSE_PATH);
962
    $exercisesSysPath = $sysPath . $courseDir . '/document/';
963
    $webPath = api_get_path(WEB_CODE_PATH);
964
    $exercisesWebPath = $webPath . 'document/document.php?' . api_get_cidreq() . '&action=download&id=';
965
    $links = array(
966
        'manifest' => array(),
967
        'system' => array(),
968
        'web' => array(),
969
    );
970
    $tableDocuments = Database::get_course_table(TABLE_DOCUMENT);
971
    $countResources = count($xml->resources->resource->file);
972
    for ($i=0; $i < $countResources; $i++) {
973
        $file = $xml->resources->resource->file[$i];
974
        $href = '';
975
        foreach ($file->attributes() as $key => $value) {
976
            if ($key == 'href') {
977
                if (substr($value, -3, 3) != 'xml') {
978
                    $href = $value;
979
                }
980
            }
981
        }
982
        if (!empty($href)) {
983
            $links['manifest'][] = (string) $href;
984
            $links['system'][] = $exercisesSysPath . strtolower($href);
985
            $specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href)));
986
            $specialHref = preg_replace('/(-){2,8}/', '-', $specialHref);
987
988
            $sql = "SELECT iid FROM " . $tableDocuments . " WHERE c_id = " . $course['real_id'] . " AND session_id = $sessionId AND path = '/" . $specialHref . "'";
989
            $result = Database::query($sql);
990
            $documentId = 0;
991
            while ($row = Database::fetch_assoc($result)) {
992
                $documentId = $row['iid'];
993
            }
994
            $links['web'][] = $exercisesWebPath . $documentId;
995
        }
996
    }
997
    return $links;
998
}