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