Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * Manage the access to the quiz.
19
 *
20
 * @package    quizaccess_seb
21
 * @author     Tim Hunt
22
 * @author     Luca Bösch <luca.boesch@bfh.ch>
23
 * @author     Andrew Madden <andrewmadden@catalyst-au.net>
24
 * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
25
 * @copyright  2019 Catalyst IT
26
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 */
28
 
29
namespace quizaccess_seb;
30
 
31
use context_module;
32
use mod_quiz\quiz_settings;
33
 
34
defined('MOODLE_INTERNAL') || die();
35
 
36
/**
37
 * Manage the access to the quiz.
38
 *
39
 * @copyright  2020 Catalyst IT
40
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 */
42
class seb_access_manager {
43
 
44
    /** Header sent by Safe Exam Browser containing the Config Key hash. */
45
    private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH';
46
 
47
    /** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */
48
    private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH';
49
 
50
    /** @var quiz_settings $quiz A quiz object containing all information pertaining to current quiz. */
51
    private $quiz;
52
 
53
    /** @var seb_quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */
54
    private $quizsettings;
55
 
56
    /** @var context_module $context Context of this quiz activity. */
57
    private $context;
58
 
59
    /** @var string|null $validconfigkey Expected valid SEB config key. */
60
    private $validconfigkey = null;
61
 
62
    /**
63
     * The access_manager constructor.
64
     *
65
     * @param quiz_settings $quiz The details of the quiz.
66
     */
67
    public function __construct(quiz_settings $quiz) {
68
        $this->quiz = $quiz;
69
        $this->context = context_module::instance($quiz->get_cmid());
70
        $this->quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->get_quizid());
71
        $this->validconfigkey = seb_quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());
72
    }
73
 
74
    /**
75
     * Validate browser exam key. It will validate a provided browser exam key if provided, then will fall back to checking
76
     * the header.
77
     *
78
     * @param string|null $browserexamkey Optional. Can validate a provided key, or will fall back to checking header.
79
     * @param string|null $url Optionally provide URL of page to validate.
80
     * @return bool
81
     */
82
    public function validate_browser_exam_key(?string $browserexamkey = null, ?string $url = null): bool {
83
        if (!$this->should_validate_browser_exam_key()) {
84
            // Browser exam key should not be checked, so do not prevent access.
85
            return true;
86
        }
87
 
88
        if (!$this->is_allowed_browser_examkeys_configured()) {
89
            return true; // If no browser exam keys, no check required.
90
        }
91
 
92
        if (empty($browserexamkey)) {
93
            $browserexamkey = $this->get_received_browser_exam_key();
94
        }
95
 
96
        $validbrowserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
97
 
98
        // If the Browser Exam Key header isn't present, prevent access.
99
        if (is_null($browserexamkey)) {
100
            return false;
101
        }
102
 
103
        return $this->check_browser_exam_keys($validbrowserexamkeys, $browserexamkey, $url);
104
    }
105
 
106
    /**
107
     * Validate a config key. It will check a provided config key if provided then will fall back to checking config
108
     * key in header.
109
     *
110
     * @param string|null $configkey Optional. Can validate a provided key, or will fall back to checking header.
111
     * @param string|null $url URL of page to validate.
112
     * @return bool
113
     */
114
    public function validate_config_key(?string $configkey = null, ?string $url = null): bool {
115
        if (!$this->should_validate_config_key()) {
116
            // Config key should not be checked, so do not prevent access.
117
            return true;
118
        }
119
 
120
        // If using client config, or with no requirement, then no check required.
121
        $requiredtype = $this->get_seb_use_type();
122
        if ($requiredtype == settings_provider::USE_SEB_NO
123
                || $requiredtype == settings_provider::USE_SEB_CLIENT_CONFIG) {
124
            return true;
125
        }
126
 
127
        if (empty($configkey)) {
128
            $configkey = $this->get_received_config_key();
129
        }
130
 
131
        if (empty($this->validconfigkey)) {
132
            return false; // No config key has been saved.
133
        }
134
 
135
        if (is_null($configkey)) {
136
            return false;
137
        }
138
 
139
        // Check if there is a valid config key supplied in the header.
140
        return $this->check_key($this->validconfigkey, $configkey, $url);
141
    }
