<?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 quiz;
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 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 $quiz A quiz object containing all information pertaining to current quiz. */
private $quiz;
/** @var 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.
< */
> /** @var string|null $validconfigkey Expected valid SEB config key. */
private $validconfigkey = null;
/**
* The access_manager constructor.
*
* @param quiz $quiz The details of the quiz.
*/
public function __construct(quiz $quiz) {
$this->quiz = $quiz;
$this->context = context_module::instance($quiz->get_cmid());
$this->quizsettings = quiz_settings::get_by_quiz_id($quiz->get_quizid());
$this->validconfigkey = quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());
}
/**
< * Check if the browser exam key hash in header matches one of the listed browser exam keys from quiz settings.
> * Validate browser exam key. It will validate a provided browser exam key if provided, then will fall back to checking
> * the header.
*
< * @return bool True if header key matches one of the saved keys.
> * @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_keys() : bool {
< // If browser exam keys are entered in settings, check they match the header.
< $browserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
< if (empty($browserexamkeys)) {
> 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)) {
// If the Browser Exam Key header isn't present, prevent access.
> $browserexamkey = $this->get_received_browser_exam_key();
if (is_null($this->get_received_browser_exam_key())) {
> }
return false;
>
}
> $validbrowserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
>
< if (is_null($this->get_received_browser_exam_key())) {
> if (is_null($browserexamkey)) {
< return $this->check_browser_exam_keys($browserexamkeys, $this->get_received_browser_exam_key());
> return $this->check_browser_exam_keys($validbrowserexamkeys, $browserexamkey, $url);
/**
< * Check if the config key hash in header matches quiz settings.
> * Validate a config key. It will check a provided config key if provided then will fall back to checking config
> * key in header.
*
< * @return bool True if header key matches saved key.
> * @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() : 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)) {
if (empty($this->validconfigkey)) {
> $configkey = $this->get_received_config_key();
return false; // No config key has been saved.
> }
}
>
< // If the Config Key header isn't present, prevent access.
< if (is_null($this->get_received_config_key())) {
> if (is_null($configkey)) {
return false;
}
< return $this->check_key($this->validconfigkey, $this->get_received_config_key());
> // 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()) {
if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) {
> // Config key should not be checked, so do not prevent access.
return $this->is_using_seb();
> return true;
}
> }
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 quiz
*/
public function get_quiz() : quiz {
return $this->quiz;
}
/**
> * Check that at least one browser exam key exists in the quiz settings.
* Check the hash from the request header against the permitted browser exam keys.
> *
*
> * @return bool True if one or more keys are set in quiz settings.
* @param array $keys Allowed browser exam keys.
> */
* @param string $header The value of the X-SafeExamBrowser-RequestHash to check.
> private function is_allowed_browser_examkeys_configured(): bool {
* @return bool True if the hash matches.
> return !empty($this->quizsettings->get('allowedbrowserexamkeys'));
*/
> }
private function check_browser_exam_keys(array $keys, string $header) : bool {
>
foreach ($keys as $key) {
> /**
if ($this->check_key($key, $header)) {
> * @param string|null $url URL of page to validate.
< private function check_browser_exam_keys(array $keys, string $header) : bool {
> private function check_browser_exam_keys(array $keys, string $header, ?string $url = null): bool {
< if ($this->check_key($key, $header)) {
> if ($this->check_key($key, $header, $url)) {
}
return false;
}
/**
* Check the hash from the request header against a single permitted key.
*
< * @param string $key an allowed key.
< * @param string $header the value of the X-SafeExamBrowser-RequestHash or X-SafeExamBrowser-ConfigKeyHash to check.
< * @return bool true if the hash matches.
> * @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($key, $header) : bool {
< return hash('sha256', $this->get_this_page_url() . $key) === $header;
> 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() {
> 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() {
> 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;