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.

Differences Between: [Versions 310 and 311] [Versions 39 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       * Validate the given file.
 194       *
 195       * For more expensive or detailed integrity checks.
 196       *
 197       * @param stored_file $file the file to check
 198       * @return string the error message that occurred while validating the given file
 199       */
 200      public function validate_file(stored_file $file): string {
 201          return $this->validate_is_utf8_file($file);
 202      }
 203  
 204      /**
 205       * Store an image file in a draft filearea
 206       * @param array $text, if itemid element don't exists it will be created
 207       * @param string tempdir path to root of image tree
 208       * @param string filepathinsidetempdir path to image in the tree
 209       * @param string filename image's name
 210       * @return string new name of the image as it was stored
 211       */
 212      protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) {
 213          global $USER;
 214          $fs = get_file_storage();
 215          if (empty($text['itemid'])) {
 216              $text['itemid'] = file_get_unused_draft_itemid();
 217          }
 218          // As question file areas don't support subdirs,
 219          // convert path to filename.
 220          // So that images with same name can be imported.
 221          $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE);
 222          $filerecord = array(
 223              'contextid' => context_user::instance($USER->id)->id,
 224              'component' => 'user',
 225              'filearea'  => 'draft',
 226              'itemid'    => $text['itemid'],
 227              'filepath'  => '/',
 228              'filename'  => $newfilename,
 229          );
 230          $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename);
 231          return $newfilename;
 232      }
 233  
 234      /**
 235       * Given an HTML text with references to images files,
 236       * store all images in a draft filearea,
 237       * and return an array with all urls in text recoded,
 238       * format set to FORMAT_HTML, and itemid set to filearea itemid
 239       * @param string text text to parse and recode
 240       * @return array with keys text, format, itemid.
 241       */
 242      public function text_field($text) {
 243          $data = array();
 244          // Step one, find all file refs then add to array.
 245          preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs.
 246  
 247          $filepaths = array();
 248          foreach ($out[1] as $path) {
 249              $fullpath = $this->tempdir . '/' . $path;
 250              if (is_readable($fullpath) && !in_array($path, $filepaths)) {
 251                  $dirpath = dirname($path);
 252                  $filename = basename($path);
 253                  $newfilename = $this->store_file_for_text_field($data, $this->tempdir, $dirpath, $filename);
 254                  $text = preg_replace("|{$path}|", "@@PLUGINFILE@@/" . $newfilename, $text);
 255                  $filepaths[] = $path;
 256              }
 257  
 258          }
 259          $data['text'] = $text;
 260          $data['format'] = FORMAT_HTML;
 261          return $data;
 262      }
 263  
 264      /**
 265       * Does any post-processing that may be desired
 266       * Clean the temporary directory if a zip file was imported
 267       * @return bool success
 268       */
 269      public function importpostprocess() {
 270          if (!empty($this->tempdir)) {
 271              fulldelete($this->tempdir);
 272          }
 273          return true;
 274      }
 275  
 276      /**
 277       * Return content of all files containing questions,
 278       * as an array one element for each file found,
 279       * For each file, the corresponding element is an array of lines.
 280       * @param string filename name of file
 281       * @return mixed contents array or false on failure
 282       */
 283      public function readdata($filename) {
 284  
 285          // Find if we are importing a .txt file.
 286          if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'txt') {
 287              if (!is_readable($filename)) {
 288                  $this->error(get_string('filenotreadable', 'error'));
 289                  return false;
 290              }
 291              return file($filename);
 292          }
 293          // We are importing a zip file.
 294          // Create name for temporary directory.
 295          $this->tempdir = make_request_directory();
 296          if (is_readable($filename)) {
 297              if (!copy($filename, $this->tempdir . '/webct.zip')) {
 298                  $this->error(get_string('cannotcopybackup', 'question'));
 299                  fulldelete($this->tempdir);
 300                  return false;
 301              }
 302              $packer = get_file_packer('application/zip');
 303              if ($packer->extract_to_pathname($this->tempdir . '/webct.zip', $this->tempdir, null, null, true)) {
 304                  $dir = $this->tempdir;
 305                  if ((($handle = opendir($dir))) == false) {
 306                      // The directory could not be opened.
 307                      fulldelete($this->tempdir);
 308                      return false;
 309                  }
 310                  // Create arrays to store files and directories.
 311                  $dirfiles = array();
 312                  $dirsubdirs = array();
 313                  $slash = '/';
 314  
 315                  // Loop through all directory entries, and construct two temporary arrays containing files and sub directories.
 316                  while (false !== ($entry = readdir($handle))) {
 317                      if (is_dir($dir. $slash .$entry) && $entry != '..' && $entry != '.') {
 318                          $dirsubdirs[] = $dir. $slash .$entry;
 319                      } else if ($entry != '..' && $entry != '.') {
 320                          $dirfiles[] = $dir. $slash .$entry;
 321                      }
 322                  }
 323                  if ((($handle = opendir($dirsubdirs[0]))) == false) {
 324                      // The directory could not be opened.
 325                      fulldelete($this->tempdir);
 326                      return false;
 327                  }
 328                  while (false !== ($entry = readdir($handle))) {
 329                      if (is_dir($dirsubdirs[0]. $slash .$entry) && $entry != '..' && $entry != '.') {
 330                          $dirsubdirs[] = $dirsubdirs[0]. $slash .$entry;
 331                      } else if ($entry != '..' && $entry != '.') {
 332                          $dirfiles[] = $dirsubdirs[0]. $slash .$entry;
 333                      }
 334                  }
 335                  return file($dirfiles[1]);
 336              } else {
 337                  $this->error(get_string('cannotunzip', 'question'));
 338                  fulldelete($this->tempdir);
 339              }
 340          } else {
 341              $this->error(get_string('cannotreaduploadfile', 'error'));
 342              fulldelete($this->tempdir);
 343          }
 344          return false;
 345      }
 346  
 347      public function readquestions ($lines) {
 348          $webctnumberregex =
 349                  '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?';
 350  
 351          $questions = array();
 352          $warnings = array();
 353          $webctoptions = array();
 354  
 355          $ignorerestofquestion = false;
 356  
 357          $nlinecounter = 0;
 358          $nquestionstartline = 0;
 359          $bishtmltext = false;
 360          $lines[] = ":EOF:";    // For an easiest processing of the last line.
 361          // We don't call defaultquestion() here, it will be called later.
 362  
 363          foreach ($lines as $line) {
 364              $nlinecounter++;
 365              $line = core_text::convert($line, 'windows-1252', 'utf-8');
 366              // Processing multiples lines strings.
 367  
 368              if (isset($questiontext) and is_string($questiontext)) {
 369                  if (preg_match("~^:~", $line)) {
 370                      $questiontext = $this->text_field(trim($questiontext));
 371                      $question->questiontext = $questiontext['text'];
 372                      $question->questiontextformat = $questiontext['format'];
 373                      if (isset($questiontext['itemid'])) {
 374                          $question->questiontextitemid = $questiontext['itemid'];
 375                      }
 376                      unset($questiontext);
 377                  } else {
 378                      $questiontext .= str_replace('\:', ':', $line);
 379                      continue;
 380                  }
 381              }
 382  
 383              if (isset($answertext) and is_string($answertext)) {
 384                  if (preg_match("~^:~", $line)) {
 385                      $answertext = trim($answertext);
 386                      if ($question->qtype == 'multichoice' || $question->qtype == 'match' ) {
 387                          $question->answer[$currentchoice] = $this->text_field($answertext);
 388                          $question->subanswers[$currentchoice] = $question->answer[$currentchoice];
 389  
 390                      } else {
 391                          $question->answer[$currentchoice] = $answertext;
 392                          $question->subanswers[$currentchoice] = $answertext;
 393                      }
 394                      unset($answertext);
 395                  } else {
 396                      $answertext .= str_replace('\:', ':', $line);
 397                      continue;
 398                  }
 399              }
 400  
 401              if (isset($responsetext) and is_string($responsetext)) {
 402                  if (preg_match("~^:~", $line)) {
 403                      $question->subquestions[$currentchoice] = trim($responsetext);
 404                      unset($responsetext);
 405                  } else {
 406                      $responsetext .= str_replace('\:', ':', $line);
 407                      continue;
 408                  }
 409              }
 410  
 411              if (isset($feedbacktext) and is_string($feedbacktext)) {
 412                  if (preg_match("~^:~", $line)) {
 413                      $question->feedback[$currentchoice] = $this->text_field(trim($feedbacktext));
 414                      unset($feedbacktext);
 415                  } else {
 416                      $feedbacktext .= str_replace('\:', ':', $line);
 417                      continue;
 418                  }
 419              }
 420  
 421              if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) {
 422                  if (preg_match("~^:~", $line)) {
 423                      $question->tempgeneralfeedback = trim($generalfeedbacktext);
 424                      unset($generalfeedbacktext);
 425                  } else {
 426                      $generalfeedbacktext .= str_replace('\:', ':', $line);
 427                      continue;
 428                  }
 429              }
 430  
 431              if (isset($graderinfo) and is_string($graderinfo)) {
 432                  if (preg_match("~^:~", $line)) {
 433                      $question->graderinfo['text'] = trim($graderinfo);
 434                      $question->graderinfo['format'] = FORMAT_HTML;
 435                      unset($graderinfo);
 436                  } else {
 437                      $graderinfo .= str_replace('\:', ':', $line);
 438                      continue;
 439                  }
 440              }
 441  
 442              $line = trim($line);
 443  
 444              if (preg_match("~^:(TYPE|EOF):~i", $line)) {
 445                  // New Question or End of File.
 446                  if (isset($question)) {            // If previous question exists, complete, check and save it.
 447  
 448                      // Setup default value of missing fields.
 449                      if (!isset($question->name)) {
 450                          $question->name = $this->create_default_question_name(
 451                                  $question->questiontext, get_string('questionname', 'question'));
 452                      }
 453                      if (!isset($question->defaultmark)) {
 454                          $question->defaultmark = 1;
 455                      }
 456                      if (!isset($question->image)) {
 457                          $question->image = '';
 458                      }
 459  
 460                      // Perform sanity checks.
 461                      $questionok = true;
 462                      if (strlen($question->questiontext) == 0) {
 463                          $warnings[] = get_string('missingquestion', 'qformat_webct', $nquestionstartline);
 464                          $questionok = false;
 465                      }
 466                      if (count($question->answer) < 1) {  // A question must have at least 1 answer.
 467                          $this->error(get_string('missinganswer', 'qformat_webct', $nquestionstartline), '', $question->name);
 468                          $questionok = false;
 469                      } else {
 470                          // Create empty feedback array.
 471                          foreach ($question->answer as $key => $dataanswer) {
 472                              if (!isset($question->feedback[$key])) {
 473                                  $question->feedback[$key]['text'] = '';
 474                                  $question->feedback[$key]['format'] = FORMAT_HTML;
 475                              }
 476                          }
 477                          // This tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9.
 478                          // When question->generalfeedback is undefined, the webct feedback is added to each answer feedback.
 479                          if (isset($question->tempgeneralfeedback)) {
 480                              if (isset($question->generalfeedback)) {
 481                                  $generalfeedback = $this->text_field($question->tempgeneralfeedback);
 482                                  $question->generalfeedback = $generalfeedback['text'];
 483                                  $question->generalfeedbackformat = $generalfeedback['format'];
 484                                  if (isset($generalfeedback['itemid'])) {
 485                                      $question->genralfeedbackitemid = $generalfeedback['itemid'];
 486                                  }
 487                              } else {
 488                                  foreach ($question->answer as $key => $dataanswer) {
 489                                      if ($question->tempgeneralfeedback != '') {
 490                                          $question->feedback[$key]['text'] = $question->tempgeneralfeedback
 491                                                  .'<br/>'.$question->feedback[$key]['text'];
 492                                      }
 493                                  }
 494                              }
 495                              unset($question->tempgeneralfeedback);
 496                          }
 497                          $maxfraction = -1;
 498                          $totalfraction = 0;
 499                          foreach ($question->fraction as $fraction) {
 500                              if ($fraction > 0) {
 501                                  $totalfraction += $fraction;
 502                              }
 503                              if ($fraction > $maxfraction) {
 504                                  $maxfraction = $fraction;
 505                              }
 506                          }
 507                          switch ($question->qtype) {
 508                              case 'shortanswer':
 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                                  break;
 516  
 517                              case 'multichoice':
 518                                  $question = $this->add_blank_combined_feedback($question);
 519  
 520                                  if ($question->single) {
 521                                      if ($maxfraction != 1) {
 522                                          $maxfraction = $maxfraction * 100;
 523                                          $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter)
 524                                                  .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name);
 525                                          $questionok = false;
 526                                      }
 527                                  } else {
 528                                      $totalfraction = round($totalfraction, 2);
 529                                      if ($totalfraction != 1) {
 530                                          $totalfraction = $totalfraction * 100;
 531                                          $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter)
 532                                                  .' '.get_string('fractionsaddwrong', 'qtype_multichoice', $totalfraction),
 533                                                  '', $question->name);
 534                                          $questionok = false;
 535                                      }
 536                                  }
 537                                  break;
 538  
 539                              case 'calculated':
 540                                  foreach ($question->answer as $answer) {
 541                                      if ($formulaerror = qtype_calculated_find_formula_errors($answer)) {
 542                                          $warnings[] = "'{$question->name}': ". $formulaerror;
 543                                          $questionok = false;
 544                                      }
 545                                  }
 546                                  foreach ($question->dataset as $dataset) {
 547                                      $dataset->itemcount = count($dataset->datasetitem);
 548                                  }
 549                                  $question->import_process = true;
 550                                  break;
 551                              case 'match':
 552                                  // MDL-10680:
 553                                  // Switch subquestions and subanswers.
 554                                  $question = $this->add_blank_combined_feedback($question);
 555                                  foreach ($question->subquestions as $id => $subquestion) {
 556                                      $temp = $question->subquestions[$id];
 557                                      $question->subquestions[$id] = $question->subanswers[$id];
 558                                      $question->subanswers[$id] = $temp;
 559                                  }
 560                                  if (count($question->answer) < 3) {
 561                                      // Add a dummy missing question.
 562                                      $question->name = 'Dummy question added '.$question->name;
 563                                      $question->answer[] = 'dummy';
 564                                      $question->subanswers[] = 'dummy';
 565                                      $question->subquestions[] = 'dummy';
 566                                      $question->fraction[] = '0.0';
 567                                      $question->feedback[] = '';
 568                                  }
 569                                  break;
 570                              default:
 571                                  // No problemo.
 572                          }
 573                      }
 574  
 575                      if ($questionok) {
 576                          $questions[] = $question;    // Store it.
 577                          unset($question);            // And prepare a new one.
 578                          $question = $this->defaultquestion();
 579                      }
 580                  }
 581                  $nquestionstartline = $nlinecounter;
 582              }
 583  
 584              // Processing Question Header.
 585  
 586              if (preg_match("~^:TYPE:MC:1(.*)~i", $line, $webctoptions)) {
 587                  // Multiple Choice Question with only one good answer.
 588                  $question = $this->defaultquestion();
 589                  $question->feedback = array();
 590                  $question->qtype = 'multichoice';
 591                  $question->single = 1;        // Only one answer is allowed.
 592                  $ignorerestofquestion = false;
 593                  continue;
 594              }
 595  
 596              if (preg_match("~^:TYPE:MC:N(.*)~i", $line, $webctoptions)) {
 597                  // Multiple Choice Question with several good answers.
 598                  $question = $this->defaultquestion();
 599                  $question->feedback = array();
 600                  $question->qtype = 'multichoice';
 601                  $question->single = 0;        // Many answers allowed.
 602                  $ignorerestofquestion = false;
 603                  continue;
 604              }
 605  
 606              if (preg_match("~^:TYPE:S~i", $line)) {
 607                  // Short Answer Question.
 608                  $question = $this->defaultquestion();
 609                  $question->feedback = array();
 610                  $question->qtype = 'shortanswer';
 611                  $question->usecase = 0;       // Ignore case.
 612                  $ignorerestofquestion = false;
 613                  continue;
 614              }
 615  
 616              if (preg_match("~^:TYPE:C~i", $line)) {
 617                  // Calculated Question.
 618                  $question = $this->defaultquestion();
 619                  $question->qtype = 'calculated';
 620                  $question->answer = array(); // No problem as they go as :FORMULA: from webct.
 621                  $question->units = array();
 622                  $question->dataset = array();
 623                  $question->fraction = array('1.0');
 624                  $question->feedback = array();
 625  
 626                  $currentchoice = -1;
 627                  $ignorerestofquestion = false;
 628                  continue;
 629              }
 630  
 631              if (preg_match("~^:TYPE:M~i", $line)) {
 632                  // Match Question.
 633                  $question = $this->defaultquestion();
 634                  $question->qtype = 'match';
 635                  $question->feedback = array();
 636                  $ignorerestofquestion = false;         // Match question processing is not debugged.
 637                  continue;
 638              }
 639  
 640              if (preg_match("~^:TYPE:P~i", $line)) {
 641                  // Paragraph Question.
 642                  $question = $this->defaultquestion();
 643                  $question->qtype = 'essay';
 644                  $question->responseformat = 'editor';
 645                  $question->responserequired = 1;
 646                  $question->responsefieldlines = 15;
 647                  $question->attachments = 0;
 648                  $question->attachmentsrequired = 0;
 649                  $question->graderinfo = array(
 650                          'text' => '',
 651                          'format' => FORMAT_HTML,
 652                      );
 653                  $question->feedback = array();
 654                  $question->generalfeedback = '';
 655                  $question->generalfeedbackformat = FORMAT_HTML;
 656                  $question->generalfeedbackfiles = array();
 657                  $question->responsetemplate = $this->text_field('');
 658                  $question->questiontextformat = FORMAT_HTML;
 659                  $ignorerestofquestion = false;
 660                  // To make us pass the end-of-question sanity checks.
 661                  $question->answer = array('dummy');
 662                  $question->fraction = array('1.0');
 663                  continue;
 664              }
 665  
 666              if (preg_match("~^:TYPE:~i", $line)) {
 667                  // Unknow question type.
 668                  $warnings[] = get_string('unknowntype', 'qformat_webct', $nlinecounter);
 669                  unset($question);
 670                  $ignorerestofquestion = true;         // Question Type not handled by Moodle.
 671                  continue;
 672              }
 673  
 674              if ($ignorerestofquestion) {
 675                  continue;
 676              }
 677  
 678              if (preg_match("~^:TITLE:(.*)~i", $line, $webctoptions)) {
 679                  $name = trim($webctoptions[1]);
 680                  $question->name = $this->clean_question_name($name);
 681                  continue;
 682              }
 683  
 684              if (preg_match("~^:IMAGE:(.*)~i", $line, $webctoptions)) {
 685                  $filename = trim($webctoptions[1]);
 686                  if (preg_match("~^http://~i", $filename)) {
 687                      $question->image = $filename;
 688                  }
 689                  continue;
 690              }
 691  
 692              // Need to put the parsing of calculated items here to avoid ambitiuosness:
 693              // if question isn't defined yet there is nothing to do here (avoid notices).
 694              if (!isset($question)) {
 695                  continue;
 696              }
 697              if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match(
 698                      "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?({$webctnumberregex})~", $line, $webctoptions)) {
 699                  $datasetname = preg_replace('/^::/', '', $webctoptions[1]);
 700                  $datasetvalue = qformat_webct_convert_formula($webctoptions[4]);
 701                  switch ($webctoptions[2]) {
 702                      case 'MIN':
 703                          $question->dataset[$datasetname]->min = $datasetvalue;
 704                          break;
 705                      case 'MAX':
 706                          $question->dataset[$datasetname]->max = $datasetvalue;
 707                          break;
 708                      case 'DEC':
 709                          $datasetvalue = floor($datasetvalue); // Int only!
 710                          $question->dataset[$datasetname]->length = max(0, $datasetvalue);
 711                          break;
 712                      default:
 713                          // The VAL case.
 714                          $question->dataset[$datasetname]->datasetitem[$webctoptions[3]] = new stdClass();
 715                          $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->itemnumber = $webctoptions[3];
 716                          $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->value  = $datasetvalue;
 717                          break;
 718                  }
 719                  continue;
 720              }
 721  
 722              $bishtmltext = preg_match("~:H$~i", $line);  // True if next lines are coded in HTML.
 723              if (preg_match("~^:QUESTION~i", $line)) {
 724                  $questiontext = '';               // Start gathering next lines.
 725                  continue;
 726              }
 727  
 728              if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i", $line, $webctoptions)) { // Shortanswer.
 729                  $currentchoice = $webctoptions[1];
 730                  $answertext = $webctoptions[2];            // Start gathering next lines.
 731                  $question->fraction[$currentchoice] = ($webctoptions[3]/100);
 732                  continue;
 733              }
 734  
 735              if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i", $line, $webctoptions)) {
 736                  $answertext = '';                 // Start gathering next lines.
 737                  $currentchoice = $webctoptions[1];
 738                  $question->fraction[$currentchoice] = ($webctoptions[2]/100);
 739                  continue;
 740              }
 741  
 742              if (preg_match('~^:ANSWER:~i', $line)) { // Essay.
 743                  $graderinfo  = '';      // Start gathering next lines.
 744                  continue;
 745              }
 746  
 747              if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) {
 748                  // Answer for a calculated question.
 749                  ++$currentchoice;
 750                  $question->answer[$currentchoice] =
 751                          qformat_webct_convert_formula($webctoptions[1]);
 752  
 753                  // Default settings.
 754                  $question->fraction[$currentchoice] = 1.0;
 755                  $question->tolerance[$currentchoice] = 0.0;
 756                  $question->tolerancetype[$currentchoice] = 2; // Nominal (units in webct).
 757                  $question->feedback[$currentchoice]['text'] = '';
 758                  $question->feedback[$currentchoice]['format'] = FORMAT_HTML;
 759                  $question->correctanswerlength[$currentchoice] = 4;
 760  
 761                  $datasetnames =
 762                          question_bank::get_qtype('calculated')->find_dataset_names($webctoptions[1]);
 763                  foreach ($datasetnames as $datasetname) {
 764                      $question->dataset[$datasetname] = new stdClass();
 765                      $question->dataset[$datasetname]->datasetitem = array();
 766                      $question->dataset[$datasetname]->name = $datasetname;
 767                      $question->dataset[$datasetname]->distribution = 'uniform';
 768                      $question->dataset[$datasetname]->status = 'private';
 769                  }
 770                  continue;
 771              }
 772  
 773              if (preg_match("~^:L([0-9]+)~i", $line, $webctoptions)) {
 774                  $answertext = '';                 // Start gathering next lines.
 775                  $currentchoice = $webctoptions[1];
 776                  $question->fraction[$currentchoice] = 1;
 777                  continue;
 778              }
 779  
 780              if (preg_match("~^:R([0-9]+)~i", $line, $webctoptions)) {
 781                  $responsetext = '';                // Start gathering next lines.
 782                  $currentchoice = $webctoptions[1];
 783                  continue;
 784              }
 785  
 786              if (preg_match("~^:REASON([0-9]+):?~i", $line, $webctoptions)) {
 787                  $feedbacktext = '';               // Start gathering next lines.
 788                  $currentchoice = $webctoptions[1];
 789                  continue;
 790              }
 791              if (preg_match("~^:FEEDBACK([0-9]+):?~i", $line, $webctoptions)) {
 792                  $generalfeedbacktext = '';               // Start gathering next lines.
 793                  $currentchoice = $webctoptions[1];
 794                  continue;
 795              }
 796              if (preg_match('~^:FEEDBACK:(.*)~i', $line, $webctoptions)) {
 797                  $generalfeedbacktext = '';               // Start gathering next lines.
 798                  continue;
 799              }
 800              if (preg_match('~^:LAYOUT:(.*)~i', $line, $webctoptions)) {
 801                  // Ignore  since layout in question_multichoice  is no more used in Moodle.
 802                  // $webctoptions[1] contains either vertical or horizontal.
 803                  continue;
 804              }
 805  
 806              if (isset($question->qtype ) && 'calculated' == $question->qtype
 807                      && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webctoptions)) {
 808                  // We can but hope that this always appear before the ANSTYPE property.
 809                  $question->correctanswerlength[$currentchoice] = $webctoptions[1];
 810                  continue;
 811              }
 812  
 813              if (isset($question->qtype )&& 'calculated' == $question->qtype
 814                      && preg_match("~^:TOL:({$webctnumberregex})~i", $line, $webctoptions)) {
 815                  // We can but hope that this always appear before the TOL property.
 816                  $question->tolerance[$currentchoice] =
 817                          qformat_webct_convert_formula($webctoptions[1]);
 818                  continue;
 819              }
 820  
 821              if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) {
 822                  // Percentage case is handled as relative in Moodle.
 823                  $question->tolerance[$currentchoice]  /= 100;
 824                  $question->tolerancetype[$currentchoice] = 1; // Relative.
 825                  continue;
 826              }
 827  
 828              if (preg_match('~^:UNITS:(.+)~i', $line, $webctoptions)
 829                      and $webctunits = trim($webctoptions[1])) {
 830                  // This is a guess - I really do not know how different webct units are separated...
 831                  $webctunits = explode(':', $webctunits);
 832                  $unitrec->multiplier = 1.0; // Webct does not seem to support this.
 833                  foreach ($webctunits as $webctunit) {
 834                      $unitrec->unit = trim($webctunit);
 835                      $question->units[] = $unitrec;
 836                  }
 837                  continue;
 838              }
 839  
 840              if (!empty($question->units) && preg_match('~^:UNITREQ:(.*)~i', $line, $webctoptions)
 841                      && !$webctoptions[1]) {
 842                  // There are units but units are not required so add the no unit alternative.
 843                  // We can but hope that the UNITS property always appear before this property.
 844                  $unitrec->unit = '';
 845                  $unitrec->multiplier = 1.0;
 846                  $question->units[] = $unitrec;
 847                  continue;
 848              }
 849  
 850              if (!empty($question->units) && preg_match('~^:UNITCASE:~i', $line)) {
 851                  // This could be important but I was not able to figure out how
 852                  // it works so I ignore it for now.
 853                  continue;
 854              }
 855  
 856              if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) {
 857                  $question->correctanswerformat[$currentchoice] = '1';
 858                  continue;
 859              }
 860              if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) {
 861                  $question->correctanswerformat[$currentchoice] = '2';
 862                  continue;
 863              }
 864          }
 865  
 866          if (count($warnings) > 0) {
 867              echo '<p>'.get_string('warningsdetected', 'qformat_webct', count($warnings)).'</p><ul>';
 868              foreach ($warnings as $warning) {
 869                  echo "<li>{$warning}</li>";
 870              }
 871              echo '</ul>';
 872          }
 873          return $questions;
 874      }
 875  }