Merge branch 'MDL-70065_310' of https://github.com/timhunt/moodle into MOODLE_310_STABLE
[moodle.git] / question / type / shortanswer / question.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/questionbase.php');
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();
44     public function __construct() {
45         parent::__construct(new question_first_matching_answer_grading_strategy($this));
46     }
48     public function get_expected_data() {
49         return array('answer' => PARAM_RAW_TRIMMED);
50     }
52     public function summarise_response(array $response) {
53         if (isset($response['answer'])) {
54             return $response['answer'];
55         } else {
56             return null;
57         }
58     }
60     public function un_summarise_response(string $summary) {
61         if (!empty($summary)) {
62             return ['answer' => $summary];
63         } else {
64             return [];
65         }
66     }
68     public function is_complete_response(array $response) {
69         return array_key_exists('answer', $response) &&
70                 ($response['answer'] || $response['answer'] === '0');
71     }
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     }
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     }
85     public function get_answers() {
86         return $this->answers;
87     }
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         }
94         return self::compare_string_with_wildcard(
95                 $response['answer'], $answer->answer, !$this->usecase);
96     }
98     public static function compare_string_with_wildcard($string, $pattern, $ignorecase) {
100         // Normalise any non-canonical UTF-8 characters before we start.
101         $pattern = self::safe_normalize($pattern);
102         $string = self::safe_normalize($string);
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);
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';
116         // Make the match insensitive if requested to.
117         if ($ignorecase) {
118             $regexp .= 'i';
119         }
121         return preg_match($regexp, trim($string));
122     }
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         }
135         if (!function_exists('normalizer_normalize')) {
136             return $string;
137         }
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         }
146         return $normalised;
147     }
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     }
157     public function clean_response($answer) {
158         // Break the string on non-escaped asterisks.
159         $bits = preg_split('/(?<!\\\\)\*/', $answer);
161         // Unescape *s in the bits.
162         $cleanbits = array();
163         foreach ($bits as $bit) {
164             $cleanbits[] = str_replace('\*', '*', $bit);
165         }
167         // Put it back together with spaces to look nice.
168         return trim(implode(' ', $cleanbits));
169     }
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;
179         } else if ($component == 'question' && $filearea == 'hint') {
180             return $this->check_hint_file_access($qa, $options, $args);
182         } else {
183             return parent::check_file_access($qa, $options, $component, $filearea,
184                     $args, $forcedownload);
185         }
186     }