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:
committed by
Dmitrii Metelkin
parent
b4493c2a16
commit
3a60aec87d
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user