Merge branch 'MDL-29058' of git://github.com/timhunt/moodle
[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     }
153     /**
154      * import parts of question common to all types
155      * @param $question array question question array from xml tree
156      * @return object question object
157      */
158     public function import_headers($question) {
159         global $CFG;
161         // get some error strings
162         $error_noname = get_string('xmlimportnoname', 'qformat_xml');
163         $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
165         // this routine initialises the question object
166         $qo = $this->defaultquestion();
168         // Question name
169         $qo->name = $this->getpath($question,
170                 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
171                 get_string('xmlimportnoname', 'qformat_xml'));
172         $qo->questiontext = $this->getpath($question,
173                 array('#', 'questiontext', 0, '#', 'text', 0, '#'), '', true);
174         $qo->questiontextformat = $this->trans_format($this->getpath(
175                 $question, array('#', 'questiontext', 0, '@', 'format'), 'html'));
177         $qo->questiontextfiles = $this->import_files($this->getpath($question,
178                 array('#', 'questiontext', 0, '#', 'file'), array(), false));
180         // Backwards compatibility, deal with the old image tag.
181         $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
182         $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
183         if ($filedata && $filename) {
184             $data = new stdClass();
185             $data->content = $filedata;
186             $data->encoding = 'base64';
187             $data->name = $filename;
188             $qo->questiontextfiles[] = $data;
189             $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
190         }
192         // restore files in generalfeedback
193         $qo->generalfeedback = $this->getpath($question,
194                 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
195         $qo->generalfeedbackfiles = array();
196         $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
197                 array('#', 'generalfeedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
198         $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
199                 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
201         $qo->defaultmark = $this->getpath($question,
202                 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
203         $qo->penalty = $this->getpath($question,
204                 array('#', 'penalty', 0, '#'), $qo->penalty);
206         // Fix problematic rounding from old files:
207         if (abs($qo->penalty - 0.3333333) < 0.005) {
208             $qo->penalty = 0.3333333;
209         }
211         // Read the question tags.
212         if (!empty($CFG->usetags) && array_key_exists('tags', $question['#'])
213                 && !empty($question['#']['tags'][0]['#']['tag'])) {
214             require_once($CFG->dirroot.'/tag/lib.php');
215             $qo->tags = array();
216             foreach ($question['#']['tags'][0]['#']['tag'] as $tagdata) {
217                 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
218             }
219         }
221         return $qo;
222     }
224     /**
225      * Import the common parts of a single answer
226      * @param array answer xml tree for single answer
227      * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
228      *      and so may contain files, otherwise the answers are plain text.
229      * @param array Default text format for the feedback, and the answers if $withanswerfiles
230      *      is true.
231      * @return object answer object
232      */
233     public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
234         $ans = new stdClass();
236         $ans->answer = array();
237         $ans->answer['text']   = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
238         $ans->answer['format'] = $this->trans_format($this->getpath($answer,
239                 array('@', 'format'), $defaultformat));
240         if ($withanswerfiles) {
241             $ans->answer['files']  = $this->import_files($this->getpath($answer,
242                     array('#', 'file'), array()));
243         } else {
244             $ans->answer['format'] = FORMAT_PLAIN;
245         }
247         $ans->feedback = array();
248         $ans->feedback['text']   = $this->getpath($answer,
249                 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
250         $ans->feedback['format'] = $this->trans_format($this->getpath($answer,
251                 array('#', 'feedback', 0, '@', 'format'), $defaultformat));
252         $ans->feedback['files']  = $this->import_files($this->getpath($answer,
253                 array('#', 'feedback', 0, '#', 'file'), array()));
255         $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
257         return $ans;
258     }
260     /**
261      * Import the common overall feedback fields.
262      * @param object $question the part of the XML relating to this question.
263      * @param object $qo the question data to add the fields to.
264      * @param bool $withshownumpartscorrect include the shownumcorrect field.
265      */
266     public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
267         $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
268         foreach ($fields as $field) {
269             $text = array();
270             $text['text'] = $this->getpath($questionxml,
271                     array('#', $field, 0, '#', 'text', 0, '#'), '', true);
272             $text['format'] = $this->trans_format($this->getpath($questionxml,
273                     array('#', $field, 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
274             $text['files'] = $this->import_files($this->getpath($questionxml,
275                     array('#', $field, 0, '#', 'file'), array(), false));
277             $qo->$field = $text;
278         }
280         if ($withshownumpartscorrect) {
281             $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
283             // Backwards compatibility:
284             if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
285                 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
286                         array('#', 'correctresponsesfeedback', 0, '#'), 1));
287             }
288         }
289     }
291     /**
292      * Import a question hint
293      * @param array $hintxml hint xml fragment.
294      * @param string $defaultformat the text format to assume for hints that do not specify.
295      * @return object hint for storing in the database.
296      */
297     public function import_hint($hintxml, $defaultformat) {
298         if (array_key_exists('hintcontent', $hintxml['#'])) {
299             // Backwards compatibility:
301             $hint = new stdClass();
302             $hint->hint = array('format' => FORMAT_HTML, 'files' => array());
303             $hint->hint['text'] = $this->getpath($hintxml,
304                     array('#', 'hintcontent', 0, '#', 'text', 0, '#'), '', true);
305             $hint->hint['format'] = $this->trans_format($defaultformat);
306             $hint->hint['files'] = array();
307             $hint->shownumcorrect = $this->getpath($hintxml,
308                     array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
309             $hint->clearwrong = $this->getpath($hintxml,
310                     array('#', 'clearincorrectresponses', 0, '#'), 0);
311             $hint->options = $this->getpath($hintxml,
312                     array('#', 'showfeedbacktoresponses', 0, '#'), 0);
314             return $hint;
315         }
317         $hint = new stdClass();
318         $hint->hint['text'] = $this->getpath($hintxml,
319                 array('#', 'text', 0, '#'), '', true);
320         $hint->hint['format'] = $this->trans_format($this->getpath($hintxml,
321                 array('@', 'format'), $defaultformat));
322         $hint->hint['files'] = $this->import_files($this->getpath($hintxml,
323                 array('#', 'file'), array(), false));
324         $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
325         $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
326         $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
328         return $hint;
329     }
331     /**
332      * Import all the question hints
333      *
334      * @param object $qo the question data that is being constructed.
335      * @param array $questionxml The xml representing the question.
336      * @param bool $withparts whether the extra fields relating to parts should be imported.
337      * @param bool $withoptions whether the extra options field should be imported.
338      * @param string $defaultformat the text format to assume for hints that do not specify.
339      * @return array of objects representing the hints in the file.
340      */
341     public function import_hints($qo, $questionxml, $withparts = false,
342             $withoptions = false, $defaultformat = 'html') {
343         if (!isset($questionxml['#']['hint'])) {
344             return;
345         }
347         foreach ($questionxml['#']['hint'] as $hintxml) {
348             $hint = $this->import_hint($hintxml, $defaultformat);
349             $qo->hint[] = $hint->hint;
351             if ($withparts) {
352                 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
353                 $qo->hintclearwrong[] = $hint->clearwrong;
354             }
356             if ($withoptions) {
357                 $qo->hintoptions[] = $hint->options;
358             }
359         }
360     }
362     /**
363      * Import files from a node in the XML.
364      * @param array $xml an array of <file> nodes from the the parsed XML.
365      * @return array of things representing files - in the form that save_question expects.
366      */
367     public function import_files($xml) {
368         $files = array();
369         foreach ($xml as $file) {
370             $data = new stdClass();
371             $data->content = $file['#'];
372             $data->encoding = $file['@']['encoding'];
373             $data->name = $file['@']['name'];
374             $files[] = $data;
375         }
376         return $files;
377     }
379     /**
380      * import multiple choice question
381      * @param array question question array from xml tree
382      * @return object question object
383      */
384     public function import_multichoice($question) {
385         // get common parts
386         $qo = $this->import_headers($question);
388         // 'header' parts particular to multichoice
389         $qo->qtype = MULTICHOICE;
390         $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
391         $qo->single = $this->trans_single($single);
392         $shuffleanswers = $this->getpath($question,
393                 array('#', 'shuffleanswers', 0, '#'), 'false');
394         $qo->answernumbering = $this->getpath($question,
395                 array('#', 'answernumbering', 0, '#'), 'abc');
396         $qo->shuffleanswers = $this->trans_single($shuffleanswers);
398         // There was a time on the 1.8 branch when it could output an empty
399         // answernumbering tag, so fix up any found.
400         if (empty($qo->answernumbering)) {
401             $qo->answernumbering = 'abc';
402         }
404         // Run through the answers
405         $answers = $question['#']['answer'];
406         $acount = 0;
407         foreach ($answers as $answer) {
408             $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
409             $qo->answer[$acount] = $ans->answer;
410             $qo->fraction[$acount] = $ans->fraction;
411             $qo->feedback[$acount] = $ans->feedback;
412             ++$acount;
413         }
415         $this->import_combined_feedback($qo, $question, true);
416         $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
418         return $qo;
419     }
421     /**
422      * Import cloze type question
423      * @param array question question array from xml tree
424      * @return object question object
425      */
426     public function import_multianswer($question) {
427         question_bank::get_qtype('multianswer');
429         $questiontext['text'] = $this->import_text($question['#']['questiontext'][0]['#']['text']);
430         $questiontext['format'] = FORMAT_HTML;
431         $questiontext['itemid'] = '';
432         $qo = qtype_multianswer_extract_question($questiontext);
434         // 'header' parts particular to multianswer
435         $qo->qtype = 'multianswer';
436         $qo->course = $this->course;
437         $qo->generalfeedback = '';
439         $qo->name = $this->import_text($question['#']['name'][0]['#']['text']);
440         $qo->questiontextformat = $questiontext['format'];
441         $qo->questiontext = $qo->questiontext['text'];
442         $qo->questiontextfiles = array();
444         // restore files in generalfeedback
445         $qo->generalfeedback = $this->getpath($question,
446                 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
447         $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
448                 array('#', 'generalfeedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
449         $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
450                 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
452         $qo->penalty = $this->getpath($question,
453                 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
454         // Fix problematic rounding from old files:
455         if (abs($qo->penalty - 0.3333333) < 0.005) {
456             $qo->penalty = 0.3333333;
457         }
459         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
461         return $qo;
462     }
464     /**
465      * Import true/false type question
466      * @param array question question array from xml tree
467      * @return object question object
468      */
469     public function import_truefalse($question) {
470         // get common parts
471         global $OUTPUT;
472         $qo = $this->import_headers($question);
474         // 'header' parts particular to true/false
475         $qo->qtype = TRUEFALSE;
477         // In the past, it used to be assumed that the two answers were in the file
478         // true first, then false. Howevever that was not always true. Now, we
479         // try to match on the answer text, but in old exports, this will be a localised
480         // string, so if we don't find true or false, we fall back to the old system.
481         $first = true;
482         $warning = false;
483         foreach ($question['#']['answer'] as $answer) {
484             $answertext = $this->getpath($answer,
485                     array('#', 'text', 0, '#'), '', true);
486             $feedback = $this->getpath($answer,
487                     array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
488             $feedbackformat = $this->getpath($answer,
489                     array('#', 'feedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat));
490             $feedbackfiles = $this->getpath($answer,
491                     array('#', 'feedback', 0, '#', 'file'), array());
492             $files = array();
493             foreach ($feedbackfiles as $file) {
494                 $data = new stdClass();
495                 $data->content = $file['#'];
496                 $data->encoding = $file['@']['encoding'];
497                 $data->name = $file['@']['name'];
498                 $files[] = $data;
499             }
500             if ($answertext != 'true' && $answertext != 'false') {
501                 // Old style file, assume order is true/false.
502                 $warning = true;
503                 if ($first) {
504                     $answertext = 'true';
505                 } else {
506                     $answertext = 'false';
507                 }
508             }
510             if ($answertext == 'true') {
511                 $qo->answer = ($answer['@']['fraction'] == 100);
512                 $qo->correctanswer = $qo->answer;
513                 $qo->feedbacktrue = array();
514                 $qo->feedbacktrue['text'] = $feedback;
515                 $qo->feedbacktrue['format'] = $this->trans_format($feedbackformat);
516                 $qo->feedbacktrue['files'] = $files;
517             } else {
518                 $qo->answer = ($answer['@']['fraction'] != 100);
519                 $qo->correctanswer = $qo->answer;
520                 $qo->feedbackfalse = array();
521                 $qo->feedbackfalse['text'] = $feedback;
522                 $qo->feedbackfalse['format'] = $this->trans_format($feedbackformat);
523                 $qo->feedbackfalse['files'] = $files;
524             }
525             $first = false;
526         }
528         if ($warning) {
529             $a = new stdClass();
530             $a->questiontext = $qo->questiontext;
531             $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
532             echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
533         }
535         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
537         return $qo;
538     }
540     /**
541      * Import short answer type question
542      * @param array question question array from xml tree
543      * @return object question object
544      */
545     public function import_shortanswer($question) {
546         // get common parts
547         $qo = $this->import_headers($question);
549         // header parts particular to shortanswer
550         $qo->qtype = SHORTANSWER;
552         // get usecase
553         $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
555         // Run through the answers
556         $answers = $question['#']['answer'];
557         $acount = 0;
558         foreach ($answers as $answer) {
559             $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
560             $qo->answer[$acount] = $ans->answer['text'];
561             $qo->fraction[$acount] = $ans->fraction;
562             $qo->feedback[$acount] = $ans->feedback;
563             ++$acount;
564         }
566         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
568         return $qo;
569     }
571     /**
572      * Import description type question
573      * @param array question question array from xml tree
574      * @return object question object
575      */
576     public function import_description($question) {
577         // get common parts
578         $qo = $this->import_headers($question);
579         // header parts particular to shortanswer
580         $qo->qtype = DESCRIPTION;
581         $qo->defaultmark = 0;
582         $qo->length = 0;
583         return $qo;
584     }
586     /**
587      * Import numerical type question
588      * @param array question question array from xml tree
589      * @return object question object
590      */
591     public function import_numerical($question) {
592         // get common parts
593         $qo = $this->import_headers($question);
595         // header parts particular to numerical
596         $qo->qtype = NUMERICAL;
598         // get answers array
599         $answers = $question['#']['answer'];
600         $qo->answer = array();
601         $qo->feedback = array();
602         $qo->fraction = array();
603         $qo->tolerance = array();
604         foreach ($answers as $answer) {
605             // answer outside of <text> is deprecated
606             $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
607             $qo->answer[] = $obj->answer['text'];
608             if (empty($qo->answer)) {
609                 $qo->answer = '*';
610             }
611             $qo->feedback[]  = $obj->feedback;
612             $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
614             // fraction as a tag is deprecated
615             $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
616             $qo->fraction[] = $this->getpath($answer,
617                     array('#', 'fraction', 0, '#'), $fraction); // deprecated
618         }
620         // Get the units array
621         $qo->unit = array();
622         $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
623         if (!empty($units)) {
624             $qo->multiplier = array();
625             foreach ($units as $unit) {
626                 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
627                 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
628             }
629         }
630         $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
631         $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
632         $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
633         $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
634         $qo->instructions['text'] = '';
635         $qo->instructions['format'] = FORMAT_HTML;
636         $instructions = $this->getpath($question, array('#', 'instructions'), array());
637         if (!empty($instructions)) {
638             $qo->instructions = array();
639             $qo->instructions['text'] = $this->getpath($instructions,
640                     array('0', '#', 'text', '0', '#'), '', true);
641             $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
642                     array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
643             $qo->instructions['files'] = $this->import_files($this->getpath(
644                     $instructions, array('0', '#', 'file'), array()));
645         }
647         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
649         return $qo;
650     }
652     /**
653      * Import matching type question
654      * @param array question question array from xml tree
655      * @return object question object
656      */
657     public function import_match($question) {
658         // get common parts
659         $qo = $this->import_headers($question);
661         // header parts particular to matching
662         $qo->qtype = 'match';
663         $qo->shuffleanswers = $this->trans_single($this->getpath($question,
664                 array('#', 'shuffleanswers', 0, '#'), 1));
666         // run through subquestions
667         $qo->subquestions = array();
668         $qo->subanswers = array();
669         foreach ($question['#']['subquestion'] as $subqxml) {
670             $subquestion = array();
671             $subquestion['text'] = $this->getpath($subqxml, array('#', 'text', 0, '#'), '', true);
672             $subquestion['format'] = $this->trans_format($this->getpath($subqxml,
673                     array('@', 'format'), $this->get_format($qo->questiontextformat)));
674             $subquestion['files'] = $this->import_files($this->getpath($subqxml,
675                     array('#', 'file'), array()));
677             $qo->subquestions[] = $subquestion;
678             $answers = $this->getpath($subqxml, array('#', 'answer'), array());
679             $qo->subanswers[] = $this->getpath($subqxml,
680                     array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
681         }
683         $this->import_combined_feedback($qo, $question, true);
684         $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
686         return $qo;
687     }
689     /**
690      * Import essay type question
691      * @param array question question array from xml tree
692      * @return object question object
693      */
694     public function import_essay($question) {
695         // get common parts
696         $qo = $this->import_headers($question);
698         // header parts particular to essay
699         $qo->qtype = ESSAY;
701         $qo->responseformat = $this->getpath($question,
702                 array('#', 'responseformat', 0, '#'), 'editor');
703         $qo->responsefieldlines = $this->getpath($question,
704                 array('#', 'responsefieldlines', 0, '#'), 15);
705         $qo->attachments = $this->getpath($question,
706                 array('#', 'attachments', 0, '#'), 0);
707         $qo->graderinfo['text'] = $this->getpath($question,
708                 array('#', 'graderinfo', 0, '#', 'text', 0, '#'), '', true);
709         $qo->graderinfo['format'] = $this->trans_format($this->getpath($question,
710                 array('#', 'graderinfo', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
711         $qo->graderinfo['files'] = $this->import_files($this->getpath($question,
712                 array('#', 'graderinfo', '0', '#', 'file'), array()));
714         return $qo;
715     }
717     /**
718      * Import a calculated question
719      * @param object $question the imported XML data.
720      */
721     public function import_calculated($question) {
723         // get common parts
724         $qo = $this->import_headers($question);
726         // header parts particular to calculated
727         $qo->qtype = CALCULATED;
728         $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
729         $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
730         $qo->single = $this->trans_single($single);
731         $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
732         $qo->answernumbering = $this->getpath($question,
733                 array('#', 'answernumbering', 0, '#'), 'abc');
734         $qo->shuffleanswers = $this->trans_single($shuffleanswers);
736         $this->import_combined_feedback($qo, $question);
738         $qo->unitgradingtype = $this->getpath($question,
739                 array('#', 'unitgradingtype', 0, '#'), 0);
740         $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0);
741         $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
742         $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
743         $qo->instructions = $this->getpath($question,
744                 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
745         if (!empty($instructions)) {
746             $qo->instructions = array();
747             $qo->instructions['text'] = $this->getpath($instructions,
748                     array('0', '#', 'text', '0', '#'), '', true);
749             $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
750                     array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
751             $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
752                     array('0', '#', 'file'), array()));
753         }
755         // get answers array
756         $answers = $question['#']['answer'];
757         $qo->answers = array();
758         $qo->feedback = array();
759         $qo->fraction = array();
760         $qo->tolerance = array();
761         $qo->tolerancetype = array();
762         $qo->correctanswerformat = array();
763         $qo->correctanswerlength = array();
764         $qo->feedback = array();
765         foreach ($answers as $answer) {
766             $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
767             // answer outside of <text> is deprecated
768             if (empty($ans->answer['text'])) {
769                 $ans->answer['text'] = '*';
770             }
771             $qo->answers[] = $ans->answer;
772             $qo->feedback[] = $ans->feedback;
773             $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
774             // fraction as a tag is deprecated
775             if (!empty($answer['#']['fraction'][0]['#'])) {
776                 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
777             } else {
778                 $qo->fraction[] = $answer['@']['fraction'] / 100;
779             }
780             $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
781             $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
782             $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
783         }
784         // get units array
785         $qo->unit = array();
786         if (isset($question['#']['units'][0]['#']['unit'])) {
787             $units = $question['#']['units'][0]['#']['unit'];
788             $qo->multiplier = array();
789             foreach ($units as $unit) {
790                 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
791                 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
792             }
793         }
794         $instructions = $this->getpath($question, array('#', 'instructions'), array());
795         if (!empty($instructions)) {
796             $qo->instructions = array();
797             $qo->instructions['text'] = $this->getpath($instructions,
798                     array('0', '#', 'text', '0', '#'), '', true);
799             $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
800                     array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
801             $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
802                     array('0', '#', 'file'), array()));
803         }
804         $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
805         $qo->dataset = array();
806         $qo->datasetindex= 0;
807         foreach ($datasets as $dataset) {
808             $qo->datasetindex++;
809             $qo->dataset[$qo->datasetindex] = new stdClass();
810             $qo->dataset[$qo->datasetindex]->status =
811                     $this->import_text($dataset['#']['status'][0]['#']['text']);
812             $qo->dataset[$qo->datasetindex]->name =
813                     $this->import_text($dataset['#']['name'][0]['#']['text']);
814             $qo->dataset[$qo->datasetindex]->type =
815                     $dataset['#']['type'][0]['#'];
816             $qo->dataset[$qo->datasetindex]->distribution =
817                     $this->import_text($dataset['#']['distribution'][0]['#']['text']);
818             $qo->dataset[$qo->datasetindex]->max =
819                     $this->import_text($dataset['#']['maximum'][0]['#']['text']);
820             $qo->dataset[$qo->datasetindex]->min =
821                     $this->import_text($dataset['#']['minimum'][0]['#']['text']);
822             $qo->dataset[$qo->datasetindex]->length =
823                     $this->import_text($dataset['#']['decimals'][0]['#']['text']);
824             $qo->dataset[$qo->datasetindex]->distribution =
825                     $this->import_text($dataset['#']['distribution'][0]['#']['text']);
826             $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
827             $qo->dataset[$qo->datasetindex]->datasetitem = array();
828             $qo->dataset[$qo->datasetindex]->itemindex = 0;
829             $qo->dataset[$qo->datasetindex]->number_of_items =
830                     $dataset['#']['number_of_items'][0]['#'];
831             $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item'];
832             foreach ($datasetitems as $datasetitem) {
833                 $qo->dataset[$qo->datasetindex]->itemindex++;
834                 $qo->dataset[$qo->datasetindex]->datasetitem[
835                         $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
836                 $qo->dataset[$qo->datasetindex]->datasetitem[
837                         $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
838                                 $datasetitem['#']['number'][0]['#'];
839                 $qo->dataset[$qo->datasetindex]->datasetitem[
840                         $qo->dataset[$qo->datasetindex]->itemindex]->value =
841                                 $datasetitem['#']['value'][0]['#'];
842             }
843         }
845         $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
847         return $qo;
848     }
850     /**
851      * This is not a real question type. It's a dummy type used to specify the
852      * import category. The format is:
853      * <question type="category">
854      *     <category>tom/dick/harry</category>
855      * </question>
856      */
857     protected function import_category($question) {
858         $qo = new stdClass();
859         $qo->qtype = 'category';
860         $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
861         return $qo;
862     }
864     /**
865      * Parse the array of lines into an array of questions
866      * this *could* burn memory - but it won't happen that much
867      * so fingers crossed!
868      * @param array of lines from the input file.
869      * @return array (of objects) question objects.
870      */
871     protected function readquestions($lines) {
872         // We just need it as one big string
873         $text = implode($lines, ' ');
874         unset($lines);
876         // This converts xml to big nasty data structure
877         // the 0 means keep white space as it is (important for markdown format)
878         try {
879             $xml = xmlize($text, 0, 'UTF-8', true);
880         } catch (xml_format_exception $e) {
881             $this->error($e->getMessage(), '');
882             return false;
883         }
884         // Set up array to hold all our questions
885         $questions = array();
887         // Iterate through questions
888         foreach ($xml['quiz']['#']['question'] as $question) {
889             $questiontype = $question['@']['type'];
891             if ($questiontype == 'multichoice') {
892                 $qo = $this->import_multichoice($question);
893             } else if ($questiontype == 'truefalse') {
894                 $qo = $this->import_truefalse($question);
895             } else if ($questiontype == 'shortanswer') {
896                 $qo = $this->import_shortanswer($question);
897             } else if ($questiontype == 'numerical') {
898                 $qo = $this->import_numerical($question);
899             } else if ($questiontype == 'description') {
900                 $qo = $this->import_description($question);
901             } else if ($questiontype == 'matching' || $questiontype == 'match') {
902                 $qo = $this->import_match($question);
903             } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
904                 $qo = $this->import_multianswer($question);
905             } else if ($questiontype == 'essay') {
906                 $qo = $this->import_essay($question);
907             } else if ($questiontype == 'calculated') {
908                 $qo = $this->import_calculated($question);
909             } else if ($questiontype == 'calculatedsimple') {
910                 $qo = $this->import_calculated($question);
911                 $qo->qtype = 'calculatedsimple';
912             } else if ($questiontype == 'calculatedmulti') {
913                 $qo = $this->import_calculated($question);
914                 $qo->qtype = 'calculatedmulti';
915             } else if ($questiontype == 'category') {
916                 $qo = $this->import_category($question);
918             } else {
919                 // Not a type we handle ourselves. See if the question type wants
920                 // to handle it.
921                 if (!$qo = $this->try_importing_using_qtypes(
922                         $question, null, null, $questiontype)) {
923                     $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
924                     $qo = null;
925                 }
926             }
928             // Stick the result in the $questions array
929             if ($qo) {
930                 $questions[] = $qo;
931             }
932         }
933         return $questions;
934     }
936     // EXPORT FUNCTIONS START HERE
938     public function export_file_extension() {
939         return '.xml';
940     }
942     /**
943      * Turn the internal question type name into a human readable form.
944      * (In the past, the code used to use integers internally. Now, it uses
945      * strings, so there is less need for this, but to maintain
946      * backwards-compatibility we change two of the type names.)
947      * @param string $qtype question type plugin name.
948      * @return string $qtype string to use in the file.
949      */
950     protected function get_qtype($qtype) {
951         switch($qtype) {
952             case 'match':
953                 return 'matching';
954             case 'multianswer':
955                 return 'cloze';
956             default:
957                 return $qtype;
958         }
959     }
961     /**
962      * Convert internal Moodle text format code into
963      * human readable form
964      * @param int id internal code
965      * @return string format text
966      */
967     public function get_format($id) {
968         switch($id) {
969             case FORMAT_MOODLE:
970                 return 'moodle_auto_format';
971             case FORMAT_HTML:
972                 return 'html';
973             case FORMAT_PLAIN:
974                 return 'plain_text';
975             case FORMAT_WIKI:
976                 return 'wiki_like';
977             case FORMAT_MARKDOWN:
978                 return 'markdown';
979             default:
980                 return 'unknown';
981         }
982     }
984     /**
985      * Convert internal single question code into
986      * human readable form
987      * @param int id single question code
988      * @return string single question string
989      */
990     public function get_single($id) {
991         switch($id) {
992             case 0:
993                 return 'false';
994             case 1:
995                 return 'true';
996             default:
997                 return 'unknown';
998         }
999     }
1001     /**
1002      * Take a string, and wrap it in a CDATA secion, if that is required to make
1003      * the output XML valid.
1004      * @param string $string a string
1005      * @return string the string, wrapped in CDATA if necessary.
1006      */
1007     public function xml_escape($string) {
1008         if (!empty($string) && htmlspecialchars($string) != $string) {
1009             return "<![CDATA[{$string}]]>";
1010         } else {
1011             return $string;
1012         }
1013     }
1015     /**
1016      * Generates <text></text> tags, processing raw text therein
1017      * @param string $raw the content to output.
1018      * @param int $indent the current indent level.
1019      * @param bool $short stick it on one line.
1020      * @return string formatted text.
1021      */
1022     public function writetext($raw, $indent = 0, $short = true) {
1023         $indent = str_repeat('  ', $indent);
1024         $raw = $this->xml_escape($raw);
1026         if ($short) {
1027             $xml = "$indent<text>$raw</text>\n";
1028         } else {
1029             $xml = "$indent<text>\n$raw\n$indent</text>\n";
1030         }
1032         return $xml;
1033     }
1035     protected function presave_process($content) {
1036         // Override to allow us to add xml headers and footers
1037         return '<?xml version="1.0" encoding="UTF-8"?>
1038 <quiz>
1039 ' . $content . '</quiz>';
1040     }
1042     /**
1043      * Turns question into an xml segment
1044      * @param object $question the question data.
1045      * @return string xml segment
1046      */
1047     public function writequestion($question) {
1048         global $CFG, $OUTPUT;
1050         $fs = get_file_storage();
1051         $contextid = $question->contextid;
1052         // Get files used by the questiontext.
1053         $question->questiontextfiles = $fs->get_area_files(
1054                 $contextid, 'question', 'questiontext', $question->id);
1055         // Get files used by the generalfeedback.
1056         $question->generalfeedbackfiles = $fs->get_area_files(
1057                 $contextid, 'question', 'generalfeedback', $question->id);
1058         if (!empty($question->options->answers)) {
1059             foreach ($question->options->answers as $answer) {
1060                 $answer->answerfiles = $fs->get_area_files(
1061                         $contextid, 'question', 'answer', $answer->id);
1062                 $answer->feedbackfiles = $fs->get_area_files(
1063                         $contextid, 'question', 'answerfeedback', $answer->id);
1064             }
1065         }
1067         $expout = '';
1069         // Add a comment linking this to the original question id.
1070         $expout .= "<!-- question: $question->id  -->\n";
1072         // Check question type
1073         $questiontype = $this->get_qtype($question->qtype);
1075         // Categories are a special case.
1076         if ($question->qtype == 'category') {
1077             $categorypath = $this->writetext($question->category);
1078             $expout .= "  <question type=\"category\">\n";
1079             $expout .= "    <category>\n";
1080             $expout .= "        $categorypath\n";
1081             $expout .= "    </category>\n";
1082             $expout .= "  </question>\n";
1083             return $expout;
1084         }
1086         // Now we know we are are handing a real question.
1087         // Output the generic information.
1088         $expout .= "  <question type=\"$questiontype\">\n";
1089         $expout .= "    <name>\n";
1090         $expout .= $this->writetext($question->name, 3);
1091         $expout .= "    </name>\n";
1092         $expout .= "    <questiontext {$this->format($question->questiontextformat)}>\n";
1093         $expout .= $this->writetext($question->questiontext, 3);
1094         $expout .= $this->writefiles($question->questiontextfiles);
1095         $expout .= "    </questiontext>\n";
1096         $expout .= "    <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1097         $expout .= $this->writetext($question->generalfeedback, 3);
1098         $expout .= $this->writefiles($question->generalfeedbackfiles);
1099         $expout .= "    </generalfeedback>\n";
1100         if ($question->qtype != 'multianswer') {
1101             $expout .= "    <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
1102         }
1103         $expout .= "    <penalty>{$question->penalty}</penalty>\n";
1104         $expout .= "    <hidden>{$question->hidden}</hidden>\n";
1106         // The rest of the output depends on question type.
1107         switch($question->qtype) {
1108             case 'category':
1109                 // not a qtype really - dummy used for category switching
1110                 break;
1112             case 'truefalse':
1113                 $trueanswer = $question->options->answers[$question->options->trueanswer];
1114                 $trueanswer->answer = 'true';
1115                 $expout .= $this->write_answer($trueanswer);
1117                 $falseanswer = $question->options->answers[$question->options->falseanswer];
1118                 $falseanswer->answer = 'false';
1119                 $expout .= $this->write_answer($falseanswer);
1120                 break;
1122             case 'multichoice':
1123                 $expout .= "    <single>" . $this->get_single($question->options->single) .
1124                         "</single>\n";
1125                 $expout .= "    <shuffleanswers>" .
1126                         $this->get_single($question->options->shuffleanswers) .
1127                         "</shuffleanswers>\n";
1128                 $expout .= "    <answernumbering>" . $question->options->answernumbering .
1129                         "</answernumbering>\n";
1130                 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1131                 $expout .= $this->write_answers($question->options->answers);
1132                 break;
1134             case 'shortanswer':
1135                 $expout .= "    <usecase>{$question->options->usecase}</usecase>\n";
1136                 $expout .= $this->write_answers($question->options->answers);
1137                 break;
1139             case 'numerical':
1140                 foreach ($question->options->answers as $answer) {
1141                     $expout .= $this->write_answer($answer,
1142                             "      <tolerance>$answer->tolerance</tolerance>\n");
1143                 }
1145                 $units = $question->options->units;
1146                 if (count($units)) {
1147                     $expout .= "<units>\n";
1148                     foreach ($units as $unit) {
1149                         $expout .= "  <unit>\n";
1150                         $expout .= "    <multiplier>{$unit->multiplier}</multiplier>\n";
1151                         $expout .= "    <unit_name>{$unit->unit}</unit_name>\n";
1152                         $expout .= "  </unit>\n";
1153                     }
1154                     $expout .= "</units>\n";
1155                 }
1156                 if (isset($question->options->unitgradingtype)) {
1157                     $expout .= "    <unitgradingtype>" . $question->options->unitgradingtype .
1158                             "</unitgradingtype>\n";
1159                 }
1160                 if (isset($question->options->unitpenalty)) {
1161                     $expout .= "    <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1162                 }
1163                 if (isset($question->options->showunits)) {
1164                     $expout .= "    <showunits>{$question->options->showunits}</showunits>\n";
1165                 }
1166                 if (isset($question->options->unitsleft)) {
1167                     $expout .= "    <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1168                 }
1169                 if (!empty($question->options->instructionsformat)) {
1170                     $files = $fs->get_area_files($contextid, 'qtype_numerical',
1171                             'instruction', $question->id);
1172                     $expout .= "    <instructions " .
1173                             $this->format($question->options->instructionsformat) . ">\n";
1174                     $expout .= $this->writetext($question->options->instructions, 3);
1175                     $expout .= $this->writefiles($files);
1176                     $expout .= "    </instructions>\n";
1177                 }
1178                 break;
1180             case 'match':
1181                 $expout .= "    <shuffleanswers>" .
1182                         $this->get_single($question->options->shuffleanswers) .
1183                         "</shuffleanswers>\n";
1184                 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
1185                 foreach ($question->options->subquestions as $subquestion) {
1186                     $files = $fs->get_area_files($contextid, 'qtype_match',
1187                             'subquestion', $subquestion->id);
1188                     $expout .= "    <subquestion " .
1189                             $this->format($subquestion->questiontextformat) . ">\n";
1190                     $expout .= $this->writetext($subquestion->questiontext, 3);
1191                     $expout .= $this->writefiles($files);
1192                     $expout .= "      <answer>\n";
1193                     $expout .= $this->writetext($subquestion->answertext, 4);
1194                     $expout .= "      </answer>\n";
1195                     $expout .= "    </subquestion>\n";
1196                 }
1197                 break;
1199             case 'description':
1200                 // Nothing else to do.
1201                 break;
1203             case 'multianswer':
1204                 foreach ($question->options->questions as $index => $subq) {
1205                     $expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
1206                 }
1207                 break;
1209             case 'essay':
1210                 $expout .= "    <responseformat>" . $question->options->responseformat .
1211                         "</responseformat>\n";
1212                 $expout .= "    <responsefieldlines>" . $question->options->responsefieldlines .
1213                         "</responsefieldlines>\n";
1214                 $expout .= "    <attachments>" . $question->options->attachments .
1215                         "</attachments>\n";
1216                 $expout .= "    <graderinfo " .
1217                         $this->format($question->options->graderinfoformat) . ">\n";
1218                 $expout .= $this->writetext($question->options->graderinfo, 3);
1219                 $expout .= $this->writefiles($fs->get_area_files($contextid, 'qtype_essay',
1220                         'graderinfo', $question->id));
1221                 $expout .= "    </graderinfo>\n";
1222                 break;
1224             case 'calculated':
1225             case 'calculatedsimple':
1226             case 'calculatedmulti':
1227                 $expout .= "    <synchronize>{$question->options->synchronize}</synchronize>\n";
1228                 $expout .= "    <single>{$question->options->single}</single>\n";
1229                 $expout .= "    <answernumbering>" . $question->options->answernumbering .
1230                         "</answernumbering>\n";
1231                 $expout .= "    <shuffleanswers>" . $question->options->shuffleanswers .
1232                         "</shuffleanswers>\n";
1234                 $component = 'qtype_' . $question->qtype;
1235                 $files = $fs->get_area_files($contextid, $component,
1236                         'correctfeedback', $question->id);
1237                 $expout .= "    <correctfeedback>\n";
1238                 $expout .= $this->writetext($question->options->correctfeedback, 3);
1239                 $expout .= $this->writefiles($files);
1240                 $expout .= "    </correctfeedback>\n";
1242                 $files = $fs->get_area_files($contextid, $component,
1243                         'partiallycorrectfeedback', $question->id);
1244                 $expout .= "    <partiallycorrectfeedback>\n";
1245                 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
1246                 $expout .= $this->writefiles($files);
1247                 $expout .= "    </partiallycorrectfeedback>\n";
1249                 $files = $fs->get_area_files($contextid, $component,
1250                         'incorrectfeedback', $question->id);
1251                 $expout .= "    <incorrectfeedback>\n";
1252                 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
1253                 $expout .= $this->writefiles($files);
1254                 $expout .= "    </incorrectfeedback>\n";
1256                 foreach ($question->options->answers as $answer) {
1257                     $percent = 100 * $answer->fraction;
1258                     $expout .= "<answer fraction=\"$percent\">\n";
1259                     // "<text/>" tags are an added feature, old files won't have them
1260                     $expout .= "    <text>{$answer->answer}</text>\n";
1261                     $expout .= "    <tolerance>{$answer->tolerance}</tolerance>\n";
1262                     $expout .= "    <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1263                     $expout .= "    <correctanswerformat>" .
1264                             $answer->correctanswerformat . "</correctanswerformat>\n";
1265                     $expout .= "    <correctanswerlength>" .
1266                             $answer->correctanswerlength . "</correctanswerlength>\n";
1267                     $expout .= "    <feedback {$this->format($answer->feedbackformat)}>\n";
1268                     $files = $fs->get_area_files($contextid, $component,
1269                             'instruction', $question->id);
1270                     $expout .= $this->writetext($answer->feedback);
1271                     $expout .= $this->writefiles($answer->feedbackfiles);
1272                     $expout .= "    </feedback>\n";
1273                     $expout .= "</answer>\n";
1274                 }
1275                 if (isset($question->options->unitgradingtype)) {
1276                     $expout .= "    <unitgradingtype>" .
1277                             $question->options->unitgradingtype . "</unitgradingtype>\n";
1278                 }
1279                 if (isset($question->options->unitpenalty)) {
1280                     $expout .= "    <unitpenalty>" .
1281                             $question->options->unitpenalty . "</unitpenalty>\n";
1282                 }
1283                 if (isset($question->options->showunits)) {
1284                     $expout .= "    <showunits>{$question->options->showunits}</showunits>\n";
1285                 }
1286                 if (isset($question->options->unitsleft)) {
1287                     $expout .= "    <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1288                 }
1290                 if (isset($question->options->instructionsformat)) {
1291                     $files = $fs->get_area_files($contextid, $component,
1292                             'instruction', $question->id);
1293                     $expout .= "    <instructions " .
1294                             $this->format($question->options->instructionsformat) . ">\n";
1295                     $expout .= $this->writetext($question->options->instructions, 3);
1296                     $expout .= $this->writefiles($files);
1297                     $expout .= "    </instructions>\n";
1298                 }
1300                 if (isset($question->options->units)) {
1301                     $units = $question->options->units;
1302                     if (count($units)) {
1303                         $expout .= "<units>\n";
1304                         foreach ($units as $unit) {
1305                             $expout .= "  <unit>\n";
1306                             $expout .= "    <multiplier>{$unit->multiplier}</multiplier>\n";
1307                             $expout .= "    <unit_name>{$unit->unit}</unit_name>\n";
1308                             $expout .= "  </unit>\n";
1309                         }
1310                         $expout .= "</units>\n";
1311                     }
1312                 }
1314                 // The tag $question->export_process has been set so we get all the
1315                 // data items in the database from the function
1316                 // qtype_calculated::get_question_options calculatedsimple defaults
1317                 // to calculated
1318                 if (isset($question->options->datasets) && count($question->options->datasets)) {
1319                     $expout .= "<dataset_definitions>\n";
1320                     foreach ($question->options->datasets as $def) {
1321                         $expout .= "<dataset_definition>\n";
1322                         $expout .= "    <status>".$this->writetext($def->status)."</status>\n";
1323                         $expout .= "    <name>".$this->writetext($def->name)."</name>\n";
1324                         if ($question->qtype == CALCULATED) {
1325                             $expout .= "    <type>calculated</type>\n";
1326                         } else {
1327                             $expout .= "    <type>calculatedsimple</type>\n";
1328                         }
1329                         $expout .= "    <distribution>" . $this->writetext($def->distribution) .
1330                                 "</distribution>\n";
1331                         $expout .= "    <minimum>" . $this->writetext($def->minimum) .
1332                                 "</minimum>\n";
1333                         $expout .= "    <maximum>" . $this->writetext($def->maximum) .
1334                                 "</maximum>\n";
1335                         $expout .= "    <decimals>" . $this->writetext($def->decimals) .
1336                                 "</decimals>\n";
1337                         $expout .= "    <itemcount>$def->itemcount</itemcount>\n";
1338                         if ($def->itemcount > 0) {
1339                             $expout .= "    <dataset_items>\n";
1340                             foreach ($def->items as $item) {
1341                                   $expout .= "        <dataset_item>\n";
1342                                   $expout .= "           <number>".$item->itemnumber."</number>\n";
1343                                   $expout .= "           <value>".$item->value."</value>\n";
1344                                   $expout .= "        </dataset_item>\n";
1345                             }
1346                             $expout .= "    </dataset_items>\n";
1347                             $expout .= "    <number_of_items>" . $def->number_of_items .
1348                                     "</number_of_items>\n";
1349                         }
1350                         $expout .= "</dataset_definition>\n";
1351                     }
1352                     $expout .= "</dataset_definitions>\n";
1353                 }
1354                 break;
1356             default:
1357                 // try support by optional plugin
1358                 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
1359                     notify(get_string('unsupportedexport', 'qformat_xml', $question->qtype));
1360                 }
1361                 $expout .= $data;
1362         }
1364         // Output any hints.
1365         $expout .= $this->write_hints($question);
1367         // Write the question tags.
1368         if (!empty($CFG->usetags)) {
1369             require_once($CFG->dirroot.'/tag/lib.php');
1370             $tags = tag_get_tags_array('question', $question->id);
1371             if (!empty($tags)) {
1372                 $expout .= "    <tags>\n";
1373                 foreach ($tags as $tag) {
1374                     $expout .= "      <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1375                 }
1376                 $expout .= "    </tags>\n";
1377             }
1378         }
1380         // close the question tag
1381         $expout .= "  </question>\n";
1383         return $expout;
1384     }
1386     public function write_answers($answers) {
1387         if (empty($answers)) {
1388             return;
1389         }
1390         $output = '';
1391         foreach ($answers as $answer) {
1392             $output .= $this->write_answer($answer);
1393         }
1394         return $output;
1395     }
1397     public function write_answer($answer, $extra = '') {
1398         $percent = $answer->fraction * 100;
1399         $output = '';
1400         $output .= "    <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
1401         $output .= $this->writetext($answer->answer, 3);
1402         $output .= $this->writefiles($answer->answerfiles);
1403         $output .= "      <feedback {$this->format($answer->feedbackformat)}>\n";
1404         $output .= $this->writetext($answer->feedback, 4);
1405         $output .= $this->writefiles($answer->feedbackfiles);
1406         $output .= "      </feedback>\n";
1407         $output .= $extra;
1408         $output .= "    </answer>\n";
1409         return $output;
1410     }
1412     /**
1413      * Write out the hints.
1414      * @param object $question the question definition data.
1415      * @return string XML to output.
1416      */
1417     public function write_hints($question) {
1418         if (empty($question->hints)) {
1419             return '';
1420         }
1422         $output = '';
1423         foreach ($question->hints as $hint) {
1424             $output .= $this->write_hint($hint, $question->contextid);
1425         }
1426         return $output;
1427     }
1429     /**
1430      * @param unknown_type $format a FORMAT_... constant.
1431      * @return string the attribute to add to an XML tag.
1432      */
1433     protected function format($format) {
1434         return 'format="' . $this->get_format($format) . '"';
1435     }
1437     public function write_hint($hint, $contextid) {
1438         $fs = get_file_storage();
1439         $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1441         $output = '';
1442         $output .= "    <hint {$this->format($hint->hintformat)}>\n";
1443         $output .= '      ' . $this->writetext($hint->hint);
1445         if (!empty($hint->shownumcorrect)) {
1446             $output .= "      <shownumcorrect/>\n";
1447         }
1448         if (!empty($hint->clearwrong)) {
1449             $output .= "      <clearwrong/>\n";
1450         }
1452         if (!empty($hint->options)) {
1453             $output .= '      <options>' . $this->xml_escape($hint->options) . "</options>\n";
1454         }
1455         $output .= $this->writefiles($files);
1456         $output .= "    </hint>\n";
1457         return $output;
1458     }
1460     /**
1461      * Output the combined feedback fields.
1462      * @param object $questionoptions the question definition data.
1463      * @param int $questionid the question id.
1464      * @param int $contextid the question context id.
1465      * @return string XML to output.
1466      */
1467     public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1468         $fs = get_file_storage();
1469         $output = '';
1471         $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1472         foreach ($fields as $field) {
1473             $formatfield = $field . 'format';
1474             $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1476             $output .= "    <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1477             $output .= '      ' . $this->writetext($questionoptions->$field);
1478             $output .= $this->writefiles($files);
1479             $output .= "    </{$field}>\n";
1480         }
1482         if (!empty($questionoptions->shownumcorrect)) {
1483             $output .= "    <shownumcorrect/>\n";
1484         }
1485         return $output;
1486     }