Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * format.php  - Default format class for file imports/exports. Doesn't do
  20   * everything on it's own -- it needs to be extended.
  21   *
  22   * Included by import.ph
  23   *
  24   * @package mod_lesson
  25   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   **/
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * Import files embedded into answer or response
  33   *
  34   * @param string $field nfield name (answer or response)
  35   * @param array $data imported data
  36   * @param object $answer answer object
  37   * @param int $contextid
  38   **/
  39  function lesson_import_question_files($field, $data, $answer, $contextid) {
  40      global $DB;
  41      if (!isset($data['itemid'])) {
  42          return;
  43      }
  44      $text = file_save_draft_area_files($data['itemid'],
  45              $contextid, 'mod_lesson', 'page_' . $field . 's', $answer->id,
  46              array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
  47              $answer->$field);
  48  
  49      $DB->set_field("lesson_answers", $field, $text, array("id" => $answer->id));
  50  }
  51  
  52  /**
  53   * Given some question info and some data about the the answers
  54   * this function parses, organises and saves the question
  55   *
  56   * This is only used when IMPORTING questions and is only called
  57   * from format.php
  58   * Lifted from mod/quiz/lib.php -
  59   *    1. all reference to oldanswers removed
  60   *    2. all reference to quiz_multichoice table removed
  61   *    3. In shortanswer questions usecase is store in the qoption field
  62   *    4. In numeric questions store the range as two answers
  63   *    5. truefalse options are ignored
  64   *    6. For multichoice questions with more than one answer the qoption field is true
  65   *
  66   * @param object $question Contains question data like question, type and answers.
  67   * @param object $lesson
  68   * @param int $contextid
  69   * @return object Returns $result->error or $result->notice.
  70   **/
  71  function lesson_save_question_options($question, $lesson, $contextid) {
  72      global $DB;
  73  
  74      // These lines are required to ensure that all page types have
  75      // been loaded for the following switch
  76      if (!($lesson instanceof lesson)) {
  77          $lesson = new lesson($lesson);
  78      }
  79      $manager = lesson_page_type_manager::get($lesson);
  80  
  81      $timenow = time();
  82      $result = new stdClass();
  83  
  84      // Default answer to avoid code duplication.
  85      $defaultanswer = new stdClass();
  86      $defaultanswer->lessonid   = $question->lessonid;
  87      $defaultanswer->pageid = $question->id;
  88      $defaultanswer->timecreated   = $timenow;
  89      $defaultanswer->answerformat = FORMAT_HTML;
  90      $defaultanswer->jumpto = LESSON_THISPAGE;
  91      $defaultanswer->grade = 0;
  92      $defaultanswer->score = 0;
  93  
  94      switch ($question->qtype) {
  95          case LESSON_PAGE_SHORTANSWER:
  96  
  97              $answers = array();
  98              $maxfraction = -1;
  99  
 100              // Insert all the new answers
 101              foreach ($question->answer as $key => $dataanswer) {
 102                  if ($dataanswer != "") {
 103                      $answer = clone($defaultanswer);
 104                      if ($question->fraction[$key] >=0.5) {
 105                          $answer->jumpto = LESSON_NEXTPAGE;
 106                          $answer->score = 1;
 107                      }
 108                      $answer->grade = round($question->fraction[$key] * 100);
 109                      $answer->answer   = $dataanswer;
 110                      $answer->response = $question->feedback[$key]['text'];
 111                      $answer->responseformat = $question->feedback[$key]['format'];
 112                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 113                      lesson_import_question_files('response', $question->feedback[$key], $answer, $contextid);
 114                      $answers[] = $answer->id;
 115                      if ($question->fraction[$key] > $maxfraction) {
 116                          $maxfraction = $question->fraction[$key];
 117                      }
 118                  }
 119              }
 120  
 121  
 122              /// Perform sanity checks on fractional grades
 123              if ($maxfraction != 1) {
 124                  $maxfraction = $maxfraction * 100;
 125                  $result->notice = get_string("fractionsnomax", "lesson", $maxfraction);
 126                  return $result;
 127              }
 128              break;
 129  
 130          case LESSON_PAGE_NUMERICAL:   // Note similarities to shortanswer.
 131  
 132              $answers = array();
 133              $maxfraction = -1;
 134  
 135  
 136              // for each answer store the pair of min and max values even if they are the same
 137              foreach ($question->answer as $key => $dataanswer) {
 138                  if ($dataanswer != "") {
 139                      $answer = clone($defaultanswer);
 140                      if ($question->fraction[$key] >= 0.5) {
 141                          $answer->jumpto = LESSON_NEXTPAGE;
 142                          $answer->score = 1;
 143                      }
 144                      $answer->grade = round($question->fraction[$key] * 100);
 145                      $min = $question->answer[$key] - $question->tolerance[$key];
 146                      $max = $question->answer[$key] + $question->tolerance[$key];
 147                      $answer->answer   = $min.":".$max;
 148                      $answer->response = $question->feedback[$key]['text'];
 149                      $answer->responseformat = $question->feedback[$key]['format'];
 150                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 151                      lesson_import_question_files('response', $question->feedback[$key], $answer, $contextid);
 152  
 153                      $answers[] = $answer->id;
 154                      if ($question->fraction[$key] > $maxfraction) {
 155                          $maxfraction = $question->fraction[$key];
 156                      }
 157                  }
 158              }
 159  
 160              /// Perform sanity checks on fractional grades
 161              if ($maxfraction != 1) {
 162                  $maxfraction = $maxfraction * 100;
 163                  $result->notice = get_string("fractionsnomax", "lesson", $maxfraction);
 164                  return $result;
 165              }
 166          break;
 167  
 168  
 169          case LESSON_PAGE_TRUEFALSE:
 170  
 171              // In lesson the correct answer always come first, as it was the case
 172              // in question bank exports years ago.
 173              $answer = clone($defaultanswer);
 174              $answer->grade = 100;
 175              $answer->jumpto = LESSON_NEXTPAGE;
 176              $answer->score = 1;
 177              if ($question->correctanswer) {
 178                  $answer->answer = get_string("true", "lesson");
 179                  if (isset($question->feedbacktrue)) {
 180                      $answer->response = $question->feedbacktrue['text'];
 181                      $answer->responseformat = $question->feedbacktrue['format'];
 182                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 183                      lesson_import_question_files('response', $question->feedbacktrue, $answer, $contextid);
 184                  }
 185              } else {
 186                  $answer->answer = get_string("false", "lesson");
 187                  if (isset($question->feedbackfalse)) {
 188                      $answer->response = $question->feedbackfalse['text'];
 189                      $answer->responseformat = $question->feedbackfalse['format'];
 190                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 191                      lesson_import_question_files('response', $question->feedbackfalse, $answer, $contextid);
 192                  }
 193              }
 194  
 195              // Now the wrong answer.
 196              $answer = clone($defaultanswer);
 197              if ($question->correctanswer) {
 198                  $answer->answer = get_string("false", "lesson");
 199                  if (isset($question->feedbackfalse)) {
 200                      $answer->response = $question->feedbackfalse['text'];
 201                      $answer->responseformat = $question->feedbackfalse['format'];
 202                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 203                      lesson_import_question_files('response', $question->feedbackfalse, $answer, $contextid);
 204                  }
 205              } else {
 206                  $answer->answer = get_string("true", "lesson");
 207                  if (isset($question->feedbacktrue)) {
 208                      $answer->response = $question->feedbacktrue['text'];
 209                      $answer->responseformat = $question->feedbacktrue['format'];
 210                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 211                      lesson_import_question_files('response', $question->feedbacktrue, $answer, $contextid);
 212                  }
 213              }
 214  
 215            break;
 216  
 217          case LESSON_PAGE_MULTICHOICE:
 218  
 219              $totalfraction = 0;
 220              $maxfraction = -1;
 221  
 222              $answers = array();
 223  
 224              // Insert all the new answers
 225              foreach ($question->answer as $key => $dataanswer) {
 226                  if ($dataanswer != "") {
 227                      $answer = clone($defaultanswer);
 228                      $answer->grade = round($question->fraction[$key] * 100);
 229  
 230                      if ($question->single) {
 231                          if ($answer->grade > 50) {
 232                              $answer->jumpto = LESSON_NEXTPAGE;
 233                              $answer->score = 1;
 234                          }
 235                      } else {
 236                          // If multi answer allowed, any answer with fraction > 0 is considered correct.
 237                          if ($question->fraction[$key] > 0) {
 238                              $answer->jumpto = LESSON_NEXTPAGE;
 239                              $answer->score = 1;
 240                          }
 241                      }
 242                      $answer->answer   = $dataanswer['text'];
 243                      $answer->answerformat   = $dataanswer['format'];
 244                      $answer->response = $question->feedback[$key]['text'];
 245                      $answer->responseformat = $question->feedback[$key]['format'];
 246                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 247                      lesson_import_question_files('answer', $dataanswer, $answer, $contextid);
 248                      lesson_import_question_files('response', $question->feedback[$key], $answer, $contextid);
 249  
 250                      // for Sanity checks
 251                      if ($question->fraction[$key] > 0) {
 252                          $totalfraction += $question->fraction[$key];
 253                      }
 254                      if ($question->fraction[$key] > $maxfraction) {
 255                          $maxfraction = $question->fraction[$key];
 256                      }
 257                  }
 258              }
 259  
 260              /// Perform sanity checks on fractional grades
 261              if ($question->single) {
 262                  if ($maxfraction != 1) {
 263                      $maxfraction = $maxfraction * 100;
 264                      $result->notice = get_string("fractionsnomax", "lesson", $maxfraction);
 265                      return $result;
 266                  }
 267              } else {
 268                  $totalfraction = round($totalfraction,2);
 269                  if ($totalfraction != 1) {
 270                      $totalfraction = $totalfraction * 100;
 271                      $result->notice = get_string("fractionsaddwrong", "lesson", $totalfraction);
 272                      return $result;
 273                  }
 274              }
 275          break;
 276  
 277          case LESSON_PAGE_MATCHING:
 278  
 279              $subquestions = array();
 280  
 281              // The first answer should always be the correct answer
 282              $correctanswer = clone($defaultanswer);
 283              $correctanswer->answer = get_string('thatsthecorrectanswer', 'lesson');
 284              $correctanswer->jumpto = LESSON_NEXTPAGE;
 285              $correctanswer->score = 1;
 286              $DB->insert_record("lesson_answers", $correctanswer);
 287  
 288              // The second answer should always be the wrong answer
 289              $wronganswer = clone($defaultanswer);
 290              $wronganswer->answer = get_string('thatsthewronganswer', 'lesson');
 291              $DB->insert_record("lesson_answers", $wronganswer);
 292  
 293              $i = 0;
 294              // Insert all the new question+answer pairs
 295              foreach ($question->subquestions as $key => $questiontext) {
 296                  $answertext = $question->subanswers[$key];
 297                  if (!empty($questiontext) and !empty($answertext)) {
 298                      $answer = clone($defaultanswer);
 299                      $answer->answer = $questiontext['text'];
 300                      $answer->answerformat   = $questiontext['format'];
 301                      $answer->response   = $answertext;
 302                      if ($i == 0) {
 303                          // first answer contains the correct answer jump
 304                          $answer->jumpto = LESSON_NEXTPAGE;
 305                      }
 306                      $answer->id = $DB->insert_record("lesson_answers", $answer);
 307                      lesson_import_question_files('answer', $questiontext, $answer, $contextid);
 308                      $subquestions[] = $answer->id;
 309                      $i++;
 310                  }
 311              }
 312  
 313              if (count($subquestions) < 3) {
 314                  $result->notice = get_string("notenoughsubquestions", "lesson");
 315                  return $result;
 316              }
 317              break;
 318  
 319          case LESSON_PAGE_ESSAY:
 320              $answer = new stdClass();
 321              $answer->lessonid = $question->lessonid;
 322              $answer->pageid = $question->id;
 323              $answer->timecreated = $timenow;
 324              $answer->answer = null;
 325              $answer->answerformat = FORMAT_MOODLE;
 326              $answer->grade = 0;
 327              $answer->score = 1;
 328              $answer->jumpto = LESSON_NEXTPAGE;
 329              $answer->response = null;
 330              $answer->responseformat = FORMAT_MOODLE;
 331              $answer->id = $DB->insert_record("lesson_answers", $answer);
 332          break;
 333          default:
 334              $result->error = "Unsupported question type ($question->qtype)!";
 335              return $result;
 336      }
 337      return true;
 338  }
 339  
 340  
 341  class qformat_default {
 342  
 343      var $displayerrors = true;
 344      var $category = null;
 345      var $questionids = array();
 346      protected $importcontext = null;
 347      var $qtypeconvert = array('numerical'   => LESSON_PAGE_NUMERICAL,
 348                                 'multichoice' => LESSON_PAGE_MULTICHOICE,
 349                                 'truefalse'   => LESSON_PAGE_TRUEFALSE,
 350                                 'shortanswer' => LESSON_PAGE_SHORTANSWER,
 351                                 'match'       => LESSON_PAGE_MATCHING,
 352                                 'essay'       => LESSON_PAGE_ESSAY
 353                                );
 354  
 355      // Importing functions
 356      function provide_import() {
 357          return false;
 358      }
 359  
 360      function set_importcontext($context) {
 361          $this->importcontext = $context;
 362      }
 363  
 364      /**
 365       * Handle parsing error
 366       *
 367       * @param string $message information about error
 368       * @param string $text imported text that triggered the error
 369       * @param string $questionname imported question name
 370       */
 371      protected function error($message, $text='', $questionname='') {
 372          $importerrorquestion = get_string('importerrorquestion', 'question');
 373  
 374          echo "<div class=\"importerror\">\n";
 375          echo "<strong>$importerrorquestion $questionname</strong>";
 376          if (!empty($text)) {
 377              $text = s($text);
 378              echo "<blockquote>$text</blockquote>\n";
 379          }
 380          echo "<strong>$message</strong>\n";
 381          echo "</div>";
 382      }
 383  
 384      /**
 385       * Import for questiontype plugins
 386       * @param mixed $data The segment of data containing the question
 387       * @param object $question processed (so far) by standard import code if appropriate
 388       * @param object $extra mixed any additional format specific data that may be passed by the format
 389       * @param string $qtypehint hint about a question type from format
 390       * @return object question object suitable for save_options() or false if cannot handle
 391       */
 392      public function try_importing_using_qtypes($data, $question = null, $extra = null,
 393              $qtypehint = '') {
 394  
 395          return false;
 396      }
 397  
 398      function importpreprocess() {
 399          // Does any pre-processing that may be desired
 400          return true;
 401      }
 402  
 403      function importprocess($filename, $lesson, $pageid) {
 404          global $DB, $OUTPUT;
 405  
 406      /// Processes a given file.  There's probably little need to change this
 407          $timenow = time();
 408  
 409          if (! $lines = $this->readdata($filename)) {
 410              echo $OUTPUT->notification("File could not be read, or was empty");
 411              return false;
 412          }
 413  
 414          if (! $questions = $this->readquestions($lines)) {   // Extract all the questions
 415              echo $OUTPUT->notification("There are no questions in this file!");
 416              return false;
 417          }
 418  
 419          //Avoid category as question type
 420          echo $OUTPUT->notification(get_string('importcount', 'lesson',
 421                  $this->count_questions($questions)), 'notifysuccess');
 422  
 423          $count = 0;
 424          $addquestionontop = false;
 425          if ($pageid == 0) {
 426              $addquestionontop = true;
 427              $updatelessonpage = $DB->get_record('lesson_pages', array('lessonid' => $lesson->id, 'prevpageid' => 0));
 428          } else {
 429              $updatelessonpage = $DB->get_record('lesson_pages', array('lessonid' => $lesson->id, 'id' => $pageid));
 430          }
 431  
 432          $unsupportedquestions = 0;
 433  
 434          foreach ($questions as $question) {   // Process and store each question
 435              switch ($question->qtype) {
 436                  //TODO: Bad way to bypass category in data... Quickfix for MDL-27964
 437                  case 'category':
 438                      break;
 439                  // the good ones
 440                  case 'shortanswer' :
 441                  case 'numerical' :
 442                  case 'truefalse' :
 443                  case 'multichoice' :
 444                  case 'match' :
 445                  case 'essay' :
 446                      $count++;
 447  
 448                      //Show nice formated question in one line.
 449                      echo "<hr><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
 450  
 451                      $newpage = new stdClass;
 452                      $newpage->lessonid = $lesson->id;
 453                      $newpage->qtype = $this->qtypeconvert[$question->qtype];
 454                      switch ($question->qtype) {
 455                          case 'shortanswer' :
 456                              if (isset($question->usecase)) {
 457                                  $newpage->qoption = $question->usecase;
 458                              }
 459                              break;
 460                          case 'multichoice' :
 461                              if (isset($question->single)) {
 462                                  $newpage->qoption = !$question->single;
 463                              }
 464                              break;
 465                      }
 466                      $newpage->timecreated = $timenow;
 467                      if ($question->name != $question->questiontext) {
 468                          $newpage->title = $question->name;
 469                      } else {
 470                          $newpage->title = "Page $count";
 471                      }
 472                      $newpage->contents = $question->questiontext;
 473                      $newpage->contentsformat = isset($question->questionformat) ? $question->questionformat : FORMAT_HTML;
 474  
 475                      // set up page links
 476                      if ($pageid) {
 477                          // the new page follows on from this page
 478                          if (!$page = $DB->get_record("lesson_pages", array("id" => $pageid))) {
 479                              throw new \moodle_exception('invalidpageid', 'lesson');
 480                          }
 481                          $newpage->prevpageid = $pageid;
 482                          $newpage->nextpageid = $page->nextpageid;
 483                          // insert the page and reset $pageid
 484                          $newpageid = $DB->insert_record("lesson_pages", $newpage);
 485                          // update the linked list
 486                          $DB->set_field("lesson_pages", "nextpageid", $newpageid, array("id" => $pageid));
 487                      } else {
 488                          // new page is the first page
 489                          // get the existing (first) page (if any)
 490                          $params = array ("lessonid" => $lesson->id, "prevpageid" => 0);
 491                          if (!$page = $DB->get_record_select("lesson_pages", "lessonid = :lessonid AND prevpageid = :prevpageid", $params)) {
 492                              // there are no existing pages
 493                              $newpage->prevpageid = 0; // this is a first page
 494                              $newpage->nextpageid = 0; // this is the only page
 495                              $newpageid = $DB->insert_record("lesson_pages", $newpage);
 496                          } else {
 497                              // there are existing pages put this at the start
 498                              $newpage->prevpageid = 0; // this is a first page
 499                              $newpage->nextpageid = $page->id;
 500                              $newpageid = $DB->insert_record("lesson_pages", $newpage);
 501                              // update the linked list
 502                              $DB->set_field("lesson_pages", "prevpageid", $newpageid, array("id" => $page->id));
 503                          }
 504                      }
 505  
 506                      // reset $pageid and put the page ID in $question, used in save_question_option()
 507                      $pageid = $newpageid;
 508                      $question->id = $newpageid;
 509  
 510                      $this->questionids[] = $question->id;
 511  
 512                      // Import images in question text.
 513                      if (isset($question->questiontextitemid)) {
 514                          $questiontext = file_save_draft_area_files($question->questiontextitemid,
 515                                  $this->importcontext->id, 'mod_lesson', 'page_contents', $newpageid,
 516                                  null , $question->questiontext);
 517                          // Update content with recoded urls.
 518                          $DB->set_field("lesson_pages", "contents", $questiontext, array("id" => $newpageid));
 519                      }
 520  
 521                      // Now to save all the answers and type-specific options
 522  
 523                      $question->lessonid = $lesson->id; // needed for foreign key
 524                      $question->qtype = $this->qtypeconvert[$question->qtype];
 525                      $result = lesson_save_question_options($question, $lesson, $this->importcontext->id);
 526  
 527                      if (!empty($result->error)) {
 528                          echo $OUTPUT->notification($result->error);
 529                          return false;
 530                      }
 531  
 532                      if (!empty($result->notice)) {
 533                          echo $OUTPUT->notification($result->notice);
 534                          return true;
 535                      }
 536                      break;
 537              // the Bad ones
 538                  default :
 539                      $unsupportedquestions++;
 540                      break;
 541              }
 542          }
 543          // Update the prev links if there were existing pages.
 544          if (!empty($updatelessonpage)) {
 545              if ($addquestionontop) {
 546                  $DB->set_field("lesson_pages", "prevpageid", $pageid, array("id" => $updatelessonpage->id));
 547              } else {
 548                  $DB->set_field("lesson_pages", "prevpageid", $pageid, array("id" => $updatelessonpage->nextpageid));
 549              }
 550          }
 551          if ($unsupportedquestions) {
 552              echo $OUTPUT->notification(get_string('unknownqtypesnotimported', 'lesson', $unsupportedquestions));
 553          }
 554          return true;
 555      }
 556  
 557      /**
 558       * Count all non-category questions in the questions array.
 559       *
 560       * @param array questions An array of question objects.
 561       * @return int The count.
 562       *
 563       */
 564      protected function count_questions($questions) {
 565          $count = 0;
 566          if (!is_array($questions)) {
 567              return $count;
 568          }
 569          foreach ($questions as $question) {
 570              if (!is_object($question) || !isset($question->qtype) ||
 571                      ($question->qtype == 'category')) {
 572                  continue;
 573              }
 574              $count++;
 575          }
 576          return $count;
 577      }
 578  
 579      function readdata($filename) {
 580      /// Returns complete file with an array, one item per line
 581  
 582          if (is_readable($filename)) {
 583              $filearray = file($filename);
 584  
 585              /// Check for Macintosh OS line returns (ie file on one line), and fix
 586              if (preg_match("/\r/", $filearray[0]) AND !preg_match("/\n/", $filearray[0])) {
 587                  return explode("\r", $filearray[0]);
 588              } else {
 589                  return $filearray;
 590              }
 591          }
 592          return false;
 593      }
 594  
 595      protected function readquestions($lines) {
 596      /// Parses an array of lines into an array of questions,
 597      /// where each item is a question object as defined by
 598      /// readquestion().   Questions are defined as anything
 599      /// between blank lines.
 600  
 601          $questions = array();
 602          $currentquestion = array();
 603  
 604          foreach ($lines as $line) {
 605              $line = trim($line);
 606              if (empty($line)) {
 607                  if (!empty($currentquestion)) {
 608                      if ($question = $this->readquestion($currentquestion)) {
 609                          $questions[] = $question;
 610                      }
 611                      $currentquestion = array();
 612                  }
 613              } else {
 614                  $currentquestion[] = $line;
 615              }
 616          }
 617  
 618          if (!empty($currentquestion)) {  // There may be a final question
 619              if ($question = $this->readquestion($currentquestion)) {
 620                  $questions[] = $question;
 621              }
 622          }
 623  
 624          return $questions;
 625      }
 626  
 627  
 628      protected function readquestion($lines) {
 629      /// Given an array of lines known to define a question in
 630      /// this format, this function converts it into a question
 631      /// object suitable for processing and insertion into Moodle.
 632  
 633          // We should never get there unless the qformat plugin is broken.
 634          throw new coding_exception('Question format plugin is missing important code: readquestion.');
 635  
 636          return null;
 637      }
 638  
 639      /**
 640       * Construct a reasonable default question name, based on the start of the question text.
 641       * @param string $questiontext the question text.
 642       * @param string $default default question name to use if the constructed one comes out blank.
 643       * @return string a reasonable question name.
 644       */
 645      public function create_default_question_name($questiontext, $default) {
 646          $name = $this->clean_question_name(shorten_text($questiontext, 80));
 647          if ($name) {
 648              return $name;
 649          } else {
 650              return $default;
 651          }
 652      }
 653  
 654      /**
 655       * Ensure that a question name does not contain anything nasty, and will fit in the DB field.
 656       * @param string $name the raw question name.
 657       * @return string a safe question name.
 658       */
 659      public function clean_question_name($name) {
 660          $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does.
 661          $name = trim($name);
 662          $trimlength = 251;
 663          while (core_text::strlen($name) > 255 && $trimlength > 0) {
 664              $name = shorten_text($name, $trimlength);
 665              $trimlength -= 10;
 666          }
 667          return $name;
 668      }
 669  
 670      /**
 671       * return an "empty" question
 672       * Somewhere to specify question parameters that are not handled
 673       * by import but are required db fields.
 674       * This should not be overridden.
 675       * @return object default question
 676       */
 677      protected function defaultquestion() {
 678          global $CFG;
 679          static $defaultshuffleanswers = null;
 680          if (is_null($defaultshuffleanswers)) {
 681              $defaultshuffleanswers = get_config('quiz', 'shuffleanswers');
 682          }
 683  
 684          $question = new stdClass();
 685          $question->shuffleanswers = $defaultshuffleanswers;
 686          $question->defaultmark = 1;
 687          $question->image = "";
 688          $question->usecase = 0;
 689          $question->multiplier = array();
 690          $question->questiontextformat = FORMAT_MOODLE;
 691          $question->generalfeedback = '';
 692          $question->generalfeedbackformat = FORMAT_MOODLE;
 693          $question->correctfeedback = '';
 694          $question->partiallycorrectfeedback = '';
 695          $question->incorrectfeedback = '';
 696          $question->answernumbering = 'abc';
 697          $question->penalty = 0.3333333;
 698          $question->length = 1;
 699          $question->qoption = 0;
 700          $question->layout = 1;
 701  
 702          // this option in case the questiontypes class wants
 703          // to know where the data came from
 704          $question->export_process = true;
 705          $question->import_process = true;
 706  
 707          return $question;
 708      }
 709  
 710      function importpostprocess() {
 711          /// Does any post-processing that may be desired
 712          /// Argument is a simple array of question ids that
 713          /// have just been added.
 714          return true;
 715      }
 716  
 717      /**
 718       * Convert the question text to plain text, so it can safely be displayed
 719       * during import to let the user see roughly what is going on.
 720       */
 721      protected function format_question_text($question) {
 722          $formatoptions = new stdClass();
 723          $formatoptions->noclean = true;
 724          // The html_to_text call strips out all URLs, but format_text complains
 725          // if it finds @@PLUGINFILE@@ tokens. So, we need to replace
 726          // @@PLUGINFILE@@ with a real URL, but it doesn't matter what.
 727          // We use http://example.com/.
 728          $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $question->questiontext);
 729          return s(html_to_text(format_text($text,
 730                  $question->questiontextformat, $formatoptions), 0, false));
 731      }
 732  
 733      /**
 734       * Since the lesson module tries to re-use the question bank import classes in
 735       * a crazy way, this is necessary to stop things breaking.
 736       */
 737      protected function add_blank_combined_feedback($question) {
 738          return $question;
 739      }
 740  }
 741  
 742  
 743  /**
 744   * Since the lesson module tries to re-use the question bank import classes in
 745   * a crazy way, this is necessary to stop things breaking. This should be exactly
 746   * the same as the class defined in question/format.php.
 747   */
 748  class qformat_based_on_xml extends qformat_default {
 749      /**
 750       * A lot of imported files contain unwanted entities.
 751       * This method tries to clean up all known problems.
 752       * @param string str string to correct
 753       * @return string the corrected string
 754       */
 755      public function cleaninput($str) {
 756  
 757          $html_code_list = array(
 758              "&#039;" => "'",
 759              "&#8217;" => "'",
 760              "&#8220;" => "\"",
 761              "&#8221;" => "\"",
 762              "&#8211;" => "-",
 763              "&#8212;" => "-",
 764          );
 765          $str = strtr($str, $html_code_list);
 766          // Use core_text entities_to_utf8 function to convert only numerical entities.
 767          $str = core_text::entities_to_utf8($str, false);
 768          return $str;
 769      }
 770  
 771      /**
 772       * Return the array moodle is expecting
 773       * for an HTML text. No processing is done on $text.
 774       * qformat classes that want to process $text
 775       * for instance to import external images files
 776       * and recode urls in $text must overwrite this method.
 777       * @param array $text some HTML text string
 778       * @return array with keys text, format and files.
 779       */
 780      public function text_field($text) {
 781          return array(
 782              'text' => trim($text),
 783              'format' => FORMAT_HTML,
 784              'files' => array(),
 785          );
 786      }
 787  
 788      /**
 789       * Return the value of a node, given a path to the node
 790       * if it doesn't exist return the default value.
 791       * @param array xml data to read
 792       * @param array path path to node expressed as array
 793       * @param mixed default
 794       * @param bool istext process as text
 795       * @param string error if set value must exist, return false and issue message if not
 796       * @return mixed value
 797       */
 798      public function getpath($xml, $path, $default, $istext=false, $error='') {
 799          foreach ($path as $index) {
 800              if (!isset($xml[$index])) {
 801                  if (!empty($error)) {
 802                      $this->error($error);
 803                      return false;
 804                  } else {
 805                      return $default;
 806                  }
 807              }
 808  
 809              $xml = $xml[$index];
 810          }
 811  
 812          if ($istext) {
 813              if (!is_string($xml)) {
 814                  $this->error(get_string('invalidxml', 'qformat_xml'));
 815              }
 816              $xml = trim($xml);
 817          }
 818  
 819          return $xml;
 820      }
 821  }