MDL-69798 XML question export: excape special chars in idnumbers
[moodle.git] / question / format / xml / format.php
CommitLineData
aeb15530 1<?php
688d8753
TH
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.
84769fd8 8//
688d8753
TH
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.
84769fd8 13//
688d8753
TH
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/>.
16
17/**
49e2bba7 18 * Code for exporting questions as Moodle XML.
688d8753 19 *
d2fd7262 20 * @package qformat_xml
688d8753 21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
7764183a 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
688d8753
TH
23 */
24
25
a17b297d
TH
26defined('MOODLE_INTERNAL') || die();
27
49e2bba7 28require_once($CFG->libdir . '/xmlize.php');
19f7bb5b
TH
29if (!class_exists('qformat_default')) {
30 // This is ugly, but this class is also (ab)used by mod/lesson, which defines
31 // a different base class in mod/lesson/format.php. Thefore, we can only
32 // include the proper base class conditionally like this. (We have to include
33 // the base class like this, otherwise it breaks third-party question types.)
34 // This may be reviewd, and a better fix found one day.
35 require_once($CFG->dirroot . '/question/format.php');
36}
49e2bba7
TH
37
38
41a89a07 39/**
688d8753
TH
40 * Importer for Moodle XML question format.
41 *
42 * See http://docs.moodle.org/en/Moodle_XML_format for a description of the format.
43 *
44 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
7764183a 45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41a89a07 46 */
f5565b69 47class qformat_xml extends qformat_default {
84769fd8 48
c7df5006 49 public function provide_import() {
84769fd8 50 return true;
51 }
52
c7df5006 53 public function provide_export() {
84769fd8 54 return true;
55 }
56
c7df5006 57 public function mime_type() {
46732124
TH
58 return 'application/xml';
59 }
60
d2fd7262 61 // IMPORT FUNCTIONS START HERE.
84769fd8 62
88bc20c3 63 /**
c81415c7 64 * Translate human readable format name
65 * into internal Moodle code number
1dab8faa 66 * Note the reverse function is called get_format.
6e557c08 67 * @param string name format name from xml file
f7970e3c 68 * @return int Moodle format code
c81415c7 69 */
4a84c94b 70 public function trans_format($name) {
88bc20c3 71 $name = trim($name);
72
2ed80177 73 if ($name == 'moodle_auto_format') {
49e2bba7 74 return FORMAT_MOODLE;
2ed80177 75 } else if ($name == 'html') {
49e2bba7 76 return FORMAT_HTML;
2ed80177 77 } else if ($name == 'plain_text') {
49e2bba7 78 return FORMAT_PLAIN;
2ed80177 79 } else if ($name == 'wiki_like') {
49e2bba7 80 return FORMAT_WIKI;
2ed80177 81 } else if ($name == 'markdown') {
49e2bba7 82 return FORMAT_MARKDOWN;
2ed80177 83 } else {
fe5f4121
TH
84 debugging("Unrecognised text format '{$name}' in the import file. Assuming 'html'.");
85 return FORMAT_HTML;
84769fd8 86 }
84769fd8 87 }
88
6e557c08 89 /**
c81415c7 90 * Translate human readable single answer option
91 * to internal code number
6e557c08 92 * @param string name true/false
f7970e3c 93 * @return int internal code number
c81415c7 94 */
c7df5006 95 public function trans_single($name) {
2da44816 96 $name = trim($name);
97 if ($name == "false" || !$name) {
98 return 0;
99 } else {
100 return 1;
101 }
84769fd8 102 }
103
6e557c08 104 /**
c81415c7 105 * process text string from xml file
6e557c08 106 * @param array $text bit of xml tree after ['text']
49e2bba7 107 * @return string processed text.
c81415c7 108 */
c7df5006 109 public function import_text($text) {
d2fd7262 110 // Quick sanity check.
17102269 111 if (empty($text)) {
112 return '';
113 }
84769fd8 114 $data = $text[0]['#'];
294ce987 115 return trim($data);
84769fd8 116 }
117
46013523 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
88bc20c3 122 * @param array path path to node expressed as array
123 * @param mixed default
f7970e3c 124 * @param bool istext process as text
46013523 125 * @param string error if set value must exist, return false and issue message if not
126 * @return mixed value
127 */
c7df5006 128 public function getpath($xml, $path, $default, $istext=false, $error='') {
46013523 129 foreach ($path as $index) {
228b6f6b 130 if (!isset($xml[$index])) {
46013523 131 if (!empty($error)) {
49e2bba7 132 $this->error($error);
46013523 133 return false;
134 } else {
135 return $default;
136 }
137 }
49e2bba7
TH
138
139 $xml = $xml[$index];
46013523 140 }
49e2bba7 141
46013523 142 if ($istext) {
fc22da99 143 if (!is_string($xml)) {
49e2bba7 144 $this->error(get_string('invalidxml', 'qformat_xml'));
fc22da99 145 }
cde2709a 146 $xml = trim($xml);
46013523 147 }
148
149 return $xml;
150 }
151
2387d1cd
JMV
152 public function import_text_with_files($data, $path, $defaultvalue = '', $defaultformat = 'html') {
153 $field = array();
154 $field['text'] = $this->getpath($data,
155 array_merge($path, array('#', 'text', 0, '#')), $defaultvalue, true);
156 $field['format'] = $this->trans_format($this->getpath($data,
157 array_merge($path, array('@', 'format')), $defaultformat));
158 $itemid = $this->import_files_as_draft($this->getpath($data,
159 array_merge($path, array('#', 'file')), array(), false));
160 if (!empty($itemid)) {
161 $field['itemid'] = $itemid;
162 }
163 return $field;
164 }
165
166 public function import_files_as_draft($xml) {
167 global $USER;
168 if (empty($xml)) {
169 return null;
170 }
171 $fs = get_file_storage();
172 $itemid = file_get_unused_draft_itemid();
624ff5ba 173 $filepaths = array();
2387d1cd 174 foreach ($xml as $file) {
624ff5ba
JMV
175 $filename = $this->getpath($file, array('@', 'name'), '', true);
176 $filepath = $this->getpath($file, array('@', 'path'), '/', true);
177 $fullpath = $filepath . $filename;
178 if (in_array($fullpath, $filepaths)) {
179 debugging('Duplicate file in XML: ' . $fullpath, DEBUG_DEVELOPER);
ded5cddb
JMV
180 continue;
181 }
2387d1cd
JMV
182 $filerecord = array(
183 'contextid' => context_user::instance($USER->id)->id,
184 'component' => 'user',
185 'filearea' => 'draft',
186 'itemid' => $itemid,
624ff5ba 187 'filepath' => $filepath,
ded5cddb 188 'filename' => $filename,
2387d1cd
JMV
189 );
190 $fs->create_file_from_string($filerecord, base64_decode($file['#']));
624ff5ba 191 $filepaths[] = $fullpath;
2387d1cd
JMV
192 }
193 return $itemid;
194 }
46013523 195
6e557c08 196 /**
c81415c7 197 * import parts of question common to all types
e7ef42f5 198 * @param $question array question question array from xml tree
6e557c08 199 * @return object question object
c81415c7 200 */
c7df5006 201 public function import_headers($question) {
dcd4464e 202 global $USER;
4f290077 203
d2fd7262 204 // This routine initialises the question object.
5bed54e1 205 $qo = $this->defaultquestion();
84769fd8 206
d2fd7262 207 // Question name.
cacb8fa0 208 $qo->name = $this->clean_question_name($this->getpath($question,
49e2bba7 209 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
cacb8fa0 210 get_string('xmlimportnoname', 'qformat_xml')));
2387d1cd
JMV
211 $questiontext = $this->import_text_with_files($question,
212 array('#', 'questiontext', 0));
213 $qo->questiontext = $questiontext['text'];
214 $qo->questiontextformat = $questiontext['format'];
215 if (!empty($questiontext['itemid'])) {
216 $qo->questiontextitemid = $questiontext['itemid'];
217 }
06f1bd03
TH
218 // Backwards compatibility, deal with the old image tag.
219 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
220 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
221 if ($filedata && $filename) {
2387d1cd
JMV
222 $fs = get_file_storage();
223 if (empty($qo->questiontextitemid)) {
224 $qo->questiontextitemid = file_get_unused_draft_itemid();
225 }
226 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
227 $filerecord = array(
228 'contextid' => context_user::instance($USER->id)->id,
229 'component' => 'user',
230 'filearea' => 'draft',
231 'itemid' => $qo->questiontextitemid,
232 'filepath' => '/',
233 'filename' => $filename,
234 );
235 $fs->create_file_from_string($filerecord, base64_decode($filedata));
236 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
06f1bd03
TH
237 }
238
6189fda4
JB
239 $qo->idnumber = $this->getpath($question, ['#', 'idnumber', 0, '#'], null);
240
d2fd7262 241 // Restore files in generalfeedback.
2387d1cd 242 $generalfeedback = $this->import_text_with_files($question,
48aad79a 243 array('#', 'generalfeedback', 0), '', $this->get_format($qo->questiontextformat));
2387d1cd
JMV
244 $qo->generalfeedback = $generalfeedback['text'];
245 $qo->generalfeedbackformat = $generalfeedback['format'];
246 if (!empty($generalfeedback['itemid'])) {
247 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
248 }
cde2709a 249
79e25fb4
TH
250 $qo->defaultmark = $this->getpath($question,
251 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
252 $qo->penalty = $this->getpath($question,
253 array('#', 'penalty', 0, '#'), $qo->penalty);
49e2bba7 254
d2fd7262 255 // Fix problematic rounding from old files.
49e2bba7
TH
256 if (abs($qo->penalty - 0.3333333) < 0.005) {
257 $qo->penalty = 0.3333333;
258 }
84769fd8 259
4f290077 260 // Read the question tags.
dcd4464e 261 $this->import_question_tags($qo, $question);
4f290077 262
84769fd8 263 return $qo;
264 }
265
6e557c08 266 /**
49e2bba7 267 * Import the common parts of a single answer
6e557c08 268 * @param array answer xml tree for single answer
fe5f4121
TH
269 * @param bool $withanswerfiles if true, the answers are HTML (or $defaultformat)
270 * and so may contain files, otherwise the answers are plain text.
271 * @param array Default text format for the feedback, and the answers if $withanswerfiles
272 * is true.
6e557c08 273 * @return object answer object
88bc20c3 274 */
fe5f4121 275 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
2a6c5c52 276 $ans = new stdClass();
277
2a6c5c52 278 if ($withanswerfiles) {
2387d1cd 279 $ans->answer = $this->import_text_with_files($answer, array(), '', $defaultformat);
fe5f4121 280 } else {
2387d1cd
JMV
281 $ans->answer = array();
282 $ans->answer['text'] = $this->getpath($answer, array('#', 'text', 0, '#'), '', true);
fe5f4121 283 $ans->answer['format'] = FORMAT_PLAIN;
2a6c5c52 284 }
cde2709a 285
2387d1cd 286 $ans->feedback = $this->import_text_with_files($answer, array('#', 'feedback', 0), '', $defaultformat);
cde2709a 287
2a6c5c52 288 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
84769fd8 289
84769fd8 290 return $ans;
291 }
292
49e2bba7
TH
293 /**
294 * Import the common overall feedback fields.
295 * @param object $question the part of the XML relating to this question.
296 * @param object $qo the question data to add the fields to.
f7970e3c 297 * @param bool $withshownumpartscorrect include the shownumcorrect field.
49e2bba7
TH
298 */
299 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
79e25fb4
TH
300 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
301 foreach ($fields as $field) {
2387d1cd
JMV
302 $qo->$field = $this->import_text_with_files($questionxml,
303 array('#', $field, 0), '', $this->get_format($qo->questiontextformat));
fe041243 304 }
49e2bba7
TH
305
306 if ($withshownumpartscorrect) {
307 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
308
d2fd7262 309 // Backwards compatibility.
49e2bba7
TH
310 if (array_key_exists('correctresponsesfeedback', $questionxml['#'])) {
311 $qo->shownumcorrect = $this->trans_single($this->getpath($questionxml,
312 array('#', 'correctresponsesfeedback', 0, '#'), 1));
313 }
314 }
315 }
316
317 /**
318 * Import a question hint
319 * @param array $hintxml hint xml fragment.
fe5f4121 320 * @param string $defaultformat the text format to assume for hints that do not specify.
49e2bba7
TH
321 * @return object hint for storing in the database.
322 */
fe5f4121 323 public function import_hint($hintxml, $defaultformat) {
2387d1cd 324 $hint = new stdClass();
49e2bba7 325 if (array_key_exists('hintcontent', $hintxml['#'])) {
d2fd7262 326 // Backwards compatibility.
49e2bba7 327
2387d1cd
JMV
328 $hint->hint = $this->import_text_with_files($hintxml,
329 array('#', 'hintcontent', 0), '', $defaultformat);
330
49e2bba7
TH
331 $hint->shownumcorrect = $this->getpath($hintxml,
332 array('#', 'statenumberofcorrectresponses', 0, '#'), 0);
333 $hint->clearwrong = $this->getpath($hintxml,
334 array('#', 'clearincorrectresponses', 0, '#'), 0);
335 $hint->options = $this->getpath($hintxml,
336 array('#', 'showfeedbacktoresponses', 0, '#'), 0);
337
338 return $hint;
339 }
2387d1cd 340 $hint->hint = $this->import_text_with_files($hintxml, array(), '', $defaultformat);
49e2bba7
TH
341 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
342 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
79e25fb4 343 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
49e2bba7
TH
344
345 return $hint;
346 }
347
348 /**
349 * Import all the question hints
350 *
351 * @param object $qo the question data that is being constructed.
fe5f4121
TH
352 * @param array $questionxml The xml representing the question.
353 * @param bool $withparts whether the extra fields relating to parts should be imported.
354 * @param bool $withoptions whether the extra options field should be imported.
355 * @param string $defaultformat the text format to assume for hints that do not specify.
356 * @return array of objects representing the hints in the file.
49e2bba7 357 */
fe5f4121
TH
358 public function import_hints($qo, $questionxml, $withparts = false,
359 $withoptions = false, $defaultformat = 'html') {
49e2bba7
TH
360 if (!isset($questionxml['#']['hint'])) {
361 return;
362 }
fe041243 363
49e2bba7 364 foreach ($questionxml['#']['hint'] as $hintxml) {
fe5f4121 365 $hint = $this->import_hint($hintxml, $defaultformat);
49e2bba7
TH
366 $qo->hint[] = $hint->hint;
367
368 if ($withparts) {
369 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
370 $qo->hintclearwrong[] = $hint->clearwrong;
371 }
372
373 if ($withoptions) {
374 $qo->hintoptions[] = $hint->options;
375 }
376 }
377 }
378
dcd4464e
EC
379 /**
380 * Import all the question tags
381 *
382 * @param object $qo the question data that is being constructed.
383 * @param array $questionxml The xml representing the question.
384 * @return array of objects representing the tags in the file.
385 */
386 public function import_question_tags($qo, $questionxml) {
387 global $CFG;
388
13596866
SL
389 if (core_tag_tag::is_enabled('core_question', 'question')) {
390
391 $qo->tags = [];
392 if (!empty($questionxml['#']['tags'][0]['#']['tag'])) {
393 foreach ($questionxml['#']['tags'][0]['#']['tag'] as $tagdata) {
394 $qo->tags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
395 }
396 }
397
398 $qo->coursetags = [];
399 if (!empty($questionxml['#']['coursetags'][0]['#']['tag'])) {
400 foreach ($questionxml['#']['coursetags'][0]['#']['tag'] as $tagdata) {
401 $qo->coursetags[] = $this->getpath($tagdata, array('#', 'text', 0, '#'), '', true);
402 }
dcd4464e
EC
403 }
404 }
405 }
406
c73c9836
TH
407 /**
408 * Import files from a node in the XML.
409 * @param array $xml an array of <file> nodes from the the parsed XML.
410 * @return array of things representing files - in the form that save_question expects.
411 */
412 public function import_files($xml) {
413 $files = array();
414 foreach ($xml as $file) {
415 $data = new stdClass();
416 $data->content = $file['#'];
417 $data->encoding = $file['@']['encoding'];
418 $data->name = $file['@']['name'];
419 $files[] = $data;
420 }
421 return $files;
422 }
423
6e557c08 424 /**
88bc20c3 425 * import multiple choice question
6e557c08 426 * @param array question question array from xml tree
427 * @return object question object
c81415c7 428 */
c7df5006 429 public function import_multichoice($question) {
d2fd7262 430 // Get common parts.
cde2709a 431 $qo = $this->import_headers($question);
84769fd8 432
d2fd7262 433 // Header parts particular to multichoice.
e231a3ff 434 $qo->qtype = 'multichoice';
49e2bba7
TH
435 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
436 $qo->single = $this->trans_single($single);
79e25fb4
TH
437 $shuffleanswers = $this->getpath($question,
438 array('#', 'shuffleanswers', 0, '#'), 'false');
439 $qo->answernumbering = $this->getpath($question,
440 array('#', 'answernumbering', 0, '#'), 'abc');
2da44816 441 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
00055ef6
MK
442 $qo->showstandardinstruction = $this->getpath($question,
443 array('#', 'showstandardinstruction', 0, '#'), '1');
cde2709a 444
79e25fb4
TH
445 // There was a time on the 1.8 branch when it could output an empty
446 // answernumbering tag, so fix up any found.
c0ffeb39 447 if (empty($qo->answernumbering)) {
448 $qo->answernumbering = 'abc';
449 }
450
d2fd7262 451 // Run through the answers.
88bc20c3 452 $answers = $question['#']['answer'];
49e2bba7 453 $acount = 0;
84769fd8 454 foreach ($answers as $answer) {
fe5f4121 455 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
49e2bba7
TH
456 $qo->answer[$acount] = $ans->answer;
457 $qo->fraction[$acount] = $ans->fraction;
458 $qo->feedback[$acount] = $ans->feedback;
459 ++$acount;
84769fd8 460 }
cde2709a 461
49e2bba7 462 $this->import_combined_feedback($qo, $question, true);
fe5f4121 463 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
49e2bba7 464
84769fd8 465 return $qo;
466 }
467
6e557c08 468 /**
49e2bba7 469 * Import cloze type question
6e557c08 470 * @param array question question array from xml tree
471 * @return object question object
c81415c7 472 */
77d0f5f0 473 public function import_multianswer($question) {
c80fb6d9 474 global $USER;
a3f92b2e 475 question_bank::get_qtype('multianswer');
1f1c60c6 476
c80fb6d9
JMV
477 $questiontext = $this->import_text_with_files($question,
478 array('#', 'questiontext', 0));
c8fdd867 479 $qo = qtype_multianswer_extract_question($questiontext);
48aad79a
JMV
480 $errors = qtype_multianswer_validate_question($qo);
481 if ($errors) {
482 $this->error(get_string('invalidmultianswerquestion', 'qtype_multianswer', implode(' ', $errors)));
483 return null;
484 }
a3f92b2e 485
d2fd7262 486 // Header parts particular to multianswer.
77d0f5f0 487 $qo->qtype = 'multianswer';
08000e53
AG
488
489 // Only set the course if the data is available.
490 if (isset($this->course)) {
491 $qo->course = $this->course;
492 }
48aad79a
JMV
493 if (isset($question['#']['name'])) {
494 $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
495 } else {
496 $qo->name = $this->create_default_question_name($qo->questiontext['text'],
497 get_string('questionname', 'question'));
498 }
fe5f4121
TH
499 $qo->questiontextformat = $questiontext['format'];
500 $qo->questiontext = $qo->questiontext['text'];
c80fb6d9
JMV
501 if (!empty($questiontext['itemid'])) {
502 $qo->questiontextitemid = $questiontext['itemid'];
2387d1cd 503 }
c80fb6d9 504
bedbfd6d
JMV
505 // Backwards compatibility, deal with the old image tag.
506 $filedata = $this->getpath($question, array('#', 'image_base64', '0', '#'), null, false);
507 $filename = $this->getpath($question, array('#', 'image', '0', '#'), null, false);
508 if ($filedata && $filename) {
c80fb6d9
JMV
509 $fs = get_file_storage();
510 if (empty($qo->questiontextitemid)) {
511 $qo->questiontextitemid = file_get_unused_draft_itemid();
512 }
513 $filename = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
514 $filerecord = array(
515 'contextid' => context_user::instance($USER->id)->id,
516 'component' => 'user',
517 'filearea' => 'draft',
518 'itemid' => $qo->questiontextitemid,
519 'filepath' => '/',
520 'filename' => $filename,
521 );
522 $fs->create_file_from_string($filerecord, base64_decode($filedata));
523 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
bedbfd6d
JMV
524 }
525
d2fd7262 526 // Restore files in generalfeedback.
2387d1cd 527 $generalfeedback = $this->import_text_with_files($question,
48aad79a 528 array('#', 'generalfeedback', 0), '', $this->get_format($qo->questiontextformat));
2387d1cd
JMV
529 $qo->generalfeedback = $generalfeedback['text'];
530 $qo->generalfeedbackformat = $generalfeedback['format'];
531 if (!empty($generalfeedback['itemid'])) {
532 $qo->generalfeedbackitemid = $generalfeedback['itemid'];
533 }
c73c9836 534
77d0f5f0
TH
535 $qo->penalty = $this->getpath($question,
536 array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
d2fd7262 537 // Fix problematic rounding from old files.
77d0f5f0
TH
538 if (abs($qo->penalty - 0.3333333) < 0.005) {
539 $qo->penalty = 0.3333333;
540 }
541
e9af6091 542 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
dcd4464e 543 $this->import_question_tags($qo, $question);
49e2bba7 544
7b8bc256 545 return $qo;
546 }
547
6e557c08 548 /**
49e2bba7 549 * Import true/false type question
6e557c08 550 * @param array question question array from xml tree
551 * @return object question object
c81415c7 552 */
c7df5006 553 public function import_truefalse($question) {
d2fd7262 554 // Get common parts.
fef8f84e 555 global $OUTPUT;
49e2bba7 556 $qo = $this->import_headers($question);
84769fd8 557
d2fd7262 558 // Header parts particular to true/false.
e231a3ff 559 $qo->qtype = 'truefalse';
84769fd8 560
3246ed33 561 // In the past, it used to be assumed that the two answers were in the file
562 // true first, then false. Howevever that was not always true. Now, we
563 // try to match on the answer text, but in old exports, this will be a localised
564 // string, so if we don't find true or false, we fall back to the old system.
565 $first = true;
566 $warning = false;
567 foreach ($question['#']['answer'] as $answer) {
79e25fb4
TH
568 $answertext = $this->getpath($answer,
569 array('#', 'text', 0, '#'), '', true);
2387d1cd
JMV
570 $feedback = $this->import_text_with_files($answer,
571 array('#', 'feedback', 0), '', $this->get_format($qo->questiontextformat));
a9081ce6 572
3246ed33 573 if ($answertext != 'true' && $answertext != 'false') {
49e2bba7 574 // Old style file, assume order is true/false.
3246ed33 575 $warning = true;
49e2bba7
TH
576 if ($first) {
577 $answertext = 'true';
578 } else {
579 $answertext = 'false';
580 }
88bc20c3 581 }
49e2bba7 582
3246ed33 583 if ($answertext == 'true') {
584 $qo->answer = ($answer['@']['fraction'] == 100);
7939a4a0 585 $qo->correctanswer = $qo->answer;
2387d1cd 586 $qo->feedbacktrue = $feedback;
3246ed33 587 } else {
588 $qo->answer = ($answer['@']['fraction'] != 100);
7939a4a0 589 $qo->correctanswer = $qo->answer;
2387d1cd 590 $qo->feedbackfalse = $feedback;
3246ed33 591 }
592 $first = false;
84769fd8 593 }
3246ed33 594
595 if ($warning) {
0ff4bd08 596 $a = new stdClass();
55c54868 597 $a->questiontext = $qo->questiontext;
5e8a85aa
TH
598 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
599 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
84769fd8 600 }
49e2bba7 601
fe5f4121 602 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 603
84769fd8 604 return $qo;
605 }
606
6e557c08 607 /**
49e2bba7 608 * Import short answer type question
6e557c08 609 * @param array question question array from xml tree
610 * @return object question object
c81415c7 611 */
c7df5006 612 public function import_shortanswer($question) {
d2fd7262 613 // Get common parts.
49e2bba7 614 $qo = $this->import_headers($question);
84769fd8 615
d2fd7262 616 // Header parts particular to shortanswer.
e231a3ff 617 $qo->qtype = 'shortanswer';
84769fd8 618
d2fd7262 619 // Get usecase.
49e2bba7 620 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
84769fd8 621
d2fd7262 622 // Run through the answers.
88bc20c3 623 $answers = $question['#']['answer'];
49e2bba7 624 $acount = 0;
84769fd8 625 foreach ($answers as $answer) {
fe5f4121 626 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
fe041243 627 $qo->answer[$acount] = $ans->answer['text'];
49e2bba7
TH
628 $qo->fraction[$acount] = $ans->fraction;
629 $qo->feedback[$acount] = $ans->feedback;
630 ++$acount;
84769fd8 631 }
632
fe5f4121 633 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 634
84769fd8 635 return $qo;
636 }
88bc20c3 637
6e557c08 638 /**
49e2bba7 639 * Import description type question
6e557c08 640 * @param array question question array from xml tree
641 * @return object question object
c81415c7 642 */
c7df5006 643 public function import_description($question) {
d2fd7262 644 // Get common parts.
49e2bba7 645 $qo = $this->import_headers($question);
d2fd7262 646 // Header parts particular to shortanswer.
e231a3ff 647 $qo->qtype = 'description';
49e2bba7 648 $qo->defaultmark = 0;
3f5633df 649 $qo->length = 0;
7b8bc256 650 return $qo;
651 }
84769fd8 652
6e557c08 653 /**
49e2bba7 654 * Import numerical type question
6e557c08 655 * @param array question question array from xml tree
656 * @return object question object
c81415c7 657 */
c7df5006 658 public function import_numerical($question) {
d2fd7262 659 // Get common parts.
2ed80177 660 $qo = $this->import_headers($question);
84769fd8 661
d2fd7262 662 // Header parts particular to numerical.
e231a3ff 663 $qo->qtype = 'numerical';
84769fd8 664
d2fd7262 665 // Get answers array.
84769fd8 666 $answers = $question['#']['answer'];
667 $qo->answer = array();
668 $qo->feedback = array();
669 $qo->fraction = array();
670 $qo->tolerance = array();
671 foreach ($answers as $answer) {
d2fd7262 672 // Answer outside of <text> is deprecated.
fe5f4121 673 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
69988ed4 674 $qo->answer[] = $obj->answer['text'];
81a7a02b 675 if (empty($qo->answer)) {
676 $qo->answer = '*';
55c54868 677 }
2ed80177
TH
678 $qo->feedback[] = $obj->feedback;
679 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
55c54868 680
d2fd7262 681 // Fraction as a tag is deprecated.
2ed80177 682 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
79e25fb4 683 $qo->fraction[] = $this->getpath($answer,
d2fd7262 684 array('#', 'fraction', 0, '#'), $fraction); // Deprecated.
84769fd8 685 }
686
d2fd7262 687 // Get the units array.
84769fd8 688 $qo->unit = array();
49e2bba7 689 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
81a7a02b 690 if (!empty($units)) {
a0d187bf 691 $qo->multiplier = array();
692 foreach ($units as $unit) {
79e25fb4
TH
693 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
694 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
a0d187bf 695 }
84769fd8 696 }
79e25fb4 697 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
de529cd2
TH
698 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
699 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null);
79e25fb4 700 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
69988ed4
TH
701 $qo->instructions['text'] = '';
702 $qo->instructions['format'] = FORMAT_HTML;
cde2709a
DC
703 $instructions = $this->getpath($question, array('#', 'instructions'), array());
704 if (!empty($instructions)) {
2387d1cd
JMV
705 $qo->instructions = $this->import_text_with_files($instructions,
706 array('0'), '', $this->get_format($qo->questiontextformat));
cde2709a 707 }
49e2bba7 708
de529cd2
TH
709 if (is_null($qo->showunits)) {
710 // Set a good default, depending on whether there are any units defined.
711 if (empty($qo->unit)) {
d2fd7262 712 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
de529cd2 713 } else {
d2fd7262 714 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
de529cd2
TH
715 }
716 }
717
fe5f4121 718 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 719
84769fd8 720 return $qo;
721 }
722
6e557c08 723 /**
49e2bba7 724 * Import matching type question
6e557c08 725 * @param array question question array from xml tree
726 * @return object question object
c81415c7 727 */
77d0f5f0 728 public function import_match($question) {
d2fd7262 729 // Get common parts.
2ed80177 730 $qo = $this->import_headers($question);
51bcdf28 731
d2fd7262 732 // Header parts particular to matching.
77d0f5f0 733 $qo->qtype = 'match';
49e2bba7
TH
734 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
735 array('#', 'shuffleanswers', 0, '#'), 1));
51bcdf28 736
d2fd7262 737 // Run through subquestions.
51bcdf28 738 $qo->subquestions = array();
739 $qo->subanswers = array();
fe041243 740 foreach ($question['#']['subquestion'] as $subqxml) {
2387d1cd
JMV
741 $qo->subquestions[] = $this->import_text_with_files($subqxml,
742 array(), '', $this->get_format($qo->questiontextformat));
743
fe041243 744 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
79e25fb4
TH
745 $qo->subanswers[] = $this->getpath($subqxml,
746 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
51bcdf28 747 }
49e2bba7
TH
748
749 $this->import_combined_feedback($qo, $question, true);
fe5f4121 750 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
49e2bba7 751
51bcdf28 752 return $qo;
753 }
754
6e557c08 755 /**
49e2bba7 756 * Import essay type question
6e557c08 757 * @param array question question array from xml tree
758 * @return object question object
c81415c7 759 */
c7df5006 760 public function import_essay($question) {
d2fd7262 761 // Get common parts.
49e2bba7 762 $qo = $this->import_headers($question);
c81415c7 763
d2fd7262 764 // Header parts particular to essay.
e231a3ff 765 $qo->qtype = 'essay';
c81415c7 766
8a5d05ee
TH
767 $qo->responseformat = $this->getpath($question,
768 array('#', 'responseformat', 0, '#'), 'editor');
769 $qo->responsefieldlines = $this->getpath($question,
770 array('#', 'responsefieldlines', 0, '#'), 15);
a4f765eb
KT
771 $qo->responserequired = $this->getpath($question,
772 array('#', 'responserequired', 0, '#'), 1);
8a5d05ee
TH
773 $qo->attachments = $this->getpath($question,
774 array('#', 'attachments', 0, '#'), 0);
a4f765eb
KT
775 $qo->attachmentsrequired = $this->getpath($question,
776 array('#', 'attachmentsrequired', 0, '#'), 0);
2387d1cd
JMV
777 $qo->graderinfo = $this->import_text_with_files($question,
778 array('#', 'graderinfo', 0), '', $this->get_format($qo->questiontextformat));
60527d0c
JMV
779 $qo->responsetemplate['text'] = $this->getpath($question,
780 array('#', 'responsetemplate', 0, '#', 'text', 0, '#'), '', true);
781 $qo->responsetemplate['format'] = $this->trans_format($this->getpath($question,
782 array('#', 'responsetemplate', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
c81415c7 783
784 return $qo;
785 }
84769fd8 786
79e25fb4
TH
787 /**
788 * Import a calculated question
789 * @param object $question the imported XML data.
790 */
791 public function import_calculated($question) {
725ba2a0 792
d2fd7262 793 // Get common parts.
49e2bba7 794 $qo = $this->import_headers($question);
725ba2a0 795
d2fd7262 796 // Header parts particular to calculated.
e231a3ff 797 $qo->qtype = 'calculated';
79e25fb4
TH
798 $qo->synchronize = $this->getpath($question, array('#', 'synchronize', 0, '#'), 0);
799 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
800 $qo->single = $this->trans_single($single);
801 $shuffleanswers = $this->getpath($question, array('#', 'shuffleanswers', 0, '#'), 'false');
802 $qo->answernumbering = $this->getpath($question,
803 array('#', 'answernumbering', 0, '#'), 'abc');
04d8268d 804 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
cde2709a 805
fe5f4121 806 $this->import_combined_feedback($qo, $question);
79e25fb4
TH
807
808 $qo->unitgradingtype = $this->getpath($question,
809 array('#', 'unitgradingtype', 0, '#'), 0);
de529cd2 810 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
79e25fb4
TH
811 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), 0);
812 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
813 $qo->instructions = $this->getpath($question,
814 array('#', 'instructions', 0, '#', 'text', 0, '#'), '', true);
ac582c3b 815 if (!empty($instructions)) {
2387d1cd
JMV
816 $qo->instructions = $this->import_text_with_files($instructions,
817 array('0'), '', $this->get_format($qo->questiontextformat));
ac582c3b 818 }
cde2709a 819
d2fd7262 820 // Get answers array.
725ba2a0 821 $answers = $question['#']['answer'];
66de66fe 822 $qo->answer = array();
725ba2a0 823 $qo->feedback = array();
824 $qo->fraction = array();
825 $qo->tolerance = array();
826 $qo->tolerancetype = array();
827 $qo->correctanswerformat = array();
828 $qo->correctanswerlength = array();
829 $qo->feedback = array();
830 foreach ($answers as $answer) {
fe5f4121 831 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
d2fd7262 832 // Answer outside of <text> is deprecated.
cde2709a
DC
833 if (empty($ans->answer['text'])) {
834 $ans->answer['text'] = '*';
725ba2a0 835 }
66de66fe 836 $qo->answer[] = $ans->answer['text'];
cde2709a 837 $qo->feedback[] = $ans->feedback;
725ba2a0 838 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
d2fd7262 839 // Fraction as a tag is deprecated.
725ba2a0 840 if (!empty($answer['#']['fraction'][0]['#'])) {
841 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
cde2709a 842 } else {
725ba2a0 843 $qo->fraction[] = $answer['@']['fraction'] / 100;
844 }
845 $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#'];
846 $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#'];
847 $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#'];
848 }
d2fd7262 849 // Get units array.
725ba2a0 850 $qo->unit = array();
851 if (isset($question['#']['units'][0]['#']['unit'])) {
852 $units = $question['#']['units'][0]['#']['unit'];
853 $qo->multiplier = array();
854 foreach ($units as $unit) {
855 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
856 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
857 }
858 }
cde2709a
DC
859 $instructions = $this->getpath($question, array('#', 'instructions'), array());
860 if (!empty($instructions)) {
2387d1cd
JMV
861 $qo->instructions = $this->import_text_with_files($instructions,
862 array('0'), '', $this->get_format($qo->questiontextformat));
cde2709a 863 }
de529cd2
TH
864
865 if (is_null($qo->unitpenalty)) {
866 // Set a good default, depending on whether there are any units defined.
867 if (empty($qo->unit)) {
d2fd7262 868 $qo->showunits = 3; // This is qtype_numerical::UNITNONE, but we cannot refer to that constant here.
de529cd2 869 } else {
d2fd7262 870 $qo->showunits = 0; // This is qtype_numerical::UNITOPTIONAL, but we cannot refer to that constant here.
de529cd2
TH
871 }
872 }
873
cde2709a
DC
874 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
875 $qo->dataset = array();
79e25fb4 876 $qo->datasetindex= 0;
725ba2a0 877 foreach ($datasets as $dataset) {
878 $qo->datasetindex++;
879 $qo->dataset[$qo->datasetindex] = new stdClass();
79e25fb4
TH
880 $qo->dataset[$qo->datasetindex]->status =
881 $this->import_text($dataset['#']['status'][0]['#']['text']);
882 $qo->dataset[$qo->datasetindex]->name =
883 $this->import_text($dataset['#']['name'][0]['#']['text']);
884 $qo->dataset[$qo->datasetindex]->type =
885 $dataset['#']['type'][0]['#'];
886 $qo->dataset[$qo->datasetindex]->distribution =
887 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
888 $qo->dataset[$qo->datasetindex]->max =
889 $this->import_text($dataset['#']['maximum'][0]['#']['text']);
890 $qo->dataset[$qo->datasetindex]->min =
891 $this->import_text($dataset['#']['minimum'][0]['#']['text']);
892 $qo->dataset[$qo->datasetindex]->length =
893 $this->import_text($dataset['#']['decimals'][0]['#']['text']);
894 $qo->dataset[$qo->datasetindex]->distribution =
895 $this->import_text($dataset['#']['distribution'][0]['#']['text']);
725ba2a0 896 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
897 $qo->dataset[$qo->datasetindex]->datasetitem = array();
898 $qo->dataset[$qo->datasetindex]->itemindex = 0;
013fa7a4
TH
899 $qo->dataset[$qo->datasetindex]->number_of_items = $this->getpath($dataset,
900 array('#', 'number_of_items', 0, '#'), 0);
901 $datasetitems = $this->getpath($dataset,
902 array('#', 'dataset_items', 0, '#', 'dataset_item'), array());
725ba2a0 903 foreach ($datasetitems as $datasetitem) {
904 $qo->dataset[$qo->datasetindex]->itemindex++;
79e25fb4
TH
905 $qo->dataset[$qo->datasetindex]->datasetitem[
906 $qo->dataset[$qo->datasetindex]->itemindex] = new stdClass();
907 $qo->dataset[$qo->datasetindex]->datasetitem[
908 $qo->dataset[$qo->datasetindex]->itemindex]->itemnumber =
909 $datasetitem['#']['number'][0]['#'];
910 $qo->dataset[$qo->datasetindex]->datasetitem[
911 $qo->dataset[$qo->datasetindex]->itemindex]->value =
912 $datasetitem['#']['value'][0]['#'];
cde2709a 913 }
725ba2a0 914 }
88bc20c3 915
fe5f4121 916 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 917
725ba2a0 918 return $qo;
919 }
920
ee259d0c 921 /**
49e2bba7
TH
922 * This is not a real question type. It's a dummy type used to specify the
923 * import category. The format is:
ee259d0c 924 * <question type="category">
925 * <category>tom/dick/harry</category>
1dab8faa 926 * <info format="moodle_auto_format"><text>Category description</text></info>
ee259d0c 927 * </question>
928 */
c7df5006 929 protected function import_category($question) {
0ff4bd08 930 $qo = new stdClass();
ee259d0c 931 $qo->qtype = 'category';
86b68520 932 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
1dab8faa
JB
933 $qo->info = '';
934 $qo->infoformat = FORMAT_MOODLE;
935 if (array_key_exists('info', $question['#'])) {
936 $qo->info = $this->import_text($question['#']['info'][0]['#']['text']);
937 // The import should have the format in human readable form, so translate to machine readable format.
938 $qo->infoformat = $this->trans_format($question['#']['info'][0]['@']['format']);
939 }
e247068d 940 $qo->idnumber = $this->getpath($question, array('#', 'idnumber', 0, '#'), null);
ee259d0c 941 return $qo;
942 }
943
c81415c7 944 /**
49e2bba7 945 * Parse the array of lines into an array of questions
c81415c7 946 * this *could* burn memory - but it won't happen that much
947 * so fingers crossed!
49e2bba7 948 * @param array of lines from the input file.
e2bb3c92 949 * @param stdClass $context
49e2bba7 950 * @return array (of objects) question objects.
c81415c7 951 */
48aad79a 952 public function readquestions($lines) {
d2fd7262 953 // We just need it as one big string.
7e252acf 954 $lines = implode('', $lines);
84769fd8 955
49e2bba7 956 // This converts xml to big nasty data structure
d2fd7262 957 // the 0 means keep white space as it is (important for markdown format).
49e2bba7 958 try {
7e252acf 959 $xml = xmlize($lines, 0, 'UTF-8', true);
79e25fb4 960 } catch (xml_format_exception $e) {
49e2bba7
TH
961 $this->error($e->getMessage(), '');
962 return false;
963 }
7e252acf 964 unset($lines); // No need to keep this in memory.
397acdfd
JP
965 return $this->import_questions($xml['quiz']['#']['question']);
966 }
7e252acf 967
397acdfd
JP
968 /**
969 * @param array $xml the xmlized xml
970 * @return stdClass[] question objects to pass to question type save_question_options
971 */
972 public function import_questions($xml) {
84769fd8 973 $questions = array();
974
d2fd7262 975 // Iterate through questions.
397acdfd
JP
976 foreach ($xml as $questionxml) {
977 $qo = $this->import_question($questionxml);
84769fd8 978
d2fd7262 979 // Stick the result in the $questions array.
84769fd8 980 if ($qo) {
981 $questions[] = $qo;
982 }
983 }
84769fd8 984 return $questions;
985 }
986
397acdfd
JP
987 /**
988 * @param array $questionxml xml describing the question
989 * @return null|stdClass an object with data to be fed to question type save_question_options
990 */
991 protected function import_question($questionxml) {
992 $questiontype = $questionxml['@']['type'];
993
994 if ($questiontype == 'multichoice') {
995 return $this->import_multichoice($questionxml);
996 } else if ($questiontype == 'truefalse') {
997 return $this->import_truefalse($questionxml);
998 } else if ($questiontype == 'shortanswer') {
999 return $this->import_shortanswer($questionxml);
1000 } else if ($questiontype == 'numerical') {
1001 return $this->import_numerical($questionxml);
1002 } else if ($questiontype == 'description') {
1003 return $this->import_description($questionxml);
1004 } else if ($questiontype == 'matching' || $questiontype == 'match') {
1005 return $this->import_match($questionxml);
1006 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
1007 return $this->import_multianswer($questionxml);
1008 } else if ($questiontype == 'essay') {
1009 return $this->import_essay($questionxml);
1010 } else if ($questiontype == 'calculated') {
1011 return $this->import_calculated($questionxml);
1012 } else if ($questiontype == 'calculatedsimple') {
1013 $qo = $this->import_calculated($questionxml);
1014 $qo->qtype = 'calculatedsimple';
1015 return $qo;
1016 } else if ($questiontype == 'calculatedmulti') {
1017 $qo = $this->import_calculated($questionxml);
1018 $qo->qtype = 'calculatedmulti';
1019 return $qo;
1020 } else if ($questiontype == 'category') {
1021 return $this->import_category($questionxml);
1022
1023 } else {
1024 // Not a type we handle ourselves. See if the question type wants
1025 // to handle it.
1026 if (!$qo = $this->try_importing_using_qtypes($questionxml, null, null, $questiontype)) {
1027 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
1028 return null;
1029 }
1030 return $qo;
1031 }
1032 }
1033
d2fd7262 1034 // EXPORT FUNCTIONS START HERE.
84769fd8 1035
c7df5006 1036 public function export_file_extension() {
46732124 1037 return '.xml';
84769fd8 1038 }
1039
c81415c7 1040 /**
77d0f5f0
TH
1041 * Turn the internal question type name into a human readable form.
1042 * (In the past, the code used to use integers internally. Now, it uses
1043 * strings, so there is less need for this, but to maintain
1044 * backwards-compatibility we change two of the type names.)
1045 * @param string $qtype question type plugin name.
1046 * @return string $qtype string to use in the file.
c81415c7 1047 */
77d0f5f0
TH
1048 protected function get_qtype($qtype) {
1049 switch($qtype) {
1050 case 'match':
49e2bba7 1051 return 'matching';
77d0f5f0 1052 case 'multianswer':
49e2bba7 1053 return 'cloze';
49e2bba7 1054 default:
77d0f5f0 1055 return $qtype;
84769fd8 1056 }
84769fd8 1057 }
1058
6e557c08 1059 /**
c81415c7 1060 * Convert internal Moodle text format code into
1061 * human readable form
6e557c08 1062 * @param int id internal code
1063 * @return string format text
c81415c7 1064 */
4a84c94b 1065 public function get_format($id) {
49e2bba7
TH
1066 switch($id) {
1067 case FORMAT_MOODLE:
1068 return 'moodle_auto_format';
1069 case FORMAT_HTML:
1070 return 'html';
1071 case FORMAT_PLAIN:
1072 return 'plain_text';
1073 case FORMAT_WIKI:
1074 return 'wiki_like';
1075 case FORMAT_MARKDOWN:
1076 return 'markdown';
1077 default:
1078 return 'unknown';
84769fd8 1079 }
84769fd8 1080 }
1081
6e557c08 1082 /**
88bc20c3 1083 * Convert internal single question code into
c81415c7 1084 * human readable form
6e557c08 1085 * @param int id single question code
1086 * @return string single question string
c81415c7 1087 */
c7df5006 1088 public function get_single($id) {
49e2bba7
TH
1089 switch($id) {
1090 case 0:
1091 return 'false';
1092 case 1:
1093 return 'true';
1094 default:
1095 return 'unknown';
84769fd8 1096 }
84769fd8 1097 }
1098
1b343d52
TH
1099 /**
1100 * Take a string, and wrap it in a CDATA secion, if that is required to make
1101 * the output XML valid.
1102 * @param string $string a string
1103 * @return string the string, wrapped in CDATA if necessary.
1104 */
1105 public function xml_escape($string) {
1106 if (!empty($string) && htmlspecialchars($string) != $string) {
46e4c35a
TH
1107 // If the string contains something that looks like the end
1108 // of a CDATA section, then we need to avoid errors by splitting
1109 // the string between two CDATA sections.
1110 $string = str_replace(']]>', ']]]]><![CDATA[>', $string);
1b343d52
TH
1111 return "<![CDATA[{$string}]]>";
1112 } else {
1113 return $string;
1114 }
1115 }
1116
6e557c08 1117 /**
49e2bba7
TH
1118 * Generates <text></text> tags, processing raw text therein
1119 * @param string $raw the content to output.
1120 * @param int $indent the current indent level.
f7970e3c 1121 * @param bool $short stick it on one line.
49e2bba7 1122 * @return string formatted text.
c81415c7 1123 */
c7df5006 1124 public function writetext($raw, $indent = 0, $short = true) {
49e2bba7 1125 $indent = str_repeat(' ', $indent);
1b343d52 1126 $raw = $this->xml_escape($raw);
84769fd8 1127
1128 if ($short) {
f4fe3968 1129 $xml = "{$indent}<text>{$raw}</text>\n";
4f290077 1130 } else {
f4fe3968 1131 $xml = "{$indent}<text>\n{$raw}\n{$indent}</text>\n";
84769fd8 1132 }
1133
1134 return $xml;
1135 }
88bc20c3 1136
7e7fdf59
TH
1137 /**
1138 * Generte the XML to represent some files.
1139 * @param array of store array of stored_file objects.
1140 * @return string $string the XML.
1141 */
1142 public function write_files($files) {
1143 if (empty($files)) {
1144 return '';
1145 }
1146 $string = '';
1147 foreach ($files as $file) {
1148 if ($file->is_directory()) {
1149 continue;
1150 }
624ff5ba 1151 $string .= '<file name="' . $file->get_filename() . '" path="' . $file->get_filepath() . '" encoding="base64">';
7e7fdf59 1152 $string .= base64_encode($file->get_content());
624ff5ba 1153 $string .= "</file>\n";
7e7fdf59
TH
1154 }
1155 return $string;
1156 }
1157
c7df5006 1158 protected function presave_process($content) {
d2fd7262 1159 // Override to allow us to add xml headers and footers.
49e2bba7
TH
1160 return '<?xml version="1.0" encoding="UTF-8"?>
1161<quiz>
1162' . $content . '</quiz>';
84769fd8 1163 }
1164
6e557c08 1165 /**
c81415c7 1166 * Turns question into an xml segment
49e2bba7 1167 * @param object $question the question data.
6e557c08 1168 * @return string xml segment
c81415c7 1169 */
c7df5006 1170 public function writequestion($question) {
d649fb02 1171 global $CFG, $OUTPUT;
cde2709a 1172
b31d6664 1173 $invalidquestion = false;
cde2709a
DC
1174 $fs = get_file_storage();
1175 $contextid = $question->contextid;
fe041243
TH
1176 // Get files used by the questiontext.
1177 $question->questiontextfiles = $fs->get_area_files(
1178 $contextid, 'question', 'questiontext', $question->id);
1179 // Get files used by the generalfeedback.
1180 $question->generalfeedbackfiles = $fs->get_area_files(
1181 $contextid, 'question', 'generalfeedback', $question->id);
49e2bba7
TH
1182 if (!empty($question->options->answers)) {
1183 foreach ($question->options->answers as $answer) {
2a6c5c52 1184 $answer->answerfiles = $fs->get_area_files(
1185 $contextid, 'question', 'answer', $answer->id);
fe041243
TH
1186 $answer->feedbackfiles = $fs->get_area_files(
1187 $contextid, 'question', 'answerfeedback', $answer->id);
49e2bba7
TH
1188 }
1189 }
1190
1191 $expout = '';
84769fd8 1192
49e2bba7 1193 // Add a comment linking this to the original question id.
f4fe3968 1194 $expout .= "<!-- question: {$question->id} -->\n";
84769fd8 1195
d2fd7262 1196 // Check question type.
77d0f5f0 1197 $questiontype = $this->get_qtype($question->qtype);
46013523 1198
651dbb72
TH
1199 $idnumber = htmlspecialchars($question->idnumber);
1200
77d0f5f0 1201 // Categories are a special case.
f1abd39f 1202 if ($question->qtype == 'category') {
49e2bba7 1203 $categorypath = $this->writetext($question->category);
1dab8faa
JB
1204 $categoryinfo = $this->writetext($question->info);
1205 $infoformat = $this->format($question->infoformat);
f1abd39f 1206 $expout .= " <question type=\"category\">\n";
1207 $expout .= " <category>\n";
f19c6f60 1208 $expout .= " {$categorypath}";
f1abd39f 1209 $expout .= " </category>\n";
1dab8faa 1210 $expout .= " <info {$infoformat}>\n";
f19c6f60 1211 $expout .= " {$categoryinfo}";
1dab8faa 1212 $expout .= " </info>\n";
651dbb72 1213 $expout .= " <idnumber>{$idnumber}</idnumber>\n";
f1abd39f 1214 $expout .= " </question>\n";
1215 return $expout;
77d0f5f0 1216 }
49e2bba7 1217
77d0f5f0
TH
1218 // Now we know we are are handing a real question.
1219 // Output the generic information.
f4fe3968 1220 $expout .= " <question type=\"{$questiontype}\">\n";
77d0f5f0
TH
1221 $expout .= " <name>\n";
1222 $expout .= $this->writetext($question->name, 3);
1223 $expout .= " </name>\n";
1224 $expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
1225 $expout .= $this->writetext($question->questiontext, 3);
7e7fdf59 1226 $expout .= $this->write_files($question->questiontextfiles);
77d0f5f0
TH
1227 $expout .= " </questiontext>\n";
1228 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1229 $expout .= $this->writetext($question->generalfeedback, 3);
7e7fdf59 1230 $expout .= $this->write_files($question->generalfeedbackfiles);
77d0f5f0
TH
1231 $expout .= " </generalfeedback>\n";
1232 if ($question->qtype != 'multianswer') {
49e2bba7 1233 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
7b8bc256 1234 }
77d0f5f0
TH
1235 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1236 $expout .= " <hidden>{$question->hidden}</hidden>\n";
651dbb72 1237 $expout .= " <idnumber>{$idnumber}</idnumber>\n";
7b8bc256 1238
77d0f5f0 1239 // The rest of the output depends on question type.
84769fd8 1240 switch($question->qtype) {
79e25fb4 1241 case 'category':
d2fd7262 1242 // Not a qtype really - dummy used for category switching.
79e25fb4
TH
1243 break;
1244
1245 case 'truefalse':
1246 $trueanswer = $question->options->answers[$question->options->trueanswer];
1247 $trueanswer->answer = 'true';
1248 $expout .= $this->write_answer($trueanswer);
1249
1250 $falseanswer = $question->options->answers[$question->options->falseanswer];
1251 $falseanswer->answer = 'false';
1252 $expout .= $this->write_answer($falseanswer);
1253 break;
1254
1255 case 'multichoice':
1256 $expout .= " <single>" . $this->get_single($question->options->single) .
1257 "</single>\n";
1258 $expout .= " <shuffleanswers>" .
1259 $this->get_single($question->options->shuffleanswers) .
1260 "</shuffleanswers>\n";
1261 $expout .= " <answernumbering>" . $question->options->answernumbering .
00055ef6
MK
1262 "</answernumbering>\n";
1263 $expout .= " <showstandardinstruction>" . $question->options->showstandardinstruction .
1264 "</showstandardinstruction>\n";
78fc17eb 1265 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
79e25fb4
TH
1266 $expout .= $this->write_answers($question->options->answers);
1267 break;
1268
1269 case 'shortanswer':
1270 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1271 $expout .= $this->write_answers($question->options->answers);
1272 break;
1273
1274 case 'numerical':
1275 foreach ($question->options->answers as $answer) {
1276 $expout .= $this->write_answer($answer,
f4fe3968 1277 " <tolerance>{$answer->tolerance}</tolerance>\n");
84769fd8 1278 }
cde2709a 1279
cde2709a
DC
1280 $units = $question->options->units;
1281 if (count($units)) {
1282 $expout .= "<units>\n";
1283 foreach ($units as $unit) {
1284 $expout .= " <unit>\n";
1285 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1286 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1287 $expout .= " </unit>\n";
1288 }
1289 $expout .= "</units>\n";
725ba2a0 1290 }
79e25fb4
TH
1291 if (isset($question->options->unitgradingtype)) {
1292 $expout .= " <unitgradingtype>" . $question->options->unitgradingtype .
1293 "</unitgradingtype>\n";
1294 }
1295 if (isset($question->options->unitpenalty)) {
1296 $expout .= " <unitpenalty>{$question->options->unitpenalty}</unitpenalty>\n";
1297 }
1298 if (isset($question->options->showunits)) {
1299 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1300 }
1301 if (isset($question->options->unitsleft)) {
1302 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1303 }
1304 if (!empty($question->options->instructionsformat)) {
1305 $files = $fs->get_area_files($contextid, 'qtype_numerical',
1306 'instruction', $question->id);
1307 $expout .= " <instructions " .
1308 $this->format($question->options->instructionsformat) . ">\n";
1309 $expout .= $this->writetext($question->options->instructions, 3);
7e7fdf59 1310 $expout .= $this->write_files($files);
79e25fb4
TH
1311 $expout .= " </instructions>\n";
1312 }
1313 break;
1314
1315 case 'match':
1316 $expout .= " <shuffleanswers>" .
1317 $this->get_single($question->options->shuffleanswers) .
1318 "</shuffleanswers>\n";
78fc17eb 1319 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
79e25fb4
TH
1320 foreach ($question->options->subquestions as $subquestion) {
1321 $files = $fs->get_area_files($contextid, 'qtype_match',
1322 'subquestion', $subquestion->id);
1323 $expout .= " <subquestion " .
1324 $this->format($subquestion->questiontextformat) . ">\n";
1325 $expout .= $this->writetext($subquestion->questiontext, 3);
7e7fdf59 1326 $expout .= $this->write_files($files);
79e25fb4
TH
1327 $expout .= " <answer>\n";
1328 $expout .= $this->writetext($subquestion->answertext, 4);
1329 $expout .= " </answer>\n";
1330 $expout .= " </subquestion>\n";
1331 }
1332 break;
1333
1334 case 'description':
1335 // Nothing else to do.
1336 break;
1337
1338 case 'multianswer':
77d0f5f0 1339 foreach ($question->options->questions as $index => $subq) {
5c0b5ba5 1340 $expout = str_replace('{#' . $index . '}', $subq->questiontext, $expout);
79e25fb4
TH
1341 }
1342 break;
1343
1344 case 'essay':
8a5d05ee
TH
1345 $expout .= " <responseformat>" . $question->options->responseformat .
1346 "</responseformat>\n";
a4f765eb
KT
1347 $expout .= " <responserequired>" . $question->options->responserequired .
1348 "</responserequired>\n";
8a5d05ee
TH
1349 $expout .= " <responsefieldlines>" . $question->options->responsefieldlines .
1350 "</responsefieldlines>\n";
1351 $expout .= " <attachments>" . $question->options->attachments .
1352 "</attachments>\n";
a4f765eb
KT
1353 $expout .= " <attachmentsrequired>" . $question->options->attachmentsrequired .
1354 "</attachmentsrequired>\n";
8a5d05ee
TH
1355 $expout .= " <graderinfo " .
1356 $this->format($question->options->graderinfoformat) . ">\n";
1357 $expout .= $this->writetext($question->options->graderinfo, 3);
7e7fdf59 1358 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
474abf12 1359 'graderinfo', $question->id));
8a5d05ee 1360 $expout .= " </graderinfo>\n";
60527d0c
JMV
1361 $expout .= " <responsetemplate " .
1362 $this->format($question->options->responsetemplateformat) . ">\n";
1363 $expout .= $this->writetext($question->options->responsetemplate, 3);
1364 $expout .= " </responsetemplate>\n";
79e25fb4
TH
1365 break;
1366
1367 case 'calculated':
1368 case 'calculatedsimple':
1369 case 'calculatedmulti':
1370 $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n";
1371 $expout .= " <single>{$question->options->single}</single>\n";
1372 $expout .= " <answernumbering>" . $question->options->answernumbering .
1373 "</answernumbering>\n";
1f1c60c6 1374 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
79e25fb4
TH
1375 "</shuffleanswers>\n";
1376
1377 $component = 'qtype_' . $question->qtype;
1378 $files = $fs->get_area_files($contextid, $component,
1379 'correctfeedback', $question->id);
1380 $expout .= " <correctfeedback>\n";
1381 $expout .= $this->writetext($question->options->correctfeedback, 3);
7e7fdf59 1382 $expout .= $this->write_files($files);
79e25fb4
TH
1383 $expout .= " </correctfeedback>\n";
1384
1385 $files = $fs->get_area_files($contextid, $component,
1386 'partiallycorrectfeedback', $question->id);
1387 $expout .= " <partiallycorrectfeedback>\n";
1388 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
7e7fdf59 1389 $expout .= $this->write_files($files);
79e25fb4
TH
1390 $expout .= " </partiallycorrectfeedback>\n";
1391
1392 $files = $fs->get_area_files($contextid, $component,
1393 'incorrectfeedback', $question->id);
1394 $expout .= " <incorrectfeedback>\n";
1395 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
7e7fdf59 1396 $expout .= $this->write_files($files);
79e25fb4
TH
1397 $expout .= " </incorrectfeedback>\n";
1398
1399 foreach ($question->options->answers as $answer) {
1400 $percent = 100 * $answer->fraction;
f4fe3968 1401 $expout .= "<answer fraction=\"{$percent}\">\n";
d2fd7262 1402 // The "<text/>" tags are an added feature, old files won't have them.
79e25fb4
TH
1403 $expout .= " <text>{$answer->answer}</text>\n";
1404 $expout .= " <tolerance>{$answer->tolerance}</tolerance>\n";
1405 $expout .= " <tolerancetype>{$answer->tolerancetype}</tolerancetype>\n";
1406 $expout .= " <correctanswerformat>" .
1407 $answer->correctanswerformat . "</correctanswerformat>\n";
1408 $expout .= " <correctanswerlength>" .
1409 $answer->correctanswerlength . "</correctanswerlength>\n";
1410 $expout .= " <feedback {$this->format($answer->feedbackformat)}>\n";
1411 $files = $fs->get_area_files($contextid, $component,
1412 'instruction', $question->id);
1413 $expout .= $this->writetext($answer->feedback);
7e7fdf59 1414 $expout .= $this->write_files($answer->feedbackfiles);
79e25fb4
TH
1415 $expout .= " </feedback>\n";
1416 $expout .= "</answer>\n";
1417 }
1418 if (isset($question->options->unitgradingtype)) {
1419 $expout .= " <unitgradingtype>" .
1420 $question->options->unitgradingtype . "</unitgradingtype>\n";
1421 }
1422 if (isset($question->options->unitpenalty)) {
1423 $expout .= " <unitpenalty>" .
1424 $question->options->unitpenalty . "</unitpenalty>\n";
1425 }
1426 if (isset($question->options->showunits)) {
1427 $expout .= " <showunits>{$question->options->showunits}</showunits>\n";
1428 }
1429 if (isset($question->options->unitsleft)) {
1430 $expout .= " <unitsleft>{$question->options->unitsleft}</unitsleft>\n";
1431 }
d649fb02 1432
79e25fb4
TH
1433 if (isset($question->options->instructionsformat)) {
1434 $files = $fs->get_area_files($contextid, $component,
1435 'instruction', $question->id);
1436 $expout .= " <instructions " .
1437 $this->format($question->options->instructionsformat) . ">\n";
1438 $expout .= $this->writetext($question->options->instructions, 3);
7e7fdf59 1439 $expout .= $this->write_files($files);
79e25fb4
TH
1440 $expout .= " </instructions>\n";
1441 }
1442
1443 if (isset($question->options->units)) {
1444 $units = $question->options->units;
1445 if (count($units)) {
1446 $expout .= "<units>\n";
1447 foreach ($units as $unit) {
1448 $expout .= " <unit>\n";
1449 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
1450 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
1451 $expout .= " </unit>\n";
1452 }
1453 $expout .= "</units>\n";
9c1c6c7f 1454 }
79e25fb4
TH
1455 }
1456
1457 // The tag $question->export_process has been set so we get all the
1458 // data items in the database from the function
1459 // qtype_calculated::get_question_options calculatedsimple defaults
d2fd7262 1460 // to calculated.
79e25fb4
TH
1461 if (isset($question->options->datasets) && count($question->options->datasets)) {
1462 $expout .= "<dataset_definitions>\n";
1463 foreach ($question->options->datasets as $def) {
1464 $expout .= "<dataset_definition>\n";
1465 $expout .= " <status>".$this->writetext($def->status)."</status>\n";
1466 $expout .= " <name>".$this->writetext($def->name)."</name>\n";
e231a3ff 1467 if ($question->qtype == 'calculated') {
79e25fb4
TH
1468 $expout .= " <type>calculated</type>\n";
1469 } else {
1470 $expout .= " <type>calculatedsimple</type>\n";
1471 }
1472 $expout .= " <distribution>" . $this->writetext($def->distribution) .
1473 "</distribution>\n";
1474 $expout .= " <minimum>" . $this->writetext($def->minimum) .
1475 "</minimum>\n";
1476 $expout .= " <maximum>" . $this->writetext($def->maximum) .
1477 "</maximum>\n";
1478 $expout .= " <decimals>" . $this->writetext($def->decimals) .
1479 "</decimals>\n";
f4fe3968 1480 $expout .= " <itemcount>{$def->itemcount}</itemcount>\n";
79e25fb4
TH
1481 if ($def->itemcount > 0) {
1482 $expout .= " <dataset_items>\n";
1483 foreach ($def->items as $item) {
1484 $expout .= " <dataset_item>\n";
1485 $expout .= " <number>".$item->itemnumber."</number>\n";
1486 $expout .= " <value>".$item->value."</value>\n";
1487 $expout .= " </dataset_item>\n";
1488 }
1489 $expout .= " </dataset_items>\n";
1490 $expout .= " <number_of_items>" . $def->number_of_items .
1491 "</number_of_items>\n";
88bc20c3 1492 }
79e25fb4
TH
1493 $expout .= "</dataset_definition>\n";
1494 }
1495 $expout .= "</dataset_definitions>\n";
88bc20c3 1496 }
79e25fb4 1497 break;
49e2bba7 1498
79e25fb4 1499 default:
b31d6664 1500 // Try support by optional plugin.
79e25fb4 1501 if (!$data = $this->try_exporting_using_qtypes($question->qtype, $question)) {
b31d6664
JMV
1502 $invalidquestion = true;
1503 } else {
1504 $expout .= $data;
79e25fb4 1505 }
84769fd8 1506 }
1507
49e2bba7
TH
1508 // Output any hints.
1509 $expout .= $this->write_hints($question);
1510
4f290077 1511 // Write the question tags.
3bf5fcf2 1512 if (core_tag_tag::is_enabled('core_question', 'question')) {
1796eb18 1513 $tagobjects = core_tag_tag::get_item_tags('core_question', 'question', $question->id);
3bf5fcf2 1514
bbd655b4 1515 if (!empty($tagobjects)) {
1796eb18
SL
1516 $context = context::instance_by_id($contextid);
1517 $sortedtagobjects = question_sort_tags($tagobjects, $context, [$this->course]);
3bf5fcf2
SL
1518
1519 if (!empty($sortedtagobjects->coursetags)) {
1520 // Set them on the form to be rendered as existing tags.
1521 $expout .= " <coursetags>\n";
1522 foreach ($sortedtagobjects->coursetags as $coursetag) {
1523 $expout .= " <tag>" . $this->writetext($coursetag, 0, true) . "</tag>\n";
1524 }
1525 $expout .= " </coursetags>\n";
3a7005c9 1526 }
3a7005c9 1527
3bf5fcf2
SL
1528 if (!empty($sortedtagobjects->tags)) {
1529 $expout .= " <tags>\n";
1530 foreach ($sortedtagobjects->tags as $tag) {
1531 $expout .= " <tag>" . $this->writetext($tag, 0, true) . "</tag>\n";
1532 }
1533 $expout .= " </tags>\n";
1534 }
4f290077
TH
1535 }
1536 }
1537
d2fd7262 1538 // Close the question tag.
49e2bba7 1539 $expout .= " </question>\n";
b31d6664
JMV
1540 if ($invalidquestion) {
1541 return '';
1542 } else {
1543 return $expout;
1544 }
84769fd8 1545 }
49e2bba7
TH
1546
1547 public function write_answers($answers) {
1548 if (empty($answers)) {
1549 return;
1550 }
1551 $output = '';
1552 foreach ($answers as $answer) {
1553 $output .= $this->write_answer($answer);
1554 }
1555 return $output;
1556 }
1557
1558 public function write_answer($answer, $extra = '') {
1559 $percent = $answer->fraction * 100;
1560 $output = '';
f4fe3968 1561 $output .= " <answer fraction=\"{$percent}\" {$this->format($answer->answerformat)}>\n";
49e2bba7 1562 $output .= $this->writetext($answer->answer, 3);
7e7fdf59 1563 $output .= $this->write_files($answer->answerfiles);
5f7cfba7 1564 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
49e2bba7 1565 $output .= $this->writetext($answer->feedback, 4);
7e7fdf59 1566 $output .= $this->write_files($answer->feedbackfiles);
49e2bba7
TH
1567 $output .= " </feedback>\n";
1568 $output .= $extra;
1569 $output .= " </answer>\n";
1570 return $output;
1571 }
1572
78fc17eb
TH
1573 /**
1574 * Write out the hints.
1575 * @param object $question the question definition data.
1576 * @return string XML to output.
1577 */
49e2bba7
TH
1578 public function write_hints($question) {
1579 if (empty($question->hints)) {
1580 return '';
1581 }
1582
1583 $output = '';
1584 foreach ($question->hints as $hint) {
78fc17eb 1585 $output .= $this->write_hint($hint, $question->contextid);
49e2bba7
TH
1586 }
1587 return $output;
1588 }
1589
5f7cfba7 1590 /**
2ecbad41 1591 * @param int $format a FORMAT_... constant.
5f7cfba7
TH
1592 * @return string the attribute to add to an XML tag.
1593 */
2ecbad41 1594 public function format($format) {
5f7cfba7
TH
1595 return 'format="' . $this->get_format($format) . '"';
1596 }
1597
78fc17eb
TH
1598 public function write_hint($hint, $contextid) {
1599 $fs = get_file_storage();
1600 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1601
49e2bba7 1602 $output = '';
5f7cfba7 1603 $output .= " <hint {$this->format($hint->hintformat)}>\n";
49e2bba7 1604 $output .= ' ' . $this->writetext($hint->hint);
78fc17eb 1605
49e2bba7
TH
1606 if (!empty($hint->shownumcorrect)) {
1607 $output .= " <shownumcorrect/>\n";
1608 }
1609 if (!empty($hint->clearwrong)) {
1610 $output .= " <clearwrong/>\n";
1611 }
78fc17eb 1612
49e2bba7 1613 if (!empty($hint->options)) {
1b343d52 1614 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
49e2bba7 1615 }
7e7fdf59 1616 $output .= $this->write_files($files);
49e2bba7
TH
1617 $output .= " </hint>\n";
1618 return $output;
1619 }
1620
78fc17eb
TH
1621 /**
1622 * Output the combined feedback fields.
1623 * @param object $questionoptions the question definition data.
1624 * @param int $questionid the question id.
1625 * @param int $contextid the question context id.
1626 * @return string XML to output.
1627 */
1628 public function write_combined_feedback($questionoptions, $questionid, $contextid) {
1629 $fs = get_file_storage();
1630 $output = '';
1631
1632 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
1633 foreach ($fields as $field) {
1634 $formatfield = $field . 'format';
1635 $files = $fs->get_area_files($contextid, 'question', $field, $questionid);
1636
1637 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1638 $output .= ' ' . $this->writetext($questionoptions->$field);
7e7fdf59 1639 $output .= $this->write_files($files);
78fc17eb
TH
1640 $output .= " </{$field}>\n";
1641 }
1642
49e2bba7
TH
1643 if (!empty($questionoptions->shownumcorrect)) {
1644 $output .= " <shownumcorrect/>\n";
1645 }
1646 return $output;
1647 }
84769fd8 1648}