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 39 and 401]

   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   * Short answer question definition class.
  19   *
  20   * @package    qtype
  21   * @subpackage shortanswer
  22   * @copyright  2009 The Open University
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once($CFG->dirroot . '/question/type/questionbase.php');
  30  
  31  /**
  32   * Represents a short answer question.
  33   *
  34   * @copyright  2009 The Open University
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class qtype_shortanswer_question extends question_graded_by_strategy
  38          implements question_response_answer_comparer {
  39      /** @var boolean whether answers should be graded case-sensitively. */
  40      public $usecase;
  41      /** @var array of question_answer. */
  42      public $answers = array();
  43  
  44      public function __construct() {
  45          parent::__construct(new question_first_matching_answer_grading_strategy($this));
  46      }
  47  
  48      public function get_expected_data() {
  49          return array('answer' => PARAM_RAW_TRIMMED);
  50      }
  51  
  52      public function summarise_response(array $response) {
  53          if (isset($response['answer'])) {
  54              return $response['answer'];
  55          } else {
  56              return null;
  57          }
  58      }
  59  
  60      public function un_summarise_response(string $summary) {
  61          if (!empty($summary)) {
  62              return ['answer' => $summary];
  63          } else {
  64              return [];
  65          }
  66      }
  67  
  68      public function is_complete_response(array $response) {
  69          return array_key_exists('answer', $response) &&
  70                  ($response['answer'] || $response['answer'] === '0');
  71      }
  72  
  73      public function get_validation_error(array $response) {
  74          if ($this->is_gradable_response($response)) {
  75              return '';
  76          }
  77          return get_string('pleaseenterananswer', 'qtype_shortanswer');
  78      }
  79  
  80      public function is_same_response(array $prevresponse, array $newresponse) {
  81          return question_utils::arrays_same_at_key_missing_is_blank(
  82                  $prevresponse, $newresponse, 'answer');
  83      }
  84  
  85      public function get_answers() {
  86          return $this->answers;
  87      }
  88  
  89      public function compare_response_with_answer(array $response, question_answer $answer) {
  90          if (!array_key_exists('answer', $response) || is_null($response['answer'])) {
  91              return false;
  92          }
  93  
  94          return self::compare_string_with_wildcard(
  95                  $response['answer'], $answer->answer, !$this->usecase);
  96      }
  97  
  98      public static function compare_string_with_wildcard($string, $pattern, $ignorecase) {
  99  
 100          // Normalise any non-canonical UTF-8 characters before we start.
 101          $pattern = self::safe_normalize($pattern);
 102          $string = self::safe_normalize($string);
 103  
 104          // Break the string on non-escaped runs of asterisks.
 105          // ** is equivalent to *, but people were doing that, and with many *s it breaks preg.
 106          $bits = preg_split('/(?<!\\\\)\*+/', $pattern);
 107  
 108          // Escape regexp special characters in the bits.
 109          $escapedbits = array();
 110          foreach ($bits as $bit) {
 111              $escapedbits[] = preg_quote(str_replace('\*', '*', $bit), '|');
 112          }
 113          // Put it back together to make the regexp.
 114          $regexp = '|^' . implode('.*', $escapedbits) . '$|u';
 115  
 116          // Make the match insensitive if requested to.
 117          if ($ignorecase) {
 118              $regexp .= 'i';
 119          }
 120  
 121          return preg_match($regexp, trim($string));
 122      }
 123  
 124      /**
 125       * Normalise a UTf-8 string to FORM_C, avoiding the pitfalls in PHP's
 126       * normalizer_normalize function.
 127       * @param string $string the input string.
 128       * @return string the normalised string.
 129       */
 130      protected static function safe_normalize($string) {
 131          if ($string === '') {
 132              return '';
 133          }
 134  
 135          if (!function_exists('normalizer_normalize')) {
 136              return $string;
 137          }
 138  
 139          $normalised = normalizer_normalize($string, Normalizer::FORM_C);
 140          if (is_null($normalised)) {
 141              // An error occurred in normalizer_normalize, but we have no idea what.
 142              debugging('Failed to normalise string: ' . $string, DEBUG_DEVELOPER);
 143              return $string; // Return the original string, since it is the best we have.
 144          }
 145  
 146          return $normalised;
 147      }
 148  
 149      public function get_correct_response() {
 150          $response = parent::get_correct_response();
 151          if ($response) {
 152              $response['answer'] = $this->clean_response($response['answer']);
 153          }
 154          return $response;
 155      }
 156  
 157      public function clean_response($answer) {
 158          // Break the string on non-escaped asterisks.
 159          $bits = preg_split('/(?<!\\\\)\*/', $answer);
 160  
 161          // Unescape *s in the bits.
 162          $cleanbits = array();
 163          foreach ($bits as $bit) {
 164              $cleanbits[] = str_replace('\*', '*', $bit);
 165          }
 166  
 167          // Put it back together with spaces to look nice.
 168          return trim(implode(' ', $cleanbits));
 169      }
 170  
 171      public function check_file_access($qa, $options, $component, $filearea,
 172              $args, $forcedownload) {
 173          if ($component == 'question' && $filearea == 'answerfeedback') {
 174              $currentanswer = $qa->get_last_qt_var('answer');
 175              $answer = $this->get_matching_answer(array('answer' => $currentanswer));
 176              $answerid = reset($args); // Itemid is answer id.
 177              return $options->feedback && $answer && $answerid == $answer->id;
 178  
 179          } else if ($component == 'question' && $filearea == 'hint') {
 180              return $this->check_hint_file_access($qa, $options, $args);
 181  
 182          } else {
 183              return parent::check_file_access($qa, $options, $component, $filearea,
 184                      $args, $forcedownload);
 185          }
 186      }
 187  
 188      /**
 189       * Return the question settings that define this question as structured data.
 190       *
 191       * @param question_attempt $qa the current attempt for which we are exporting the settings.
 192       * @param question_display_options $options the question display options which say which aspects of the question
 193       * should be visible.
 194       * @return mixed structure representing the question settings. In web services, this will be JSON-encoded.
 195       */
 196      public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) {
 197          // No need to return anything, external clients do not need additional information for rendering this question type.
 198          return null;
 199      }
 200  }