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