MDL-39283 GIFT and XML question export broken for unsupported qtypes
[moodle.git] / question / format / xml / format.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Code for exporting questions as Moodle XML.
19  *
20  * @package    qformat
21  * @subpackage xml
22  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->libdir . '/xmlize.php');
30 if (!class_exists('qformat_default')) {
31     // This is ugly, but this class is also (ab)used by mod/lesson, which defines
32     // a different base class in mod/lesson/format.php. Thefore, we can only
33     // include the proper base class conditionally like this. (We have to include
34     // the base class like this, otherwise it breaks third-party question types.)
35     // This may be reviewd, and a better fix found one day.
36     require_once($CFG->dirroot . '/question/format.php');
37 }
40 /**
41  * Importer for Moodle XML question format.
42  *
43  * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
44  *
45  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
46  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47  */
48 class qformat_xml extends qformat_default {
50     public function provide_import() {
51         return true;
52     }
54     public function provide_export() {
55         return true;
56     }
58     public function mime_type() {
59         return 'application/xml';
60     }
62     // IMPORT FUNCTIONS START HERE
64     /**
65      * Translate human readable format name
66      * into internal Moodle code number
67      * @param string name format name from xml file
68      * @return int Moodle format code
69      */
70     public function trans_format($name) {
71         $name = trim($name);
73         if ($name == 'moodle_auto_format') {
74             return FORMAT_MOODLE;
75         } else if ($name == 'html') {
76             return FORMAT_HTML;
77         } else if ($name == 'plain_text') {
78             return FORMAT_PLAIN;
79         } else if ($name == 'wiki_like') {
80             return FORMAT_WIKI;
81         } else if ($name == 'markdown') {
82             return FORMAT_MARKDOWN;
83         } else {
84             debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'.");
85             return FORMAT_HTML;
86         }
87     }
89     /**
90      * Translate human readable single answer option
91      * to internal code number
92      * @param string name true/false
93      * @return int internal code number
94      */
95     public function trans_single($name) {
96         $name = trim($name);
97         if ($name == "false" || !$name) {
98             return 0;
99         } else {
100             return 1;
101         }
102     }
104     /**
105      * process text string from xml file
106      * @param array $text bit of xml tree after ['text']
107      * @return string processed text.
108      */
109     public function import_text($text) {
110         // quick sanity check
111         if (empty($text)) {
112             return '';
113         }
114         $data = $text[0]['#'];
115         return trim($data);
116     }
118     /**
119      * return the value of a node, given a path to the node
120      * if it doesn't exist return the default value
121      * @param array xml data to read
122      * @param array path path to node expressed as array
123      * @param mixed default
124      * @param bool istext process as text
125      * @param string error if set value must exist, return false and issue message if not
126      * @return mixed value
127      */
128     public function getpath($xml, $path, $default, $istext=false, $error='') {
129         foreach ($path as $index) {
130             if (!isset($xml[$index])) {
131                 if (!empty($error)) {
132                     $this->error($error);
133                     return false;
134                 } else {
135                     return $default;
136                 }
137             }
139             $xml = $xml[$index];
140         }
142         if ($istext) {
143             if (!is_string($xml)) {
144                 $this->error(get_string('invalidxml', 'qformat_xml'));
145             }
146             $xml = trim($xml);
147         }
149         return $xml;
150     }
152     public function import_text_with_files($data, $path, $defaultvalue = '', $defaultformat = 'html') {
153         $field  = array();
154         $field['text'] = $this->getpath($data,
155                 array_merge($path, array('#', 'text', 0, '#')), $defaultvalue, true);
156         $field['format'] = $this->trans_format($this->getpath($data,
157                 array_merge($path, array('@', 'format')), $defaultformat));
158         $itemid = $this->import_files_as_draft($this->getpath($data,
159                 array_merge($path, array('#', 'file')), array(), false));
160         if (!empty($itemid)) {
161             $field['itemid'] = $itemid;
162         }
163         return $field;
164     }
166     public function import_files_as_draft($xml) {
167         global $USER;
168         if (empty($xml)) {
169             return null;
170         }
171         $fs = get_file_storage();
172         $itemid = file_get_unused_draft_itemid();
173         $filenames = array();
174         foreach ($xml as $file) {
175             $filename = $file['@']['name'];
176             if (in_array($filename, $filenames)) {
177                 debugging('Duplicate file in XML: ' . $filename, DEBUG_DEVELOPER);
178                 continue;
179             }
180             $filerecord = array(
181                 'contextid' => context_user::instance($USER->id)->id,
182                 'component' => 'user',
183                 'filearea'  => 'draft',
184                 'itemid'    => $itemid,
185                 'filepath'  => '/',
186                 'filename'  => $filename,
187             );
188             $fs->create_file_from_string($filerecord, base64_decode($file['#']));
189             $filenames[] = $filename;
190         }
191         return $itemid;
192     }
194     /**
195      * import parts of question common to all types
196      * @param $question array question question array from xml tree
197      * @return object question object
198      */
199     public function import_headers($question) {
200         global $CFG, $USER;
202         // get some error strings
203         $error_noname = get_string('xmlimportnoname', 'qformat_xml');
204         $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
206         // this routine initialises the question object
207         $qo = $this->defaultquestion();
209         // Question name
210         $qo->name = $this->clean_question_name($this->getpath($question,
211                 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
212                 get_string('xmlimportnoname', 'qformat_xml')));
213         $questiontext = $this->import_text_with_files($question,
214                 array('#', 'questiontext', 0));
215         $qo->questiontext = $questiontext['text'];
216         $qo->questiontextformat = $questiontext['format'];
217         if (!empty($questiontext['itemid'])) {
218             $qo->questiontextitemid = $questiontext['itemid'];
219         }
220         // Backwards compatibility, deal with the old image tag.
221         $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
222         $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
223         if ($filedata && $filename) {
224             $fs = get_file_storage();
225             if (empty($qo->questiontextitemid)) {
226                 $qo->questiontextitemid = file_get_unused_draft_itemid();
227             }
228             $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
229             $filerecord = array(
230                 'contextid' => context_user::instance($USER->id)->id,
231                 'component' => 'user',
232                 'filearea'  => 'draft',
233                 'itemid'    => $qo->questiontextitemid,
234                 'filepath'  => '/',
235                 'filename'  => $filename,
236             );
237             $fs->create_file_from_string($filerecord, base64_decode($filedata));
238             $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
239         }
241         // restore files in generalfeedback
242         $generalfeedback = $this->import_text_with_files($question,
243                 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
244         $qo->generalfeedback = $generalfeedback['text'];
245         $qo->generalfeedbackformat = $generalfeedback['format'];
246         if (!empty($generalfeedback['itemid'])) {
247             $qo->generalfeedbackitemid = $generalfeedback['itemid'];
248         }
250         $qo->defaultmark = $this->getpath($question,
251                 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
252         $qo->penalty = $this->getpath($question,
253                 array('#', 'penalty', 0, '#'), $qo->penalty);
255         // Fix problematic rounding from old files:
256         if (abs($qo->penalty - 0.3333333) < 0.005) {
257             $qo->penalty = 0.3333333;
258         }
260         // Read the question tags.
261         if (!empty($CFG->usetags) && array_key_exists('tags', $question['#'])
262                 && !empty($question['#']['tags'][0]['#']['tag'])) {
263             require_once($CFG->dirroot.'/tag/lib.php');
264             $qo->tags = array();
265             foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
266                 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
267             }
268         }
270         return $qo;
271     }
273     /**
274      * Import the common parts of a single answer
275      * @param array answer xml tree for single answer
276      * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
277      *      and so may contain files, otherwise the answers are plain text.
278      * @param array Default text format for the feedback, and the answers if $withanswerfiles
279      *      is true.
280      * @return object answer object
281      */
282     public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
283         $ans = new stdClass();
285         if ($withanswerfiles) {
286             $ans->answer = $this->import_text_with_files($answer, array(), '', $defaultformat);
287         } else {
288             $ans->answer = array();
289             $ans->answer['text']   = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
290             $ans->answer['format'] = FORMAT_PLAIN;
291         }
293         $ans->feedback = $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat);
295         $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
297         return $ans;
298     }
300     /**
301      * Import the common overall feedback fields.
302      * @param object $question the part of the XML relating to this question.
303      * @param object $qo the question data to add the fields to.
304      * @param bool $withshownumpartscorrect include the shownumcorrect field.
305      */
306     public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
307         $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
308         foreach ($fields as $field) {
309             $qo->$field = $this->import_text_with_files($questionxml,
310                     array('#', $field, 0), '', $this->get_format($qo->questiontextformat));
311         }
313         if ($withshownumpartscorrect) {
314             $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
316             // Backwards compatibility:
317             if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
318                 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
319                         array('#', 'correctresponsesfeedback', 0, '#'), 1));
320             }
321         }
322     }
324     /**
325      * Import a question hint
326      * @param array $hintxml hint xml fragment.
327      * @param string $defaultformat the text format to assume for hints that do not specify.
328      * @return object hint for storing in the database.
329      */
330     public function import_hint($hintxml, $defaultformat) {
331         $hint = new stdClass();
332         if (array_key_exists('hintcontent', $hintxml['#'])) {
333             // Backwards compatibility:
335             $hint->hint = $this->import_text_with_files($hintxml,
336                     array('#', 'hintcontent', 0), '', $defaultformat);
338             $hint->shownumcorrect = $this->getpath($hintxml,
339                     array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
340             $hint->clearwrong = $this->getpath($hintxml,
341                     array('#', 'clearincorrectresponses', 0, '#'), 0);
342             $hint->options = $this->getpath($hintxml,
343                     array('#', 'showfeedbacktoresponses', 0, '#'), 0);
345             return $hint;
346         }
347         $hint->hint = $this->import_text_with_files($hintxml, array(), '', $defaultformat);
348         $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
349         $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
350         $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
352         return $hint;
353     }
355     /**
356      * Import all the question hints
357      *
358      * @param object $qo the question data that is being constructed.
359      * @param array $questionxml The xml representing the question.
360      * @param bool $withparts whether the extra fields relating to parts should be imported.
361      * @param bool $withoptions whether the extra options field should be imported.
362      * @param string $defaultformat the text format to assume for hints that do not specify.
363      * @return array of objects representing the hints in the file.
364      */
365     public function import_hints($qo, $questionxml, $withparts = false,
366             $withoptions = false, $defaultformat = 'html') {
367         if (!isset($questionxml['#']['hint'])) {
368             return;
369         }
371         foreach ($questionxml['#']['hint'] as $hintxml) {
372             $hint = $this->import_hint($hintxml, $defaultformat);
373             $qo->hint[] = $hint->hint;
375             if ($withparts) {
376                 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
377                 $qo->hintclearwrong[] = $hint->clearwrong;
378             }
380             if ($withoptions) {
381                 $qo->hintoptions[] = $hint->options;
382             }
383         }
384     }
386     /**
387      * Import files from a node in the XML.
388      * @param array $xml an array of <file> nodes from the the parsed XML.
389      * @return array of things representing files - in the form that save_question expects.
390      */
391     public function import_files($xml) {
392         $files = array();
393         foreach ($xml as $file) {
394             $data = new stdClass();
395             $data->content = $file['#'];
396             $data->encoding = $file['@']['encoding'];
397             $data->name = $file['@']['name'];
398             $files[] = $data;
399         }
400         return $files;
401     }
403     /**
404      * import multiple choice question
405      * @param array question question array from xml tree
406      * @return object question object
407      */
408     public function import_multichoice($question) {
409         // get common parts
410         $qo = $this->import_headers($question);
412         // 'header' parts particular to multichoice
413         $qo->qtype = MULTICHOICE;
414         $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
415         $qo->single = $this->trans_single($single);
416         $shuffleanswers = $this->getpath($question,
417                 array('#', 'shuffleanswers', 0, '#'), 'false');
418         $qo->answernumbering = $this->getpath($question,
419                 array('#', 'answernumbering', 0, '#'), 'abc');
420         $qo->shuffleanswers = $this->trans_single($shuffleanswers);
422         // There was a time on the 1.8 branch when it could output an empty
423         // answernumbering tag, so fix up any found.
424         if (empty($qo->answernumbering)) {
425             $qo->answernumbering = 'abc';
426         }
428         // Run through the answers
429         $answers = $question['#']['answer'];
430         $acount = 0;
431         foreach ($answers as $answer) {
432             $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
433             $qo->answer[$acount] = $ans->answer;
434             $qo->fraction[$acount] = $ans->fraction;
435             $qo->feedback[$acount] = $ans->feedback;
436             ++$acount;
437         }
439         $this->import_combined_feedback($qo, $question, true);
440         $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
442         return $qo;
443     }
445     /**
446      * Import cloze type question
447      * @param array question question array from xml tree
448      * @return object question object
449      */
450     public function import_multianswer($question) {
451         global $USER;
452         question_bank::get_qtype('multianswer');
454         $questiontext = $this->import_text_with_files($question,
455                 array('#', 'questiontext', 0));
456         $qo = qtype_multianswer_extract_question($questiontext);
458         // 'header' parts particular to multianswer
459         $qo->qtype = 'multianswer';
460         $qo->course = $this->course;
462         $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
463         $qo->questiontextformat = $questiontext['format'];
464         $qo->questiontext = $qo->questiontext['text'];
465         if (!empty($questiontext['itemid'])) {
466             $qo->questiontextitemid = $questiontext['itemid'];
467         }
469         // Backwards compatibility, deal with the old image tag.
470         $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
471         $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
472         if ($filedata && $filename) {
473             $fs = get_file_storage();
474             if (empty($qo->questiontextitemid)) {
475                 $qo->questiontextitemid = file_get_unused_draft_itemid();
476             }
477             $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
478             $filerecord = array(
479                 'contextid' => context_user::instance($USER->id)->id,
480                 'component' => 'user',
481                 'filearea'  => 'draft',
482                 'itemid'    => $qo->questiontextitemid,
483                 'filepath'  => '/',
484                 'filename'  => $filename,
485             );
486             $fs->create_file_from_string($filerecord, base64_decode($filedata));
487             $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
488         }
490         // restore files in generalfeedback
491         $generalfeedback = $this->import_text_with_files($question,
492                 array('#', 'generalfeedback', 0), $qo->generalfeedback, $this->get_format($qo->questiontextformat));
493         $qo->generalfeedback = $generalfeedback['text'];
494         $qo->generalfeedbackformat = $generalfeedback['format'];
495         if (!empty($generalfeedback['itemid'])) {
496             $qo->generalfeedbackitemid = $generalfeedback['itemid'];
497         }
499         $qo->penalty = $this->getpath($question,
500                 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
501         // Fix problematic rounding from old files:
502         if (abs($qo->penalty - 0.3333333) < 0.005) {
503             $qo->penalty = 0.3333333;
504         }
506         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
508         return $qo;
509     }
511     /**
512      * Import true/false type question
513      * @param array question question array from xml tree
514      * @return object question object
515      */
516     public function import_truefalse($question) {
517         // get common parts
518         global $OUTPUT;
519         $qo = $this->import_headers($question);
521         // 'header' parts particular to true/false
522         $qo->qtype = TRUEFALSE;
524         // In the past, it used to be assumed that the two answers were in the file
525         // true first, then false. Howevever that was not always true. Now, we
526         // try to match on the answer text, but in old exports, this will be a localised
527         // string, so if we don't find true or false, we fall back to the old system.
528         $first = true;
529         $warning = false;
530         foreach ($question['#']['answer'] as $answer) {
531             $answertext = $this->getpath($answer,
532                     array('#', 'text', 0, '#'), '', true);
533             $feedback = $this->import_text_with_files($answer,
534                     array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat));
536             if ($answertext != 'true' && $answertext != 'false') {
537                 // Old style file, assume order is true/false.
538                 $warning = true;
539                 if ($first) {
540                     $answertext = 'true';
541                 } else {
542                     $answertext = 'false';
543                 }
544             }
546             if ($answertext == 'true') {
547                 $qo->answer = ($answer['@']['fraction'] == 100);
548                 $qo->correctanswer = $qo->answer;
549                 $qo->feedbacktrue = $feedback;
550             } else {
551                 $qo->answer = ($answer['@']['fraction'] != 100);
552                 $qo->correctanswer = $qo->answer;
553                 $qo->feedbackfalse = $feedback;
554             }
555             $first = false;
556         }
558         if ($warning) {
559             $a = new stdClass();
560             $a->questiontext = $qo->questiontext;
561             $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
562             echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
563         }
565         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
567         return $qo;
568     }
570     /**
571      * Import short answer type question
572      * @param array question question array from xml tree
573      * @return object question object
574      */
575     public function import_shortanswer($question) {
576         // get common parts
577         $qo = $this->import_headers($question);
579         // header parts particular to shortanswer
580         $qo->qtype = SHORTANSWER;
582         // get usecase
583         $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
585         // Run through the answers
586         $answers = $question['#']['answer'];
587         $acount = 0;
588         foreach ($answers as $answer) {
589             $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
590             $qo->answer[$acount] = $ans->answer['text'];
591             $qo->fraction[$acount] = $ans->fraction;
592             $qo->feedback[$acount] = $ans->feedback;
593             ++$acount;
594         }
596         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
598         return $qo;
599     }
601     /**
602      * Import description type question
603      * @param array question question array from xml tree
604      * @return object question object
605      */
606     public function import_description($question) {
607         // get common parts
608         $qo = $this->import_headers($question);
609         // header parts particular to shortanswer
610         $qo->qtype = DESCRIPTION;
611         $qo->defaultmark = 0;
612         $qo->length = 0;
613         return $qo;
614     }
616     /**
617      * Import numerical type question
618      * @param array question question array from xml tree
619      * @return object question object
620      */
621     public function import_numerical($question) {
622         // get common parts
623         $qo = $this->import_headers($question);
625         // header parts particular to numerical
626         $qo->qtype = NUMERICAL;
628         // get answers array
629         $answers = $question['#']['answer'];
630         $qo->answer = array();
631         $qo->feedback = array();
632         $qo->fraction = array();
633         $qo->tolerance = array();
634         foreach ($answers as $answer) {
635             // answer outside of <text> is deprecated
636             $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
637             $qo->answer[] = $obj->answer['text'];
638             if (empty($qo->answer)) {
639                 $qo->answer = '*';
640             }
641             $qo->feedback[]  = $obj->feedback;
642             $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
644             // fraction as a tag is deprecated
645             $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
646             $qo->fraction[] = $this->getpath($answer,
647                     array('#', 'fraction', 0, '#'), $fraction); // deprecated
648         }
650         // Get the units array
651         $qo->unit = array();
652         $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
653         if (!empty($units)) {
654             $qo->multiplier = array();
655             foreach ($units as $unit) {
656                 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
657                 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
658             }
659         }
660         $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
661         $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
662         $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null);
663         $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
664         $qo->instructions['text'] = '';
665         $qo->instructions['format'] = FORMAT_HTML;
666         $instructions = $this->getpath($question, array('#', 'instructions'), array());
667         if (!empty($instructions)) {
668             $qo->instructions = $this->import_text_with_files($instructions,
669                     array('0'), '', $this->get_format($qo->questiontextformat));
670         }
672         if (is_null($qo->showunits)) {
673             // Set a good default, depending on whether there are any units defined.
674             if (empty($qo->unit)) {
675                 $qo->showunits = 3; // qtype_numerical::UNITNONE;
676             } else {
677                 $qo->showunits = 0; // qtype_numerical::UNITOPTIONAL;
678             }
679         }
681         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
683         return $qo;
684     }
686     /**
687      * Import matching type question
688      * @param array question question array from xml tree
689      * @return object question object
690      */
691     public function import_match($question) {
692         // get common parts
693         $qo = $this->import_headers($question);
695         // header parts particular to matching
696         $qo->qtype = 'match';
697         $qo->shuffleanswers = $this->trans_single($this->getpath($question,
698                 array('#', 'shuffleanswers', 0, '#'), 1));
700         // run through subquestions
701         $qo->subquestions = array();
702         $qo->subanswers = array();
703         foreach ($question['#']['subquestion'] as $subqxml) {
704             $qo->subquestions[] = $this->import_text_with_files($subqxml,
705                     array(), '', $this->get_format($qo->questiontextformat));
707             $answers = $this->getpath($subqxml, array('#', 'answer'), array());
708             $qo->subanswers[] = $this->getpath($subqxml,
709                     array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
710         }
712         $this->import_combined_feedback($qo, $question, true);
713         $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
715         return $qo;
716     }
718     /**
719      * Import essay type question
720      * @param array question question array from xml tree
721      * @return object question object
722      */
723     public function import_essay($question) {
724         // get common parts
725         $qo = $this->import_headers($question);
727         // header parts particular to essay
728         $qo->qtype = ESSAY;
730         $qo->responseformat = $this->getpath($question,
731                 array('#', 'responseformat', 0, '#'), 'editor');
732         $qo->responsefieldlines = $this->getpath($question,
733                 array('#', 'responsefieldlines', 0, '#'), 15);
734         $qo->attachments = $this->getpath($question,
735                 array('#', 'attachments', 0, '#'), 0);
736         $qo->graderinfo = $this->import_text_with_files($question,
737                 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat));
739         return $qo;
740     }
742     /**
743      * Import a calculated question
744      * @param object $question the imported XML data.
745      */
746     public function import_calculated($question) {
748         // get common parts
749         $qo = $this->import_headers($question);
751         // header parts particular to calculated
752         $qo->qtype = CALCULATED;
753         $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
754         $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
755         $qo->single = $this->trans_single($single);
756         $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
757         $qo->answernumbering = $this->getpath($question,
758                 array('#', 'answernumbering', 0, '#'), 'abc');
759         $qo->shuffleanswers = $this->trans_single($shuffleanswers);
761         $this->import_combined_feedback($qo, $question);
763         $qo->unitgradingtype = $this->getpath($question,
764                 array('#', 'unitgradingtype', 0, '#'), 0);
765         $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
766         $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
767         $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
768         $qo->instructions = $this->getpath($question,
769                 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
770         if (!empty($instructions)) {
771             $qo->instructions = $this->import_text_with_files($instructions,
772                     array('0'), '', $this->get_format($qo->questiontextformat));
773         }
775         // get answers array
776         $answers = $question['#']['answer'];
777         $qo->answers = array();
778         $qo->feedback = array();
779         $qo->fraction = array();
780         $qo->tolerance = array();
781         $qo->tolerancetype = array();
782         $qo->correctanswerformat = array();
783         $qo->correctanswerlength = array();
784         $qo->feedback = array();
785         foreach ($answers as $answer) {
786             $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
787             // answer outside of <text> is deprecated
788             if (empty($ans->answer['text'])) {
789                 $ans->answer['text'] = '*';
790             }
791             $qo->answers[] = $ans->answer;
792             $qo->feedback[] = $ans->feedback;
793             $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
794             // fraction as a tag is deprecated
795             if (!empty($answer['#']['fraction'][0]['#'])) {
796                 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
797             } else {
798                 $qo->fraction[] = $answer['@']['fraction'] / 100;
799             }
800             $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
801             $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
802             $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
803         }
804         // get units array
805         $qo->unit = array();
806         if (isset($question['#']['units'][0]['#']['unit'])) {
807             $units = $question['#']['units'][0]['#']['unit'];
808             $qo->multiplier = array();
809             foreach ($units as $unit) {
810                 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
811                 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
812             }
813         }
814         $instructions = $this->getpath($question, array('#', 'instructions'), array());
815         if (!empty($instructions)) {
816             $qo->instructions = $this->import_text_with_files($instructions,
817                     array('0'), '', $this->get_format($qo->questiontextformat));
818         }
820         if (is_null($qo->unitpenalty)) {
821             // Set a good default, depending on whether there are any units defined.
822             if (empty($qo->unit)) {
823                 $qo->showunits = 3; // qtype_numerical::UNITNONE;
824             } else {
825                 $qo->showunits = 0; // qtype_numerical::UNITOPTIONAL;
826             }
827         }
829         $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
830         $qo->dataset = array();
831         $qo->datasetindex= 0;
832         foreach ($datasets as $dataset) {
833             $qo->datasetindex++;
834             $qo->dataset[$qo->datasetindex] = new stdClass();
835             $qo->dataset[$qo->datasetindex]->status =
836                     $this->import_text($dataset['#']['status'][0]['#']['text']);
837             $qo->dataset[$qo->datasetindex]->name =
838                     $this->import_text($dataset['#']['name'][0]['#']['text']);
839             $qo->dataset[$qo->datasetindex]->type =
840                     $dataset['#']['type'][0]['#'];
841             $qo->dataset[$qo->datasetindex]->distribution =
842                     $this->import_text($dataset['#']['distribution'][0]['#']['text']);
843             $qo->dataset[$qo->datasetindex]->max =
844                     $this->import_text($dataset['#']['maximum'][0]['#']['text']);
845             $qo->dataset[$qo->datasetindex]->min =
846                     $this->import_text($dataset['#']['minimum'][0]['#']['text']);
847             $qo->dataset[$qo->datasetindex]->length =
848                     $this->import_text($dataset['#']['decimals'][0]['#']['text']);
849             $qo->dataset[$qo->datasetindex]->distribution =
850                     $this->import_text($dataset['#']['distribution'][0]['#']['text']);
851             $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
852             $qo->dataset[$qo->datasetindex]->datasetitem = array();
853             $qo->dataset[$qo->datasetindex]->itemindex = 0;
854             $qo->dataset[$qo->datasetindex]->number_of_items =
855                     $dataset['#']['number_of_items'][0]['#'];
856             $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item'];
857             foreach ($datasetitems as $datasetitem) {
858                 $qo->dataset[$qo->datasetindex]->itemindex++;
859                 $qo->dataset[$qo->datasetindex]->datasetitem[
860                         $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
861                 $qo->dataset[$qo->datasetindex]->datasetitem[
862                         $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
863                                 $datasetitem['#']['number'][0]['#'];
864                 $qo->dataset[$qo->datasetindex]->datasetitem[
865                         $qo->dataset[$qo->datasetindex]->itemindex]->value =
866                                 $datasetitem['#']['value'][0]['#'];
867             }
868         }
870         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
872         return $qo;
873     }
875     /**
876      * This is not a real question type. It's a dummy type used to specify the
877      * import category. The format is:
878      * <question type="category">
879      *     <category>tom/dick/harry</category>
880      * </question>
881      */
882     protected function import_category($question) {
883         $qo = new stdClass();
884         $qo->qtype = 'category';
885         $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
886         return $qo;
887     }
889     /**
890      * Parse the array of lines into an array of questions
891      * this *could* burn memory - but it won't happen that much
892      * so fingers crossed!
893      * @param array of lines from the input file.
894      * @param stdClass $context
895      * @return array (of objects) question objects.
896      */
897     protected function readquestions($lines) {
898         // We just need it as one big string
899         $lines = implode('', $lines);
901         // This converts xml to big nasty data structure
902         // the 0 means keep white space as it is (important for markdown format)
903         try {
904             $xml = xmlize($lines, 0, 'UTF-8', true);
905         } catch (xml_format_exception $e) {
906             $this->error($e->getMessage(), '');
907             return false;
908         }
909         unset($lines); // No need to keep this in memory.
911         // Set up array to hold all our questions
912         $questions = array();
914         // Iterate through questions
915         foreach ($xml['quiz']['#']['question'] as $question) {
916             $questiontype = $question['@']['type'];
918             if ($questiontype == 'multichoice') {
919                 $qo = $this->import_multichoice($question);
920             } else if ($questiontype == 'truefalse') {
921                 $qo = $this->import_truefalse($question);
922             } else if ($questiontype == 'shortanswer') {
923                 $qo = $this->import_shortanswer($question);
924             } else if ($questiontype == 'numerical') {
925                 $qo = $this->import_numerical($question);
926             } else if ($questiontype == 'description') {
927                 $qo = $this->import_description($question);
928             } else if ($questiontype == 'matching' || $questiontype == 'match') {
929                 $qo = $this->import_match($question);
930             } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
931                 $qo = $this->import_multianswer($question);
932             } else if ($questiontype == 'essay') {
933                 $qo = $this->import_essay($question);
934             } else if ($questiontype == 'calculated') {
935                 $qo = $this->import_calculated($question);
936             } else if ($questiontype == 'calculatedsimple') {
937                 $qo = $this->import_calculated($question);
938                 $qo->qtype = 'calculatedsimple';
939             } else if ($questiontype == 'calculatedmulti') {
940                 $qo = $this->import_calculated($question);
941                 $qo->qtype = 'calculatedmulti';
942             } else if ($questiontype == 'category') {
943                 $qo = $this->import_category($question);
945             } else {
946                 // Not a type we handle ourselves. See if the question type wants
947                 // to handle it.
948                 if (!$qo = $this->try_importing_using_qtypes(
949                         $question, null, null, $questiontype)) {
950                     $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
951                     $qo = null;
952                 }
953             }
955             // Stick the result in the $questions array
956             if ($qo) {
957                 $questions[] = $qo;
958             }
959         }
960         return $questions;
961     }
963     // EXPORT FUNCTIONS START HERE
965     public function export_file_extension() {
966         return '.xml';
967     }
969     /**
970      * Turn the internal question type name into a human readable form.
971      * (In the past, the code used to use integers internally. Now, it uses
972      * strings, so there is less need for this, but to maintain
973      * backwards-compatibility we change two of the type names.)
974      * @param string $qtype question type plugin name.
975      * @return string $qtype string to use in the file.
976      */
977     protected function get_qtype($qtype) {
978         switch($qtype) {
979             case 'match':
980                 return 'matching';
981             case 'multianswer':
982                 return 'cloze';
983             default:
984                 return $qtype;
985         }
986     }
988     /**
989      * Convert internal Moodle text format code into
990      * human readable form
991      * @param int id internal code
992      * @return string format text
993      */
994     public function get_format($id) {
995         switch($id) {
996             case FORMAT_MOODLE:
997                 return 'moodle_auto_format';
998             case FORMAT_HTML:
999                 return 'html';
1000             case FORMAT_PLAIN:
1001                 return 'plain_text';
1002             case FORMAT_WIKI:
1003                 return 'wiki_like';
1004             case FORMAT_MARKDOWN:
1005                 return 'markdown';
1006             default:
1007                 return 'unknown';
1008         }
1009     }
1011     /**
1012      * Convert internal single question code into
1013      * human readable form
1014      * @param int id single question code
1015      * @return string single question string
1016      */
1017     public function get_single($id) {
1018         switch($id) {
1019             case 0:
1020                 return 'false';
1021             case 1:
1022                 return 'true';
1023             default:
1024                 return 'unknown';
1025         }
1026     }
1028     /**
1029      * Take a string, and wrap it in a CDATA secion, if that is required to make
1030      * the output XML valid.
1031      * @param string $string a string
1032      * @return string the string, wrapped in CDATA if necessary.
1033      */
1034     public function xml_escape($string) {
1035         if (!empty($string) && htmlspecialchars($string) != $string) {
1036             return "<![CDATA[{$string}]]>";
1037         } else {
1038             return $string;
1039         }
1040     }
1042     /**
1043      * Generates <text></text> tags, processing raw text therein
1044      * @param string $raw the content to output.
1045      * @param int $indent the current indent level.
1046      * @param bool $short stick it on one line.
1047      * @return string formatted text.
1048      */
1049     public function writetext($raw, $indent = 0, $short = true) {
1050         $indent = str_repeat('  ', $indent);
1051         $raw = $this->xml_escape($raw);
1053         if ($short) {
1054             $xml = "$indent<text>$raw</text>\n";
1055         } else {
1056             $xml = "$indent<text>\n$raw\n$indent</text>\n";
1057         }
1059         return $xml;
1060     }
1062     /**
1063      * Generte the XML to represent some files.
1064      * @param array of store array of stored_file objects.
1065      * @return string $string the XML.
1066      */
1067     public function write_files($files) {
1068         if (empty($files)) {
1069             return '';
1070         }
1071         $string = '';
1072         foreach ($files as $file) {
1073             if ($file->is_directory()) {
1074                 continue;
1075             }
1076             $string .= '<file name="' . $file->get_filename() . '" encoding="base64">';
1077             $string .= base64_encode($file->get_content());
1078             $string .= '</file>';
1079         }
1080         return $string;
1081     }
1083     protected function presave_process($content) {
1084         // Override to allow us to add xml headers and footers
1085         return '<?xml version="1.0" encoding="UTF-8"?>
1086 <quiz>
1087 ' . $content . '</quiz>';
1088     }
1090     /**
1091      * Turns question into an xml segment
1092      * @param object $question the question data.
1093      * @return string xml segment
1094      */
1095     public function writequestion($question) {
1096         global $CFG, $OUTPUT;
1098         $invalidquestion = false;
1099         $fs = get_file_storage();
1100         $contextid = $question->contextid;
1101         // Get files used by the questiontext.
1102         $question->questiontextfiles = $fs->get_area_files(
1103                 $contextid, 'question', 'questiontext', $question->id);
1104         // Get files used by the generalfeedback.
1105         $question->generalfeedbackfiles = $fs->get_area_files(
1106                 $contextid, 'question', 'generalfeedback', $question->id);
1107         if (!empty($question->options->answers)) {
1108             foreach ($question->options->answers as $answer) {
1109                 $answer->answerfiles = $fs->get_area_files(
1110                         $contextid, 'question', 'answer', $answer->id);
1111                 $answer->feedbackfiles = $fs->get_area_files(
1112                         $contextid, 'question', 'answerfeedback', $answer->id);
1113             }
1114         }
1116         $expout = '';
1118         // Add a comment linking this to the original question id.
1119         $expout .= "<!-- question: $question->id  -->\n";
1121         // Check question type
1122         $questiontype = $this->get_qtype($question->qtype);
1124         // Categories are a special case.
1125         if ($question->qtype == 'category') {
1126             $categorypath = $this->writetext($question->category);
1127             $expout .= "  <question type=\"category\">\n";
1128             $expout .= "    <category>\n";
1129             $expout .= "        $categorypath\n";
1130             $expout .= "    </category>\n";
1131             $expout .= "  </question>\n";
1132             return $expout;
1133         }
1135         // Now we know we are are handing a real question.
1136         // Output the generic information.
1137         $expout .= "  <question type=\"$questiontype\">\n";
1138         $expout .= "    <name>\n";
1139         $expout .= $this->writetext($question->name, 3);
1140         $expout .= "    </name>\n";
1141         $expout .= "    <questiontext {$this->format($question->questiontextformat)}>\n";
1142         $expout .= $this->writetext($question->questiontext, 3);
1143         $expout .= $this->write_files($question->questiontextfiles);
1144         $expout .= "    </questiontext>\n";
1145         $expout .= "    <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1146         $expout .= $this->writetext($question->generalfeedback, 3);
1147         $expout .= $this->write_files($question->generalfeedbackfiles);
1148         $expout .= "    </generalfeedback>\n";
1149         if ($question->qtype != 'multianswer') {
1150             $expout .= "    <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1151         }
1152         $expout .= "    <penalty>{$question->penalty}</penalty>\n";
1153         $expout .= "    <hidden>{$question->hidden}</hidden>\n";
1155         // The rest of the output depends on question type.
1156         switch($question->qtype) {
1157             case 'category':
1158                 // not a qtype really - dummy used for category switching
1159                 break;
1161             case 'truefalse':
1162                 $trueanswer = $question->options->answers[$question->options->trueanswer];
1163                 $trueanswer->answer = 'true';
1164                 $expout .= $this->write_answer($trueanswer);
1166                 $falseanswer = $question->options->answers[$question->options->falseanswer];
1167                 $falseanswer->answer = 'false';
1168                 $expout .= $this->write_answer($falseanswer);
1169                 break;
1171             case 'multichoice':
1172                 $expout .= "    <single>" . $this->get_single($question->options->single) .
1173                         "</single>\n";
1174                 $expout .= "    <shuffleanswers>" .
1175                         $this->get_single($question->options->shuffleanswers) .
1176                         "</shuffleanswers>\n";
1177                 $expout .= "    <answernumbering>" . $question->options->answernumbering .
1178                         "</answernumbering>\n";
1179                 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1180                 $expout .= $this->write_answers($question->options->answers);
1181                 break;
1183             case 'shortanswer':
1184                 $expout .= "    <usecase>{$question->options->usecase}</usecase>\n";
1185                 $expout .= $this->write_answers($question->options->answers);
1186                 break;
1188             case 'numerical':
1189                 foreach ($question->options->answers as $answer) {
1190                     $expout .= $this->write_answer($answer,
1191                             "      <tolerance>$answer->tolerance</tolerance>\n");
1192                 }
1194                 $units = $question->options->units;
1195                 if (count($units)) {
1196                     $expout .= "<units>\n";
1197                     foreach ($units as $unit) {
1198                         $expout .= "  <unit>\n";
1199                         $expout .= "    <multiplier>{$unit->multiplier}</multiplier>\n";
1200                         $expout .= "    <unit_name>{$unit->unit}</unit_name>\n";
1201                         $expout .= "  </unit>\n";
1202                     }
1203                     $expout .= "</units>\n";
1204                 }
1205                 if (isset($question->options->unitgradingtype)) {
1206                     $expout .= "    <unitgradingtype>" . $question->options->unitgradingtype .
1207                             "</unitgradingtype>\n";
1208                 }
1209                 if (isset($question->options->unitpenalty)) {
1210                     $expout .= "    <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1211                 }
1212                 if (isset($question->options->showunits)) {
1213                     $expout .= "    <showunits>{$question->options->showunits}</showunits>\n";
1214                 }
1215                 if (isset($question->options->unitsleft)) {
1216                     $expout .= "    <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1217                 }
1218                 if (!empty($question->options->instructionsformat)) {
1219                     $files = $fs->get_area_files($contextid, 'qtype_numerical',
1220                             'instruction', $question->id);
1221                     $expout .= "    <instructions " .
1222                             $this->format($question->options->instructionsformat) . ">\n";
1223                     $expout .= $this->writetext($question->options->instructions, 3);
1224                     $expout .= $this->write_files($files);
1225                     $expout .= "    </instructions>\n";
1226                 }
1227                 break;
1229             case 'match':
1230                 $expout .= "    <shuffleanswers>" .
1231                         $this->get_single($question->options->shuffleanswers) .
1232                         "</shuffleanswers>\n";
1233                 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1234                 foreach ($question->options->subquestions as $subquestion) {
1235                     $files = $fs->get_area_files($contextid, 'qtype_match',
1236                             'subquestion', $subquestion->id);
1237                     $expout .= "    <subquestion " .
1238                             $this->format($subquestion->questiontextformat) . ">\n";
1239                     $expout .= $this->writetext($subquestion->questiontext, 3);
1240                     $expout .= $this->write_files($files);
1241                     $expout .= "      <answer>\n";
1242                     $expout .= $this->writetext($subquestion->answertext, 4);
1243                     $expout .= "      </answer>\n";
1244                     $expout .= "    </subquestion>\n";
1245                 }
1246                 break;
1248             case 'description':
1249                 // Nothing else to do.
1250                 break;
1252             case 'multianswer':
1253                 foreach ($question->options->questions as $index => $subq) {
1254                     $expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
1255                 }
1256                 break;
1258             case 'essay':
1259                 $expout .= "    <responseformat>" . $question->options->responseformat .
1260                         "</responseformat>\n";
1261                 $expout .= "    <responsefieldlines>" . $question->options->responsefieldlines .
1262                         "</responsefieldlines>\n";
1263                 $expout .= "    <attachments>" . $question->options->attachments .
1264                         "</attachments>\n";
1265                 $expout .= "    <graderinfo " .
1266                         $this->format($question->options->graderinfoformat) . ">\n";
1267                 $expout .= $this->writetext($question->options->graderinfo, 3);
1268                 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
1269                         'graderinfo', $question->id));
1270                 $expout .= "    </graderinfo>\n";
1271                 break;
1273             case 'calculated':
1274             case 'calculatedsimple':
1275             case 'calculatedmulti':
1276                 $expout .= "    <synchronize>{$question->options->synchronize}</synchronize>\n";
1277                 $expout .= "    <single>{$question->options->single}</single>\n";
1278                 $expout .= "    <answernumbering>" . $question->options->answernumbering .
1279                         "</answernumbering>\n";
1280                 $expout .= "    <shuffleanswers>" . $question->options->shuffleanswers .
1281                         "</shuffleanswers>\n";
1283                 $component = 'qtype_' . $question->qtype;
1284                 $files = $fs->get_area_files($contextid, $component,
1285                         'correctfeedback', $question->id);
1286                 $expout .= "    <correctfeedback>\n";
1287                 $expout .= $this->writetext($question->options->correctfeedback, 3);
1288                 $expout .= $this->write_files($files);
1289                 $expout .= "    </correctfeedback>\n";
1291                 $files = $fs->get_area_files($contextid, $component,
1292                         'partiallycorrectfeedback', $question->id);
1293                 $expout .= "    <partiallycorrectfeedback>\n";
1294                 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1295                 $expout .= $this->write_files($files);
1296                 $expout .= "    </partiallycorrectfeedback>\n";
1298                 $files = $fs->get_area_files($contextid, $component,
1299                         'incorrectfeedback', $question->id);
1300                 $expout .= "    <incorrectfeedback>\n";
1301                 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1302                 $expout .= $this->write_files($files);
1303                 $expout .= "    </incorrectfeedback>\n";
1305                 foreach ($question->options->answers as $answer) {
1306                     $percent = 100 * $answer->fraction;
1307                     $expout .= "<answer fraction=\"$percent\">\n";
1308                     // "<text/>" tags are an added feature, old files won't have them
1309                     $expout .= "    <text>{$answer->answer}</text>\n";
1310                     $expout .= "    <tolerance>{$answer->tolerance}</tolerance>\n";
1311                     $expout .= "    <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1312                     $expout .= "    <correctanswerformat>" .
1313                             $answer->correctanswerformat . "</correctanswerformat>\n";
1314                     $expout .= "    <correctanswerlength>" .
1315                             $answer->correctanswerlength . "</correctanswerlength>\n";
1316                     $expout .= "    <feedback {$this->format($answer->feedbackformat)}>\n";
1317                     $files = $fs->get_area_files($contextid, $component,
1318                             'instruction', $question->id);
1319                     $expout .= $this->writetext($answer->feedback);
1320                     $expout .= $this->write_files($answer->feedbackfiles);
1321                     $expout .= "    </feedback>\n";
1322                     $expout .= "</answer>\n";
1323                 }
1324                 if (isset($question->options->unitgradingtype)) {
1325                     $expout .= "    <unitgradingtype>" .
1326                             $question->options->unitgradingtype . "</unitgradingtype>\n";
1327                 }
1328                 if (isset($question->options->unitpenalty)) {
1329                     $expout .= "    <unitpenalty>" .
1330                             $question->options->unitpenalty . "</unitpenalty>\n";
1331                 }
1332                 if (isset($question->options->showunits)) {
1333                     $expout .= "    <showunits>{$question->options->showunits}</showunits>\n";
1334                 }
1335                 if (isset($question->options->unitsleft)) {
1336                     $expout .= "    <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1337                 }
1339                 if (isset($question->options->instructionsformat)) {
1340                     $files = $fs->get_area_files($contextid, $component,
1341                             'instruction', $question->id);
1342                     $expout .= "    <instructions " .
1343                             $this->format($question->options->instructionsformat) . ">\n";
1344                     $expout .= $this->writetext($question->options->instructions, 3);
1345                     $expout .= $this->write_files($files);
1346                     $expout .= "    </instructions>\n";
1347                 }
1349                 if (isset($question->options->units)) {
1350                     $units = $question->options->units;
1351                     if (count($units)) {
1352                         $expout .= "<units>\n";
1353                         foreach ($units as $unit) {
1354                             $expout .= "  <unit>\n";
1355                             $expout .= "    <multiplier>{$unit->multiplier}</multiplier>\n";
1356                             $expout .= "    <unit_name>{$unit->unit}</unit_name>\n";
1357                             $expout .= "  </unit>\n";
1358                         }
1359                         $expout .= "</units>\n";
1360                     }
1361                 }
1363                 // The tag $question->export_process has been set so we get all the
1364                 // data items in the database from the function
1365                 // qtype_calculated::get_question_options calculatedsimple defaults
1366                 // to calculated
1367                 if (isset($question->options->datasets) && count($question->options->datasets)) {
1368                     $expout .= "<dataset_definitions>\n";
1369                     foreach ($question->options->datasets as $def) {
1370                         $expout .= "<dataset_definition>\n";
1371                         $expout .= "    <status>".$this->writetext($def->status)."</status>\n";
1372                         $expout .= "    <name>".$this->writetext($def->name)."</name>\n";
1373                         if ($question->qtype == CALCULATED) {
1374                             $expout .= "    <type>calculated</type>\n";
1375                         } else {
1376                             $expout .= "    <type>calculatedsimple</type>\n";
1377                         }
1378                         $expout .= "    <distribution>" . $this->writetext($def->distribution) .
1379                                 "</distribution>\n";
1380                         $expout .= "    <minimum>" . $this->writetext($def->minimum) .
1381                                 "</minimum>\n";
1382                         $expout .= "    <maximum>" . $this->writetext($def->maximum) .
1383                                 "</maximum>\n";
1384                         $expout .= "    <decimals>" . $this->writetext($def->decimals) .
1385                                 "</decimals>\n";
1386                         $expout .= "    <itemcount>$def->itemcount</itemcount>\n";
1387                         if ($def->itemcount > 0) {
1388                             $expout .= "    <dataset_items>\n";
1389                             foreach ($def->items as $item) {
1390                                   $expout .= "        <dataset_item>\n";
1391                                   $expout .= "           <number>".$item->itemnumber."</number>\n";
1392                                   $expout .= "           <value>".$item->value."</value>\n";
1393                                   $expout .= "        </dataset_item>\n";
1394                             }
1395                             $expout .= "    </dataset_items>\n";
1396                             $expout .= "    <number_of_items>" . $def->number_of_items .
1397                                     "</number_of_items>\n";
1398                         }
1399                         $expout .= "</dataset_definition>\n";
1400                     }
1401                     $expout .= "</dataset_definitions>\n";
1402                 }
1403                 break;
1405             default:
1406                 // Try support by optional plugin.
1407                 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1408                     $invalidquestion = true;
1409                 } else {
1410                     $expout .= $data;
1411                 }
1412         }
1414         // Output any hints.
1415         $expout .= $this->write_hints($question);
1417         // Write the question tags.
1418         if (!empty($CFG->usetags)) {
1419             require_once($CFG->dirroot.'/tag/lib.php');
1420             $tags = tag_get_tags_array('question', $question->id);
1421             if (!empty($tags)) {
1422                 $expout .= "    <tags>\n";
1423                 foreach ($tags as $tag) {
1424                     $expout .= "      <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1425                 }
1426                 $expout .= "    </tags>\n";
1427             }
1428         }
1430         // close the question tag
1431         $expout .= "  </question>\n";
1432         if ($invalidquestion) {
1433             return '';
1434         } else {
1435             return $expout;
1436         }
1437     }
1439     public function write_answers($answers) {
1440         if (empty($answers)) {
1441             return;
1442         }
1443         $output = '';
1444         foreach ($answers as $answer) {
1445             $output .= $this->write_answer($answer);
1446         }
1447         return $output;
1448     }
1450     public function write_answer($answer, $extra = '') {
1451         $percent = $answer->fraction * 100;
1452         $output = '';
1453         $output .= "    <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
1454         $output .= $this->writetext($answer->answer, 3);
1455         $output .= $this->write_files($answer->answerfiles);
1456         $output .= "      <feedback {$this->format($answer->feedbackformat)}>\n";
1457         $output .= $this->writetext($answer->feedback, 4);
1458         $output .= $this->write_files($answer->feedbackfiles);
1459         $output .= "      </feedback>\n";
1460         $output .= $extra;
1461         $output .= "    </answer>\n";
1462         return $output;
1463     }
1465     /**
1466      * Write out the hints.
1467      * @param object $question the question definition data.
1468      * @return string XML to output.
1469      */
1470     public function write_hints($question) {
1471         if (empty($question->hints)) {
1472             return '';
1473         }
1475         $output = '';
1476         foreach ($question->hints as $hint) {
1477             $output .= $this->write_hint($hint, $question->contextid);
1478         }
1479         return $output;
1480     }
1482     /**
1483      * @param int $format a FORMAT_... constant.
1484      * @return string the attribute to add to an XML tag.
1485      */
1486     public function format($format) {
1487         return 'format="' . $this->get_format($format) . '"';
1488     }
1490     public function write_hint($hint, $contextid) {
1491         $fs = get_file_storage();
1492         $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1494         $output = '';
1495         $output .= "    <hint {$this->format($hint->hintformat)}>\n";
1496         $output .= '      ' . $this->writetext($hint->hint);
1498         if (!empty($hint->shownumcorrect)) {
1499             $output .= "      <shownumcorrect/>\n";
1500         }
1501         if (!empty($hint->clearwrong)) {
1502             $output .= "      <clearwrong/>\n";
1503         }
1505         if (!empty($hint->options)) {
1506             $output .= '      <options>' . $this->xml_escape($hint->options) . "</options>\n";
1507         }
1508         $output .= $this->write_files($files);
1509         $output .= "    </hint>\n";
1510         return $output;
1511     }
1513     /**
1514      * Output the combined feedback fields.
1515      * @param object $questionoptions the question definition data.
1516      * @param int $questionid the question id.
1517      * @param int $contextid the question context id.
1518      * @return string XML to output.
1519      */
1520     public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1521         $fs = get_file_storage();
1522         $output = '';
1524         $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1525         foreach ($fields as $field) {
1526             $formatfield = $field . 'format';
1527             $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1529             $output .= "    <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1530             $output .= '      ' . $this->writetext($questionoptions->$field);
1531             $output .= $this->write_files($files);
1532             $output .= "    </{$field}>\n";
1533         }
1535         if (!empty($questionoptions->shownumcorrect)) {
1536             $output .= "    <shownumcorrect/>\n";
1537         }
1538         return $output;
1539     }