Merge branch 'MDL-70065_310' of https://github.com/timhunt/moodle into MOODLE_310_STABLE
[moodle.git] / question / type / shortanswer / question.php
CommitLineData
068b4594 1<?php
068b4594
TH
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
068b4594
TH
17/**
18 * Short answer question definition class.
19 *
7764183a
TH
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
068b4594
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
97562c4d 29require_once($CFG->dirroot . '/question/type/questionbase.php');
a17b297d 30
068b4594
TH
31/**
32 * Represents a short answer question.
33 *
7764183a
TH
34 * @copyright 2009 The Open University
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
068b4594
TH
36 */
37class 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() {
74c479f2 49 return array('answer' => PARAM_RAW_TRIMMED);
068b4594
TH
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
71fc74ce
SL
60 public function un_summarise_response(string $summary) {
61 if (!empty($summary)) {
62 return ['answer' => $summary];
63 } else {
64 return [];
65 }
66 }
67
068b4594
TH
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) {
b2a79cc1
TH
90 if (!array_key_exists('answer', $response) || is_null($response['answer'])) {
91 return false;
92 }
93
8cfc4fbd
TH
94 return self::compare_string_with_wildcard(
95 $response['answer'], $answer->answer, !$this->usecase);
068b4594
TH
96 }
97
98 public static function compare_string_with_wildcard($string, $pattern, $ignorecase) {
a74d924c
TH
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
5dbfbc82
TH
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
068b4594 108 // Escape regexp special characters in the bits.
03cefcc9 109 $escapedbits = array();
068b4594 110 foreach ($bits as $bit) {
03cefcc9 111 $escapedbits[] = preg_quote(str_replace('\*', '*', $bit), '|');
068b4594
TH
112 }
113 // Put it back together to make the regexp.
03cefcc9 114 $regexp = '|^' . implode('.*', $escapedbits) . '$|u';
068b4594
TH
115
116 // Make the match insensitive if requested to.
117 if ($ignorecase) {
118 $regexp .= 'i';
119 }
120
a74d924c
TH
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) {
1736fe3f 131 if ($string === '') {
a74d924c 132 return '';
5a449045
DR
133 }
134
a74d924c
TH
135 if (!function_exists('normalizer_normalize')) {
136 return $string;
137 }
138
139 $normalised = normalizer_normalize($string, Normalizer::FORM_C);
1736fe3f 140 if (is_null($normalised)) {
a74d924c
TH
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;
068b4594 147 }
7a719748 148
9eb62b33
TH
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
52ad7e0c
TH
171 public function check_file_access($qa, $options, $component, $filearea,
172 $args, $forcedownload) {
7a719748
TH
173 if ($component == 'question' && $filearea == 'answerfeedback') {
174 $currentanswer = $qa->get_last_qt_var('answer');
15dd7727 175 $answer = $this->get_matching_answer(array('answer' => $currentanswer));
3d9645ae 176 $answerid = reset($args); // Itemid is answer id.
b2a79cc1 177 return $options->feedback && $answer && $answerid == $answer->id;
7a719748
TH
178
179 } else if ($component == 'question' && $filearea == 'hint') {
93cadb1e 180 return $this->check_hint_file_access($qa, $options, $args);
7a719748
TH
181
182 } else {
8cfc4fbd
TH
183 return parent::check_file_access($qa, $options, $component, $filearea,
184 $args, $forcedownload);
7a719748
TH
185 }
186 }
068b4594 187}