From 13190a3aff8726c7c9cd46a6f7f4eeddffe24eff Mon Sep 17 00:00:00 2001 From: Michael Kotlyar Date: Wed, 24 Jul 2024 14:16:52 +0100 Subject: [PATCH] WR437567: Add SEB options to override settings. --- .../seb/classes/seb_access_manager.php | 20 +++ .../seb/classes/settings_provider.php | 2 +- mod/quiz/classes/form/edit_override_form.php | 164 ++++++++++++++++++ mod/quiz/classes/local/override_manager.php | 62 ++++++- mod/quiz/db/install.xml | 1 + mod/quiz/db/upgrade.php | 15 ++ mod/quiz/lib.php | 8 + mod/quiz/overrideedit.php | 9 + mod/quiz/overrides.php | 10 ++ mod/quiz/version.php | 2 +- 10 files changed, 282 insertions(+), 11 deletions(-) diff --git a/mod/quiz/accessrule/seb/classes/seb_access_manager.php b/mod/quiz/accessrule/seb/classes/seb_access_manager.php index 6664419fa1e43..ecbbc798bedfd 100644 --- a/mod/quiz/accessrule/seb/classes/seb_access_manager.php +++ b/mod/quiz/accessrule/seb/classes/seb_access_manager.php @@ -30,6 +30,7 @@ use context_module; use mod_quiz\quiz_settings; +use mod_quiz\local\override_manager; defined('MOODLE_INTERNAL') || die(); @@ -69,6 +70,25 @@ public function __construct(quiz_settings $quiz) { $this->context = context_module::instance($quiz->get_cmid()); $this->quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->get_quizid()); $this->validconfigkey = seb_quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid()); + + $settings = $quiz->get_quiz(); + if (isset($settings->enableseboverride) && !!$settings->enableseboverride) { + $prefix = 'seb_'; + $prelen = strlen($prefix); + if (!$this->quizsettings) { + $this->quizsettings = new seb_quiz_settings(); + } + foreach (override_manager::get_seb_override_settings() as $key) { + if (str_starts_with($key, $prefix)) { + $key = substr($key, $prelen); + } else { + continue; + } + if ($settings->{$prefix.$key} !== null && $settings->{$prefix.$key} != $this->quizsettings->get($key)) { + $this->quizsettings->set($key, $settings->{$prefix.$key}); + } + } + } } /** diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php index f7a8ffdaa10f4..e265835d15a6c 100644 --- a/mod/quiz/accessrule/seb/classes/settings_provider.php +++ b/mod/quiz/accessrule/seb/classes/settings_provider.php @@ -584,7 +584,7 @@ public static function get_requiresafeexambrowser_options(\context $context): ar * Returns a list of templates. * @return array */ - protected static function get_template_options(): array { + public static function get_template_options(): array { $templates = []; $records = template::get_records(['enabled' => 1], 'name'); if ($records) { diff --git a/mod/quiz/classes/form/edit_override_form.php b/mod/quiz/classes/form/edit_override_form.php index 4f5c7c6786074..d00ab4d1c956f 100644 --- a/mod/quiz/classes/form/edit_override_form.php +++ b/mod/quiz/classes/form/edit_override_form.php @@ -23,6 +23,7 @@ use moodle_url; use moodleform; use stdClass; +use quizaccess_seb\{seb_quiz_settings,settings_provider}; defined('MOODLE_INTERNAL') || die(); @@ -59,6 +60,9 @@ class edit_override_form extends moodleform { /** @var int overrideid, if provided. */ protected int $overrideid; + /** @var array array of seb settings to override. */ + protected array $sebdata; + /** * Constructor. * @@ -80,6 +84,7 @@ public function __construct(moodle_url $submiturl, $this->groupid = empty($override->groupid) ? 0 : $override->groupid; $this->userid = empty($override->userid) ? 0 : $override->userid; $this->overrideid = $override->id ?? 0; + $this->sebdata = empty($override->sebdata) ? [] : unserialize($override->sebdata); parent::__construct($submiturl); } @@ -224,6 +229,9 @@ protected function definition() { $mform->addHelpButton('attempts', 'attempts', 'quiz'); $mform->setDefault('attempts', $this->quiz->attempts); + // SEB override settings. + $this->display_seb_settings($mform); + // Submit buttons. $mform->addElement('submit', 'resetbutton', get_string('reverttodefaults', 'quiz')); @@ -239,6 +247,162 @@ protected function definition() { $mform->closeHeaderBefore('buttonbar'); } + /** + * Add SEB settings to the form. + * + * @param \MoodleQuickForm $mform + * @return void + */ + protected function display_seb_settings($mform) { + $mform->addElement('header', 'seb', get_string('seb', 'quizaccess_seb')); + + $mform->addElement('checkbox', 'enableseboverride', 'Enable SEB override'); + $mform->setDefault('enableseboverride', $this->sebdata['enableseboverride'] ?? false); + + // "Require the use of Safe Exam Browser". + $requireseboptions[settings_provider::USE_SEB_NO] = get_string('no'); + + if (settings_provider::can_configure_manually($this->context) || self::is_conflicting_permissions($this->context)) { + $requireseboptions[settings_provider::USE_SEB_CONFIG_MANUALLY] = get_string('seb_use_manually', 'quizaccess_seb'); + } + + if (settings_provider::can_use_seb_template($this->context) || self::is_conflicting_permissions($this->context)) { + if (!empty(settings_provider::get_template_options())) { + $requireseboptions[settings_provider::USE_SEB_TEMPLATE] = get_string('seb_use_template', 'quizaccess_seb'); + } + } + + $requireseboptions[settings_provider::USE_SEB_CLIENT_CONFIG] = get_string('seb_use_client', 'quizaccess_seb'); + + $mform->addElement( + 'select', + 'seb_requiresafeexambrowser', + get_string('seb_requiresafeexambrowser', 'quizaccess_seb'), + $requireseboptions + ); + + $mform->setType('seb_requiresafeexambrowser', PARAM_INT); + $mform->setDefault('seb_requiresafeexambrowser', $this->sebdata['seb_requiresafeexambrowser'] ?? $this->quiz->seb_requiresafeexambrowser ?? 0); + $mform->addHelpButton('seb_requiresafeexambrowser', 'seb_requiresafeexambrowser', 'quizaccess_seb'); + $mform->disabledIf('seb_requiresafeexambrowser', 'enableseboverride'); + + if (settings_provider::is_conflicting_permissions($this->context)) { + $mform->freeze('seb_requiresafeexambrowser'); + } + + // "Safe Exam Browser config template". + if (settings_provider::can_use_seb_template($this->context) || settings_provider::is_conflicting_permissions($this->context)) { + $element = $mform->addElement( + 'select', + 'seb_templateid', + get_string('seb_templateid', 'quizaccess_seb'), + settings_provider::get_template_options() + ); + } else { + $element = $mform->addElement('hidden', 'seb_templateid'); + } + + $mform->setType('seb_templateid', PARAM_INT); + $mform->setDefault('seb_templateid', $this->sebdata['seb_templateid'] ?? $this->quiz->seb_templateid ?? 0); + $mform->addHelpButton('seb_templateid', 'seb_templateid', 'quizaccess_seb'); + $mform->disabledIf('seb_templateid', 'enableseboverride'); + + if (settings_provider::is_conflicting_permissions($this->context)) { + $mform->freeze('seb_templateid'); + } + + // "Show Safe Exam browser download button". + if (settings_provider::can_change_seb_showsebdownloadlink($this->context)) { + $mform->addElement('selectyesno', + 'seb_showsebdownloadlink', + get_string('seb_showsebdownloadlink', 'quizaccess_seb') + ); + + $mform->setType('seb_showsebdownloadlink', PARAM_BOOL); + $mform->setDefault('seb_showsebdownloadlink', $this->sebdata['seb_showsebdownloadlink'] ?? $this->quiz->seb_showsebdownloadlink ?? 1); + $mform->addHelpButton('seb_showsebdownloadlink', 'seb_showsebdownloadlink', 'quizaccess_seb'); + $mform->disabledIf('seb_showsebdownloadlink', 'enableseboverride'); + } + + // Manual config elements. + $defaults = settings_provider::get_seb_config_element_defaults(); + $types = settings_provider::get_seb_config_element_types(); + + foreach (settings_provider::get_seb_config_elements() as $name => $type) { + if (!settings_provider::can_manage_seb_config_setting($name, $this->context)) { + $type = 'hidden'; + } + + $mform->addElement($type, $name, get_string($name, 'quizaccess_seb')); + + $mform->addHelpButton($name, $name, 'quizaccess_seb'); + $mform->setType('seb_showsebdownloadlink', PARAM_BOOL); + $mform->setDefault('seb_showsebdownloadlink', $this->sebdata['seb_showsebdownloadlink'] ?? $this->quiz->seb_showsebdownloadlink ?? 1); + $mform->disabledIf($name, 'enableseboverride'); + + if (isset($defaults[$name])) { + $mform->setDefault($name, $this->sebdata[$name] ?? $this->quiz->{$name} ?? $defaults[$name]); + } + + if (isset($types[$name])) { + $mform->setType($name, $types[$name]); + } + } + + if (settings_provider::can_change_seb_allowedbrowserexamkeys($this->context)) { + $mform->addElement('textarea', + 'seb_allowedbrowserexamkeys', + get_string('seb_allowedbrowserexamkeys', 'quizaccess_seb') + ); + + $mform->setType('seb_allowedbrowserexamkeys', PARAM_RAW); + $mform->setDefault('seb_allowedbrowserexamkeys', $this->sebdata['seb_allowedbrowserexamkeys'] ?? $this->quiz->seb_allowedbrowserexamkeys ?? ''); + $mform->addHelpButton('seb_allowedbrowserexamkeys', 'seb_allowedbrowserexamkeys', 'quizaccess_seb'); + $mform->disabledIf('seb_allowedbrowserexamkeys', 'enableseboverride'); + } + + // Hideifs. + foreach (settings_provider::get_quiz_hideifs() as $elname => $rules) { + if ($mform->elementExists($elname)) { + foreach ($rules as $hideif) { + $mform->hideIf( + $hideif->get_element(), + $hideif->get_dependantname(), + $hideif->get_condition(), + $hideif->get_dependantvalue() + ); + } + } + } + + // Lock elements. + if (settings_provider::is_conflicting_permissions($this->context)) { + // Freeze common quiz settings. + $mform->addElement('enableseboverride'); + $mform->freeze('seb_requiresafeexambrowser'); + $mform->freeze('seb_templateid'); + $mform->freeze('seb_showsebdownloadlink'); + $mform->freeze('seb_allowedbrowserexamkeys'); + + $quizsettings = seb_quiz_settings::get_by_quiz_id((int) $this->quiz->id); + + // Remove template ID if not using template for this quiz. + if (empty($quizsettings) || $quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_TEMPLATE) { + $mform->removeElement('seb_templateid'); + } + + // Freeze all SEB specific settings. + foreach (settings_provider::get_seb_config_elements() as $element => $type) { + if ($mform->elementExists($element)) { + $mform->freeze($element); + } + } + } + + // Close header before next field. + $mform->closeHeaderBefore('resetbutton'); + } + /** * Get a user's name and identity ready to display. * diff --git a/mod/quiz/classes/local/override_manager.php b/mod/quiz/classes/local/override_manager.php index a79c739b1a56f..d1721aed4fa0c 100644 --- a/mod/quiz/classes/local/override_manager.php +++ b/mod/quiz/classes/local/override_manager.php @@ -31,8 +31,43 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class override_manager { + /** @var array quiz SEB setting keys that can be overwritten **/ + private const OVERRIDEABLE_QUIZ_SEB_SETTINGS = [ + 'enableseboverride', + 'seb_activateurlfiltering', + 'seb_allowedbrowserexamkeys', + 'seb_allowreloadinexam', + 'seb_allowspellchecking', + 'seb_allowuserquitseb', + 'seb_enableaudiocontrol', + 'seb_expressionsallowed', + 'seb_expressionsblocked', + 'seb_filterembeddedcontent', + 'seb_linkquitseb', + 'seb_muteonstartup', + 'seb_quitpassword', + 'seb_regexallowed', + 'seb_regexblocked', + 'seb_requiresafeexambrowser', + 'seb_showkeyboardlayout', + 'seb_showreloadbutton', + 'seb_showsebdownloadlink', + 'seb_showsebtaskbar', + 'seb_showtime', + 'seb_showwificontrol', + 'seb_templateid', + 'seb_userconfirmquit', + ]; + /** @var array quiz setting keys that can be overwritten **/ - private const OVERRIDEABLE_QUIZ_SETTINGS = ['timeopen', 'timeclose', 'timelimit', 'attempts', 'password']; + private const OVERRIDEABLE_QUIZ_SETTINGS = [ + 'timeopen', + 'timeclose', + 'timelimit', + 'attempts', + 'password', + ...self::OVERRIDEABLE_QUIZ_SEB_SETTINGS + ]; /** * Create override manager @@ -88,10 +123,12 @@ public function validate_data(array $formdata): array { // Ensure at least one of the overrideable settings is set. $keysthatareset = array_map(function ($key) use ($formdata) { return isset($formdata->$key) && !is_null($formdata->$key); - }, self::OVERRIDEABLE_QUIZ_SETTINGS); + }, array_diff(self::OVERRIDEABLE_QUIZ_SETTINGS, self::OVERRIDEABLE_QUIZ_SEB_SETTINGS)); if (!in_array(true, $keysthatareset)) { - $errors['general'][] = new \lang_string('nooverridedata', 'quiz'); + if (!(isset($formdata->enableseboverride) && !is_null($formdata->enableseboverride))) { + $errors['general'][] = new \lang_string('nooverridedata', 'quiz'); + } } // Ensure quiz is a valid quiz. @@ -238,6 +275,12 @@ public function save_override(array $formdata): int { // Extract only the necessary data. $datatoset = $this->parse_formdata($formdata); + + // Create sebdata field value. + $sebdata = array_intersect_key($datatoset, array_flip(self::OVERRIDEABLE_QUIZ_SEB_SETTINGS)); + $sebdata = serialize($sebdata); + + $datatoset['sebdata'] = $sebdata; $datatoset['quiz'] = $this->quiz->id; // Validate the data is OK. @@ -557,12 +600,9 @@ private function clear_unused_values(array $formdata): array { // If the formdata is empty, set it to null. // This avoids putting 0, false, or '' into the DB since the override logic expects null. // Attempts is the exception, it can have a integer value of '0', so we use is_numeric instead. - if ($key != 'attempts' && empty($formdata[$key])) { - $formdata[$key] = null; - } - - if ($key == 'attempts' && !is_numeric($formdata[$key])) { - $formdata[$key] = null; + if (!in_array($key, ['attempts', ...self::OVERRIDEABLE_QUIZ_SEB_SETTINGS]) + && (empty($formdata[$key]) || !is_numeric($formdata[$key]))) { + $formdata[$key] = null; } } @@ -600,4 +640,8 @@ public static function delete_orphaned_group_overrides_in_course(int $courseid): } return array_unique(array_column($records, 'quiz')); } + + public static function get_seb_override_settings() { + return self::OVERRIDEABLE_QUIZ_SEB_SETTINGS; + } } diff --git a/mod/quiz/db/install.xml b/mod/quiz/db/install.xml index 8a7d8ee9f304f..8420cf3d6c5bc 100644 --- a/mod/quiz/db/install.xml +++ b/mod/quiz/db/install.xml @@ -132,6 +132,7 @@ + diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php index 4748c3ad20011..feafe30e900b5 100644 --- a/mod/quiz/db/upgrade.php +++ b/mod/quiz/db/upgrade.php @@ -132,6 +132,21 @@ function xmldb_quiz_upgrade($oldversion) { upgrade_mod_savepoint(true, 2023112402, 'quiz'); } + if ($oldversion < 2024072400) { + + // Define field sebdata to be added to quiz_overrides. + $table = new xmldb_table('quiz_overrides'); + $field = new xmldb_field('sebdata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'password'); + + // Conditionally launch add field quizgradeitemid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Quiz savepoint reached. + upgrade_mod_savepoint(true, 2024072400, 'quiz'); + } + // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 375f3cc9e40b4..8ecc3f8810f40 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -326,6 +326,14 @@ function quiz_update_effective_access($quiz, $userid) { } } + // Merge SEB override settings if available. + $seboverride = isset($override->sebdata) ? unserialize($override->sebdata) : null; + if (!empty($seboverride) && !!$seboverride['enableseboverride']) { + foreach ($seboverride as $key => $value) { + $quiz->{$key} = $value; + } + } + return $quiz; } diff --git a/mod/quiz/overrideedit.php b/mod/quiz/overrideedit.php index 878e263770562..a9f7379d5dc81 100644 --- a/mod/quiz/overrideedit.php +++ b/mod/quiz/overrideedit.php @@ -72,6 +72,15 @@ // Editing an override. $data = clone $override; + // Unpack SEB settings into data object. + $data->sebdata = unserialize($data->sebdata); + if ($data->sebdata->enableseboverride) { + foreach($data->sebdata as $sebkey => $sebval) { + $data->{$sebkey} = $sebval; + } + } + unset($data->sebdata); + if ($override->groupid) { if (!groups_group_visible($override->groupid, $course, $cm)) { throw new \moodle_exception('invalidoverrideid', 'quiz'); diff --git a/mod/quiz/overrides.php b/mod/quiz/overrides.php index be467be62435f..6392d72286e30 100644 --- a/mod/quiz/overrides.php +++ b/mod/quiz/overrides.php @@ -23,6 +23,7 @@ */ use mod_quiz\quiz_settings; +use quizaccess_seb\settings_provider; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot.'/mod/quiz/lib.php'); @@ -229,6 +230,15 @@ get_string('enabled', 'quiz') : get_string('none', 'quiz'); } + // Safe exam browser. + if (isset($override->sebdata)) { + $sebdata = unserialize($override->sebdata); + if (!!$sebdata['enableseboverride']) { + $fields[] = get_string('seb_requiresafeexambrowser', 'quizaccess_seb'); + $values[] = settings_provider::get_requiresafeexambrowser_options($context)[$sebdata['seb_requiresafeexambrowser']]; + } + } + // Prepare the information about who this override applies to. $extranamebit = $active ? '' : '*'; $usercells = []; diff --git a/mod/quiz/version.php b/mod/quiz/version.php index fa22a93ba4087..8d648feba1090 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024051700; +$plugin->version = 2024072400; $plugin->requires = 2024041600; $plugin->component = 'mod_quiz';