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