Merge branch 'MDL-32865_23' of git://github.com/jmvedrine/moodle into MOODLE_23_STABLE
[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 *
7764183a 20 * @package qformat
49e2bba7 21 * @subpackage xml
688d8753 22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
7764183a 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
688d8753
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
49e2bba7 29require_once($CFG->libdir . '/xmlize.php');
19f7bb5b
TH
30if (!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}
49e2bba7
TH
38
39
41a89a07 40/**
688d8753
TH
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}
7764183a 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41a89a07 47 */
f5565b69 48class qformat_xml extends qformat_default {
84769fd8 49
c7df5006 50 public function provide_import() {
84769fd8 51 return true;
52 }
53
c7df5006 54 public function provide_export() {
84769fd8 55 return true;
56 }
57
c7df5006 58 public function mime_type() {
46732124
TH
59 return 'application/xml';
60 }
61
84769fd8 62 // IMPORT FUNCTIONS START HERE
63
88bc20c3 64 /**
c81415c7 65 * Translate human readable format name
66 * into internal Moodle code number
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) {
17102269 110 // quick sanity check
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
152
6e557c08 153 /**
c81415c7 154 * import parts of question common to all types
e7ef42f5 155 * @param $question array question question array from xml tree
6e557c08 156 * @return object question object
c81415c7 157 */
c7df5006 158 public function import_headers($question) {
4f290077
TH
159 global $CFG;
160
46013523 161 // get some error strings
79e25fb4
TH
162 $error_noname = get_string('xmlimportnoname', 'qformat_xml');
163 $error_noquestion = get_string('xmlimportnoquestion', 'qformat_xml');
46013523 164
84769fd8 165 // this routine initialises the question object
5bed54e1 166 $qo = $this->defaultquestion();
84769fd8 167
49e2bba7 168 // Question name
5df0a6b4 169 $qo->name = $this->clean_question_name($this->getpath($question,
49e2bba7 170 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
5df0a6b4 171 get_string('xmlimportnoname', 'qformat_xml')));
49e2bba7
TH
172 $qo->questiontext = $this->getpath($question,
173 array('#', 'questiontext', 0, '#', 'text', 0, '#'), '', true);
174 $qo->questiontextformat = $this->trans_format($this->getpath(
fe5f4121 175 $question, array('#', 'questiontext', 0, '@', 'format'), 'html'));
cde2709a 176
c73c9836
TH
177 $qo->questiontextfiles = $this->import_files($this->getpath($question,
178 array('#', 'questiontext', 0, '#', 'file'), array(), false));
cde2709a 179
06f1bd03
TH
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) {
0ff4bd08 184 $data = new stdClass();
06f1bd03
TH
185 $data->content = $filedata;
186 $data->encoding = 'base64';
3385a144 187 // Question file areas don't support subdirs, so convert path to filename if necessary.
b64a3e1a 188 $data->name = clean_param(str_replace('/', '_', $filename), PARAM_FILE);
06f1bd03 189 $qo->questiontextfiles[] = $data;
3385a144 190 $qo->questiontext .= ' <img src="@@PLUGINFILE@@/' . $data->name . '" />';
06f1bd03
TH
191 }
192
cde2709a 193 // restore files in generalfeedback
49e2bba7
TH
194 $qo->generalfeedback = $this->getpath($question,
195 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
cde2709a 196 $qo->generalfeedbackfiles = array();
79e25fb4 197 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
fe5f4121 198 array('#', 'generalfeedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
c73c9836
TH
199 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
200 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
cde2709a 201
79e25fb4
TH
202 $qo->defaultmark = $this->getpath($question,
203 array('#', 'defaultgrade', 0, '#'), $qo->defaultmark);
204 $qo->penalty = $this->getpath($question,
205 array('#', 'penalty', 0, '#'), $qo->penalty);
49e2bba7
TH
206
207 // Fix problematic rounding from old files:
208 if (abs($qo->penalty - 0.3333333) < 0.005) {
209 $qo->penalty = 0.3333333;
210 }
84769fd8 211
4f290077
TH
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 }
221
84769fd8 222 return $qo;
223 }
224
6e557c08 225 /**
49e2bba7 226 * Import the common parts of a single answer
6e557c08 227 * @param array answer xml tree for single answer
fe5f4121
TH
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.
6e557c08 232 * @return object answer object
88bc20c3 233 */
fe5f4121 234 public function import_answer($answer, $withanswerfiles = false, $defaultformat = 'html') {
2a6c5c52 235 $ans = new stdClass();
236
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,
fe5f4121 240 array('@', 'format'), $defaultformat));
2a6c5c52 241 if ($withanswerfiles) {
242 $ans->answer['files'] = $this->import_files($this->getpath($answer,
243 array('#', 'file'), array()));
fe5f4121
TH
244 } else {
245 $ans->answer['format'] = FORMAT_PLAIN;
2a6c5c52 246 }
cde2709a 247
2a6c5c52 248 $ans->feedback = array();
249 $ans->feedback['text'] = $this->getpath($answer,
79e25fb4 250 array('#', 'feedback', 0, '#', 'text', 0, '#'), '', true);
2a6c5c52 251 $ans->feedback['format'] = $this->trans_format($this->getpath($answer,
fe5f4121 252 array('#', 'feedback', 0, '@', 'format'), $defaultformat));
2a6c5c52 253 $ans->feedback['files'] = $this->import_files($this->getpath($answer,
c73c9836 254 array('#', 'feedback', 0, '#', 'file'), array()));
cde2709a 255
2a6c5c52 256 $ans->fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
84769fd8 257
84769fd8 258 return $ans;
259 }
260
49e2bba7
TH
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.
f7970e3c 265 * @param bool $withshownumpartscorrect include the shownumcorrect field.
49e2bba7
TH
266 */
267 public function import_combined_feedback($qo, $questionxml, $withshownumpartscorrect = false) {
79e25fb4
TH
268 $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
269 foreach ($fields as $field) {
fe041243
TH
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,
fe5f4121 274 array('#', $field, 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
c73c9836
TH
275 $text['files'] = $this->import_files($this->getpath($questionxml,
276 array('#', $field, 0, '#', 'file'), array(), false));
fe041243
TH
277
278 $qo->$field = $text;
279 }
49e2bba7
TH
280
281 if ($withshownumpartscorrect) {
282 $qo->shownumcorrect = array_key_exists('shownumcorrect', $questionxml['#']);
283
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 }
291
292 /**
293 * Import a question hint
294 * @param array $hintxml hint xml fragment.
fe5f4121 295 * @param string $defaultformat the text format to assume for hints that do not specify.
49e2bba7
TH
296 * @return object hint for storing in the database.
297 */
fe5f4121 298 public function import_hint($hintxml, $defaultformat) {
49e2bba7
TH
299 if (array_key_exists('hintcontent', $hintxml['#'])) {
300 // Backwards compatibility:
301
0ff4bd08 302 $hint = new stdClass();
c73c9836
TH
303 $hint->hint = array('format' => FORMAT_HTML, 'files' => array());
304 $hint->hint['text'] = $this->getpath($hintxml,
79e25fb4 305 array('#', 'hintcontent', 0, '#', 'text', 0, '#'), '', true);
fe5f4121
TH
306 $hint->hint['format'] = $this->trans_format($defaultformat);
307 $hint->hint['files'] = array();
49e2bba7
TH
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);
314
315 return $hint;
316 }
317
2a6c5c52 318 $hint = new stdClass();
319 $hint->hint['text'] = $this->getpath($hintxml,
c73c9836 320 array('#', 'text', 0, '#'), '', true);
2a6c5c52 321 $hint->hint['format'] = $this->trans_format($this->getpath($hintxml,
fe5f4121 322 array('@', 'format'), $defaultformat));
2a6c5c52 323 $hint->hint['files'] = $this->import_files($this->getpath($hintxml,
c73c9836 324 array('#', 'file'), array(), false));
49e2bba7
TH
325 $hint->shownumcorrect = array_key_exists('shownumcorrect', $hintxml['#']);
326 $hint->clearwrong = array_key_exists('clearwrong', $hintxml['#']);
79e25fb4 327 $hint->options = $this->getpath($hintxml, array('#', 'options', 0, '#'), '', true);
49e2bba7
TH
328
329 return $hint;
330 }
331
332 /**
333 * Import all the question hints
334 *
335 * @param object $qo the question data that is being constructed.
fe5f4121
TH
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.
49e2bba7 341 */
fe5f4121
TH
342 public function import_hints($qo, $questionxml, $withparts = false,
343 $withoptions = false, $defaultformat = 'html') {
49e2bba7
TH
344 if (!isset($questionxml['#']['hint'])) {
345 return;
346 }
fe041243 347
49e2bba7 348 foreach ($questionxml['#']['hint'] as $hintxml) {
fe5f4121 349 $hint = $this->import_hint($hintxml, $defaultformat);
49e2bba7
TH
350 $qo->hint[] = $hint->hint;
351
352 if ($withparts) {
353 $qo->hintshownumcorrect[] = $hint->shownumcorrect;
354 $qo->hintclearwrong[] = $hint->clearwrong;
355 }
356
357 if ($withoptions) {
358 $qo->hintoptions[] = $hint->options;
359 }
360 }
361 }
362
c73c9836
TH
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 }
379
6e557c08 380 /**
88bc20c3 381 * import multiple choice question
6e557c08 382 * @param array question question array from xml tree
383 * @return object question object
c81415c7 384 */
c7df5006 385 public function import_multichoice($question) {
84769fd8 386 // get common parts
cde2709a 387 $qo = $this->import_headers($question);
84769fd8 388
389 // 'header' parts particular to multichoice
390 $qo->qtype = MULTICHOICE;
49e2bba7
TH
391 $single = $this->getpath($question, array('#', 'single', 0, '#'), 'true');
392 $qo->single = $this->trans_single($single);
79e25fb4
TH
393 $shuffleanswers = $this->getpath($question,
394 array('#', 'shuffleanswers', 0, '#'), 'false');
395 $qo->answernumbering = $this->getpath($question,
396 array('#', 'answernumbering', 0, '#'), 'abc');
2da44816 397 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
cde2709a 398
79e25fb4
TH
399 // There was a time on the 1.8 branch when it could output an empty
400 // answernumbering tag, so fix up any found.
c0ffeb39 401 if (empty($qo->answernumbering)) {
402 $qo->answernumbering = 'abc';
403 }
404
49e2bba7 405 // Run through the answers
88bc20c3 406 $answers = $question['#']['answer'];
49e2bba7 407 $acount = 0;
84769fd8 408 foreach ($answers as $answer) {
fe5f4121 409 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
49e2bba7
TH
410 $qo->answer[$acount] = $ans->answer;
411 $qo->fraction[$acount] = $ans->fraction;
412 $qo->feedback[$acount] = $ans->feedback;
413 ++$acount;
84769fd8 414 }
cde2709a 415
49e2bba7 416 $this->import_combined_feedback($qo, $question, true);
fe5f4121 417 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
49e2bba7 418
84769fd8 419 return $qo;
420 }
421
6e557c08 422 /**
49e2bba7 423 * Import cloze type question
6e557c08 424 * @param array question question array from xml tree
425 * @return object question object
c81415c7 426 */
77d0f5f0 427 public function import_multianswer($question) {
1f1c60c6
TH
428 question_bank::get_qtype('multianswer');
429
77d0f5f0 430 $questiontext['text'] = $this->import_text($question['#']['questiontext'][0]['#']['text']);
fe5f4121 431 $questiontext['format'] = FORMAT_HTML;
f9b0500f 432 $questiontext['itemid'] = '';
c8fdd867 433 $qo = qtype_multianswer_extract_question($questiontext);
7b8bc256 434
435 // 'header' parts particular to multianswer
77d0f5f0 436 $qo->qtype = 'multianswer';
7b8bc256 437 $qo->course = $this->course;
79e25fb4 438 $qo->generalfeedback = '';
fe5f4121 439
5df0a6b4 440 $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
fe5f4121
TH
441 $qo->questiontextformat = $questiontext['format'];
442 $qo->questiontext = $qo->questiontext['text'];
58310f88
JMV
443 $qo->questiontextfiles = $this->import_files($this->getpath($question,
444 array('#', 'questiontext', 0, '#', 'file'), array(), false));
445
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 }
fe5f4121 458
c8fdd867 459 // restore files in generalfeedback
77d0f5f0 460 $qo->generalfeedback = $this->getpath($question,
79e25fb4 461 array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
77d0f5f0 462 $qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
fe5f4121 463 array('#', 'generalfeedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
77d0f5f0 464 $qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
c73c9836
TH
465 array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
466
77d0f5f0
TH
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 }
473
fe5f4121 474 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 475
7b8bc256 476 return $qo;
477 }
478
6e557c08 479 /**
49e2bba7 480 * Import true/false type question
6e557c08 481 * @param array question question array from xml tree
482 * @return object question object
c81415c7 483 */
c7df5006 484 public function import_truefalse($question) {
84769fd8 485 // get common parts
fef8f84e 486 global $OUTPUT;
49e2bba7 487 $qo = $this->import_headers($question);
84769fd8 488
489 // 'header' parts particular to true/false
490 $qo->qtype = TRUEFALSE;
491
3246ed33 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) {
79e25fb4
TH
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,
fe5f4121 504 array('#', 'feedback', 0, '@', 'format'), $this->get_format($qo->questiontextformat));
79e25fb4
TH
505 $feedbackfiles = $this->getpath($answer,
506 array('#', 'feedback', 0, '#', 'file'), array());
cde2709a
DC
507 $files = array();
508 foreach ($feedbackfiles as $file) {
0ff4bd08 509 $data = new stdClass();
cde2709a
DC
510 $data->content = $file['#'];
511 $data->encoding = $file['@']['encoding'];
512 $data->name = $file['@']['name'];
513 $files[] = $data;
514 }
3246ed33 515 if ($answertext != 'true' && $answertext != 'false') {
49e2bba7 516 // Old style file, assume order is true/false.
3246ed33 517 $warning = true;
49e2bba7
TH
518 if ($first) {
519 $answertext = 'true';
520 } else {
521 $answertext = 'false';
522 }
88bc20c3 523 }
49e2bba7 524
3246ed33 525 if ($answertext == 'true') {
526 $qo->answer = ($answer['@']['fraction'] == 100);
7939a4a0 527 $qo->correctanswer = $qo->answer;
cde2709a
DC
528 $qo->feedbacktrue = array();
529 $qo->feedbacktrue['text'] = $feedback;
530 $qo->feedbacktrue['format'] = $this->trans_format($feedbackformat);
fe041243 531 $qo->feedbacktrue['files'] = $files;
3246ed33 532 } else {
533 $qo->answer = ($answer['@']['fraction'] != 100);
7939a4a0 534 $qo->correctanswer = $qo->answer;
cde2709a
DC
535 $qo->feedbackfalse = array();
536 $qo->feedbackfalse['text'] = $feedback;
537 $qo->feedbackfalse['format'] = $this->trans_format($feedbackformat);
fe041243 538 $qo->feedbackfalse['files'] = $files;
3246ed33 539 }
540 $first = false;
84769fd8 541 }
3246ed33 542
543 if ($warning) {
0ff4bd08 544 $a = new stdClass();
55c54868 545 $a->questiontext = $qo->questiontext;
5e8a85aa
TH
546 $a->answer = get_string($qo->correctanswer ? 'true' : 'false', 'qtype_truefalse');
547 echo $OUTPUT->notification(get_string('truefalseimporterror', 'qformat_xml', $a));
84769fd8 548 }
49e2bba7 549
fe5f4121 550 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 551
84769fd8 552 return $qo;
553 }
554
6e557c08 555 /**
49e2bba7 556 * Import short answer type question
6e557c08 557 * @param array question question array from xml tree
558 * @return object question object
c81415c7 559 */
c7df5006 560 public function import_shortanswer($question) {
84769fd8 561 // get common parts
49e2bba7 562 $qo = $this->import_headers($question);
84769fd8 563
564 // header parts particular to shortanswer
565 $qo->qtype = SHORTANSWER;
566
567 // get usecase
49e2bba7 568 $qo->usecase = $this->getpath($question, array('#', 'usecase', 0, '#'), $qo->usecase);
84769fd8 569
49e2bba7 570 // Run through the answers
88bc20c3 571 $answers = $question['#']['answer'];
49e2bba7 572 $acount = 0;
84769fd8 573 foreach ($answers as $answer) {
fe5f4121 574 $ans = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
fe041243 575 $qo->answer[$acount] = $ans->answer['text'];
49e2bba7
TH
576 $qo->fraction[$acount] = $ans->fraction;
577 $qo->feedback[$acount] = $ans->feedback;
578 ++$acount;
84769fd8 579 }
580
fe5f4121 581 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 582
84769fd8 583 return $qo;
584 }
88bc20c3 585
6e557c08 586 /**
49e2bba7 587 * Import description type question
6e557c08 588 * @param array question question array from xml tree
589 * @return object question object
c81415c7 590 */
c7df5006 591 public function import_description($question) {
7b8bc256 592 // get common parts
49e2bba7 593 $qo = $this->import_headers($question);
7b8bc256 594 // header parts particular to shortanswer
595 $qo->qtype = DESCRIPTION;
49e2bba7 596 $qo->defaultmark = 0;
3f5633df 597 $qo->length = 0;
7b8bc256 598 return $qo;
599 }
84769fd8 600
6e557c08 601 /**
49e2bba7 602 * Import numerical type question
6e557c08 603 * @param array question question array from xml tree
604 * @return object question object
c81415c7 605 */
c7df5006 606 public function import_numerical($question) {
84769fd8 607 // get common parts
2ed80177 608 $qo = $this->import_headers($question);
84769fd8 609
610 // header parts particular to numerical
611 $qo->qtype = NUMERICAL;
612
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) {
55c54868 620 // answer outside of <text> is deprecated
fe5f4121 621 $obj = $this->import_answer($answer, false, $this->get_format($qo->questiontextformat));
69988ed4 622 $qo->answer[] = $obj->answer['text'];
81a7a02b 623 if (empty($qo->answer)) {
624 $qo->answer = '*';
55c54868 625 }
2ed80177
TH
626 $qo->feedback[] = $obj->feedback;
627 $qo->tolerance[] = $this->getpath($answer, array('#', 'tolerance', 0, '#'), 0);
55c54868 628
629 // fraction as a tag is deprecated
2ed80177 630 $fraction = $this->getpath($answer, array('@', 'fraction'), 0) / 100;
79e25fb4
TH
631 $qo->fraction[] = $this->getpath($answer,
632 array('#', 'fraction', 0, '#'), $fraction); // deprecated
84769fd8 633 }
634
49e2bba7 635 // Get the units array
84769fd8 636 $qo->unit = array();
49e2bba7 637 $units = $this->getpath($question, array('#', 'units', 0, '#', 'unit'), array());
81a7a02b 638 if (!empty($units)) {
a0d187bf 639 $qo->multiplier = array();
640 foreach ($units as $unit) {
79e25fb4
TH
641 $qo->multiplier[] = $this->getpath($unit, array('#', 'multiplier', 0, '#'), 1);
642 $qo->unit[] = $this->getpath($unit, array('#', 'unit_name', 0, '#'), '', true);
a0d187bf 643 }
84769fd8 644 }
79e25fb4 645 $qo->unitgradingtype = $this->getpath($question, array('#', 'unitgradingtype', 0, '#'), 0);
de529cd2
TH
646 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), 0.1);
647 $qo->showunits = $this->getpath($question, array('#', 'showunits', 0, '#'), null);
79e25fb4 648 $qo->unitsleft = $this->getpath($question, array('#', 'unitsleft', 0, '#'), 0);
69988ed4
TH
649 $qo->instructions['text'] = '';
650 $qo->instructions['format'] = FORMAT_HTML;
cde2709a
DC
651 $instructions = $this->getpath($question, array('#', 'instructions'), array());
652 if (!empty($instructions)) {
653 $qo->instructions = array();
2ed80177
TH
654 $qo->instructions['text'] = $this->getpath($instructions,
655 array('0', '#', 'text', '0', '#'), '', true);
ae2c091f 656 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
fe5f4121 657 array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
c73c9836
TH
658 $qo->instructions['files'] = $this->import_files($this->getpath(
659 $instructions, array('0', '#', 'file'), array()));
cde2709a 660 }
49e2bba7 661
de529cd2
TH
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 }
670
fe5f4121 671 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 672
84769fd8 673 return $qo;
674 }
675
6e557c08 676 /**
49e2bba7 677 * Import matching type question
6e557c08 678 * @param array question question array from xml tree
679 * @return object question object
c81415c7 680 */
77d0f5f0 681 public function import_match($question) {
51bcdf28 682 // get common parts
2ed80177 683 $qo = $this->import_headers($question);
51bcdf28 684
685 // header parts particular to matching
77d0f5f0 686 $qo->qtype = 'match';
49e2bba7
TH
687 $qo->shuffleanswers = $this->trans_single($this->getpath($question,
688 array('#', 'shuffleanswers', 0, '#'), 1));
51bcdf28 689
fe041243 690 // run through subquestions
51bcdf28 691 $qo->subquestions = array();
692 $qo->subanswers = array();
fe041243
TH
693 foreach ($question['#']['subquestion'] as $subqxml) {
694 $subquestion = array();
695 $subquestion['text'] = $this->getpath($subqxml, array('#', 'text', 0, '#'), '', true);
79e25fb4 696 $subquestion['format'] = $this->trans_format($this->getpath($subqxml,
fe5f4121 697 array('@', 'format'), $this->get_format($qo->questiontextformat)));
c73c9836
TH
698 $subquestion['files'] = $this->import_files($this->getpath($subqxml,
699 array('#', 'file'), array()));
fe041243 700
fe041243
TH
701 $qo->subquestions[] = $subquestion;
702 $answers = $this->getpath($subqxml, array('#', 'answer'), array());
79e25fb4
TH
703 $qo->subanswers[] = $this->getpath($subqxml,
704 array('#', 'answer', 0, '#', 'text', 0, '#'), '', true);
51bcdf28 705 }
49e2bba7
TH
706
707 $this->import_combined_feedback($qo, $question, true);
fe5f4121 708 $this->import_hints($qo, $question, true, false, $this->get_format($qo->questiontextformat));
49e2bba7 709
51bcdf28 710 return $qo;
711 }
712
6e557c08 713 /**
49e2bba7 714 * Import essay type question
6e557c08 715 * @param array question question array from xml tree
716 * @return object question object
c81415c7 717 */
c7df5006 718 public function import_essay($question) {
c81415c7 719 // get common parts
49e2bba7 720 $qo = $this->import_headers($question);
c81415c7 721
722 // header parts particular to essay
723 $qo->qtype = ESSAY;
724
8a5d05ee
TH
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,
fe5f4121 734 array('#', 'graderinfo', 0, '@', 'format'), $this->get_format($qo->questiontextformat)));
474abf12
TH
735 $qo->graderinfo['files'] = $this->import_files($this->getpath($question,
736 array('#', 'graderinfo', '0', '#', 'file'), array()));
c81415c7 737
738 return $qo;
739 }
84769fd8 740
79e25fb4
TH
741 /**
742 * Import a calculated question
743 * @param object $question the imported XML data.
744 */
745 public function import_calculated($question) {
725ba2a0 746
747 // get common parts
49e2bba7 748 $qo = $this->import_headers($question);
725ba2a0 749
04d8268d 750 // header parts particular to calculated
79e25fb4
TH
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');
04d8268d 758 $qo->shuffleanswers = $this->trans_single($shuffleanswers);
cde2709a 759
fe5f4121 760 $this->import_combined_feedback($qo, $question);
79e25fb4
TH
761
762 $qo->unitgradingtype = $this->getpath($question,
763 array('#', 'unitgradingtype', 0, '#'), 0);
de529cd2 764 $qo->unitpenalty = $this->getpath($question, array('#', 'unitpenalty', 0, '#'), null);
79e25fb4
TH
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);
ac582c3b
PP
769 if (!empty($instructions)) {
770 $qo->instructions = array();
2ed80177
TH
771 $qo->instructions['text'] = $this->getpath($instructions,
772 array('0', '#', 'text', '0', '#'), '', true);
773 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
fe5f4121 774 array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
c73c9836
TH
775 $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
776 array('0', '#', 'file'), array()));
ac582c3b 777 }
cde2709a 778
725ba2a0 779 // get answers array
725ba2a0 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) {
fe5f4121 790 $ans = $this->import_answer($answer, true, $this->get_format($qo->questiontextformat));
725ba2a0 791 // answer outside of <text> is deprecated
cde2709a
DC
792 if (empty($ans->answer['text'])) {
793 $ans->answer['text'] = '*';
725ba2a0 794 }
cde2709a
DC
795 $qo->answers[] = $ans->answer;
796 $qo->feedback[] = $ans->feedback;
725ba2a0 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]['#'];
cde2709a 801 } else {
725ba2a0 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 }
cde2709a
DC
818 $instructions = $this->getpath($question, array('#', 'instructions'), array());
819 if (!empty($instructions)) {
820 $qo->instructions = array();
2ed80177
TH
821 $qo->instructions['text'] = $this->getpath($instructions,
822 array('0', '#', 'text', '0', '#'), '', true);
823 $qo->instructions['format'] = $this->trans_format($this->getpath($instructions,
fe5f4121 824 array('0', '@', 'format'), $this->get_format($qo->questiontextformat)));
79e25fb4 825 $qo->instructions['files'] = $this->import_files($this->getpath($instructions,
c73c9836 826 array('0', '#', 'file'), array()));
cde2709a 827 }
de529cd2
TH
828
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 }
837
cde2709a
DC
838 $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition'];
839 $qo->dataset = array();
79e25fb4 840 $qo->datasetindex= 0;
725ba2a0 841 foreach ($datasets as $dataset) {
842 $qo->datasetindex++;
843 $qo->dataset[$qo->datasetindex] = new stdClass();
79e25fb4
TH
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']);
725ba2a0 860 $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#'];
861 $qo->dataset[$qo->datasetindex]->datasetitem = array();
862 $qo->dataset[$qo->datasetindex]->itemindex = 0;
79e25fb4
TH
863 $qo->dataset[$qo->datasetindex]->number_of_items =
864 $dataset['#']['number_of_items'][0]['#'];
725ba2a0 865 $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item'];
866 foreach ($datasetitems as $datasetitem) {
867 $qo->dataset[$qo->datasetindex]->itemindex++;
79e25fb4
TH
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]['#'];
cde2709a 876 }
725ba2a0 877 }
88bc20c3 878
fe5f4121 879 $this->import_hints($qo, $question, false, false, $this->get_format($qo->questiontextformat));
49e2bba7 880
725ba2a0 881 return $qo;
882 }
883
ee259d0c 884 /**
49e2bba7
TH
885 * This is not a real question type. It's a dummy type used to specify the
886 * import category. The format is:
ee259d0c 887 * <question type="category">
888 * <category>tom/dick/harry</category>
889 * </question>
890 */
c7df5006 891 protected function import_category($question) {
0ff4bd08 892 $qo = new stdClass();
ee259d0c 893 $qo->qtype = 'category';
86b68520 894 $qo->category = $this->import_text($question['#']['category'][0]['#']['text']);
ee259d0c 895 return $qo;
896 }
897
c81415c7 898 /**
49e2bba7 899 * Parse the array of lines into an array of questions
c81415c7 900 * this *could* burn memory - but it won't happen that much
901 * so fingers crossed!
49e2bba7 902 * @param array of lines from the input file.
e2bb3c92 903 * @param stdClass $context
49e2bba7 904 * @return array (of objects) question objects.
c81415c7 905 */
4d188926 906 protected function readquestions($lines) {
49e2bba7
TH
907 // We just need it as one big string
908 $text = implode($lines, ' ');
cde2709a 909 unset($lines);
84769fd8 910
49e2bba7 911 // This converts xml to big nasty data structure
84769fd8 912 // the 0 means keep white space as it is (important for markdown format)
49e2bba7
TH
913 try {
914 $xml = xmlize($text, 0, 'UTF-8', true);
79e25fb4 915 } catch (xml_format_exception $e) {
49e2bba7
TH
916 $this->error($e->getMessage(), '');
917 return false;
918 }
919 // Set up array to hold all our questions
84769fd8 920 $questions = array();
921
49e2bba7 922 // Iterate through questions
84769fd8 923 foreach ($xml['quiz']['#']['question'] as $question) {
49e2bba7
TH
924 $questiontype = $question['@']['type'];
925
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);
77d0f5f0
TH
936 } else if ($questiontype == 'matching' || $questiontype == 'match') {
937 $qo = $this->import_match($question);
938 } else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
49e2bba7
TH
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);
1f1c60c6
TH
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';
49e2bba7
TH
950 } else if ($questiontype == 'category') {
951 $qo = $this->import_category($question);
84769fd8 952
49e2bba7
TH
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)) {
5e8a85aa 958 $this->error(get_string('xmltypeunsupported', 'qformat_xml', $questiontype));
a41e3287 959 $qo = null;
960 }
84769fd8 961 }
962
49e2bba7 963 // Stick the result in the $questions array
84769fd8 964 if ($qo) {
965 $questions[] = $qo;
966 }
967 }
84769fd8 968 return $questions;
969 }
970
971 // EXPORT FUNCTIONS START HERE
972
c7df5006 973 public function export_file_extension() {
46732124 974 return '.xml';
84769fd8 975 }
976
c81415c7 977 /**
77d0f5f0
TH
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.
c81415c7 984 */
77d0f5f0
TH
985 protected function get_qtype($qtype) {
986 switch($qtype) {
987 case 'match':
49e2bba7 988 return 'matching';
77d0f5f0 989 case 'multianswer':
49e2bba7 990 return 'cloze';
49e2bba7 991 default:
77d0f5f0 992 return $qtype;
84769fd8 993 }
84769fd8 994 }
995
6e557c08 996 /**
c81415c7 997 * Convert internal Moodle text format code into
998 * human readable form
6e557c08 999 * @param int id internal code
1000 * @return string format text
c81415c7 1001 */
4a84c94b 1002 public function get_format($id) {
49e2bba7
TH
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';
84769fd8 1016 }
84769fd8 1017 }
1018
6e557c08 1019 /**
88bc20c3 1020 * Convert internal single question code into
c81415c7 1021 * human readable form
6e557c08 1022 * @param int id single question code
1023 * @return string single question string
c81415c7 1024 */
c7df5006 1025 public function get_single($id) {
49e2bba7
TH
1026 switch($id) {
1027 case 0:
1028 return 'false';
1029 case 1:
1030 return 'true';
1031 default:
1032 return 'unknown';
84769fd8 1033 }
84769fd8 1034 }
1035
1b343d52
TH
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 }
1049
6e557c08 1050 /**
49e2bba7
TH
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.
f7970e3c 1054 * @param bool $short stick it on one line.
49e2bba7 1055 * @return string formatted text.
c81415c7 1056 */
c7df5006 1057 public function writetext($raw, $indent = 0, $short = true) {
49e2bba7 1058 $indent = str_repeat(' ', $indent);
1b343d52 1059 $raw = $this->xml_escape($raw);
84769fd8 1060
1061 if ($short) {
49e2bba7 1062 $xml = "$indent<text>$raw</text>\n";
4f290077 1063 } else {
84769fd8 1064 $xml = "$indent<text>\n$raw\n$indent</text>\n";
1065 }
1066
1067 return $xml;
1068 }
88bc20c3 1069
7e7fdf59
TH
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 }
1090
c7df5006 1091 protected function presave_process($content) {
49e2bba7
TH
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>';
84769fd8 1096 }
1097
6e557c08 1098 /**
c81415c7 1099 * Turns question into an xml segment
49e2bba7 1100 * @param object $question the question data.
6e557c08 1101 * @return string xml segment
c81415c7 1102 */
c7df5006 1103 public function writequestion($question) {
d649fb02 1104 global $CFG, $OUTPUT;
cde2709a
DC
1105
1106 $fs = get_file_storage();
1107 $contextid = $question->contextid;
fe041243
TH
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);
49e2bba7
TH
1114 if (!empty($question->options->answers)) {
1115 foreach ($question->options->answers as $answer) {
2a6c5c52 1116 $answer->answerfiles = $fs->get_area_files(
1117 $contextid, 'question', 'answer', $answer->id);
fe041243
TH
1118 $answer->feedbackfiles = $fs->get_area_files(
1119 $contextid, 'question', 'answerfeedback', $answer->id);
49e2bba7
TH
1120 }
1121 }
1122
1123 $expout = '';
84769fd8 1124
49e2bba7
TH
1125 // Add a comment linking this to the original question id.
1126 $expout .= "<!-- question: $question->id -->\n";
84769fd8 1127
49e2bba7 1128 // Check question type
77d0f5f0 1129 $questiontype = $this->get_qtype($question->qtype);
46013523 1130
77d0f5f0 1131 // Categories are a special case.
f1abd39f 1132 if ($question->qtype == 'category') {
49e2bba7 1133 $categorypath = $this->writetext($question->category);
f1abd39f 1134 $expout .= " <question type=\"category\">\n";
1135 $expout .= " <category>\n";
6f8481ec 1136 $expout .= " $categorypath\n";
f1abd39f 1137 $expout .= " </category>\n";
1138 $expout .= " </question>\n";
1139 return $expout;
77d0f5f0 1140 }
49e2bba7 1141
77d0f5f0
TH
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);
7e7fdf59 1150 $expout .= $this->write_files($question->questiontextfiles);
77d0f5f0
TH
1151 $expout .= " </questiontext>\n";
1152 $expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
1153 $expout .= $this->writetext($question->generalfeedback, 3);
7e7fdf59 1154 $expout .= $this->write_files($question->generalfeedbackfiles);
77d0f5f0
TH
1155 $expout .= " </generalfeedback>\n";
1156 if ($question->qtype != 'multianswer') {
49e2bba7 1157 $expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
7b8bc256 1158 }
77d0f5f0
TH
1159 $expout .= " <penalty>{$question->penalty}</penalty>\n";
1160 $expout .= " <hidden>{$question->hidden}</hidden>\n";
7b8bc256 1161
77d0f5f0 1162 // The rest of the output depends on question type.
84769fd8 1163 switch($question->qtype) {
79e25fb4
TH
1164 case 'category':
1165 // not a qtype really - dummy used for category switching
1166 break;
1167
1168 case 'truefalse':
1169 $trueanswer = $question->options->answers[$question->options->trueanswer];
1170 $trueanswer->answer = 'true';
1171 $expout .= $this->write_answer($trueanswer);
1172
1173 $falseanswer = $question->options->answers[$question->options->falseanswer];
1174 $falseanswer->answer = 'false';
1175 $expout .= $this->write_answer($falseanswer);
1176 break;
1177
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";
78fc17eb 1186 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
79e25fb4
TH
1187 $expout .= $this->write_answers($question->options->answers);
1188 break;
1189
1190 case 'shortanswer':
1191 $expout .= " <usecase>{$question->options->usecase}</usecase>\n";
1192 $expout .= $this->write_answers($question->options->answers);
1193 break;
1194
1195 case 'numerical':
1196 foreach ($question->options->answers as $answer) {
1197 $expout .= $this->write_answer($answer,
1198 " <tolerance>$answer->tolerance</tolerance>\n");
84769fd8 1199 }
cde2709a 1200
cde2709a
DC
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";
725ba2a0 1211 }
79e25fb4
TH
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);
7e7fdf59 1231 $expout .= $this->write_files($files);
79e25fb4
TH
1232 $expout .= " </instructions>\n";
1233 }
1234 break;
1235
1236 case 'match':
1237 $expout .= " <shuffleanswers>" .
1238 $this->get_single($question->options->shuffleanswers) .
1239 "</shuffleanswers>\n";
78fc17eb 1240 $expout .= $this->write_combined_feedback($question->options, $question->id, $question->contextid);
79e25fb4
TH
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);
7e7fdf59 1247 $expout .= $this->write_files($files);
79e25fb4
TH
1248 $expout .= " <answer>\n";
1249 $expout .= $this->writetext($subquestion->answertext, 4);
1250 $expout .= " </answer>\n";
1251 $expout .= " </subquestion>\n";
1252 }
1253 break;
1254
1255 case 'description':
1256 // Nothing else to do.
1257 break;
1258
1259 case 'multianswer':
77d0f5f0
TH
1260 foreach ($question->options->questions as $index => $subq) {
1261 $expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
79e25fb4
TH
1262 }
1263 break;
1264
1265 case 'essay':
8a5d05ee
TH
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);
7e7fdf59 1275 $expout .= $this->write_files($fs->get_area_files($contextid, 'qtype_essay',
474abf12 1276 'graderinfo', $question->id));
8a5d05ee 1277 $expout .= " </graderinfo>\n";
79e25fb4
TH
1278 break;
1279
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";
1f1c60c6 1287 $expout .= " <shuffleanswers>" . $question->options->shuffleanswers .
79e25fb4
TH
1288 "</shuffleanswers>\n";
1289
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);
7e7fdf59 1295 $expout .= $this->write_files($files);
79e25fb4
TH
1296 $expout .= " </correctfeedback>\n";
1297
1298 $files = $fs->get_area_files($contextid, $component,
1299 'partiallycorrectfeedback', $question->id);
1300 $expout .= " <partiallycorrectfeedback>\n";
1301 $expout .= $this->writetext($question->options->partiallycorrectfeedback, 3);
7e7fdf59 1302 $expout .= $this->write_files($files);
79e25fb4
TH
1303 $expout .= " </partiallycorrectfeedback>\n";
1304
1305 $files = $fs->get_area_files($contextid, $component,
1306 'incorrectfeedback', $question->id);
1307 $expout .= " <incorrectfeedback>\n";
1308 $expout .= $this->writetext($question->options->incorrectfeedback, 3);
7e7fdf59 1309 $expout .= $this->write_files($files);
79e25fb4
TH
1310 $expout .= " </incorrectfeedback>\n";
1311
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);
7e7fdf59 1327 $expout .= $this->write_files($answer->feedbackfiles);
79e25fb4
TH
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 }
d649fb02 1345
79e25fb4
TH
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);
7e7fdf59 1352 $expout .= $this->write_files($files);
79e25fb4
TH
1353 $expout .= " </instructions>\n";
1354 }
1355
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";
9c1c6c7f 1367 }
79e25fb4
TH
1368 }
1369
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";
88bc20c3 1405 }
79e25fb4
TH
1406 $expout .= "</dataset_definition>\n";
1407 }
1408 $expout .= "</dataset_definitions>\n";
88bc20c3 1409 }
79e25fb4 1410 break;
49e2bba7 1411
79e25fb4
TH
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;
84769fd8 1418 }
1419
49e2bba7
TH
1420 // Output any hints.
1421 $expout .= $this->write_hints($question);
1422
4f290077
TH
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 }
1435
84769fd8 1436 // close the question tag
49e2bba7 1437 $expout .= " </question>\n";
84769fd8 1438
84769fd8 1439 return $expout;
1440 }
49e2bba7
TH
1441
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 }
1452
1453 public function write_answer($answer, $extra = '') {
1454 $percent = $answer->fraction * 100;
1455 $output = '';
c73c9836 1456 $output .= " <answer fraction=\"$percent\" {$this->format($answer->answerformat)}>\n";
49e2bba7 1457 $output .= $this->writetext($answer->answer, 3);
7e7fdf59 1458 $output .= $this->write_files($answer->answerfiles);
5f7cfba7 1459 $output .= " <feedback {$this->format($answer->feedbackformat)}>\n";
49e2bba7 1460 $output .= $this->writetext($answer->feedback, 4);
7e7fdf59 1461 $output .= $this->write_files($answer->feedbackfiles);
49e2bba7
TH
1462 $output .= " </feedback>\n";
1463 $output .= $extra;
1464 $output .= " </answer>\n";
1465 return $output;
1466 }
1467
78fc17eb
TH
1468 /**
1469 * Write out the hints.
1470 * @param object $question the question definition data.
1471 * @return string XML to output.
1472 */
49e2bba7
TH
1473 public function write_hints($question) {
1474 if (empty($question->hints)) {
1475 return '';
1476 }
1477
1478 $output = '';
1479 foreach ($question->hints as $hint) {
78fc17eb 1480 $output .= $this->write_hint($hint, $question->contextid);
49e2bba7
TH
1481 }
1482 return $output;
1483 }
1484
5f7cfba7 1485 /**
2ecbad41 1486 * @param int $format a FORMAT_... constant.
5f7cfba7
TH
1487 * @return string the attribute to add to an XML tag.
1488 */
2ecbad41 1489 public function format($format) {
5f7cfba7
TH
1490 return 'format="' . $this->get_format($format) . '"';
1491 }
1492
78fc17eb
TH
1493 public function write_hint($hint, $contextid) {
1494 $fs = get_file_storage();
1495 $files = $fs->get_area_files($contextid, 'question', 'hint', $hint->id);
1496
49e2bba7 1497 $output = '';
5f7cfba7 1498 $output .= " <hint {$this->format($hint->hintformat)}>\n";
49e2bba7 1499 $output .= ' ' . $this->writetext($hint->hint);
78fc17eb 1500
49e2bba7
TH
1501 if (!empty($hint->shownumcorrect)) {
1502 $output .= " <shownumcorrect/>\n";
1503 }
1504 if (!empty($hint->clearwrong)) {
1505 $output .= " <clearwrong/>\n";
1506 }
78fc17eb 1507
49e2bba7 1508 if (!empty($hint->options)) {
1b343d52 1509 $output .= ' <options>' . $this->xml_escape($hint->options) . "</options>\n";
49e2bba7 1510 }
7e7fdf59 1511 $output .= $this->write_files($files);
49e2bba7
TH
1512 $output .= " </hint>\n";
1513 return $output;
1514 }
1515
78fc17eb
TH
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 = '';
1526
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);
1531
1532 $output .= " <{$field} {$this->format($questionoptions->$formatfield)}>\n";
1533 $output .= ' ' . $this->writetext($questionoptions->$field);
7e7fdf59 1534 $output .= $this->write_files($files);
78fc17eb
TH
1535 $output .= " </{$field}>\n";
1536 }
1537
49e2bba7
TH
1538 if (!empty($questionoptions->shownumcorrect)) {
1539 $output .= " <shownumcorrect/>\n";
1540 }
1541 return $output;
1542 }
84769fd8 1543}