Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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   * Blackboard V5 and V6 question importer.
  19   *
  20   * @package    qformat_blackboard_six
  21   * @copyright  2005 Michael Penney
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once($CFG->libdir . '/xmlize.php');
  28  
  29  /**
  30   * Blackboard 6.0 question importer.
  31   *
  32   * @copyright  2005 Michael Penney
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class qformat_blackboard_six_qti extends qformat_blackboard_six_base {
  36      /**
  37       * Parse the xml document into an array of questions
  38       * this *could* burn memory - but it won't happen that much
  39       * so fingers crossed!
  40       * @param array $text array of lines from the input file.
  41       * @return array (of objects) questions objects.
  42       */
  43      protected function readquestions($text) {
  44  
  45          // This converts xml to big nasty data structure,
  46          // the 0 means keep white space as it is.
  47          try {
  48              $xml = xmlize($text, 0, 'UTF-8', true);
  49          } catch (xml_format_exception $e) {
  50              $this->error($e->getMessage(), '');
  51              return false;
  52          }
  53  
  54          $questions = array();
  55  
  56          // Treat the assessment title as a category title.
  57          $this->process_category($xml, $questions);
  58  
  59          // First step : we are only interested in the <item> tags.
  60          $rawquestions = $this->getpath($xml,
  61                  array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'),
  62                  array(), false);
  63          // Each <item> tag contains data related to a single question.
  64          foreach ($rawquestions as $quest) {
  65              // Second step : parse each question data into the intermediate
  66              // rawquestion structure array.
  67              // Warning : rawquestions are not Moodle questions.
  68              $question = $this->create_raw_question($quest);
  69              // Third step : convert a rawquestion into a Moodle question.
  70              switch($question->qtype) {
  71                  case "Matching":
  72                      $this->process_matching($question, $questions);
  73                      break;
  74                  case "Multiple Choice":
  75                      $this->process_mc($question, $questions);
  76                      break;
  77                  case "Essay":
  78                      $this->process_essay($question, $questions);
  79                      break;
  80                  case "Multiple Answer":
  81                      $this->process_ma($question, $questions);
  82                      break;
  83                  case "True/False":
  84                      $this->process_tf($question, $questions);
  85                      break;
  86                  case 'Fill in the Blank':
  87                      $this->process_fblank($question, $questions);
  88                      break;
  89                  case 'Short Response':
  90                      $this->process_essay($question, $questions);
  91                      break;
  92                  default:
  93                      $this->error(get_string('unknownorunhandledtype', 'question', $question->qtype));
  94                      break;
  95              }
  96          }
  97          return $questions;
  98      }
  99  
 100      /**
 101       * Creates a cleaner object to deal with for processing into Moodle.
 102       * The object returned is NOT a moodle question object.
 103       * @param array $quest XML <item> question  data
 104       * @return object rawquestion
 105       */
 106      public function create_raw_question($quest) {
 107  
 108          $rawquestion = new stdClass();
 109          $rawquestion->qtype = $this->getpath($quest,
 110                  array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'),
 111                  '', true);
 112          $rawquestion->id = $this->getpath($quest,
 113                  array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'),
 114                  '', true);
 115          $presentation = new stdClass();
 116          $presentation->blocks = $this->getpath($quest,
 117                  array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'),
 118                  array(), false);
 119  
 120          foreach ($presentation->blocks as $pblock) {
 121              $block = new stdClass();
 122              $block->type = $this->getpath($pblock,
 123                      array('@', 'class'),
 124                      '', true);
 125  
 126              switch($block->type) {
 127                  case 'QUESTION_BLOCK':
 128                      $subblocks = $this->getpath($pblock,
 129                              array('#', 'flow'),
 130                              array(), false);
 131                      foreach ($subblocks as $sblock) {
 132                          $this->process_block($sblock, $block);
 133                      }
 134                      break;
 135  
 136                  case 'RESPONSE_BLOCK':
 137                      $choices = null;
 138                      switch($rawquestion->qtype) {
 139                          case 'Matching':
 140                              $bbsubquestions = $this->getpath($pblock,
 141                                      array('#', 'flow'),
 142                                      array(), false);
 143                              foreach ($bbsubquestions as $bbsubquestion) {
 144                                  $subquestion = new stdClass();
 145                                  $subquestion->ident = $this->getpath($bbsubquestion,
 146                                          array('#', 'response_lid', 0, '@', 'ident'),
 147                                          '', true);
 148                                  $this->process_block($this->getpath($bbsubquestion,
 149                                          array('#', 'flow', 0),
 150                                          false, false), $subquestion);
 151                                  $bbchoices = $this->getpath($bbsubquestion,
 152                                          array('#', 'response_lid', 0, '#', 'render_choice', 0,
 153                                          '#', 'flow_label', 0, '#', 'response_label'),
 154                                          array(), false);
 155                                  $choices = array();
 156                                  $this->process_choices($bbchoices, $choices);
 157                                  $subquestion->choices = $choices;
 158                                  if (!isset($block->subquestions)) {
 159                                      $block->subquestions = array();
 160                                  }
 161                                  $block->subquestions[] = $subquestion;
 162                              }
 163                              break;
 164                          case 'Multiple Answer':
 165                              $bbchoices = $this->getpath($pblock,
 166                                      array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
 167                                      array(), false);
 168                              $choices = array();
 169                              $this->process_choices($bbchoices, $choices);
 170                              $block->choices = $choices;
 171                              break;
 172                          case 'Essay':
 173                              // Doesn't apply since the user responds with text input.
 174                              break;
 175                          case 'Multiple Choice':
 176                              $mcchoices = $this->getpath($pblock,
 177                                      array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
 178                                      array(), false);
 179                              foreach ($mcchoices as $mcchoice) {
 180                                  $choices = new stdClass();
 181                                  $choices = $this->process_block($mcchoice, $choices);
 182                                  $block->choices[] = $choices;
 183                              }
 184                              break;
 185                          case 'Short Response':
 186                              // Do nothing?
 187                              break;
 188                          case 'Fill in the Blank':
 189                              // Do nothing?
 190                              break;
 191                          default:
 192                              $bbchoices = $this->getpath($pblock,
 193                                      array('#', 'response_lid', 0, '#', 'render_choice', 0, '#',
 194                                      'flow_label', 0, '#', 'response_label'),
 195                                      array(), false);
 196                              $choices = array();
 197                              $this->process_choices($bbchoices, $choices);
 198                              $block->choices = $choices;
 199                      }
 200                      break;
 201                  case 'RIGHT_MATCH_BLOCK':
 202                      $matchinganswerset = $this->getpath($pblock,
 203                              array('#', 'flow'),
 204                              false, false);
 205  
 206                      $answerset = array();
 207                      foreach ($matchinganswerset as $answer) {
 208                          $bbanswer = new stdClass;
 209                          $bbanswer->text = $this->getpath($answer,
 210                                  array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension',
 211                                  0, '#', 'mat_formattedtext', 0, '#'),
 212                                  false, false);
 213                          $answerset[] = $bbanswer;
 214                      }
 215                      $block->matchinganswerset = $answerset;
 216                      break;
 217                  default:
 218                      $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six'));
 219                      break;
 220              }
 221              $rawquestion->{$block->type} = $block;
 222          }
 223  
 224          // Determine response processing.
 225          // There is a section called 'outcomes' that I don't know what to do with.
 226          $resprocessing = $this->getpath($quest,
 227                  array('#', 'resprocessing'),
 228                  array(), false);
 229  
 230          $respconditions = $this->getpath($resprocessing[0],
 231                  array('#', 'respcondition'),
 232                  array(), false);
 233          $responses = array();
 234          if ($rawquestion->qtype == 'Matching') {
 235              $this->process_matching_responses($respconditions, $responses);
 236          } else {
 237              $this->process_responses($respconditions, $responses);
 238          }
 239          $rawquestion->responses = $responses;
 240          $feedbackset = $this->getpath($quest,
 241                  array('#', 'itemfeedback'),
 242                  array(), false);
 243  
 244          $feedbacks = array();
 245          $this->process_feedback($feedbackset, $feedbacks);
 246          $rawquestion->feedback = $feedbacks;
 247          return $rawquestion;
 248      }
 249  
 250      /**
 251       * Helper function to process an XML block into an object.
 252       * Can call himself recursively if necessary to parse this branch of the XML tree.
 253       * @param array $curblock XML block to parse
 254       * @param object $block block already parsed so far
 255       * @return object $block parsed
 256       */
 257      public function process_block($curblock, $block) {
 258  
 259          $curtype = $this->getpath($curblock,
 260                  array('@', 'class'),
 261                  '', true);
 262  
 263          switch($curtype) {
 264              case 'FORMATTED_TEXT_BLOCK':
 265                  $text = $this->getpath($curblock,
 266                          array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
 267                          '', true);
 268                  $block->text = $this->strip_applet_tags_get_mathml($text);
 269                  break;
 270              case 'FILE_BLOCK':
 271                  $block->filename = $this->getpath($curblock,
 272                          array('#', 'material', 0, '#'),
 273                          '', true);
 274                  if ($block->filename != '') {
 275                      // TODO : determine what to do with the file's content.
 276                      $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename));
 277                  }
 278                  break;
 279              case 'Block':
 280                  if ($this->getpath($curblock,
 281                          array('#', 'material', 0, '#', 'mattext'),
 282                          false, false)) {
 283                      $block->text = $this->getpath($curblock,
 284                              array('#', 'material', 0, '#', 'mattext', 0, '#'),
 285                              '', true);
 286                  } else if ($this->getpath($curblock,
 287                          array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'),
 288                          false, false)) {
 289                      $block->text = $this->getpath($curblock,
 290                              array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
 291                              '', true);
 292                  } else if ($this->getpath($curblock,
 293                          array('#', 'response_label'),
 294                          false, false)) {
 295                      // This is a response label block.
 296                      $subblocks = $this->getpath($curblock,
 297                              array('#', 'response_label', 0),
 298                              array(), false);
 299                      if (!isset($block->ident)) {
 300  
 301                          if ($this->getpath($subblocks,
 302                                  array('@', 'ident'), '', true)) {
 303                              $block->ident = $this->getpath($subblocks,
 304                                  array('@', 'ident'), '', true);
 305                          }
 306                      }
 307                      foreach ($this->getpath($subblocks,
 308                              array('#', 'flow_mat'), array(), false) as $subblock) {
 309                          $this->process_block($subblock, $block);
 310                      }
 311                  } else {
 312                      if ($this->getpath($curblock,
 313                                  array('#', 'flow_mat'), false, false)
 314                              || $this->getpath($curblock,
 315                                  array('#', 'flow'), false, false)) {
 316                          if ($this->getpath($curblock,
 317                                  array('#', 'flow_mat'), false, false)) {
 318                              $subblocks = $this->getpath($curblock,
 319                                      array('#', 'flow_mat'), array(), false);
 320                          } else if ($this->getpath($curblock,
 321                                  array('#', 'flow'), false, false)) {
 322                              $subblocks = $this->getpath($curblock,
 323                                      array('#', 'flow'), array(), false);
 324                          }
 325                          foreach ($subblocks as $sblock) {
 326                              // This will recursively grab the sub blocks which should be of one of the other types.
 327                              $this->process_block($sblock, $block);
 328                          }
 329                      }
 330                  }
 331                  break;
 332              case 'LINK_BLOCK':
 333                  // Not sure how this should be included?
 334                  $link = $this->getpath($curblock,
 335                              array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true);
 336                  if (!empty($link)) {
 337                      $block->link = $link;
 338                  } else {
 339                      $block->link = '';
 340                  }
 341                  break;
 342          }
 343          return $block;
 344      }
 345  
 346      /**
 347       * Preprocess XML blocks containing data for questions' choices.
 348       * Called by {@link create_raw_question()}
 349       * for matching, multichoice and fill in the blank questions.
 350       * @param array $bbchoices XML block to parse
 351       * @param array $choices array of choices suitable for a rawquestion.
 352       */
 353      protected function process_choices($bbchoices, &$choices) {
 354          foreach ($bbchoices as $choice) {
 355              if ($this->getpath($choice,
 356                      array('@', 'ident'), '', true)) {
 357                  $curchoice = $this->getpath($choice,
 358                          array('@', 'ident'), '', true);
 359              } else { // For multiple answers.
 360                  $curchoice = $this->getpath($choice,
 361                           array('#', 'response_label', 0), array(), false);
 362              }
 363              if ($this->getpath($choice,
 364                      array('#', 'flow_mat', 0), false, false)) { // For multiple answers.
 365                  $curblock = $this->getpath($choice,
 366                      array('#', 'flow_mat', 0), false, false);
 367                  // Reset $curchoice to new stdClass because process_block is expecting an object
 368                  // for the second argument and not a string,
 369                  // which is what is was set as originally - CT 8/7/06.
 370                  $curchoice = new stdClass();
 371                  $this->process_block($curblock, $curchoice);
 372              } else if ($this->getpath($choice,
 373                      array('#', 'response_label'), false, false)) {
 374                  // Reset $curchoice to new stdClass because process_block is expecting an object
 375                  // for the second argument and not a string,
 376                  // which is what is was set as originally - CT 8/7/06.
 377                  $curchoice = new stdClass();
 378                  $this->process_block($choice, $curchoice);
 379              }
 380              $choices[] = $curchoice;
 381          }
 382      }
 383  
 384      /**
 385       * Preprocess XML blocks containing data for subanswers
 386       * Called by {@link create_raw_question()}
 387       * for matching questions only.
 388       * @param array $bbresponses XML block to parse
 389       * @param array $responses array of responses suitable for a matching rawquestion.
 390       */
 391      protected function process_matching_responses($bbresponses, &$responses) {
 392          foreach ($bbresponses as $bbresponse) {
 393              $response = new stdClass;
 394              if ($this->getpath($bbresponse,
 395                      array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) {
 396                  $response->correct = $this->getpath($bbresponse,
 397                          array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true);
 398                  $response->ident = $this->getpath($bbresponse,
 399                          array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true);
 400              }
 401              // Suppressed an else block because if the above if condition is false,
 402              // the question is not necessary a broken one, most of the time it's an <other> tag.
 403  
 404              $response->feedback = $this->getpath($bbresponse,
 405                      array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
 406              $responses[] = $response;
 407          }
 408      }
 409  
 410      /**
 411       * Preprocess XML blocks containing data for responses processing.
 412       * Called by {@link create_raw_question()}
 413       * for all questions types.
 414       * @param array $bbresponses XML block to parse
 415       * @param array $responses array of responses suitable for a rawquestion.
 416       */
 417      protected function process_responses($bbresponses, &$responses) {
 418          foreach ($bbresponses as $bbresponse) {
 419              $response = new stdClass();
 420              if ($this->getpath($bbresponse,
 421                      array('@', 'title'), '', true)) {
 422                  $response->title = $this->getpath($bbresponse,
 423                          array('@', 'title'), '', true);
 424              } else {
 425                  $response->title = $this->getpath($bbresponse,
 426                          array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
 427              }
 428              $response->ident = array();
 429              if ($this->getpath($bbresponse,
 430                      array('#', 'conditionvar', 0, '#'), false, false)) {
 431                  $response->ident[0] = $this->getpath($bbresponse,
 432                          array('#', 'conditionvar', 0, '#'), array(), false);
 433              } else if ($this->getpath($bbresponse,
 434                      array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) {
 435                  $response->ident[0] = $this->getpath($bbresponse,
 436                          array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false);
 437              }
 438              if ($this->getpath($bbresponse,
 439                      array('#', 'conditionvar', 0, '#', 'and'), false, false)) {
 440                  $responseset = $this->getpath($bbresponse,
 441                      array('#', 'conditionvar', 0, '#', 'and'), array(), false);
 442                  foreach ($responseset as $rs) {
 443                      $response->ident[] = $this->getpath($rs, array('#'), array(), false);
 444                      if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) {
 445                          $response->feedback = $this->getpath($rs,
 446                                  array('@', 'respident'), '', true);
 447                      }
 448                  }
 449              } else {
 450                  $response->feedback = $this->getpath($bbresponse,
 451                          array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
 452              }
 453  
 454              // Determine what fraction to give response.
 455              if ($this->getpath($bbresponse,
 456                          array('#', 'setvar'), false, false)) {
 457                  switch ($this->getpath($bbresponse,
 458                          array('#', 'setvar', 0, '#'), false, false)) {
 459                      case "SCORE.max":
 460                          $response->fraction = 1;
 461                          break;
 462                      default:
 463                          // I have only seen this being 0 or unset.
 464                          // There are probably fractional values of SCORE.max, but I'm not sure what they look like.
 465                          $response->fraction = 0;
 466                          break;
 467                  }
 468              } else {
 469                  // Just going to assume this is the case this is probably not correct.
 470                  $response->fraction = 0;
 471              }
 472  
 473              $responses[] = $response;
 474          }
 475      }
 476  
 477      /**
 478       * Preprocess XML blocks containing data for responses feedbacks.
 479       * Called by {@link create_raw_question()}
 480       * for all questions types.
 481       * @param array $feedbackset XML block to parse
 482       * @param array $feedbacks array of feedbacks suitable for a rawquestion.
 483       */
 484      public function process_feedback($feedbackset, &$feedbacks) {
 485          foreach ($feedbackset as $bbfeedback) {
 486              $feedback = new stdClass();
 487              $feedback->ident = $this->getpath($bbfeedback,
 488                      array('@', 'ident'), '', true);
 489              $feedback->text = '';
 490              if ($this->getpath($bbfeedback,
 491                      array('#', 'flow_mat', 0), false, false)) {
 492                  $this->process_block($this->getpath($bbfeedback,
 493                          array('#', 'flow_mat', 0), false, false), $feedback);
 494              } else if ($this->getpath($bbfeedback,
 495                      array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) {
 496                  $this->process_block($this->getpath($bbfeedback,
 497                          array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback);
 498              }
 499  
 500              $feedbacks[$feedback->ident] = $feedback;
 501          }
 502      }
 503  
 504      /**
 505       * Create common parts of question
 506       * @param object $quest rawquestion
 507       * @return object Moodle question.
 508       */
 509      public function process_common($quest) {
 510          $question = $this->defaultquestion();
 511          $text = $quest->QUESTION_BLOCK->text;
 512          $questiontext = $this->cleaned_text_field($text);
 513          $question->questiontext = $questiontext['text'];
 514          $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it.
 515          if (isset($questiontext['itemid'])) {
 516              $question->questiontextitemid = $questiontext['itemid'];
 517          }
 518          $question->name = $this->create_default_question_name($question->questiontext,
 519                  get_string('defaultname', 'qformat_blackboard_six' , $quest->id));
 520          $question->generalfeedback = '';
 521          $question->generalfeedbackformat = FORMAT_HTML;
 522          $question->generalfeedbackfiles = array();
 523  
 524          return $question;
 525      }
 526  
 527      /**
 528       * Process True / False Questions
 529       * Parse a truefalse rawquestion and add the result
 530       * to the array of questions already parsed.
 531       * @param object $quest rawquestion
 532       * @param array $questions array of Moodle questions already done
 533       */
 534      protected function process_tf($quest, &$questions) {
 535          $question = $this->process_common($quest);
 536  
 537          $question->qtype = 'truefalse';
 538          $question->single = 1; // Only one answer is allowed.
 539          $question->penalty = 1; // Penalty = 1 for truefalse questions.
 540          // 0th [response] is the correct answer.
 541          $responses = $quest->responses;
 542          $correctresponse = $this->getpath($responses[0]->ident[0],
 543                  array('varequal', 0, '#'), '', true);
 544          if ($correctresponse != 'false') {
 545              $correct = true;
 546          } else {
 547              $correct = false;
 548          }
 549          $fback = new stdClass();
 550  
 551          foreach ($quest->feedback as $fb) {
 552              $fback->{$fb->ident} = $fb->text;
 553          }
 554  
 555          if ($correct) {  // True is correct.
 556              $question->answer = 1;
 557              $question->feedbacktrue = $this->cleaned_text_field($fback->correct);
 558              $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect);
 559          } else {  // False is correct.
 560              $question->answer = 0;
 561              $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect);
 562              $question->feedbackfalse = $this->cleaned_text_field($fback->correct);
 563          }
 564          $question->correctanswer = $question->answer;
 565          $questions[] = $question;
 566      }
 567  
 568      /**
 569       * Process Fill in the Blank Questions
 570       * Parse a fillintheblank rawquestion and add the result
 571       * to the array of questions already parsed.
 572       * @param object $quest rawquestion
 573       * @param array $questions array of Moodle questions already done.
 574       */
 575      protected function process_fblank($quest, &$questions) {
 576          $question = $this->process_common($quest);
 577          $question->qtype = 'shortanswer';
 578          $question->usecase = 0; // Ignore case.
 579  
 580          $answers = array();
 581          $fractions = array();
 582          $feedbacks = array();
 583  
 584          // Extract the feedback.
 585          $feedback = array();
 586          foreach ($quest->feedback as $fback) {
 587              if (isset($fback->ident)) {
 588                  if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
 589                      $feedback[$fback->ident] = $fback->text;
 590                  }
 591              }
 592          }
 593  
 594          foreach ($quest->responses as $response) {
 595              if (isset($response->title)) {
 596                  if ($this->getpath($response->ident[0],
 597                          array('varequal', 0, '#'), false, false)) {
 598                      // For BB Fill in the Blank, only interested in correct answers.
 599                      if ($response->feedback = 'correct') {
 600                          $answers[] = $this->getpath($response->ident[0],
 601                                  array('varequal', 0, '#'), '', true);
 602                          $fractions[] = 1;
 603                          if (isset($feedback['correct'])) {
 604                              $feedbacks[] = $this->cleaned_text_field($feedback['correct']);
 605                          } else {
 606                              $feedbacks[] = $this->text_field('');
 607                          }
 608                      }
 609                  }
 610  
 611              }
 612          }
 613  
 614          // Adding catchall to so that students can see feedback for incorrect answers when they enter something,
 615          // the instructor did not enter.
 616          $answers[] = '*';
 617          $fractions[] = 0;
 618          if (isset($feedback['incorrect'])) {
 619              $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']);
 620          } else {
 621              $feedbacks[] = $this->text_field('');
 622          }
 623  
 624          $question->answer = $answers;
 625          $question->fraction = $fractions;
 626          $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of.
 627  
 628          if (!empty($question)) {
 629              $questions[] = $question;
 630          }
 631  
 632      }
 633  
 634      /**
 635       * Process Multichoice Questions
 636       * Parse a multichoice single answer rawquestion and add the result
 637       * to the array of questions already parsed.
 638       * @param object $quest rawquestion
 639       * @param array $questions array of Moodle questions already done.
 640       */
 641      protected function process_mc($quest, &$questions) {
 642          $question = $this->process_common($quest);
 643          $question->qtype = 'multichoice';
 644          $question = $this->add_blank_combined_feedback($question);
 645          $question->single = 1;
 646          $feedback = array();
 647          foreach ($quest->feedback as $fback) {
 648              $feedback[$fback->ident] = $fback->text;
 649          }
 650  
 651          foreach ($quest->responses as $response) {
 652              if (isset($response->title)) {
 653                  if ($response->title == 'correct') {
 654                      // Only one answer possible for this qtype so first index is correct answer.
 655                      $correct = $this->getpath($response->ident[0],
 656                              array('varequal', 0, '#'), '', true);
 657                  }
 658              } else {
 659                  // Fallback method for when the title is not set.
 660                  if ($response->feedback == 'correct') {
 661                      // Only one answer possible for this qtype so first index is correct answer.
 662                      $correct = $this->getpath($response->ident[0],
 663                              array('varequal', 0, '#'), '', true);
 664                  }
 665              }
 666          }
 667  
 668          $i = 0;
 669          foreach ($quest->RESPONSE_BLOCK->choices as $response) {
 670              $question->answer[$i] = $this->cleaned_text_field($response->text);
 671              if ($correct == $response->ident) {
 672                  $question->fraction[$i] = 1;
 673                  // This is a bit of a hack to catch the feedback... first we see if a  'specific'
 674                  // feedback for this response exists, then if a 'correct' feedback exists.
 675  
 676                  if (!empty($feedback[$response->ident]) ) {
 677                      $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
 678                  } else if (!empty($feedback['correct'])) {
 679                      $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']);
 680                  } else if (!empty($feedback[$i])) {
 681                      $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
 682                  } else {
 683                      $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question'));
 684                  }
 685              } else {
 686                  $question->fraction[$i] = 0;
 687                  if (!empty($feedback[$response->ident]) ) {
 688                      $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
 689                  } else if (!empty($feedback['incorrect'])) {
 690                      $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']);
 691                  } else if (!empty($feedback[$i])) {
 692                      $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
 693                  } else {
 694                      $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question'));
 695                  }
 696              }
 697              $i++;
 698          }
 699  
 700          if (!empty($question)) {
 701              $questions[] = $question;
 702          }
 703      }
 704  
 705      /**
 706       * Process Multiple Choice Questions With Multiple Answers.
 707       * Parse a multichoice multianswer rawquestion and add the result
 708       * to the array of questions already parsed.
 709       * @param object $quest rawquestion
 710       * @param array $questions array of Moodle questions already done.
 711       */
 712      public function process_ma($quest, &$questions) {
 713          $question = $this->process_common($quest);
 714          $question->qtype = 'multichoice';
 715          $question = $this->add_blank_combined_feedback($question);
 716          $question->single = 0; // More than one answer allowed.
 717  
 718          $answers = $quest->responses;
 719          $correctanswers = array();
 720          foreach ($answers as $answer) {
 721              if ($answer->title == 'correct') {
 722                  $answerset = $this->getpath($answer->ident[0],
 723                          array('and', 0, '#', 'varequal'), array(), false);
 724                  foreach ($answerset as $ans) {
 725                      $correctanswers[] = $ans['#'];
 726                  }
 727              }
 728          }
 729          $feedback = new stdClass();
 730          foreach ($quest->feedback as $fb) {
 731              $feedback->{$fb->ident} = trim($fb->text);
 732          }
 733  
 734          $correctanswercount = count($correctanswers);
 735          $fraction = 1 / $correctanswercount;
 736          $choiceset = $quest->RESPONSE_BLOCK->choices;
 737          $i = 0;
 738          foreach ($choiceset as $choice) {
 739              $question->answer[$i] = $this->cleaned_text_field(trim($choice->text));
 740              if (in_array($choice->ident, $correctanswers)) {
 741                  // Correct answer.
 742                  $question->fraction[$i] = $fraction;
 743                  $question->feedback[$i] = $this->cleaned_text_field($feedback->correct);
 744              } else {
 745                  // Wrong answer.
 746                  $question->fraction[$i] = 0;
 747                  $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect);
 748              }
 749              $i++;
 750          }
 751  
 752          $questions[] = $question;
 753      }
 754  
 755      /**
 756       * Process Essay Questions
 757       * Parse an essay rawquestion and add the result
 758       * to the array of questions already parsed.
 759       * @param object $quest rawquestion
 760       * @param array $questions array of Moodle questions already done.
 761       */
 762      public function process_essay($quest, &$questions) {
 763  
 764          $question = $this->process_common($quest);
 765          $question->qtype = 'essay';
 766  
 767          $question->feedback = array();
 768          // Not sure where to get the correct answer from?
 769          foreach ($quest->feedback as $feedback) {
 770              // Added this code to put the possible solution that the
 771              // instructor gives as the Moodle answer for an essay question.
 772              if ($feedback->ident == 'solution') {
 773                  $question->graderinfo = $this->cleaned_text_field($feedback->text);
 774              }
 775          }
 776          // Added because essay/questiontype.php:save_question_option is expecting a
 777          // fraction property - CT 8/10/06.
 778          $question->fraction[] = 1;
 779          $question->defaultmark = 1;
 780          $question->responseformat = 'editor';
 781          $question->responserequired = 1;
 782          $question->responsefieldlines = 15;
 783          $question->attachments = 0;
 784          $question->attachmentsrequired = 0;
 785          $question->responsetemplate = $this->text_field('');
 786  
 787          $questions[] = $question;
 788      }
 789  
 790      /**
 791       * Process Matching Questions
 792       * Parse a matching rawquestion and add the result
 793       * to the array of questions already parsed.
 794       * @param object $quest rawquestion
 795       * @param array $questions array of Moodle questions already done.
 796       */
 797      public function process_matching($quest, &$questions) {
 798  
 799          // Blackboard matching questions can't be imported in core Moodle without a loss in data,
 800          // as core match question don't allow HTML in subanswers. The contributed ddmatch
 801          // question type support HTML in subanswers.
 802          // The ddmatch question type is not part of core, so we need to check if it is defined.
 803          $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
 804  
 805          $question = $this->process_common($quest);
 806          $question = $this->add_blank_combined_feedback($question);
 807          $question->valid = true;
 808          if ($ddmatchisinstalled) {
 809              $question->qtype = 'ddmatch';
 810          } else {
 811              $question->qtype = 'match';
 812          }
 813          // Construction of the array holding mappings between subanswers and subquestions.
 814          foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
 815              foreach ($quest->responses as $rid => $resp) {
 816                  if (isset($resp->ident) && $resp->ident == $subq->ident) {
 817                      $correct = $resp->correct;
 818                  }
 819              }
 820  
 821              foreach ($subq->choices as $cid => $choice) {
 822                  if ($choice == $correct) {
 823                      $mappings[$subq->ident] = $cid;
 824                  }
 825              }
 826          }
 827  
 828          foreach ($subq->choices as $choiceid => $choice) {
 829              $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text;
 830              if ($ddmatchisinstalled) {
 831                  $subanswer = $this->cleaned_text_field($subanswertext);
 832              } else {
 833                  $subanswertext = html_to_text($this->cleaninput($subanswertext), 0);
 834                  $subanswer = $subanswertext;
 835              }
 836  
 837              if ($subanswertext != '') { // Only import non empty subanswers.
 838                  $subquestion = '';
 839  
 840                  $fiber = array_keys ($mappings, $choiceid);
 841                  foreach ($fiber as $correctanswerid) {
 842                      // We have found a correspondance for this subanswer so we need to take the associated subquestion.
 843                      foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
 844                          $currentsubqid = $subq->ident;
 845                          if (strcmp ($currentsubqid, $correctanswerid) == 0) {
 846                              $subquestion = $subq->text;
 847                              break;
 848                          }
 849                      }
 850                      $question->subquestions[] = $this->cleaned_text_field($subquestion);
 851                      $question->subanswers[] = $subanswer;
 852                  }
 853  
 854                  if ($subquestion == '') { // Then in this case, $choice is a distractor.
 855                      $question->subquestions[] = $this->text_field('');
 856                      $question->subanswers[] = $subanswer;
 857                  }
 858              }
 859          }
 860  
 861          // Verify that this matching question has enough subquestions and subanswers.
 862          $subquestioncount = 0;
 863          $subanswercount = 0;
 864          $subanswers = $question->subanswers;
 865          foreach ($question->subquestions as $key => $subquestion) {
 866              $subquestion = $subquestion['text'];
 867              $subanswer = $subanswers[$key];
 868              if ($subquestion != '') {
 869                  $subquestioncount++;
 870              }
 871              $subanswercount++;
 872          }
 873          if ($subquestioncount < 2 || $subanswercount < 3) {
 874                  $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
 875          } else {
 876              $questions[] = $question;
 877          }
 878      }
 879  
 880      /**
 881       * Add a category question entry based on the assessment title
 882       * @param array $xml the xml tree
 883       * @param array $questions the questions already parsed
 884       */
 885      public function process_category($xml, &$questions) {
 886          $title = $this->getpath($xml, array('questestinterop', '#', 'assessment', 0, '@', 'title'), '', true);
 887  
 888          $dummyquestion = new stdClass();
 889          $dummyquestion->qtype = 'category';
 890          $dummyquestion->category = $this->cleaninput($this->clean_question_name($title));
 891  
 892          $questions[] = $dummyquestion;
 893      }
 894  
 895      /**
 896       * Strip the applet tag used by Blackboard to render mathml formulas,
 897       * keeping the mathml tag.
 898       * @param string $string
 899       * @return string
 900       */
 901      public function strip_applet_tags_get_mathml($string) {
 902          if (stristr($string, '</APPLET>') === false) {
 903              return $string;
 904          } else {
 905              // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them.
 906              while (stristr($string, '</APPLET>') !== false) {
 907                  preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls);
 908                  $string = $mathmls[1].$mathmls[2].$mathmls[3];
 909              }
 910              return $string;
 911          }
 912      }
 913  
 914  }