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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body