142
 
143
    /**
144
     * Check if Safe Exam Browser is required to access quiz.
145
     * If quizsettings do not exist, then there is no requirement for using SEB.
146
     *
147
     * @return bool If required.
148
     */
149
    public function seb_required(): bool {
150
        if (!$this->quizsettings) {
151
            return false;
152
        } else {
153
            return $this->get_seb_use_type() != settings_provider::USE_SEB_NO;
154
        }
155
    }
156
 
157
    /**
158
     * This is the basic check for the Safe Exam Browser previously used in the quizaccess_safebrowser plugin that
159
     * managed basic Moodle interactions with SEB.
160
     *
161
     * @return bool
162
     */
163
    public function validate_basic_header(): bool {
164
        if (!$this->should_validate_basic_header()) {
165
            // Config key should not be checked, so do not prevent access.
166
            return true;
167
        }
168
 
169
        if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) {
170
            return $this->is_using_seb();
171
        }
172
        return true;
173
    }
174
 
175
    /**
176
     * Check if using Safe Exam Browser.
177
     *
178
     * @return bool
179
     */
180
    public function is_using_seb(): bool {
181
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
182
            return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;
183
        }
184
 
185
        return false;
186
    }
187
 
188
    /**
189
     * Check if user has any capability to bypass the Safe Exam Browser requirement.
190
     *
191
     * @return bool True if user can bypass check.
192
     */
193
    public function can_bypass_seb(): bool {
194
        return has_capability('quizaccess/seb:bypassseb', $this->context);
195
    }
196
 
197
    /**
198
     * Return the full URL that was used to request the current page, which is
199
     * what we need for verifying the X-SafeExamBrowser-RequestHash header.
200
     */
201
    private function get_this_page_url(): string {
202
        global $CFG, $FULLME;
203
        // If $FULLME not set fall back to wwwroot.
204
        if ($FULLME == null) {
205
            return $CFG->wwwroot;
206
        }
207
        return $FULLME;
208
    }
209
 
210
    /**
211
     * Return expected SEB config key.
212
     *
213
     * @return string|null
214
     */
215
    public function get_valid_config_key(): ?string {
216
        return $this->validconfigkey;
217
    }
218
 
219
    /**
220
     * Getter for the quiz object.
221
     *
222
     * @return \mod_quiz\quiz_settings
223
     */
224
    public function get_quiz(): quiz_settings {
225
        return $this->quiz;
226
    }
227
 
228
    /**
229
     * Check that at least one browser exam key exists in the quiz settings.
230
     *
231
     * @return bool True if one or more keys are set in quiz settings.
232
     */
233
    private function is_allowed_browser_examkeys_configured(): bool {
234
        return !empty($this->quizsettings->get('allowedbrowserexamkeys'));
235
    }
236
 
237
    /**
238
     * Check the hash from the request header against the permitted browser exam keys.
239
     *
240
     * @param array $keys Allowed browser exam keys.
241
     * @param string $header The value of the X-SafeExamBrowser-RequestHash to check.
242
     * @param string|null $url URL of page to validate.
243
     * @return bool True if the hash matches.
244
     */
245
    private function check_browser_exam_keys(array $keys, string $header, ?string $url = null): bool {
246
        foreach ($keys as $key) {
247
            if ($this->check_key($key, $header, $url)) {
248
                return true;
249
            }
250
        }
251
        return false;
252
    }
253
 
254
    /**
255
     * Check the hash from the request header against a single permitted key.
256
     *
257
     * @param string $validkey An allowed key.
258
     * @param string $key The value of X-SafeExamBrowser-RequestHash, X-SafeExamBrowser-ConfigKeyHash or a provided key to check.
259
     * @param string|null $url URL of page to validate.
260
     * @return bool True if the hash matches.
261
     */
262
    private function check_key(string $validkey, string $key, ?string $url = null): bool {
263
        if (empty($url)) {
264
            $url = $this->get_this_page_url();
265
        }
266
        return hash('sha256', $url . $validkey) === $key;
267
    }
268
 
269
    /**
270
     * Returns Safe Exam Browser Config Key hash.
271
     *
272
     * @return string|null
273
     */
