Autoría | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Manage the access to the quiz.** @package quizaccess_seb* @author Tim Hunt* @author Luca Bösch <luca.boesch@bfh.ch>* @author Andrew Madden <andrewmadden@catalyst-au.net>* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>* @copyright 2019 Catalyst IT* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/namespace quizaccess_seb;use context_module;use mod_quiz\quiz_settings;defined('MOODLE_INTERNAL') || die();/*** Manage the access to the quiz.** @copyright 2020 Catalyst IT* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class seb_access_manager {/** Header sent by Safe Exam Browser containing the Config Key hash. */private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH';/** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH';/** @var quiz_settings $quiz A quiz object containing all information pertaining to current quiz. */private $quiz;/** @var seb_quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */private $quizsettings;/** @var context_module $context Context of this quiz activity. */private $context;/** @var string|null $validconfigkey Expected valid SEB config key. */private $validconfigkey = null;/*** The access_manager constructor.** @param quiz_settings $quiz The details of the quiz.*/public function __construct(quiz_settings $quiz) {$this->quiz = $quiz;$this->context = context_module::instance($quiz->get_cmid());$this->quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->get_quizid());$this->validconfigkey = seb_quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());}/*** Validate browser exam key. It will validate a provided browser exam key if provided, then will fall back to checking* the header.** @param string|null $browserexamkey Optional. Can validate a provided key, or will fall back to checking header.* @param string|null $url Optionally provide URL of page to validate.* @return bool*/public function validate_browser_exam_key(?string $browserexamkey = null, ?string $url = null): bool {if (!$this->should_validate_browser_exam_key()) {// Browser exam key should not be checked, so do not prevent access.return true;}if (!$this->is_allowed_browser_examkeys_configured()) {return true; // If no browser exam keys, no check required.}if (empty($browserexamkey)) {$browserexamkey = $this->get_received_browser_exam_key();}$validbrowserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');// If the Browser Exam Key header isn't present, prevent access.if (is_null($browserexamkey)) {return false;}return $this->check_browser_exam_keys($validbrowserexamkeys, $browserexamkey, $url);}/*** Validate a config key. It will check a provided config key if provided then will fall back to checking config* key in header.** @param string|null $configkey Optional. Can validate a provided key, or will fall back to checking header.* @param string|null $url URL of page to validate.* @return bool*/public function validate_config_key(?string $configkey = null, ?string $url = null): bool {if (!$this->should_validate_config_key()) {// Config key should not be checked, so do not prevent access.return true;}// If using client config, or with no requirement, then no check required.$requiredtype = $this->get_seb_use_type();if ($requiredtype == settings_provider::USE_SEB_NO|| $requiredtype == settings_provider::USE_SEB_CLIENT_CONFIG) {return true;}if (empty($configkey)) {$configkey = $this->get_received_config_key();}if (empty($this->validconfigkey)) {return false; // No config key has been saved.}if (is_null($configkey)) {return false;}// Check if there is a valid config key supplied in the header.return $this->check_key($this->validconfigkey, $configkey, $url);}/*** Check if Safe Exam Browser is required to access quiz.* If quizsettings do not exist, then there is no requirement for using SEB.** @return bool If required.*/public function seb_required(): bool {if (!$this->quizsettings) {return false;} else {return $this->get_seb_use_type() != settings_provider::USE_SEB_NO;}}/*** This is the basic check for the Safe Exam Browser previously used in the quizaccess_safebrowser plugin that* managed basic Moodle interactions with SEB.** @return bool*/public function validate_basic_header(): bool {if (!$this->should_validate_basic_header()) {// Config key should not be checked, so do not prevent access.return true;}if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) {return $this->is_using_seb();}return true;}/*** Check if using Safe Exam Browser.** @return bool*/public function is_using_seb(): bool {if (isset($_SERVER['HTTP_USER_AGENT'])) {return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;}return false;}/*** Check if user has any capability to bypass the Safe Exam Browser requirement.** @return bool True if user can bypass check.*/public function can_bypass_seb(): bool {return has_capability('quizaccess/seb:bypassseb', $this->context);}/*** Return the full URL that was used to request the current page, which is* what we need for verifying the X-SafeExamBrowser-RequestHash header.*/private function get_this_page_url(): string {global $CFG, $FULLME;// If $FULLME not set fall back to wwwroot.if ($FULLME == null) {return $CFG->wwwroot;}return $FULLME;}/*** Return expected SEB config key.** @return string|null*/public function get_valid_config_key(): ?string {return $this->validconfigkey;}/*** Getter for the quiz object.** @return \mod_quiz\quiz_settings*/public function get_quiz(): quiz_settings {return $this->quiz;}/*** Check that at least one browser exam key exists in the quiz settings.** @return bool True if one or more keys are set in quiz settings.*/private function is_allowed_browser_examkeys_configured(): bool {return !empty($this->quizsettings->get('allowedbrowserexamkeys'));}/*** Check the hash from the request header against the permitted browser exam keys.** @param array $keys Allowed browser exam keys.* @param string $header The value of the X-SafeExamBrowser-RequestHash to check.* @param string|null $url URL of page to validate.* @return bool True if the hash matches.*/private function check_browser_exam_keys(array $keys, string $header, ?string $url = null): bool {foreach ($keys as $key) {if ($this->check_key($key, $header, $url)) {return true;}}return false;}/*** Check the hash from the request header against a single permitted key.** @param string $validkey An allowed key.* @param string $key The value of X-SafeExamBrowser-RequestHash, X-SafeExamBrowser-ConfigKeyHash or a provided key to check.* @param string|null $url URL of page to validate.* @return bool True if the hash matches.*/private function check_key(string $validkey, string $key, ?string $url = null): bool {if (empty($url)) {$url = $this->get_this_page_url();}return hash('sha256', $url . $validkey) === $key;}/*** Returns Safe Exam Browser Config Key hash.** @return string|null*/public function get_received_config_key(): ?string {if (isset($_SERVER[self::CONFIG_KEY_HEADER])) {return trim($_SERVER[self::CONFIG_KEY_HEADER]);}return null;}/*** Returns the Browser Exam Key hash.** @return string|null*/public function get_received_browser_exam_key(): ?string {if (isset($_SERVER[self::BROWSER_EXAM_KEY_HEADER])) {return trim($_SERVER[self::BROWSER_EXAM_KEY_HEADER]);}return null;}/*** Get type of SEB usage for the quiz.** @return int*/public function get_seb_use_type(): int {if (empty($this->quizsettings)) {return settings_provider::USE_SEB_NO;} else {return $this->quizsettings->get('requiresafeexambrowser');}}/*** Should validate basic header?** @return bool*/public function should_validate_basic_header(): bool {return in_array($this->get_seb_use_type(), [settings_provider::USE_SEB_CLIENT_CONFIG,]);}/*** Should validate SEB config key?* @return bool*/public function should_validate_config_key(): bool {return in_array($this->get_seb_use_type(), [settings_provider::USE_SEB_CONFIG_MANUALLY,settings_provider::USE_SEB_TEMPLATE,settings_provider::USE_SEB_UPLOAD_CONFIG,]);}/*** Should validate browser exam key?** @return bool*/public function should_validate_browser_exam_key(): bool {return in_array($this->get_seb_use_type(), [settings_provider::USE_SEB_UPLOAD_CONFIG,settings_provider::USE_SEB_CLIENT_CONFIG,]);}/*** Set session access for quiz.** @param bool $accessallowed*/public function set_session_access(bool $accessallowed): void {global $SESSION;if (!isset($SESSION->quizaccess_seb_access)) {$SESSION->quizaccess_seb_access = [];}$SESSION->quizaccess_seb_access[$this->quiz->get_cmid()] = $accessallowed;}/*** Check session access for quiz if already set.** @return bool*/public function validate_session_access(): bool {global $SESSION;return !empty($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]);}/*** Unset the global session access variable for this quiz.*/public function clear_session_access(): void {global $SESSION;unset($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]);}/*** Redirect to SEB config link. This will force Safe Exam Browser to be reconfigured.*/public function redirect_to_seb_config_link(): void {global $PAGE;$seblink = \quizaccess_seb\link_generator::get_link($this->quiz->get_cmid(), true, is_https());$PAGE->requires->js_amd_inline("document.location.replace('" . $seblink . "')");}/*** Check if we need to redirect to SEB config link.** @return bool*/public function should_redirect_to_seb_config_link(): bool {// We check if there is an existing config key header. If there is none, we assume that// the SEB application is not using header verification so auto redirect should not proceed.$haskeyinheader = !is_null($this->get_received_config_key());return $this->is_using_seb()&& get_config('quizaccess_seb', 'autoreconfigureseb')&& $haskeyinheader;}}