From c6676feddfb90d1a21889e58e7303a4992b0cd12 Mon Sep 17 00:00:00 2001 From: Michael Kotlyar Date: Wed, 24 Jul 2024 14:16:52 +0100 Subject: [PATCH] MDL-80945 quiz: Add SEB options to override settings --- .../seb/classes/seb_quiz_settings.php | 111 +++++++++-- .../seb/classes/settings_provider.php | 38 +++- mod/quiz/accessrule/seb/db/access.php | 16 ++ .../accessrule/seb/lang/en/quizaccess_seb.php | 2 + .../seb/tests/settings_provider_test.php | 9 + mod/quiz/accessrule/seb/version.php | 2 +- mod/quiz/classes/form/edit_override_form.php | 179 ++++++++++++++++++ mod/quiz/classes/local/override_manager.php | 62 +++++- mod/quiz/db/install.xml | 1 + mod/quiz/db/upgrade.php | 15 ++ mod/quiz/lib.php | 48 +++++ mod/quiz/overrideedit.php | 9 + mod/quiz/overrides.php | 10 + mod/quiz/version.php | 2 +- 14 files changed, 481 insertions(+), 23 deletions(-) diff --git a/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php index 1e338b497869a..1af0f53769d8e 100644 --- a/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php +++ b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php @@ -34,6 +34,7 @@ use lang_string; use moodle_exception; use moodle_url; +use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); @@ -196,11 +197,76 @@ protected static function define_properties(): array { * @return false|\quizaccess_seb\seb_quiz_settings */ public static function get_by_quiz_id(int $quizid) { - if ($data = self::get_quiz_settings_cache()->get($quizid)) { - return new static(0, $data); + global $USER; + // Skip cache if quiz has overrides for user. + if (!($hasoverrides = quiz_has_user_overrides($quizid))) { + if ($data = self::get_quiz_settings_cache()->get($quizid)) { + return new static(0, $data); + } + } + + $sebquizsetting = self::get_record(['quizid' => $quizid]); + + // Overwrite settings from override manager if available. + if ($hasoverrides) { + // Create blank seb_quiz_settings instance if none exists. + if (!$sebquizsetting) { + $record = new \stdClass(); + $record->quizid = $quizid; + $record->cmid = get_coursemodule_from_instance('quiz', $quizid)->id; + $sebquizsetting = new self(0, $record); + } + + $quizsetting = quiz_settings::create_for_cmid($sebquizsetting->get('cmid'), $USER->id)->get_quiz(); + // If overriding enabled, overwrite seb settings. + if (isset($quizsetting->enableseboverride) && !!$quizsetting->enableseboverride) { + $prefix = 'seb_'; + foreach (array_keys(self::properties_definition()) as $key) { + if (isset($quizsetting->{$prefix.$key})) { + $sebquizsetting->set($key, $quizsetting->{$prefix.$key}); + } + } + } } - return self::get_record(['quizid' => $quizid]); + return $sebquizsetting; + } + + /** + * Get override record if there is for the current user. + * + * @return \stdClass|false + */ + protected function get_override() { + global $DB, $USER; + $userid = $USER->id; + $quizid = $this->get('quizid'); + + $override = false; + + if (!($override = $DB->get_record('quiz_overrides', ['quiz' => $quizid, 'userid' => $userid]))) { + $quiz = $DB->get_record('quiz', ['id' => $quizid]); + $groupings = groups_get_user_groups($quiz->course, $userid); + + if (!empty($groupings[0])) { + list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); + $sql = "SELECT * FROM {quiz_overrides} + WHERE groupid $extra AND quiz = ?"; + $params[] = $quiz->id; + $override = $DB->get_record_sql($sql, $params); + } + } + + return $override; + } + + /** + * Get overridden sebdata if there is for the current user. + * + * @return array|false + */ + protected function get_override_data() { + return ($override = $this->get_override()) ? unserialize($override->sebdata) : false; } /** @@ -210,16 +276,20 @@ public static function get_by_quiz_id(int $quizid) { * @return string|null */ public static function get_config_by_quiz_id(int $quizid): ?string { - $config = self::get_config_cache()->get($quizid); - - if ($config !== false) { - return $config; + // Skip cache if quiz has overrides for user. + if (!($hasoverrides = quiz_has_user_overrides($quizid))) { + $config = self::get_config_cache()->get($quizid); + if ($config !== false) { + return $config; + } } $config = null; if ($settings = self::get_by_quiz_id($quizid)) { $config = $settings->get_config(); - self::get_config_cache()->set($quizid, $config); + if (!$hasoverrides) { + self::get_config_cache()->set($quizid, $config); + } } return $config; @@ -232,16 +302,21 @@ public static function get_config_by_quiz_id(int $quizid): ?string { * @return string|null */ public static function get_config_key_by_quiz_id(int $quizid): ?string { - $configkey = self::get_config_key_cache()->get($quizid); + // Skip cache if quiz has overrides for user. + if (!($hasoverrides = quiz_has_user_overrides($quizid))) { + $configkey = self::get_config_key_cache()->get($quizid); - if ($configkey !== false) { - return $configkey; + if ($configkey !== false) { + return $configkey; + } } $configkey = null; if ($settings = self::get_by_quiz_id($quizid)) { $configkey = $settings->get_config_key(); - self::get_config_key_cache()->set($quizid, $configkey); + if (!$hasoverrides) { + self::get_config_key_cache()->set($quizid, $configkey); + } } return $configkey; @@ -294,9 +369,12 @@ protected function after_update($result) { * Helper method to execute common stuff after create and update. */ private function after_save() { - self::get_quiz_settings_cache()->set($this->get('quizid'), $this->to_record()); - self::get_config_cache()->set($this->get('quizid'), $this->config); - self::get_config_key_cache()->set($this->get('quizid'), $this->configkey); + if (!$this->get_override()) { + $key = $this->get('quizid'); + self::get_quiz_settings_cache()->set($key, $this->to_record()); + self::get_config_cache()->set($key, $this->config); + self::get_config_key_cache()->set($key, $this->configkey); + } } /** @@ -557,6 +635,7 @@ private function process_bool_setting(string $name) { */ private function process_quit_password_settings() { $settings = $this->to_record(); + if (!empty($settings->quitpassword) && is_string($settings->quitpassword)) { // Hash quit password. $hashedpassword = hash('SHA256', $settings->quitpassword); @@ -571,6 +650,7 @@ private function process_quit_password_settings() { */ private function process_quit_url_from_settings() { $settings = $this->to_record(); + if (!empty($settings->linkquitseb) && is_string($settings->linkquitseb)) { $this->plist->set_or_update_value('quitURL', new CFString($settings->linkquitseb)); } @@ -592,6 +672,7 @@ private function process_quit_url_from_template_or_config() { */ private function process_url_filters() { $settings = $this->to_record(); + // Create rules to each expression provided and add to config. $urlfilterrules = []; // Get all rules separated by newlines and remove empty rules. diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php index f7a8ffdaa10f4..8983920b8d9ee 100644 --- a/mod/quiz/accessrule/seb/classes/settings_provider.php +++ b/mod/quiz/accessrule/seb/classes/settings_provider.php @@ -188,16 +188,17 @@ protected static function add_seb_header_element(\mod_quiz_mod_form $quizform, \ * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm. */ protected static function add_seb_usage_options(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) { + $options = self::get_requiresafeexambrowser_options($quizform->get_context()); $element = $mform->createElement( 'select', 'seb_requiresafeexambrowser', get_string('seb_requiresafeexambrowser', 'quizaccess_seb'), - self::get_requiresafeexambrowser_options($quizform->get_context()) + $options ); self::insert_element($quizform, $mform, $element); self::set_type($quizform, $mform, 'seb_requiresafeexambrowser', PARAM_INT); - self::set_default($quizform, $mform, 'seb_requiresafeexambrowser', self::USE_SEB_NO); + self::set_default($quizform, $mform, 'seb_requiresafeexambrowser', array_key_first($options)); self::add_help_button($quizform, $mform, 'seb_requiresafeexambrowser'); if (self::is_conflicting_permissions($quizform->get_context())) { @@ -549,6 +550,11 @@ public static function is_conflicting_permissions(\context $context) { return true; } + if (!self::can_unrequire($context) && + $settings->get('requiresafeexambrowser') == self::USE_SEB_NO) { + return true; + } + return false; } @@ -559,7 +565,11 @@ public static function is_conflicting_permissions(\context $context) { * @return array */ public static function get_requiresafeexambrowser_options(\context $context): array { - $options[self::USE_SEB_NO] = get_string('no'); + $options = []; + + if (self::can_unrequire($context) || self::is_conflicting_permissions($context)) { + $options[self::USE_SEB_NO] = get_string('no'); + } if (self::can_configure_manually($context) || self::is_conflicting_permissions($context)) { $options[self::USE_SEB_CONFIG_MANUALLY] = get_string('seb_use_manually', 'quizaccess_seb'); @@ -584,7 +594,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) { @@ -785,6 +795,26 @@ public static function can_change_seb_allowedbrowserexamkeys(\context $context): return has_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', $context); } + /** + * Check if the current user can unrequire SEB from quiz. + * + * @param \context $context Context to check access in. + * @return bool + */ + public static function can_unrequire(\context $context): bool { + return has_capability('quizaccess/seb:manage_seb_unrequiresafeexambrowser', $context); + } + + /** + * Check if the current user can unrequire SEB from quiz in the override menu. + * + * @param \context $context Context to check access in. + * @return bool + */ + public static function can_override_unrequire(\context $context): bool { + return has_capability('quizaccess/seb:override_seb_unrequiresafeexambrowser', $context); + } + /** * Check if the current user can config SEB manually. * diff --git a/mod/quiz/accessrule/seb/db/access.php b/mod/quiz/accessrule/seb/db/access.php index d79df40695928..e8070c1a69e46 100644 --- a/mod/quiz/accessrule/seb/db/access.php +++ b/mod/quiz/accessrule/seb/db/access.php @@ -49,6 +49,22 @@ 'editingteacher' => CAP_ALLOW ] ], + 'quizaccess/seb:manage_seb_unrequiresafeexambrowser' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + 'quizaccess/seb:override_seb_unrequiresafeexambrowser' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], 'quizaccess/seb:manage_seb_templateid' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, diff --git a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php index c340f295a2f2d..8e233fb979b4e 100644 --- a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php +++ b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php @@ -106,6 +106,8 @@ $string['seb:manage_seb_regexallowed'] = 'Change SEB quiz setting: Regex expressions allowed'; $string['seb:manage_seb_regexblocked'] = 'Change SEB quiz setting: Regex expressions blocked'; $string['seb:manage_seb_requiresafeexambrowser'] = 'Change SEB quiz setting: Require Safe Exam Browser'; +$string['seb:manage_seb_unrequiresafeexambrowser'] = 'Change SEB quiz setting: Do not require Safe Exam Browser'; +$string['seb:override_seb_unrequiresafeexambrowser'] = 'Override SEB quiz setting: Do not require Safe Exam Browser'; $string['seb:manage_seb_showkeyboardlayout'] = 'Change SEB quiz setting: Show keyboard layout'; $string['seb:manage_seb_showreloadbutton'] = 'Change SEB quiz setting: Show reload button'; $string['seb:manage_seb_showsebtaskbar'] = 'Change SEB quiz setting: Show task bar'; diff --git a/mod/quiz/accessrule/seb/tests/settings_provider_test.php b/mod/quiz/accessrule/seb/tests/settings_provider_test.php index 821d1c9e36aad..0626fc7dbb5d1 100644 --- a/mod/quiz/accessrule/seb/tests/settings_provider_test.php +++ b/mod/quiz/accessrule/seb/tests/settings_provider_test.php @@ -656,6 +656,15 @@ public function test_get_requiresafeexambrowser_options($settingcapability): voi $options = settings_provider::get_requiresafeexambrowser_options($this->context); + $this->assertCount(1, $options); + $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); + $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); + $this->assertFalse(array_key_exists(settings_provider::USE_SEB_UPLOAD_CONFIG, $options)); + $this->assertTrue(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); + $this->assertFalse(array_key_exists(settings_provider::USE_SEB_NO, $options)); + + assign_capability('quizaccess/seb:manage_seb_unrequiresafeexambrowser', CAP_ALLOW, $this->roleid, $this->context->id); + $options = settings_provider::get_requiresafeexambrowser_options($this->context); $this->assertCount(2, $options); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_CONFIG_MANUALLY, $options)); $this->assertFalse(array_key_exists(settings_provider::USE_SEB_TEMPLATE, $options)); diff --git a/mod/quiz/accessrule/seb/version.php b/mod/quiz/accessrule/seb/version.php index 93d40beabaa19..312231bc2648e 100644 --- a/mod/quiz/accessrule/seb/version.php +++ b/mod/quiz/accessrule/seb/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024042200; +$plugin->version = 2024073101; $plugin->requires = 2024041600; $plugin->component = 'quizaccess_seb'; $plugin->maturity = MATURITY_STABLE; diff --git a/mod/quiz/classes/form/edit_override_form.php b/mod/quiz/classes/form/edit_override_form.php index 4f5c7c6786074..5175bd875a98c 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,177 @@ 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', get_string('enabled', 'quizaccess_seb')); + $mform->setDefault('enableseboverride', $this->sebdata['enableseboverride'] ?? false); + + // ... "Require the use of Safe Exam Browser" + if (settings_provider::can_override_unrequire($this->context)) { + $requireseboptions[settings_provider::USE_SEB_NO] = get_string('no'); + } + + if (settings_provider::can_configure_manually($this->context) || settings_provider::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) || settings_provider::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..3a466c781e810 100644 --- a/mod/quiz/classes/local/override_manager.php +++ b/mod/quiz/classes/local/override_manager.php @@ -32,7 +32,34 @@ */ class override_manager { /** @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', 'enableseboverride']; + + /** @var array quiz SEB setting keys that can be overwritten **/ + private const OVERRIDEABLE_QUIZ_SEB_SETTINGS = [ + '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', + ]; /** * Create override manager @@ -56,6 +83,15 @@ public function __construct( } } + /** + * Retrieve list of overridable quiz settings. + * + * @return array of quiz settings + */ + private static function get_overridable_quiz_settings() { + return [...self::OVERRIDEABLE_QUIZ_SETTINGS, ...self::OVERRIDEABLE_QUIZ_SEB_SETTINGS]; + } + /** * Returns all overrides for the linked quiz. * @@ -215,7 +251,7 @@ private static function validate_against_existing_record(int $existingid, \stdCl */ public function parse_formdata(array $formdata): array { // Get the data from the form that we want to update. - $settings = array_intersect_key($formdata, array_flip(self::OVERRIDEABLE_QUIZ_SETTINGS)); + $settings = array_intersect_key($formdata, array_flip(self::get_overridable_quiz_settings())); // Remove values that are the same as currently in the quiz. $settings = $this->clear_unused_values($settings); @@ -238,6 +274,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(['enableseboverride', ...self::OVERRIDEABLE_QUIZ_SEB_SETTINGS])); + $sebdata = serialize($sebdata); + + $datatoset['sebdata'] = $sebdata; $datatoset['quiz'] = $this->quiz->id; // Validate the data is OK. @@ -379,6 +421,12 @@ public function delete_overrides(array $overrides, bool $shouldlog = true): void $this->fire_deleted_event($override->id, $userid, $groupid); } } + + // Clear SEB cache. + $areas = ['config', 'configkey', 'quizsettings']; + foreach ($areas as $area) { + \cache::make('quizaccess_seb', $area)->delete($this->quiz->id); + } } /** @@ -546,6 +594,11 @@ private function fire_updated_event(int $id, ?int $userid = null, ?int $groupid */ private function clear_unused_values(array $formdata): array { foreach (self::OVERRIDEABLE_QUIZ_SETTINGS as $key) { + // Skip for this key as it's not an actual quiz property. + if ($key == 'enableseboverride') { + continue; + } + // If the formdata is the same as the current quiz object data, clear it. if (isset($formdata[$key]) && $formdata[$key] == $this->quiz->$key) { $formdata[$key] = null; @@ -566,9 +619,14 @@ private function clear_unused_values(array $formdata): array { } } + if (empty($formdata['seb_allowedbrowserexamkeys'])) { + $formdata['seb_allowedbrowserexamkeys'] = null; + } + return $formdata; } + /** * Deletes orphaned group overrides in a given course. * Note - permissions are not checked and events are not logged for performance reasons. 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..efab7764ae6a4 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -219,6 +219,46 @@ function quiz_delete_instance($id) { return true; } +/** + * Checks if the user has overrides for the quiz whether individually or in a group. + * + * @param int $quizid The quiz object. + * @return bool + */ +function quiz_has_user_overrides($quizid) { + global $DB, $USER; + $userid = $USER->id; + + $quiz = $DB->get_record('quiz', ['id' => $quizid]); + + // No quiz, no override. + if (!$quiz) { + return false; + } + + // Check for user override. + $useroverride = $DB->record_exists('quiz_overrides', ['quiz' => $quiz->id, 'userid' => $userid]); + if ($useroverride) { + return true; + } + + // Check for group overrides. + $groupings = groups_get_user_groups($quiz->course, $userid); + if (!empty($groupings[0])) { + list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); + $sql = "SELECT * FROM {quiz_overrides} + WHERE groupid $extra AND quiz = ?"; + $params[] = $quiz->id; + $gpoverrides = $DB->record_exists_sql($sql, $params); + + if ($gpoverrides) { + return true; + } + } + + return false; +} + /** * Updates a quiz object with override information for a user. * @@ -326,6 +366,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..404aa15034d6a 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';