Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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.
   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   * @package    moodlecore
  19   * @subpackage backup-moodle2
  20   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  
  28  /**
  29   * Restore plugin class that provides the necessary information
  30   * needed to restore one match qtype plugin.
  31   *
  32   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class restore_qtype_match_plugin extends restore_qtype_plugin {
  36  
  37      /**
  38       * A simple answer, questiontext to id cache for a match answers.
  39       * @var array
  40       */
  41      private $questionsubcache = array();
  42  
  43      /**
  44       * The id of the current question in the questionsubcache.
  45       * @var int
  46       */
  47      private $questionsubcacheid = null;
  48  
  49  
  50      /**
  51       * Returns the paths to be handled by the plugin at question level.
  52       */
  53      protected function define_question_plugin_structure() {
  54  
  55          $paths = array();
  56  
  57          // Add own qtype stuff.
  58          $elename = 'matchoptions';
  59          // We used get_recommended_name() so this works.
  60          $elepath = $this->get_pathfor('/matchoptions');
  61          $paths[] = new restore_path_element($elename, $elepath);
  62  
  63          $elename = 'match';
  64          // We used get_recommended_name() so this works.
  65          $elepath = $this->get_pathfor('/matches/match');
  66          $paths[] = new restore_path_element($elename, $elepath);
  67  
  68          return $paths;
  69      }
  70  
  71      /**
  72       * Process the qtype/matchoptions element
  73       */
  74      public function process_matchoptions($data) {
  75          global $DB;
  76  
  77          $data = (object)$data;
  78          $oldid = $data->id;
  79  
  80          // Detect if the question is created or mapped.
  81          $oldquestionid   = $this->get_old_parentid('question');
  82          $newquestionid   = $this->get_new_parentid('question');
  83          $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
  84  
  85          // If the question has been created by restore, we need to create its qtype_match_options too.
  86          if ($questioncreated) {
  87              // Fill in some field that were added in 2.1, and so which may be missing
  88              // from backups made in older versions of Moodle.
  89              if (!isset($data->correctfeedback)) {
  90                  $data->correctfeedback = '';
  91                  $data->correctfeedbackformat = FORMAT_HTML;
  92              }
  93              if (!isset($data->partiallycorrectfeedback)) {
  94                  $data->partiallycorrectfeedback = '';
  95                  $data->partiallycorrectfeedbackformat = FORMAT_HTML;
  96              }
  97              if (!isset($data->incorrectfeedback)) {
  98                  $data->incorrectfeedback = '';
  99                  $data->incorrectfeedbackformat = FORMAT_HTML;
 100              }
 101              if (!isset($data->shownumcorrect)) {
 102                  $data->shownumcorrect = 0;
 103              }
 104  
 105              // Adjust some columns.
 106              $data->questionid = $newquestionid;
 107  
 108              // It is possible for old backup files to contain unique key violations.
 109              // We need to check to avoid that.
 110              if (!$DB->record_exists('qtype_match_options', array('questionid' => $data->questionid))) {
 111                  $newitemid = $DB->insert_record('qtype_match_options', $data);
 112                  $this->set_mapping('qtype_match_options', $oldid, $newitemid);
 113              }
 114          }
 115      }
 116  
 117      /**
 118       * Process the qtype/matches/match element
 119       */
 120      public function process_match($data) {
 121          global $DB;
 122  
 123          $data = (object)$data;
 124          $oldid = $data->id;
 125  
 126          // Detect if the question is created or mapped.
 127          $oldquestionid   = $this->get_old_parentid('question');
 128          $newquestionid   = $this->get_new_parentid('question');
 129          $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
 130  
 131          if ($questioncreated) {
 132              // If the question has been created by restore, we need to create its
 133              // qtype_match_subquestions too.
 134  
 135              // Adjust some columns.
 136              $data->questionid = $newquestionid;
 137              // Insert record.
 138              $newitemid = $DB->insert_record('qtype_match_subquestions', $data);
 139              // Create mapping (there are files and states based on this).
 140              if (isset($data->code)) {
 141                  $this->set_mapping('qtype_match_subquestion_codes', $data->code, $newitemid);
 142              }
 143  
 144          } else {
 145              // Match questions require mapping of qtype_match_subquestions, because
 146              // they are used by question_states->answer.
 147  
 148              // Have we cached the current question?
 149              if ($this->questionsubcacheid !== $newquestionid) {
 150                  // The question changed, purge and start again!
 151                  $this->questionsubcache = array();
 152  
 153                  $params = array('question' => $newquestionid);
 154                  $potentialsubs = $DB->get_records('qtype_match_subquestions',
 155                      array('questionid' => $newquestionid), '', 'id, questiontext, answertext');
 156  
 157                  $this->questionsubcacheid = $newquestionid;
 158                  // Cache all cleaned answers and questiontext.
 159                  foreach ($potentialsubs as $potentialsub) {
 160                      // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
 161                      $cleanquestion = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is',
 162                              '', $potentialsub->questiontext); // Clean CTRL chars.
 163                      $cleanquestion = preg_replace("/\r\n|\r/", "\n", $cleanquestion); // Normalize line ending.
 164  
 165                      $cleananswer = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is',
 166                              '', $potentialsub->answertext); // Clean CTRL chars.
 167                      $cleananswer = preg_replace("/\r\n|\r/", "\n", $cleananswer); // Normalize line ending.
 168  
 169                      $this->questionsubcache[$cleanquestion][$cleananswer] = $potentialsub->id;
 170                  }
 171              }
 172  
 173              if (!isset($this->questionsubcache[$data->questiontext][$data->answertext])) {
 174                  throw new restore_step_exception('error_qtype_match_subquestion_missing_in_db', $data);
 175              }
 176              $newitemid = $this->questionsubcache[$data->questiontext][$data->answertext];
 177          }
 178  
 179          // Found one. Let's create the mapping.
 180          $this->set_mapping('qtype_match_subquestions', $oldid, $newitemid);
 181      }
 182  
 183      public function recode_response($questionid, $sequencenumber, array $response) {
 184          if (array_key_exists('_stemorder', $response)) {
 185              $response['_stemorder'] = $this->recode_match_sub_order($response['_stemorder']);
 186          }
 187          if (array_key_exists('_choiceorder', $response)) {
 188              $response['_choiceorder'] = $this->recode_match_sub_order($response['_choiceorder']);
 189          }
 190          return $response;
 191      }
 192  
 193      /**
 194       * Given one question_states record, return the answer
 195       * recoded pointing to all the restored stuff for match questions.
 196       *
 197       * answer is one comma separated list of hypen separated pairs
 198       * containing question_match_sub->id and question_match_sub->code, which
 199       * has been remapped to be qtype_match_subquestions->id, since code no longer exists.
 200       */
 201      public function recode_legacy_state_answer($state) {
 202          $answer = $state->answer;
 203          $resultarr = array();
 204          foreach (explode(',', $answer) as $pair) {
 205              $pairarr = explode('-', $pair);
 206              $id = $pairarr[0];
 207              $code = $pairarr[1];
 208              $newid = $this->get_mappingid('qtype_match_subquestions', $id);
 209              if ($code) {
 210                  $newcode = $this->get_mappingid('qtype_match_subquestion_codes', $code);
 211              } else {
 212                  $newcode = $code;
 213              }
 214              $resultarr[] = $newid . '-' . $newcode;
 215          }
 216          return implode(',', $resultarr);
 217      }
 218  
 219      /**
 220       * Recode the choice order as stored in the response.
 221       * @param string $order the original order.
 222       * @return string the recoded order.
 223       */
 224      protected function recode_match_sub_order($order) {
 225          $neworder = array();
 226          foreach (explode(',', $order) as $id) {
 227              if ($newid = $this->get_mappingid('qtype_match_subquestions', $id)) {
 228                  $neworder[] = $newid;
 229              }
 230          }
 231          return implode(',', $neworder);
 232      }
 233  
 234      /**
 235       * Return the contents of this qtype to be processed by the links decoder.
 236       */
 237      public static function define_decode_contents() {
 238  
 239          $contents = array();
 240  
 241          $contents[] = new restore_decode_content('qtype_match_subquestions',
 242                  array('questiontext'), 'qtype_match_subquestions');
 243  
 244          $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
 245          $contents[] = new restore_decode_content('qtype_match_options', $fields, 'qtype_match_options');
 246  
 247          return $contents;
 248      }
 249  }