Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311]

   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   * Web CT question importer.
  19   *
  20   * @package    qformat_webct
  21   * @copyright  2004 ASP Consulting http://www.asp-consulting.net
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Manipulate HTML editites in a string. Used by WebCT import.
  30   * @param string $string
  31   * @return string
  32   */
  33  function unhtmlentities($string) {
  34      $search = array ("'<script[?>]*?>.*?</script>'si",  // Remove javascript.
  35                   "'<[\/\!]*?[^<?>]*?>'si",  // Remove HTML tags.
  36                   "'([\r\n])[\s]+'",  // Remove spaces.
  37                   "'&(quot|#34);'i",  // Remove HTML entites.
  38                   "'&(amp|#38);'i",
  39                   "'&(lt|#60);'i",
  40                   "'&(gt|#62);'i",
  41                   "'&(nbsp|#160);'i",
  42                   "'&(iexcl|#161);'i",
  43                   "'&(cent|#162);'i",
  44                   "'&(pound|#163);'i",
  45                   "'&(copy|#169);'i",
  46                   "'&#(\d+);'e");  // Evaluate like PHP.
  47      $replace = array ("",
  48                    "",
  49                    "\\1",
  50                    "\"",
  51                    "&",
  52                    "<",
  53                    "?>",
  54                    " ",
  55                    chr(161),
  56                    chr(162),
  57                    chr(163),
  58                    chr(169),
  59                    "chr(\\1)");
  60      return preg_replace ($search, $replace, $string);
  61  }
  62  
  63  /**
  64   * Helper function for WebCT import.
  65   * @param unknown_type $formula
  66   */
  67  function qformat_webct_convert_formula($formula) {
  68  
  69      // Remove empty space, as it would cause problems otherwise.
  70      $formula = str_replace(' ', '', $formula);
  71  
  72      // Remove paranthesis after e,E and *10**.
  73      while (preg_match('~[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)~', $formula, $regs)) {
  74          $formula = str_replace(
  75                  $regs[0], preg_replace('/[)(]/', '', $regs[0]), $formula);
  76      }
  77  
  78      // Replace *10** with e where possible.
  79      while (preg_match('~(^[+-]?|[^eE][+-]|[^0-9eE+-])[0-9.]+\\*10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~',
  80              $formula, $regs)) {
  81          $formula = str_replace(
  82                  $regs[0], str_replace('*10**', 'e', $regs[0]), $formula);
  83      }
  84  
  85      // Replace other 10** with 1e where possible.
  86      while (preg_match('~(^|[^0-9.eE])10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~', $formula, $regs)) {
  87          $formula = str_replace(
  88                  $regs[0], str_replace('10**', '1e', $regs[0]), $formula);
  89      }
  90  
  91      // Replace all other base**exp with the PHP equivalent function pow(base,exp)
  92      // (Pretty tricky to exchange an operator with a function).
  93      while (2 == count($splits = explode('**', $formula, 2))) {
  94  
  95          // Find $base.
  96          if (preg_match('~^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$~',
  97                  $splits[0], $regs)) {
  98              // The simple cases.
  99              $base = $regs[2];
 100              $splits[0] = $regs[1];
 101  
 102          } else if (preg_match('~\\)$~', $splits[0])) {
 103              // Find the start of this parenthesis.
 104              $deep = 1;
 105              for ($i = 1; $deep; ++$i) {
 106                  if (!preg_match('~^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$~',
 107                          $splits[0], $regs)) {
 108                      print_error('parenthesisinproperstart', 'question', '', $splits[0]);
 109                  }
 110                  if ('(' == $regs[3]) {
 111                      --$deep;
 112                  } else if (')' == $regs[3]) {
 113                      ++$deep;
 114                  } else {
 115                      print_error('impossiblechar', 'question', '', $regs[3]);
 116                  }
 117              }
 118              $base = $regs[2];
 119              $splits[0] = $regs[1];
 120  
 121          } else {
 122              print_error('badbase', 'question', '', $splits[0]);
 123          }
 124  
 125          // Find $exp (similar to above but a little easier).
 126          if (preg_match('~^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)~',
 127                  $splits[1], $regs)) {
 128              // The simple case.
 129              $exp = $regs[1];
 130              $splits[1] = $regs[6];
 131  
 132          } else if (preg_match('~^[+-]?[[:alnum:]_]*\\(~', $splits[1])) {
 133              // Find the end of the parenthesis.
 134              $deep = 1;
 135              for ($i = 1; $deep; ++$i) {
 136                  if (!preg_match('~^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)~',
 137                          $splits[1], $regs)) {
 138                      print_error('parenthesisinproperclose', 'question', '', $splits[1]);
 139                  }
 140                  if (')' == $regs[3]) {
 141                      --$deep;
 142                  } else if ('(' == $regs[3]) {
 143                      ++$deep;
 144                  } else {
 145                      print_error('impossiblechar', 'question');
 146                  }
 147              }
 148              $exp = $regs[1];
 149              $splits[1] = $regs[4];
 150          }
 151  
 152          // Replace it!
 153          $formula = "{$splits[0]}pow({$base},{$exp}){$splits[1]}";
 154      }
 155  
 156      // Nothing more is known to need to be converted.
 157  
 158      return $formula;
 159  }
 160  
 161  
 162  /**
 163   * Web CT question importer.
 164   *
 165   * @copyright  2004 ASP Consulting http://www.asp-consulting.net
 166   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 167   */
 168  class qformat_webct extends qformat_default {
 169      /** @var string path to the temporary directory. */
 170      public $tempdir = '';
 171  
 172      /**
 173       * This plugin provide import
 174       * @return bool true
 175       */
 176      public function provide_import() {
 177          return true;
 178      }
 179  
 180      public function can_import_file($file) {
 181          $mimetypes = array(
 182              mimeinfo('type', '.txt'),
 183              mimeinfo('type', '.zip')
 184          );
 185          return in_array($file->get_mimetype(), $mimetypes);
 186      }
 187  
 188      public function mime_type() {
 189          return mimeinfo('type', '.zip');
 190      }
 191  
 192      /**
 193       * Store an image file in a draft filearea
 194       * @param array $text, if itemid element don't exists it will be created
 195       * @param string tempdir path to root of image tree
 196       * @param string filepathinsidetempdir path to image in the tree
 197       * @param string filename image's name
 198       * @return string new name of the image as it was stored
 199       */
 200      protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) {
 201          global $USER;
 202          $fs = get_file_storage();
 203          if (empty($text['itemid'])) {
 204              $text['itemid'] = file_get_unused_draft_itemid();
 205          }
 206          // As question file areas don't support subdirs,
 207          // convert path to filename.
 208          // So that images with same name can be imported.
 209          $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE);
 210          $filerecord = array(
 211              'contextid' => context_user::instance($USER->id)->id,
 212              'component' => 'user',
 213              'filearea'  => 'draft',
 214              'itemid'    => $text['itemid'],
 215              'filepath'  => '/',
 216              'filename'  => $newfilename,
 217          );
 218          $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename);
 219          return $newfilename;
 220      }
 221  
 222      /**
 223       * Given an HTML text with references to images files,
 224       * store all images in a draft filearea,
 225       * and return an array with all urls in text recoded,
 226       * format set to FORMAT_HTML, and itemid set to filearea itemid
 227       * @param string text text to parse and recode
 228       * @return array with keys text, format, itemid.
 229       */
 230      public function text_field($text) {
 231          $data = array();
 232          // Step one, find all file refs then add to array.
 233          preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs.
 234  
 235          $filepaths = array();
 236          foreach ($out[1] as $path) {
 237              $fullpath = $this->tempdir . '/' . $path;
 238              if (is_readable($fullpath) && !in_array($path, $filepaths)) {
 239                  $dirpath = dirname($path);
 240                  $filename = basename($path);
 241                  $newfilename = $this->store_file_for_text_field($data, $this->tempdir, $dirpath, $filename);
 242                  $text = preg_replace("|{$path}|", "@@PLUGINFILE@@/" . $newfilename, $text);
 243                  $filepaths[] = $path;
 244              }
 245  
 246          }
 247          $data['text'] = $text;
 248          $data['format'] = FORMAT_HTML;
 249          return $data;
 250      }
 251  
 252      /**
 253       * Does any post-processing that may be desired
 254       * Clean the temporary directory if a zip file was imported
 255       * @return bool success
 256       */
 257      public function importpostprocess() {
 258          if (!empty($this->tempdir)) {
 259              fulldelete($this->tempdir);
 260          }
 261          return true;
 262      }
 263  
 264      /**
 265       * Return content of all files containing questions,
 266       * as an array one element for each file found,
 267       * For each file, the corresponding element is an array of lines.
 268       * @param string filename name of file
 269       * @return mixed contents array or false on failure
 270       */
 271      public function readdata($filename) {
 272  
 273          // Find if we are importing a .txt file.
 274          if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'txt') {
 275              if (!is_readable($filename)) {
 276                  $this->error(get_string('filenotreadable', 'error'));
 277                  return false;
 278              }
 279              return file($filename);
 280          }
 281          // We are importing a zip file.
 282          // Create name for temporary directory.
 283          $this->tempdir = make_request_directory();
 284          if (is_readable($filename)) {
 285              if (!copy($filename, $this->tempdir . '/webct.zip')) {
 286                  $this->error(get_string('cannotcopybackup', 'question'));
 287                  fulldelete($this->tempdir);
 288                  return false;
 289              }
 290              $packer = get_file_packer('application/zip');
 291              if ($packer->extract_to_pathname($this->tempdir . '/webct.zip', $this->tempdir, null, null, true)) {
 292                  $dir = $this->tempdir;
 293                  if ((($handle = opendir($dir))) == false) {
 294                      // The directory could not be opened.
 295                      fulldelete($this->tempdir);
 296                      return false;
 297                  }
 298                  // Create arrays to store files and directories.
 299                  $dirfiles = array();
 300                  $dirsubdirs = array();
 301                  $slash = '/';
 302  
 303                  // Loop through all directory entries, and construct two temporary arrays containing files and sub directories.
 304                  while (false !== ($entry = readdir($handle))) {
 305                      if (is_dir($dir. $slash .$entry) && $entry != '..' && $entry != '.') {
 306                          $dirsubdirs[] = $dir. $slash .$entry;
 307                      } else if ($entry != '..' && $entry != '.') {
 308                          $dirfiles[] = $dir. $slash .$entry;
 309                      }
 310                  }
 311                  if ((($handle = opendir($dirsubdirs[0]))) == false) {
 312                      // The directory could not be opened.
 313                      fulldelete($this->tempdir);
 314                      return false;
 315                  }
 316                  while (false !== ($entry = readdir($handle))) {
 317                      if (is_dir($dirsubdirs[0]. $slash .$entry) && $entry != '..' && $entry != '.') {
 318                          $dirsubdirs[] = $dirsubdirs[0]. $slash .$entry;
 319                      } else if ($entry != '..' && $entry != '.') {
 320                          $dirfiles[] = $dirsubdirs[0]. $slash .$entry;
 321                      }
 322                  }
 323                  return file($dirfiles[1]);
 324              } else {
 325                  $this->error(get_string('cannotunzip', 'question'));
 326                  fulldelete($this->tempdir);
 327              }
 328          } else {
 329              $this->error(get_string('cannotreaduploadfile', 'error'));
 330              fulldelete($this->tempdir);
 331          }
 332          return false;
 333      }
 334  
 335      public function readquestions ($lines) {
 336          $webctnumberregex =
 337                  '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?';
 338  
 339          $questions = array();
 340          $warnings = array();
 341          $webctoptions = array();
 342  
 343          $ignorerestofquestion = false;
 344  
 345          $nlinecounter = 0;
 346          $nquestionstartline = 0;
 347          $bishtmltext = false;
 348          $lines[] = ":EOF:";    // For an easiest processing of the last line.
 349          // We don't call defaultquestion() here, it will be called later.
 350  
 351          foreach ($lines as $line) {
 352              $nlinecounter++;
 353              $line = core_text::convert($line, 'windows-1252', 'utf-8');
 354              // Processing multiples lines strings.
 355  
 356              if (isset($questiontext) and is_string($questiontext)) {
 357                  if (preg_match("~^:~", $line)) {
 358                      $questiontext = $this->text_field(trim($questiontext));
 359                      $question->questiontext = $questiontext['text'];
 360                      $question->questiontextformat = $questiontext['format'];
 361                      if (isset($questiontext['itemid'])) {
 362                          $question->questiontextitemid = $questiontext['itemid'];
 363                      }
 364                      unset($questiontext);
 365                  } else {
 366                      $questiontext .= str_replace('\:', ':', $line);
 367                      continue;
 368                  }
 369              }
 370  
 371              if (isset($answertext) and is_string($answertext)) {
 372                  if (preg_match("~^:~", $line)) {
 373                      $answertext = trim($answertext);
 374                      if ($question->qtype == 'multichoice' || $question->qtype == 'match' ) {
 375                          $question->answer[$currentchoice] = $this->text_field($answertext);
 376                          $question->subanswers[$currentchoice] = $question->answer[$currentchoice];
 377  
 378                      } else {
 379                          $question->answer[$currentchoice] = $answertext;
 380                          $question->subanswers[$currentchoice] = $answertext;
 381                      }
 382                      unset($answertext);
 383                  } else {
 384                      $answertext .= str_replace('\:', ':', $line);
 385                      continue;
 386                  }
 387              }
 388  
 389              if (isset($responsetext) and is_string($responsetext)) {
 390                  if (preg_match("~^:~", $line)) {
 391                      $question->subquestions[$currentchoice] = trim($responsetext);
 392                      unset($responsetext);
 393                  } else {
 394                      $responsetext .= str_replace('\:', ':', $line);
 395                      continue;
 396                  }
 397              }
 398  
 399              if (isset($feedbacktext) and is_string($feedbacktext)) {
 400                  if (preg_match("~^:~", $line)) {
 401                      $question->feedback[$currentchoice] = $this->text_field(trim($feedbacktext));
 402                      unset($feedbacktext);
 403                  } else {
 404                      $feedbacktext .= str_replace('\:', ':', $line);
 405                      continue;
 406                  }
 407              }
 408  
 409              if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) {
 410                  if (preg_match("~^:~", $line)) {
 411                      $question->tempgeneralfeedback = trim($generalfeedbacktext);
 412                      unset($generalfeedbacktext);
 413                  } else {
 414                      $generalfeedbacktext .= str_replace('\:', ':', $line);
 415                      continue;
 416                  }
 417              }
 418  
 419              if (isset($graderinfo) and is_string($graderinfo)) {
 420                  if (preg_match("~^:~", $line)) {
 421                      $question->graderinfo['text'] = trim($graderinfo);
 422                      $question->graderinfo['format'] = FORMAT_HTML;
 423                      unset($graderinfo);
 424                  } else {
 425                      $graderinfo .= str_replace('\:', ':', $line);
 426                      continue;
 427                  }
 428              }
 429  
 430              $line = trim($line);
 431  
 432              if (preg_match("~^:(TYPE|EOF):~i", $line)) {
 433                  // New Question or End of File.
 434                  if (isset($question)) {            // If previous question exists, complete, check and save it.
 435  
 436                      // Setup default value of missing fields.
 437                      if (!isset($question->name)) {
 438                          $question->name = $this->create_default_question_name(
 439                                  $question->questiontext, get_string('questionname', 'question'));
 440                      }
 441                      if (!isset($question->defaultmark)) {
 442                          $question->defaultmark = 1;
 443                      }
 444                      if (!isset($question->image)) {
 445                          $question->image = '';
 446                      }
 447  
 448                      // Perform sanity checks.
 449                      $questionok = true;
 450                      if (strlen($question->questiontext) == 0) {
 451                          $warnings[] = get_string('missingquestion', 'qformat_webct', $nquestionstartline);
 452                          $questionok = false;
 453                      }
 454                      if (count($question->answer) < 1) {  // A question must have at least 1 answer.
 455                          $this->error(get_string('missinganswer', 'qformat_webct', $nquestionstartline), '', $question->name);
 456                          $questionok = false;
 457                      } else {
 458                          // Create empty feedback array.
 459                          foreach ($question->answer as $key => $dataanswer) {
 460                              if (!isset($question->feedback[$key])) {
 461                                  $question->feedback[$key]['text'] = '';
 462                                  $question->feedback[$key]['format'] = FORMAT_HTML;
 463                              }
 464                          }
 465                          // This tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9.
 466                          // When question->generalfeedback is undefined, the webct feedback is added to each answer feedback.
 467                          if (isset($question->tempgeneralfeedback)) {
 468                              if (isset($question->generalfeedback)) {
 469                                  $generalfeedback = $this->text_field($question->tempgeneralfeedback);
 470                                  $question->generalfeedback = $generalfeedback['text'];
 471                                  $question->generalfeedbackformat = $generalfeedback['format'];
 472                                  if (isset($generalfeedback['itemid'])) {
 473                                      $question->genralfeedbackitemid = $generalfeedback['itemid'];
 474                                  }
 475                              } else {
 476                                  foreach ($question->answer as $key => $dataanswer) {
 477                                      if ($question->tempgeneralfeedback != '') {
 478                                          $question->feedback[$key]['text'] = $question->tempgeneralfeedback
 479                                                  .'<br/>'.$question->feedback[$key]['text'];
 480                                      }
 481                                  }
 482                              }
 483                              unset($question->tempgeneralfeedback);
 484                          }
 485                          $maxfraction = -1;
 486                          $totalfraction = 0;
 487                          foreach ($question->fraction as $fraction) {
 488                              if ($fraction > 0) {
 489                                  $totalfraction += $fraction;
 490                              }
 491                              if ($fraction > $maxfraction) {
 492                                  $maxfraction = $fraction;
 493                              }
 494                          }
 495                          switch ($question->qtype) {
 496                              case 'shortanswer':
 497                                  if ($maxfraction != 1) {
 498                                      $maxfraction = $maxfraction * 100;
 499                                      $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter)
 500                                              .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name);;
 501                                      $questionok = false;
 502                                  }
 503                                  break;
 504  
 505                              case 'multichoice':
 506                                  $question = $this->add_blank_combined_feedback($question);
 507  
 508                                  if ($question->single) {
 509                                      if ($maxfraction != 1) {
 510                                          $maxfraction = $maxfraction * 100;
 511                                          $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter)
 512                                                  .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name);
 513                                          $questionok = false;
 514                                      }
 515                                  } else {
 516                                      $totalfraction = round($totalfraction, 2);
 517                                      if ($totalfraction != 1) {
 518                                          $totalfraction = $totalfraction * 100;
 519                                          $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter)
 520                                                  .' '.get_string('fractionsaddwrong', 'qtype_multichoice', $totalfraction),
 521                                                  '', $question->name);
 522                                          $questionok = false;
 523                                      }
 524                                  }
 525                                  break;
 526  
 527                              case 'calculated':
 528                                  foreach ($question->answer as $answer) {
 529                                      if ($formulaerror = qtype_calculated_find_formula_errors($answer)) {
 530                                          $warnings[] = "'{$question->name}': ". $formulaerror;
 531                                          $questionok = false;
 532                                      }
 533                                  }
 534                                  foreach ($question->dataset as $dataset) {
 535                                      $dataset->itemcount = count($dataset->datasetitem);
 536                                  }
 537                                  $question->import_process = true;
 538                                  break;
 539                              case 'match':
 540                                  // MDL-10680:
 541                                  // Switch subquestions and subanswers.
 542                                  $question = $this->add_blank_combined_feedback($question);
 543                                  foreach ($question->subquestions as $id => $subquestion) {
 544                                      $temp = $question->subquestions[$id];
 545                                      $question->subquestions[$id] = $question->subanswers[$id];
 546                                      $question->subanswers[$id] = $temp;
 547                                  }
 548                                  if (count($question->answer) < 3) {
 549                                      // Add a dummy missing question.
 550                                      $question->name = 'Dummy question added '.$question->name;
 551                                      $question->answer[] = 'dummy';
 552                                      $question->subanswers[] = 'dummy';
 553                                      $question->subquestions[] = 'dummy';
 554                                      $question->fraction[] = '0.0';
 555                                      $question->feedback[] = '';
 556                                  }
 557                                  break;
 558                              default:
 559                                  // No problemo.
 560                          }
 561                      }
 562  
 563                      if ($questionok) {
 564                          $questions[] = $question;    // Store it.
 565                          unset($question);            // And prepare a new one.
 566                          $question = $this->defaultquestion();
 567                      }
 568                  }
 569                  $nquestionstartline = $nlinecounter;
 570              }
 571  
 572              // Processing Question Header.
 573  
 574              if (preg_match("~^:TYPE:MC:1(.*)~i", $line, $webctoptions)) {
 575                  // Multiple Choice Question with only one good answer.
 576                  $question = $this->defaultquestion();
 577                  $question->feedback = array();
 578                  $question->qtype = 'multichoice';
 579                  $question->single = 1;        // Only one answer is allowed.
 580                  $ignorerestofquestion = false;
 581                  continue;
 582              }
 583  
 584              if (preg_match("~^:TYPE:MC:N(.*)~i", $line, $webctoptions)) {
 585                  // Multiple Choice Question with several good answers.
 586                  $question = $this->defaultquestion();
 587                  $question->feedback = array();
 588                  $question->qtype = 'multichoice';
 589                  $question->single = 0;        // Many answers allowed.
 590                  $ignorerestofquestion = false;
 591                  continue;
 592              }
 593  
 594              if (preg_match("~^:TYPE:S~i", $line)) {
 595                  // Short Answer Question.
 596                  $question = $this->defaultquestion();
 597                  $question->feedback = array();
 598                  $question->qtype = 'shortanswer';
 599                  $question->usecase = 0;       // Ignore case.
 600                  $ignorerestofquestion = false;
 601                  continue;
 602              }
 603  
 604              if (preg_match("~^:TYPE:C~i", $line)) {
 605                  // Calculated Question.
 606                  $question = $this->defaultquestion();
 607                  $question->qtype = 'calculated';
 608                  $question->answer = array(); // No problem as they go as :FORMULA: from webct.
 609                  $question->units = array();
 610                  $question->dataset = array();
 611                  $question->fraction = array('1.0');
 612                  $question->feedback = array();
 613  
 614                  $currentchoice = -1;
 615                  $ignorerestofquestion = false;
 616                  continue;
 617              }
 618  
 619              if (preg_match("~^:TYPE:M~i", $line)) {
 620                  // Match Question.
 621                  $question = $this->defaultquestion();
 622                  $question->qtype = 'match';
 623                  $question->feedback = array();
 624                  $ignorerestofquestion = false;         // Match question processing is not debugged.
 625                  continue;
 626              }
 627  
 628              if (preg_match("~^:TYPE:P~i", $line)) {
 629                  // Paragraph Question.
 630                  $question = $this->defaultquestion();
 631                  $question->qtype = 'essay';
 632                  $question->responseformat = 'editor';
 633                  $question->responserequired = 1;
 634                  $question->responsefieldlines = 15;
 635                  $question->attachments = 0;
 636                  $question->attachmentsrequired = 0;
 637                  $question->graderinfo = array(
 638                          'text' => '',
 639                          'format' => FORMAT_HTML,
 640                      );
 641                  $question->feedback = array();
 642                  $question->generalfeedback = '';
 643                  $question->generalfeedbackformat = FORMAT_HTML;
 644                  $question->generalfeedbackfiles = array();
 645                  $question->responsetemplate = $this->text_field('');
 646                  $question->questiontextformat = FORMAT_HTML;
 647                  $ignorerestofquestion = false;
 648                  // To make us pass the end-of-question sanity checks.
 649                  $question->answer = array('dummy');
 650                  $question->fraction = array('1.0');
 651                  continue;
 652              }
 653  
 654              if (preg_match("~^:TYPE:~i", $line)) {
 655                  // Unknow question type.
 656                  $warnings[] = get_string('unknowntype', 'qformat_webct', $nlinecounter);
 657                  unset($question);
 658                  $ignorerestofquestion = true;         // Question Type not handled by Moodle.
 659                  continue;
 660              }
 661  
 662              if ($ignorerestofquestion) {
 663                  continue;
 664              }
 665  
 666              if (preg_match("~^:TITLE:(.*)~i", $line, $webctoptions)) {
 667                  $name = trim($webctoptions[1]);
 668                  $question->name = $this->clean_question_name($name);
 669                  continue;
 670              }
 671  
 672              if (preg_match("~^:IMAGE:(.*)~i", $line, $webctoptions)) {
 673                  $filename = trim($webctoptions[1]);
 674                  if (preg_match("~^http://~i", $filename)) {
 675                      $question->image = $filename;
 676                  }
 677                  continue;
 678              }
 679  
 680              // Need to put the parsing of calculated items here to avoid ambitiuosness:
 681              // if question isn't defined yet there is nothing to do here (avoid notices).
 682              if (!isset($question)) {
 683                  continue;
 684              }
 685              if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match(
 686                      "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?({$webctnumberregex})~", $line, $webctoptions)) {
 687                  $datasetname = preg_replace('/^::/', '', $webctoptions[1]);
 688                  $datasetvalue = qformat_webct_convert_formula($webctoptions[4]);
 689                  switch ($webctoptions[2]) {
 690                      case 'MIN':
 691                          $question->dataset[$datasetname]->min = $datasetvalue;
 692                          break;
 693                      case 'MAX':
 694                          $question->dataset[$datasetname]->max = $datasetvalue;
 695                          break;
 696                      case 'DEC':
 697                          $datasetvalue = floor($datasetvalue); // Int only!
 698                          $question->dataset[$datasetname]->length = max(0, $datasetvalue);
 699                          break;
 700                      default:
 701                          // The VAL case.
 702                          $question->dataset[$datasetname]->datasetitem[$webctoptions[3]] = new stdClass();
 703                          $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->itemnumber = $webctoptions[3];
 704                          $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->value  = $datasetvalue;
 705                          break;
 706                  }
 707                  continue;
 708              }
 709  
 710              $bishtmltext = preg_match("~:H$~i", $line);  // True if next lines are coded in HTML.
 711              if (preg_match("~^:QUESTION~i", $line)) {
 712                  $questiontext = '';               // Start gathering next lines.
 713                  continue;
 714              }
 715  
 716              if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i", $line, $webctoptions)) { // Shortanswer.
 717                  $currentchoice = $webctoptions[1];
 718                  $answertext = $webctoptions[2];            // Start gathering next lines.
 719                  $question->fraction[$currentchoice] = ($webctoptions[3]/100);
 720                  continue;
 721              }
 722  
 723              if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i", $line, $webctoptions)) {
 724                  $answertext = '';                 // Start gathering next lines.
 725                  $currentchoice = $webctoptions[1];
 726                  $question->fraction[$currentchoice] = ($webctoptions[2]/100);
 727                  continue;
 728              }
 729  
 730              if (preg_match('~^:ANSWER:~i', $line)) { // Essay.
 731                  $graderinfo  = '';      // Start gathering next lines.
 732                  continue;
 733              }
 734  
 735              if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) {
 736                  // Answer for a calculated question.
 737                  ++$currentchoice;
 738                  $question->answer[$currentchoice] =
 739                          qformat_webct_convert_formula($webctoptions[1]);
 740  
 741                  // Default settings.
 742                  $question->fraction[$currentchoice] = 1.0;
 743                  $question->tolerance[$currentchoice] = 0.0;
 744                  $question->tolerancetype[$currentchoice] = 2; // Nominal (units in webct).
 745                  $question->feedback[$currentchoice]['text'] = '';
 746                  $question->feedback[$currentchoice]['format'] = FORMAT_HTML;
 747                  $question->correctanswerlength[$currentchoice] = 4;
 748  
 749                  $datasetnames =
 750                          question_bank::get_qtype('calculated')->find_dataset_names($webctoptions[1]);
 751                  foreach ($datasetnames as $datasetname) {
 752                      $question->dataset[$datasetname] = new stdClass();
 753                      $question->dataset[$datasetname]->datasetitem = array();
 754                      $question->dataset[$datasetname]->name = $datasetname;
 755                      $question->dataset[$datasetname]->distribution = 'uniform';
 756                      $question->dataset[$datasetname]->status = 'private';
 757                  }
 758                  continue;
 759              }
 760  
 761              if (preg_match("~^:L([0-9]+)~i", $line, $webctoptions)) {
 762                  $answertext = '';                 // Start gathering next lines.
 763                  $currentchoice = $webctoptions[1];
 764                  $question->fraction[$currentchoice] = 1;
 765                  continue;
 766              }
 767  
 768              if (preg_match("~^:R([0-9]+)~i", $line, $webctoptions)) {
 769                  $responsetext = '';                // Start gathering next lines.
 770                  $currentchoice = $webctoptions[1];
 771                  continue;
 772              }
 773  
 774              if (preg_match("~^:REASON([0-9]+):?~i", $line, $webctoptions)) {
 775                  $feedbacktext = '';               // Start gathering next lines.
 776                  $currentchoice = $webctoptions[1];
 777                  continue;
 778              }
 779              if (preg_match("~^:FEEDBACK([0-9]+):?~i", $line, $webctoptions)) {
 780                  $generalfeedbacktext = '';               // Start gathering next lines.
 781                  $currentchoice = $webctoptions[1];
 782                  continue;
 783              }
 784              if (preg_match('~^:FEEDBACK:(.*)~i', $line, $webctoptions)) {
 785                  $generalfeedbacktext = '';               // Start gathering next lines.
 786                  continue;
 787              }
 788              if (preg_match('~^:LAYOUT:(.*)~i', $line, $webctoptions)) {
 789                  // Ignore  since layout in question_multichoice  is no more used in Moodle.
 790                  // $webctoptions[1] contains either vertical or horizontal.
 791                  continue;
 792              }
 793  
 794              if (isset($question->qtype ) && 'calculated' == $question->qtype
 795                      && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webctoptions)) {
 796                  // We can but hope that this always appear before the ANSTYPE property.
 797                  $question->correctanswerlength[$currentchoice] = $webctoptions[1];
 798                  continue;
 799              }
 800  
 801              if (isset($question->qtype )&& 'calculated' == $question->qtype
 802                      && preg_match("~^:TOL:({$webctnumberregex})~i", $line, $webctoptions)) {
 803                  // We can but hope that this always appear before the TOL property.
 804                  $question->tolerance[$currentchoice] =
 805                          qformat_webct_convert_formula($webctoptions[1]);
 806                  continue;
 807              }
 808  
 809              if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) {
 810                  // Percentage case is handled as relative in Moodle.
 811                  $question->tolerance[$currentchoice]  /= 100;
 812                  $question->tolerancetype[$currentchoice] = 1; // Relative.
 813                  continue;
 814              }
 815  
 816              if (preg_match('~^:UNITS:(.+)~i', $line, $webctoptions)
 817                      and $webctunits = trim($webctoptions[1])) {
 818                  // This is a guess - I really do not know how different webct units are separated...
 819                  $webctunits = explode(':', $webctunits);
 820                  $unitrec->multiplier = 1.0; // Webct does not seem to support this.
 821                  foreach ($webctunits as $webctunit) {
 822                      $unitrec->unit = trim($webctunit);
 823                      $question->units[] = $unitrec;
 824                  }
 825                  continue;
 826              }
 827  
 828              if (!empty($question->units) && preg_match('~^:UNITREQ:(.*)~i', $line, $webctoptions)
 829                      && !$webctoptions[1]) {
 830                  // There are units but units are not required so add the no unit alternative.
 831                  // We can but hope that the UNITS property always appear before this property.
 832                  $unitrec->unit = '';
 833                  $unitrec->multiplier = 1.0;
 834                  $question->units[] = $unitrec;
 835                  continue;
 836              }
 837  
 838              if (!empty($question->units) && preg_match('~^:UNITCASE:~i', $line)) {
 839                  // This could be important but I was not able to figure out how
 840                  // it works so I ignore it for now.
 841                  continue;
 842              }
 843  
 844              if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) {
 845                  $question->correctanswerformat[$currentchoice] = '1';
 846                  continue;
 847              }
 848              if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) {
 849                  $question->correctanswerformat[$currentchoice] = '2';
 850                  continue;
 851              }
 852          }
 853  
 854          if (count($warnings) > 0) {
 855              echo '<p>'.get_string('warningsdetected', 'qformat_webct', count($warnings)).'</p><ul>';
 856              foreach ($warnings as $warning) {
 857                  echo "<li>{$warning}</li>";
 858              }
 859              echo '</ul>';
 860          }
 861          return $questions;
 862      }
 863  }