274
    public function get_received_config_key(): ?string {
275
        if (isset($_SERVER[self::CONFIG_KEY_HEADER])) {
276
            return trim($_SERVER[self::CONFIG_KEY_HEADER]);
277
        }
278
 
279
        return null;
280
    }
281
 
282
    /**
283
     * Returns the Browser Exam Key hash.
284
     *
285
     * @return string|null
286
     */
287
    public function get_received_browser_exam_key(): ?string {
288
        if (isset($_SERVER[self::BROWSER_EXAM_KEY_HEADER])) {
289
            return trim($_SERVER[self::BROWSER_EXAM_KEY_HEADER]);
290
        }
291
 
292
        return null;
293
    }
294
 
295
    /**
296
     * Get type of SEB usage for the quiz.
297
     *
298
     * @return int
299
     */
300
    public function get_seb_use_type(): int {
301
        if (empty($this->quizsettings)) {
302
            return settings_provider::USE_SEB_NO;
303
        } else {
304
            return $this->quizsettings->get('requiresafeexambrowser');
305
        }
306
    }
307
 
308
    /**
309
     * Should validate basic header?
310
     *
311
     * @return bool
312
     */
313
    public function should_validate_basic_header(): bool {
314
        return in_array($this->get_seb_use_type(), [
315
            settings_provider::USE_SEB_CLIENT_CONFIG,
316
        ]);
317
    }
318
 
319
    /**
320
     * Should validate SEB config key?
321
     * @return bool
322
     */
323
    public function should_validate_config_key(): bool {
324
        return in_array($this->get_seb_use_type(), [
325
            settings_provider::USE_SEB_CONFIG_MANUALLY,
326
            settings_provider::USE_SEB_TEMPLATE,
327
            settings_provider::USE_SEB_UPLOAD_CONFIG,
328
        ]);
329
    }
330
 
331
    /**
332
     * Should validate browser exam key?
333
     *
334
     * @return bool
335
     */
336
    public function should_validate_browser_exam_key(): bool {
337
        return in_array($this->get_seb_use_type(), [
338
            settings_provider::USE_SEB_UPLOAD_CONFIG,
339
            settings_provider::USE_SEB_CLIENT_CONFIG,
340
        ]);
341
    }
342
 
343
    /**
344
     * Set session access for quiz.
345
     *
346
     * @param bool $accessallowed
347
     */
348
    public function set_session_access(bool $accessallowed): void {
349
        global $SESSION;
350
        if (!isset($SESSION->quizaccess_seb_access)) {
351
            $SESSION->quizaccess_seb_access = [];
352
        }
353
        $SESSION->quizaccess_seb_access[$this->quiz->get_cmid()] = $accessallowed;
354
    }
355
 
356
    /**
357
     * Check session access for quiz if already set.
358
     *
359
     * @return bool
360
     */
361
    public function validate_session_access(): bool {
362
        global $SESSION;
363
        return !empty($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]);
364
    }
365
 
366
    /**
367
     * Unset the global session access variable for this quiz.
368
     */
369
    public function clear_session_access(): void {
370
        global $SESSION;
371
        unset($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]);
372
    }
373
 
374
    /**
375
     * Redirect to SEB config link. This will force Safe Exam Browser to be reconfigured.
376
     */
377
    public function redirect_to_seb_config_link(): void {
378
        global $PAGE;
379
 
380
        $seblink = \quizaccess_seb\link_generator::get_link($this->quiz->get_cmid(), true, is_https());
381
        $PAGE->requires->js_amd_inline("document.location.replace('" . $seblink . "')");
382
    }
383
 
384
    /**
385
     * Check if we need to redirect to SEB config link.
386
     *
387
     * @return bool
388
     */
389
    public function should_redirect_to_seb_config_link(): bool {
390
        // We check if there is an existing config key header. If there is none, we assume that
391
        // the SEB application is not using header verification so auto redirect should not proceed.
392
        $haskeyinheader = !is_null($this->get_received_config_key());
393
 
394
        return $this->is_using_seb()
395
                && get_config('quizaccess_seb', 'autoreconfigureseb')
396
                && $haskeyinheader;
397
    }
398
}