[ Index ]

PHP Cross Reference of moodle-2.8

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1