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