MDL-75635 quizaccess_seb: Implement Safe Exam Browser JS API

This is a backport of MDL-72188

* Replaces checking header for SEB config keys to assess quiz access.
* Adds new web service accessible via Ajax
* Forces the use of the new API where available
* Stores access in Moodle SESSION for quiz instead of checking every
page.
This commit is contained in:
Andrew Madden
2022-10-11 08:33:30 +11:00
committed by Dmitrii Metelkin
parent b4493c2a16
commit 3a60aec87d
22 changed files with 1176 additions and 120 deletions
@@ -0,0 +1,11 @@
define("quizaccess_seb/validate_quiz_access",["exports","core/ajax","core/config","core/notification","quizaccess_seb/view"],(function(_exports,_ajax,_config,_notification,View){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Validate Safe Exam Browser access keys.
*
* @module quizaccess_seb/validate_quiz_access
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_config=_interopRequireDefault(_config),_notification=_interopRequireDefault(_notification),View=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(View),window.SafeExamBrowser=window.SafeExamBrowser||null;const safeExamBrowserKeysUpdated=function(cmid){let autoreconfigure=arguments.length>1&&void 0!==arguments[1]&&arguments[1];isQuizAccessValid(cmid).then((response=>(setTimeout(View.clearLoadingAlert,1e3),response.configkey&&response.browserexamkey?View.allowAccess():(!0===autoreconfigure&&!1===response.configkey&&reconfigureSafeExamBrowser(cmid),setTimeout(View.showValidationFailedModal,1e3)),response))).catch((err=>{_notification.default.exception(err)}))},isQuizAccessValid=cmid=>{const request={methodname:"quizaccess_seb_validate_quiz_keys",args:{cmid:cmid,url:window.location.href,configkey:window.SafeExamBrowser.security.configKey,browserexamkey:window.SafeExamBrowser.security.browserExamKey}};return _ajax.default.call([request])[0]},isKeyEmpty=key=>":"===key,reconfigureSafeExamBrowser=cmid=>{const redirecturl=_config.default.wwwroot.replace(/^http/i,"seb")+"/mod/quiz/accessrule/seb/config.php?cmid="+cmid;document.location.replace(redirecturl)};_exports.init=async function(cmid){let autoreconfigure=arguments.length>1&&void 0!==arguments[1]&&arguments[1];null!==window.SafeExamBrowser&&(await View.addLoadingAlert(),isKeyEmpty(window.SafeExamBrowser.security.configKey)&&isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)?window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated):safeExamBrowserKeysUpdated(cmid,autoreconfigure))}}));
//# sourceMappingURL=validate_quiz_access.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("quizaccess_seb/view",["exports","core/notification","core/templates","core/str","core/modal_factory"],(function(_exports,_notification,Templates,Str,ModalFactory){var obj;
/**
* Manage the quiz views.
*
* @module quizaccess_seb/view
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showValidationFailedModal=_exports.clearLoadingAlert=_exports.allowAccess=_exports.addLoadingAlert=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj},Templates=_interopRequireWildcard(Templates),Str=_interopRequireWildcard(Str),ModalFactory=_interopRequireWildcard(ModalFactory);const SELECTOR_MAIN="#region-main",SELECTOR_LOADING=".seb-loading",TEMPLATE_LOADING="quizaccess_seb/loading";_exports.allowAccess=()=>{window.location.reload()};_exports.addLoadingAlert=()=>Templates.render(TEMPLATE_LOADING,{}).then(((html,js)=>{const alertRegion=window.document.querySelector(SELECTOR_MAIN);return Templates.prependNodeContents(alertRegion,html,js)})).catch(_notification.default.exception);_exports.clearLoadingAlert=()=>{const alert=window.document.querySelector(SELECTOR_LOADING);alert&&Templates.replaceNode(alert,"","")};_exports.showValidationFailedModal=()=>{ModalFactory.create({type:ModalFactory.types.ALERT,title:Str.get_string("sebkeysvalidationfailed","quizaccess_seb"),body:Str.get_string("invalidkeys","quizaccess_seb"),large:!1}).then((modal=>{modal.show()})).fail(_notification.default.exception)}}));
//# sourceMappingURL=view.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"view.min.js","sources":["../src/view.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Manage the quiz views.\n *\n * @module quizaccess_seb/view\n * @author Andrew Madden <[email protected]>\n * @copyright 2021 Catalyst IT\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from \"core/notification\";\nimport * as Templates from \"core/templates\";\nimport * as Str from \"core/str\";\nimport * as ModalFactory from \"core/modal_factory\";\n\n/** @var SELECTOR List of CSS selectors. */\nconst SELECTOR = {\n MAIN: '#region-main',\n LOADING: '.seb-loading',\n};\n\n/** @var Template List of mustache templates. */\nconst TEMPLATE = {\n LOADING: 'quizaccess_seb/loading',\n};\n\n/**\n * Manages view when access has been granted.\n */\nexport const allowAccess = () => {\n window.location.reload();\n};\n\n/**\n * Add an alert to page to inform that Safe Exam Browser access is being checked.\n *\n * @return {Promise}\n */\nexport const addLoadingAlert = () => {\n return Templates.render(TEMPLATE.LOADING, {}).then((html, js) => {\n const alertRegion = window.document.querySelector(SELECTOR.MAIN);\n return Templates.prependNodeContents(alertRegion, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Remove the Safe Exam Browser access check alert from the page.\n */\nexport const clearLoadingAlert = () => {\n const alert = window.document.querySelector(SELECTOR.LOADING);\n if (alert) {\n Templates.replaceNode(alert, '', '');\n }\n};\n\n/**\n * Display validation failed modal.\n */\nexport const showValidationFailedModal = () => {\n ModalFactory.create({\n type: ModalFactory.types.ALERT,\n title: Str.get_string('sebkeysvalidationfailed', 'quizaccess_seb'),\n body: Str.get_string('invalidkeys', 'quizaccess_seb'),\n large: false,\n }).then((modal) => {\n modal.show();\n }).fail(Notification.exception);\n};\n"],"names":["SELECTOR","TEMPLATE","window","location","reload","Templates","render","then","html","js","alertRegion","document","querySelector","prependNodeContents","catch","Notification","exception","alert","replaceNode","ModalFactory","create","type","types","ALERT","title","Str","get_string","body","large","modal","show","fail"],"mappings":";;;;;;;;kzCA8BMA,cACI,eADJA,iBAEO,eAIPC,iBACO,8CAMc,KACvBC,OAAOC,SAASC,mCAQW,IACpBC,UAAUC,OAAOL,iBAAkB,IAAIM,MAAK,CAACC,KAAMC,YAChDC,YAAcR,OAAOS,SAASC,cAAcZ,sBAC3CK,UAAUQ,oBAAoBH,YAAaF,KAAMC,OACzDK,MAAMC,sBAAaC,sCAMO,WACvBC,MAAQf,OAAOS,SAASC,cAAcZ,kBACxCiB,OACAZ,UAAUa,YAAYD,MAAO,GAAI,wCAOA,KACrCE,aAAaC,OAAO,CAChBC,KAAMF,aAAaG,MAAMC,MACzBC,MAAOC,IAAIC,WAAW,0BAA2B,kBACjDC,KAAMF,IAAIC,WAAW,cAAe,kBACpCE,OAAO,IACRrB,MAAMsB,QACLA,MAAMC,UACPC,KAAKhB,sBAAaC"}
@@ -0,0 +1,121 @@
// 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/>.
/**
* Validate Safe Exam Browser access keys.
*
* @module quizaccess_seb/validate_quiz_access
* @author Andrew Madden <[email protected]>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import Config from 'core/config';
import Notification from "core/notification";
import * as View from 'quizaccess_seb/view';
// SafeExamBrowser object will be automatically initialized if using the SafeExamBrowser application.
window.SafeExamBrowser = window.SafeExamBrowser || null;
/**
* Once the keys are fetched, action checking access.
*
* @param {init} cmid Value of course module id of the quiz.
* @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb.
*/
const safeExamBrowserKeysUpdated = (cmid, autoreconfigure = false) => {
// Action opening up the quiz.
isQuizAccessValid(cmid).then((response) => {
// Show the alert for an extra second to allow user to see it.
setTimeout(View.clearLoadingAlert, 1000);
if (response.configkey && response.browserexamkey) {
View.allowAccess();
} else {
// If autoreconfigureseb is enabled, attempt to reconfigure page with quiz settings.
if (autoreconfigure === true && response.configkey === false) {
reconfigureSafeExamBrowser(cmid);
}
setTimeout(View.showValidationFailedModal, 1000);
}
return response;
}).catch(err => {
Notification.exception(err);
});
};
/**
* Validate keys in Moodle backend.
*
* @param {init} cmid Value of course module id of the quiz.
* @return {Promise}
*/
const isQuizAccessValid = (cmid) => {
const request = {
methodname: 'quizaccess_seb_validate_quiz_keys',
args: {
cmid: cmid,
url: window.location.href,
configkey: window.SafeExamBrowser.security.configKey,
browserexamkey: window.SafeExamBrowser.security.browserExamKey
},
};
return Ajax.call([request])[0];
};
/**
* Check if the key is not yet set.
*
* @param {string} key config key or browser exam key.
* @return {boolean}
*/
const isKeyEmpty = (key) => {
// If the SafeExamBrowser object is defined, the default 'empty' value of the configKey and browserExamKey is ':'.
return key === ":";
};
/**
* Reload Safe Exam Browser with current quiz configuration.
*
* @param {init} cmid Value of course module id of the quiz.
*/
const reconfigureSafeExamBrowser = (cmid) => {
const domain = Config.wwwroot.replace(/^http/i, 'seb');
const redirecturl = domain + '/mod/quiz/accessrule/seb/config.php?cmid=' + cmid;
document.location.replace(redirecturl);
};
/**
* Initialize the process of fetching the keys.
*
* @param {init} cmid Value of course module id of the quiz.
* @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb.
*/
export const init = async(cmid, autoreconfigure = false) => {
// If the SafeExamBrowser object is instantiated, try and use it to fetch the access keys.
if (window.SafeExamBrowser !== null) {
await View.addLoadingAlert();
// If the SEB keys are already set, we can call our callback directly.
if (!isKeyEmpty(window.SafeExamBrowser.security.configKey) || !isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)) {
safeExamBrowserKeysUpdated(cmid, autoreconfigure);
} else {
window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated);
}
}
};
+82
View File
@@ -0,0 +1,82 @@
// 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 quiz views.
*
* @module quizaccess_seb/view
* @author Andrew Madden <[email protected]>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Notification from "core/notification";
import * as Templates from "core/templates";
import * as Str from "core/str";
import * as ModalFactory from "core/modal_factory";
/** @var SELECTOR List of CSS selectors. */
const SELECTOR = {
MAIN: '#region-main',
LOADING: '.seb-loading',
};
/** @var Template List of mustache templates. */
const TEMPLATE = {
LOADING: 'quizaccess_seb/loading',
};
/**
* Manages view when access has been granted.
*/
export const allowAccess = () => {
window.location.reload();
};
/**
* Add an alert to page to inform that Safe Exam Browser access is being checked.
*
* @return {Promise}
*/
export const addLoadingAlert = () => {
return Templates.render(TEMPLATE.LOADING, {}).then((html, js) => {
const alertRegion = window.document.querySelector(SELECTOR.MAIN);
return Templates.prependNodeContents(alertRegion, html, js);
}).catch(Notification.exception);
};
/**
* Remove the Safe Exam Browser access check alert from the page.
*/
export const clearLoadingAlert = () => {
const alert = window.document.querySelector(SELECTOR.LOADING);
if (alert) {
Templates.replaceNode(alert, '', '');
}
};
/**
* Display validation failed modal.
*/
export const showValidationFailedModal = () => {
ModalFactory.create({
type: ModalFactory.types.ALERT,
title: Str.get_string('sebkeysvalidationfailed', 'quizaccess_seb'),
body: Str.get_string('invalidkeys', 'quizaccess_seb'),
large: false,
}).then((modal) => {
modal.show();
}).fail(Notification.exception);
};
@@ -56,8 +56,7 @@ class access_manager {
/** @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;
/**
@@ -73,31 +72,51 @@ class access_manager {
}
/**
* 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)) {
$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($this->get_received_browser_exam_key())) {
if (is_null($browserexamkey)) {
return false;
}
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
@@ -105,16 +124,20 @@ class access_manager {
return true;
}
if (empty($configkey)) {
$configkey = $this->get_received_config_key();
}
if (empty($this->validconfigkey)) {
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);
}
/**
@@ -137,7 +160,12 @@ class access_manager {
*
* @return bool
*/
public function validate_basic_header() : 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();
}
@@ -149,7 +177,7 @@ class access_manager {
*
* @return bool
*/
public function is_using_seb() : bool {
public function is_using_seb(): bool {
if (isset($_SERVER['HTTP_USER_AGENT'])) {
return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;
}
@@ -162,7 +190,7 @@ class access_manager {
*
* @return bool True if user can bypass check.
*/
public function can_bypass_seb() : bool {
public function can_bypass_seb(): bool {
return has_capability('quizaccess/seb:bypassseb', $this->context);
}
@@ -170,7 +198,7 @@ class access_manager {
* 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 {
private function get_this_page_url(): string {
global $CFG, $FULLME;
// If $FULLME not set fall back to wwwroot.
if ($FULLME == null) {
@@ -184,7 +212,7 @@ class access_manager {
*
* @return string|null
*/
public function get_valid_config_key() : ?string {
public function get_valid_config_key(): ?string {
return $this->validconfigkey;
}
@@ -197,16 +225,26 @@ class access_manager {
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) : bool {
private function check_browser_exam_keys(array $keys, string $header, ?string $url = null): bool {
foreach ($keys as $key) {
if ($this->check_key($key, $header)) {
if ($this->check_key($key, $header, $url)) {
return true;
}
}
@@ -216,12 +254,16 @@ class access_manager {
/**
* 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;
}
/**
@@ -229,7 +271,7 @@ class access_manager {
*
* @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]);
}
@@ -242,7 +284,7 @@ class access_manager {
*
* @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]);
}
@@ -255,7 +297,7 @@ class access_manager {
*
* @return int
*/
public function get_seb_use_type() : int {
public function get_seb_use_type(): int {
if (empty($this->quizsettings)) {
return settings_provider::USE_SEB_NO;
} else {
@@ -268,7 +310,7 @@ class access_manager {
*
* @return bool
*/
public function should_validate_basic_header() : bool {
public function should_validate_basic_header(): bool {
return in_array($this->get_seb_use_type(), [
settings_provider::USE_SEB_CLIENT_CONFIG,
]);
@@ -278,7 +320,7 @@ class access_manager {
* Should validate SEB config key?
* @return bool
*/
public function should_validate_config_key() : 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,
@@ -291,10 +333,66 @@ class access_manager {
*
* @return bool
*/
public function should_validate_browser_exam_key() : 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;
}
}
@@ -46,16 +46,20 @@ class access_prevented extends base {
*
* @param access_manager $accessmanager Access manager.
* @param string $reason Reason that access was prevented.
* @param string|null $configkey A Safe Exam Browser config key.
* @param string|null $browserexamkey A Safe Exam Browser browser exam key.
* @return base
*/
public static function create_strict(access_manager $accessmanager, string $reason) : base {
public static function create_strict(access_manager $accessmanager, string $reason,
?string $configkey = null, ?string $browserexamkey = null) : base {
global $USER;
$other = [];
$other['reason'] = $reason;
$other['savedconfigkey'] = $accessmanager->get_valid_config_key();
$other['receivedconfigkey'] = $accessmanager->get_received_config_key();
$other['receivedbrowserexamkey'] = $accessmanager->get_received_browser_exam_key();
$other['receivedconfigkey'] = !empty($configkey) ? $configkey : $accessmanager->get_received_config_key();
$other['receivedbrowserexamkey'] = !empty($browserexamkey) ? $browserexamkey
: $accessmanager->get_received_browser_exam_key();
return self::create([
'userid' => $USER->id,
@@ -120,4 +124,4 @@ class access_prevented extends base {
'cmid' => ['db' => 'course_modules', 'restore' => 'course_modules']
];
}
}
}
@@ -0,0 +1,155 @@
<?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/>.
namespace quizaccess_seb\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
use invalid_parameter_exception;
use quiz;
use quizaccess_seb\event\access_prevented;
use quizaccess_seb\access_manager;
require_once($CFG->dirroot . '/mod/quiz/accessmanager.php');
require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
require_once($CFG->libdir . '/externallib.php');
/**
* Validate browser exam key and config key.
*
* @package quizaccess_seb
* @author Andrew Madden <[email protected]>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class validate_quiz_keys extends external_api {
/**
* External function parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module ID',
VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
'url' => new external_value(PARAM_URL, 'Page URL to check',
VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
'configkey' => new external_value(PARAM_ALPHANUMEXT, 'SEB config key',
VALUE_DEFAULT, null),
'browserexamkey' => new external_value(PARAM_ALPHANUMEXT, 'SEB browser exam key',
VALUE_DEFAULT, null),
]);
}
/**
* Validate a SEB config key or browser exam key.
*
* @param string $cmid Course module ID.
* @param string $url URL of the page on which the SEB JS API generated the keys.
* @param string|null $configkey A SEB config key hash. Includes URL in the hash.
* @param string|null $browserexamkey A SEB browser exam key hash. Includes the URL in the hash.
* @return array
*/
public static function execute(string $cmid, string $url, ?string $configkey = null, ?string $browserexamkey = null): array {
list(
'cmid' => $cmid,
'url' => $url,
'configkey' => $configkey,
'browserexamkey' => $browserexamkey
) = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'url' => $url,
'configkey' => $configkey,
'browserexamkey' => $browserexamkey,
]);
self::validate_context(\context_module::instance($cmid));
// At least one SEB key must be provided.
if (empty($configkey) && empty($browserexamkey)) {
throw new invalid_parameter_exception(get_string('error:ws:nokeyprovided', 'quizaccess_seb'));
}
// Check quiz exists corresponding to cmid.
if (($quizid = self::get_quiz_id($cmid)) === 0) {
throw new invalid_parameter_exception(get_string('error:ws:quiznotexists', 'quizaccess_seb', $cmid));
}
$result = ['configkey' => true, 'browserexamkey' => true];
$accessmanager = new access_manager(quiz::create($quizid));
// Check if there is a valid config key.
if (!$accessmanager->validate_config_key($configkey, $url)) {
access_prevented::create_strict($accessmanager, get_string('invalid_config_key', 'quizaccess_seb'),
$configkey, $browserexamkey)->trigger();
$result['configkey'] = false;
}
// Check if there is a valid browser exam key.
if (!$accessmanager->validate_browser_exam_key($browserexamkey, $url)) {
access_prevented::create_strict($accessmanager, get_string('invalid_browser_key', 'quizaccess_seb'),
$configkey, $browserexamkey)->trigger();
$result['browserexamkey'] = false;
}
if ($result['configkey'] && $result['browserexamkey']) {
// Set the state of the access for this Moodle session.
$accessmanager->set_session_access(true);
}
return $result;
}
/**
* External function returns.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'configkey' => new external_value(PARAM_BOOL, 'Is a provided config key valid?',
VALUE_REQUIRED, 0, NULL_NOT_ALLOWED),
'browserexamkey' => new external_value(PARAM_BOOL, 'Is a provided browser exam key valid?',
VALUE_REQUIRED, 0, NULL_NOT_ALLOWED)
]);
}
/**
* Check if there is a valid quiz corresponding to a course module it.
*
* @param string $cmid Course module ID.
* @return int Returns quiz id if cmid matches valid quiz, or 0 if there is no match.
*/
private static function get_quiz_id(string $cmid): int {
$quizid = 0;
$coursemodule = get_coursemodule_from_id('quiz', $cmid);
if (!empty($coursemodule)) {
$quizid = $coursemodule->instance;
}
return $quizid;
}
}
@@ -516,6 +516,11 @@ class quiz_settings extends persistent {
$quizurl = new moodle_url($CFG->wwwroot . "/mod/quiz/view.php", ['id' => $this->get('cmid')]);
$this->plist->set_or_update_value('startURL', new CFString($quizurl->out(true)));
$this->plist->set_or_update_value('sendBrowserExamKey', new CFBoolean(true));
// Use the modern WebView and JS API if the SEB version supports it.
// Documentation: https://safeexambrowser.org/developer/seb-config-key.html .
// "Set the key browserWindowWebView to the policy "Prefer Modern" (value 3)".
$this->plist->set_or_update_value('browserWindowWebView', new CFNumber(3));
}
/**
+36
View File
@@ -0,0 +1,36 @@
<?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/>.
/**
* List web services and external functions for plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <[email protected]>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$functions = [
'quizaccess_seb_validate_quiz_keys' => [
'classname' => 'quizaccess_seb\external\validate_quiz_keys',
'methodname' => 'execute',
'description' => 'Validate a Safe Exam Browser config key or a browser exam key.',
'type' => 'read',
'ajax' => true,
],
];
@@ -35,6 +35,7 @@ $string['cachedef_configkey'] = 'SEB config key cache';
$string['cachedef_quizsettings'] = 'SEB quiz settings cache';
$string['cantdelete'] = 'The template can\'t be deleted as it has been used for one or more quizzes.';
$string['cantedit'] = 'The template can\'t be edited as it has been used for one or more quizzes.';
$string['checkingaccess'] = 'Checking access to Safe Exam Browser...';
$string['clientrequiresseb'] = 'This quiz has been configured to use the Safe Exam Browser with client configuration.';
$string['confirmtemplateremovalquestion'] = 'Are you sure you want to remove this template?';
$string['confirmtemplateremovaltitle'] = 'Confirm template removal?';
@@ -47,6 +48,8 @@ $string['downloadsebconfig'] = 'Download SEB config file';
$string['duplicatetemplate'] = 'A template with the same name already exists.';
$string['edittemplate'] = 'Edit template';
$string['enabled'] = 'Enabled';
$string['error:ws:nokeyprovided'] = 'At least one SEB key must be provided.';
$string['error:ws:quiznotexists'] = 'Quiz not found matching course module id: {$a}';
$string['event:accessprevented'] = "Quiz access was prevented";
$string['event:templatecreated'] = 'SEB template was created';
$string['event:templatedeleted'] = 'SEB template was deleted';
@@ -177,6 +180,7 @@ $string['seb_use_upload'] = 'Yes – Upload my own config';
$string['seb_userconfirmquit'] = 'Ask user to confirm quitting';
$string['seb_userconfirmquit_help'] = 'If enabled, users have to confirm quitting of SEB when a quit link is detected.';
$string['sebdownloadbutton'] = 'Download Safe Exam Browser';
$string['sebkeysvalidationfailed'] = 'SEB keys validation failed';
$string['seblinkbutton'] = 'Launch Safe Exam Browser';
$string['sebrequired'] = "This quiz has been configured so that students may only attempt it using the Safe Exam Browser.";
$string['setting:autoreconfigureseb'] = 'Auto-configure SEB';
+25 -24
View File
@@ -298,25 +298,33 @@ class quizaccess_seb extends quiz_access_rule_base {
$PAGE->set_pagelayout('secure');
$this->prevent_display_blocks();
if ($this->accessmanager->should_validate_basic_header() && !$this->accessmanager->validate_basic_header()) {
// Access has previously been validated for this session and quiz.
if ($this->accessmanager->validate_session_access()) {
return false;
}
if (!$this->accessmanager->validate_basic_header()) {
access_prevented::create_strict($this->accessmanager, $this->get_reason_text('not_seb'))->trigger();
return $this->get_require_seb_error_message();
}
if ($this->accessmanager->should_validate_config_key() && !$this->accessmanager->validate_config_key()) {
if ($this->should_redirect_to_seb_config_link()) {
$this->redirect_to_seb_config_link();
if (!$this->accessmanager->validate_config_key()) {
if ($this->accessmanager->should_redirect_to_seb_config_link()) {
$this->accessmanager->redirect_to_seb_config_link();
}
access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_config_key'))->trigger();
return $this->get_invalid_key_error_message();
}
if ($this->accessmanager->should_validate_browser_exam_key() && !$this->accessmanager->validate_browser_exam_keys()) {
if (!$this->accessmanager->validate_browser_exam_key()) {
access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_browser_key'))->trigger();
return $this->get_invalid_key_error_message();
}
// Set the state of the access for this Moodle session.
$this->accessmanager->set_session_access(true);
return false;
}
@@ -431,6 +439,8 @@ class quizaccess_seb extends quiz_access_rule_base {
* (may be '' if no message is appropriate).
*/
public function description() : array {
global $PAGE;
$messages = [get_string('sebrequired', 'quizaccess_seb')];
// Display download SEB config link for those who can bypass using SEB.
@@ -441,6 +451,9 @@ class quizaccess_seb extends quiz_access_rule_base {
// Those with higher level access will be able to see the button if they've made an attempt.
if (!$this->prevent_access()) {
$messages[] = $this->display_buttons($this->get_quit_button());
} else {
$PAGE->requires->js_call_amd('quizaccess_seb/validate_quiz_access', 'init',
[$this->quiz->cmid, (bool)get_config('quizaccess_seb', 'autoreconfigureseb')]);
}
return $messages;
@@ -459,6 +472,13 @@ class quizaccess_seb extends quiz_access_rule_base {
$page->set_pagelayout('secure');
}
/**
* This is called when the current attempt at the quiz is finished.
*/
public function current_attempt_finished() {
$this->accessmanager->clear_session_access();
}
/**
* Prepare buttons HTML code for being displayed on the screen.
*
@@ -575,23 +595,4 @@ class quizaccess_seb extends quiz_access_rule_base {
private function should_display_download_seb_link() : bool {
return !empty($this->quiz->seb_showsebdownloadlink);
}
/**
* Redirect to SEB config link. This will force Safe Exam Browser to be reconfigured.
*/
private function redirect_to_seb_config_link() {
global $PAGE;
$seblink = \quizaccess_seb\link_generator::get_link($this->quiz->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
*/
private function should_redirect_to_seb_config_link() : bool {
return $this->accessmanager->is_using_seb() && get_config('quizaccess_seb', 'autoreconfigureseb');
}
}
@@ -0,0 +1,34 @@
{{!
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/>.
}}
{{!
@template quizaccess_seb/loading
Loading container.
Example context (json):
{
"extraclasses": "class"
}
}}
<div class="seb-loading alert alert-info fade in {{ extraclasses }}" role="alert" data-aria-autofocus="true">
<div class="d-flex justify-content-between">
<span>
{{#str}}checkingaccess, quizaccess_seb{{/str}}
</span>
{{> core/loading }}
</div>
</div>
@@ -27,6 +27,7 @@ require_once(__DIR__ . '/test_helper_trait.php');
* @author Andrew Madden <[email protected]>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quizaccess_seb\access_manager
*/
class access_manager_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
@@ -95,6 +96,24 @@ class access_manager_test extends \advanced_testcase {
$this->assertTrue($accessmanager->can_bypass_seb());
}
/**
* Test that user has capability to bypass SEB check.
*/
public function test_admin_user_can_bypass_seb_check() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
// Test normal user cannot bypass check.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->can_bypass_seb());
// Test with admin user.
$this->setAdminUser();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->can_bypass_seb());
}
/**
* Test user does not have capability to bypass SEB check.
*/
@@ -141,10 +160,23 @@ class access_manager_test extends \advanced_testcase {
$expectedhash = hash('sha256', $FULLME . $configkey);
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
$this->assertTrue($accessmanager->validate_browser_exam_keys());
$this->assertTrue($accessmanager->validate_config_key());
}
/**
* Test that the quiz Config Key matches a provided config key with no incoming request header.
*/
public function test_access_keys_validate_with_provided_config_key() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$url = 'https://www.example.com/moodle';
$accessmanager = $this->get_access_manager();
$configkey = quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key();
$fullconfigkey = hash('sha256', $url . $configkey);
$this->assertTrue($accessmanager->validate_config_key($fullconfigkey, $url));
}
/**
* Test that the quiz Config Key does not match the incoming request header.
*/
@@ -153,7 +185,6 @@ class access_manager_test extends \advanced_testcase {
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->validate_config_key());
$this->assertTrue($accessmanager->validate_browser_exam_keys());
}
/**
@@ -162,22 +193,21 @@ class access_manager_test extends \advanced_testcase {
public function test_config_key_not_checked_if_client_requirement_is_selected() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_config_key());
$this->assertTrue($accessmanager->validate_browser_exam_keys());
$this->assertFalse($accessmanager->should_validate_config_key());
}
/**
* Test that if there are no browser exam keys for quiz, check is skipped.
*/
public function test_no_browser_exam_keys_cause_check_to_be_skipped() {
public function test_no_browser_exam_keys_cause_check_to_be_successful() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
$settings->set('allowedbrowserexamkeys', '');
$settings->save();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_config_key());
$this->assertTrue($accessmanager->validate_browser_exam_keys());
$this->assertTrue($accessmanager->should_validate_browser_exam_key());
$this->assertTrue($accessmanager->validate_browser_exam_key());
}
/**
@@ -190,8 +220,7 @@ class access_manager_test extends \advanced_testcase {
$settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
$settings->save();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_config_key());
$this->assertFalse($accessmanager->validate_browser_exam_keys());
$this->assertFalse($accessmanager->validate_browser_exam_key());
}
/**
@@ -205,8 +234,7 @@ class access_manager_test extends \advanced_testcase {
$settings->save();
$accessmanager = $this->get_access_manager();
$_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = hash('sha256', 'notwhatyouwereexpectinghuh');
$this->assertTrue($accessmanager->validate_config_key());
$this->assertFalse($accessmanager->validate_browser_exam_keys());
$this->assertFalse($accessmanager->validate_browser_exam_key());
}
/**
@@ -226,8 +254,23 @@ class access_manager_test extends \advanced_testcase {
$FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
$expectedhash = hash('sha256', $FULLME . $browserexamkey);
$_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash;
$this->assertTrue($accessmanager->validate_config_key());
$this->assertTrue($accessmanager->validate_browser_exam_keys());
$this->assertTrue($accessmanager->validate_browser_exam_key());
}
/**
* Test that browser exam key matches a provided browser exam key.
*/
public function test_browser_exam_keys_match_provided_browser_exam_key() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$url = 'https://www.example.com/moodle';
$settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
$browserexamkey = hash('sha256', 'browserexamkey');
$fullbrowserexamkey = hash('sha256', $url . $browserexamkey);
$settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK.
$settings->save();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_browser_exam_key($fullbrowserexamkey, $url));
}
/**
@@ -441,4 +484,105 @@ class access_manager_test extends \advanced_testcase {
}
/**
* Test if config key should not be validated.
*/
public function test_if_config_key_should_not_be_validated() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_config_key());
}
/**
* Test if browser exam key should not be validated.
*/
public function test_if_browser_exam_key_should_not_be_validated() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_browser_exam_key());
}
/**
* Test that access is set correctly in Moodle session.
*/
public function test_set_session_access() {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid]));
$accessmanager->set_session_access(true);
$this->assertTrue($SESSION->quizaccess_seb_access[$this->quiz->cmid]);
}
/**
* Test that access is set in Moodle session for only course module associated with access manager.
*/
public function test_session_access_set_for_specific_course_module() {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$quiz2 = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$accessmanager->set_session_access(true);
$this->assertCount(1, $SESSION->quizaccess_seb_access);
$this->assertTrue($SESSION->quizaccess_seb_access[$this->quiz->cmid]);
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$quiz2->cmid]));
}
/**
* Test that access state can be retrieved from Moodle session.
*/
public function test_validate_session_access() {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertEmpty($accessmanager->validate_session_access());
$SESSION->quizaccess_seb_access[$this->quiz->cmid] = true;
$this->assertTrue($accessmanager->validate_session_access());
}
/**
* Test that access can be cleared from Moodle session.
*/
public function test_clear_session_access() {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$SESSION->quizaccess_seb_access[$this->quiz->cmid] = true;
$accessmanager->clear_session_access();
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid]));
}
/**
* Test we can decide if need to redirect to SEB config link.
*/
public function test_should_redirect_to_seb_config_link() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
set_config('autoreconfigureseb', '1', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$this->assertFalse($accessmanager->should_redirect_to_seb_config_link());
set_config('autoreconfigureseb', '1', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = hash('sha256', 'configkey');
$this->assertTrue($accessmanager->should_redirect_to_seb_config_link());
}
}
@@ -45,6 +45,8 @@ class events_test extends \advanced_testcase {
/**
* Test creating the access_prevented event.
*
* @covers \quizaccess_seb\event\access_prevented
*/
public function test_event_access_prevented() {
$this->resetAfterTest();
@@ -91,8 +93,58 @@ class events_test extends \advanced_testcase {
$this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']);
}
/**
* Test creating the access_prevented event with provided SEB keys.
*
* @covers \quizaccess_seb\event\access_prevented
*/
public function test_event_access_prevented_with_keys() {
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = new \quizaccess_seb\access_manager(new quiz($quiz,
get_coursemodule_from_id('quiz', $quiz->cmid), $this->course));
// Set up event with data.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$event = \quizaccess_seb\event\access_prevented::create_strict($accessmanager, 'Because I said so.',
'configkey', 'browserexamkey');
// Create an event sink, trigger event and retrieve event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertEquals(1, count($events));
$event = reset($events);
$expectedconfigkey = $accessmanager->get_valid_config_key();
// Test that the event data is as expected.
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertEquals('Quiz access was prevented', $event->get_name());
$this->assertEquals(
"The user with id '$user->id' has been prevented from accessing quiz with id '$quiz->id' by the "
. "Safe Exam Browser access plugin. The reason was 'Because I said so.'. "
. "Expected config key: '$expectedconfigkey'. "
. "Received config key: 'configkey'. Received browser exam key: 'browserexamkey'.",
$event->get_description());
$this->assertEquals(\context_module::instance($quiz->cmid), $event->get_context());
$this->assertEquals($user->id, $event->userid);
$this->assertEquals($quiz->id, $event->objectid);
$this->assertEquals($this->course->id, $event->courseid);
$this->assertEquals('Because I said so.', $event->other['reason']);
$this->assertEquals($expectedconfigkey, $event->other['savedconfigkey']);
$this->assertEquals('configkey', $event->other['receivedconfigkey']);
$this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']);
}
/**
* Test creating the template_created event.
*
* @covers \quizaccess_seb\event\template_created
*/
public function test_event_create_template() {
$this->resetAfterTest();
@@ -0,0 +1,260 @@
<?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/>.
namespace quizaccess_seb\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use quizaccess_seb\quiz_settings;
require_once($CFG->libdir . '/externallib.php');
/**
* PHPUnit tests for external function.
*
* @package quizaccess_seb
* @author Andrew Madden <[email protected]>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quizaccess_seb\external\validate_quiz_access
*/
class validate_quiz_access_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/**
* This method runs before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
// Generate data objects.
$this->course = $this->getDataGenerator()->create_course();
$this->quiz = $this->create_test_quiz($this->course);
$this->user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, 'student');
$this->setUser($this->user);
}
/**
* Bad parameter provider.
*
* @return array
*/
public function bad_parameters_provider(): array {
return [
'no params' => [
'cmid' => null,
'url' => null,
'configkey' => null,
'/Invalid parameter value detected \(Missing required key in single structure: cmid\)/'
],
'no course module id' => [
'cmid' => null,
'url' => 'https://www.example.com/moodle',
'configkey' => hash('sha256', 'configkey'),
'/Invalid parameter value detected \(Missing required key in single structure: cmid\)/'
],
'no url' => [
'cmid' => 123,
'url' => null,
'configkey' => hash('sha256', 'configkey'),
'/Invalid parameter value detected \(Missing required key in single structure: url\)/'
],
'cmid is not an int' => [
'cmid' => 'test',
'url' => 'https://www.example.com/moodle',
'configkey' => null,
'/Invalid external api parameter: the value is "test", the server was expecting "int" type/'
],
'url is not a url' => [
'cmid' => 123,
'url' => 123,
'configkey' => hash('sha256', 'configkey'),
'/Invalid external api parameter: the value is "123", the server was expecting "url" type/'
],
];
}
/**
* Test exception thrown for bad parameters.
*
* @param mixed $cmid Course module id.
* @param mixed $url Page URL.
* @param mixed $configkey SEB config key.
* @param mixed $messageregex Error message regex to check.
*
* @dataProvider bad_parameters_provider
*/
public function test_invalid_parameters($cmid, $url, $configkey, $messageregex) {
$params = [];
if (!empty($cmid)) {
$params['cmid'] = $cmid;
}
if (!empty($url)) {
$params['url'] = $url;
}
if (!empty($configkey)) {
$params['configkey'] = $configkey;
}
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessageMatches($messageregex);
\external_api::validate_parameters(validate_quiz_keys::execute_parameters(), $params);
}
/**
* Test that the user has permissions to access context.
*/
public function test_context_is_not_valid_for_user() {
// Set user as user not enrolled in course and quiz.
$this->user = $this->getDataGenerator()->create_user();
$this->setUser($this->user);
$this->expectException(\require_login_exception::class);
$this->expectExceptionMessage('Course or activity not accessible. (Not enrolled)');
validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'configkey');
}
/**
* Test exception thrown when no key provided.
*/
public function test_no_keys_provided() {
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage('At least one SEB key must be provided.');
validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle');
}
/**
* Test exception thrown if cmid doesn't match a quiz.
*/
public function test_quiz_does_not_exist() {
$this->setAdminUser();
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $this->course->id]);
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage('Quiz not found matching course module id: ' . $forum->cmid);
validate_quiz_keys::execute($forum->cmid, 'https://www.example.com/moodle', 'configkey');
}
/**
* Test config key is valid.
*/
public function test_config_key_valid() {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
$url = 'https://www.example.com/moodle';
// Create the quiz settings.
$quizsettings = new quiz_settings(0, $settings);
$quizsettings->save();
$fullconfigkey = hash('sha256', $url . $quizsettings->get_config_key());
$result = validate_quiz_keys::execute($this->quiz->cmid, $url, $fullconfigkey);
$this->assertTrue($result['configkey']);
$this->assertTrue($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(0, $events);
}
/**
* Test config key is not valid.
*/
public function test_config_key_not_valid() {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
// Create the quiz settings.
$quizsettings = new quiz_settings(0, $settings);
$quizsettings->save();
$result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'badconfigkey');
$this->assertFalse($result['configkey']);
$this->assertTrue($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertStringContainsString('Invalid SEB config key', $event->get_description());
}
/**
* Test browser exam key is valid.
*/
public function test_browser_exam_key_valid() {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$url = 'https://www.example.com/moodle';
$validbrowserexamkey = hash('sha256', 'validbrowserexamkey');
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
'requiresafeexambrowser' => \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG,
'allowedbrowserexamkeys' => $validbrowserexamkey,
]);
// Create the quiz settings.
$quizsettings = new quiz_settings(0, $settings);
$quizsettings->save();
$fullbrowserexamkey = hash('sha256', $url . $validbrowserexamkey);
$result = validate_quiz_keys::execute($this->quiz->cmid, $url, null, $fullbrowserexamkey);
$this->assertTrue($result['configkey']);
$this->assertTrue($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(0, $events);
}
/**
* Test browser exam key is not valid.
*/
public function test_browser_exam_key_not_valid() {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$validbrowserexamkey = hash('sha256', 'validbrowserexamkey');
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
'requiresafeexambrowser' => \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG,
'allowedbrowserexamkeys' => $validbrowserexamkey,
]);
// Create the quiz settings.
$quizsettings = new quiz_settings(0, $settings);
$quizsettings->save();
$result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', null,
hash('sha256', 'badbrowserexamkey'));
$this->assertTrue($result['configkey']);
$this->assertFalse($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertStringContainsString('Invalid SEB browser key', $event->get_description());
}
}
@@ -180,7 +180,8 @@ class helper_test extends \advanced_testcase {
. "<false/><key>audioMute</key><false/><key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key>"
. "<true/><key>URLFilterEnable</key><false/><key>URLFilterEnableContentFilter</key><false/>"
. "<key>URLFilterRules</key><array/><key>startURL</key><string>$url</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n", $config);
}
@@ -60,9 +60,10 @@ class quiz_settings_test extends \advanced_testcase {
*/
public function test_config_is_created_from_quiz_settings() {
// Test settings to populate the in the object.
$settings = $this->get_test_settings();
$settings->quizid = $this->quiz->id;
$settings->cmid = $this->quiz->cmid;
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
// Obtain the existing record that is created when using a generator.
$quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
@@ -83,8 +84,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array><key>startURL</key><string>$this->url</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/><key>allowPreferencesWindow</key><false/></dict></plist>\n",
$config);
}
@@ -93,9 +94,10 @@ class quiz_settings_test extends \advanced_testcase {
*/
public function test_config_is_updated_from_quiz_settings() {
// Test settings to populate the in the object.
$settings = $this->get_test_settings();
$settings->quizid = $this->quiz->id;
$settings->cmid = $this->quiz->cmid;
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
// Obtain the existing record that is created when using a generator.
$quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
@@ -115,7 +117,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array><key>startURL</key><string>$this->url</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n", $config);
$quizsettings->set('filterembeddedcontent', 1); // Alter the settings.
@@ -131,7 +134,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array><key>startURL</key><string>$this->url</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n", $config);
}
@@ -143,7 +147,7 @@ class quiz_settings_test extends \advanced_testcase {
$quizsettings = new quiz_settings(0, $settings);
$configkey = $quizsettings->get_config_key();
$this->assertEquals("b35510bd754f9d106ff88b9d2dc1bb297cddc9fc7b4bdde2dbda4e7d9e4b50d8",
$this->assertEquals("65ff7a3b8aec80e58fbe2e7968826c33cbf0ac444a748055ebe665829cbf4201",
$configkey
);
}
@@ -156,12 +160,12 @@ class quiz_settings_test extends \advanced_testcase {
$quizsettings = new quiz_settings(0, $settings);
$configkey = $quizsettings->get_config_key();
$this->assertEquals("b35510bd754f9d106ff88b9d2dc1bb297cddc9fc7b4bdde2dbda4e7d9e4b50d8",
$this->assertEquals("65ff7a3b8aec80e58fbe2e7968826c33cbf0ac444a748055ebe665829cbf4201",
$configkey);
$quizsettings->set('filterembeddedcontent', 1); // Alter the settings.
$configkey = $quizsettings->get_config_key();
$this->assertEquals("58010792504cccc18f7b0e5c9680fe60b567e8c1b5fb9798654cc9bad9ddf30c",
$this->assertEquals("d975b8a2ec4472495a8be7c64d7c8cc960dbb62472d5e88a8847ac0e5d77e533",
$configkey);
}
@@ -214,7 +218,7 @@ class quiz_settings_test extends \advanced_testcase {
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>hashedQuitPassword</key><string>hashedpassword</string>"
. "<key>allowWlan</key><false/><key>startURL</key><string>$url</string>"
. "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer></dict></plist>\n";
$itemid = $this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
@@ -566,7 +570,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><false/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'blocked simple expessions' => [
@@ -593,7 +598,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><false/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'enabled regex expessions' => [
@@ -620,7 +626,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><true/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'blocked regex expessions' => [
@@ -647,7 +654,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><true/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'multiple simple expessions' => [
@@ -676,7 +684,8 @@ class quiz_settings_test extends \advanced_testcase {
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><true/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
];
+47 -21
View File
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/test_helper_trait.php');
* @author Andrew Madden <[email protected]>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quizaccess_seb
*/
class rule_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
@@ -43,6 +44,16 @@ class rule_test extends \advanced_testcase {
$this->course = $this->getDataGenerator()->create_course();
}
/**
* Called after every test.
*/
public function tearDown(): void {
global $SESSION;
if (!empty($this->quiz)) {
unset($SESSION->quizaccess_seb_access);
}
}
/**
* Helper method to get SEB download link for testing.
@@ -558,6 +569,24 @@ class rule_test extends \advanced_testcase {
$this->assertFalse($this->make_rule()->prevent_access());
}
public function test_access_allowed_if_access_state_stored_in_session() {
global $SESSION;
$this->setAdminUser();
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Check that access is prevented.
$this->check_invalid_basic_header();
$SESSION->quizaccess_seb_access = [$this->quiz->cmid => true];
// Check access is now not prevented.
$this->assertFalse($this->make_rule()->prevent_access());
}
/**
* A helper method to check invalid browser key.
*
@@ -730,6 +759,14 @@ class rule_test extends \advanced_testcase {
// Set up basic dummy request.
$_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE';
// Create an event sink, trigger event and retrieve event.
$this->check_invalid_basic_header();
}
/**
* A helper method to check invalid basic header.
*/
protected function check_invalid_basic_header() {
// Create an event sink, trigger event and retrieve event.
$sink = $this->redirectEvents();
@@ -1224,30 +1261,19 @@ class rule_test extends \advanced_testcase {
}
/**
* Test we can decide if need to redirect to SEB config link.
* Test cleanup when quiz is completed.
*/
public function test_should_redirect_to_seb_config_link() {
public function test_current_attempt_finished() {
global $SESSION;
$this->setAdminUser();
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->save();
// Set access for Moodle session.
$SESSION->quizaccess_seb_access = [$this->quiz->cmid => true];
$this->make_rule()->current_attempt_finished();
$reflection = new \ReflectionClass('quizaccess_seb');
$method = $reflection->getMethod('should_redirect_to_seb_config_link');
$method->setAccessible(true);
set_config('autoreconfigureseb', '0', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'TEST';
$this->assertFalse($method->invoke($this->make_rule()));
set_config('autoreconfigureseb', '0', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$this->assertFalse($method->invoke($this->make_rule()));
set_config('autoreconfigureseb', '1', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'TEST';
$this->assertFalse($method->invoke($this->make_rule()));
set_config('autoreconfigureseb', '1', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$this->assertTrue($method->invoke($this->make_rule()));
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid]));
}
}
@@ -278,8 +278,8 @@ trait quizaccess_seb_test_helper_trait {
*
* @return \stdClass Settings.
*/
protected function get_test_settings() : \stdClass {
return (object) [
protected function get_test_settings(array $settings = []) : \stdClass {
return (object) array_merge([
'quizid' => 1,
'cmid' => 1,
'requiresafeexambrowser' => '1',
@@ -303,7 +303,7 @@ trait quizaccess_seb_test_helper_trait {
'expressionsblocked' => '',
'regexblocked' => '',
'showsebdownloadlink' => '1',
];
], $settings);
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2022041900;
$plugin->version = 2022041901;
$plugin->requires = 2022041200;
$plugin->component = 'quizaccess_seb';
$plugin->maturity = MATURITY_STABLE;