[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/format/xml/ -> format.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1