From 5f3e62b0d007211332b64f26c6995d56ba9ab948 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 | 139 +++++-------- .../seb/classes/settings_provider.php | 38 +++- mod/quiz/accessrule/seb/db/access.php | 16 ++ mod/quiz/accessrule/seb/db/caches.php | 47 ----- .../accessrule/seb/lang/en/quizaccess_seb.php | 5 +- .../seb/tests/quiz_settings_test.php | 60 +----- .../seb/tests/settings_provider_test.php | 9 + mod/quiz/accessrule/seb/version.php | 2 +- mod/quiz/classes/form/edit_override_form.php | 191 ++++++++++++++++++ mod/quiz/classes/local/override_manager.php | 56 ++++- mod/quiz/db/install.xml | 1 + mod/quiz/db/upgrade.php | 15 ++ mod/quiz/lib.php | 48 +++++ mod/quiz/overrideedit.php | 19 ++ mod/quiz/overrides.php | 10 + 15 files changed, 456 insertions(+), 200 deletions(-) delete mode 100644 mod/quiz/accessrule/seb/db/caches.php diff --git a/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php index 1e338b497869a..0997518050f3c 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(); @@ -190,125 +191,95 @@ protected static function define_properties(): array { /** * Return an instance by quiz id. * - * This method gets data from cache before doing any DB calls. - * * @param int $quizid Quiz id. * @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; + + $sebquizsetting = self::get_record(['quizid' => $quizid]); + + // Overwrite settings from override manager if available. + if (quiz_has_user_overrides($quizid)) { + // 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) && (bool) $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; } /** - * Return cached SEB config represented as a string by quiz ID. + * Get override record if there is for the current user. * - * @param int $quizid Quiz id. - * @return string|null + * @return \stdClass|false */ - public static function get_config_by_quiz_id(int $quizid): ?string { - $config = self::get_config_cache()->get($quizid); + protected function get_override() { + global $DB, $USER; + $userid = $USER->id; + $quizid = $this->get('quizid'); - if ($config !== false) { - return $config; + $override = $DB->get_record('quiz_overrides', ['quiz' => $quizid, 'userid' => $userid]); + + if (!$override) { + $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])); + $select = "groupid $extra AND quiz = ?"; + $params[] = $quiz->id; + $override = $DB->get_records_select('quiz_overrides', $select, $params); + } } + return $override; + } + + /** + * Return SEB config represented as a string by quiz ID. + * + * @param int $quizid Quiz id. + * @return string|null + */ + public static function get_config_by_quiz_id(int $quizid): ?string { $config = null; if ($settings = self::get_by_quiz_id($quizid)) { $config = $settings->get_config(); - self::get_config_cache()->set($quizid, $config); } return $config; } /** - * Return cached SEB config key by quiz ID. + * Return SEB config key by quiz ID. * * @param int $quizid Quiz id. * @return string|null */ public static function get_config_key_by_quiz_id(int $quizid): ?string { - $configkey = self::get_config_key_cache()->get($quizid); - - 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); } - return $configkey; } - /** - * Return SEB config key cache instance. - * - * @return \cache_application - */ - private static function get_config_key_cache(): \cache_application { - return \cache::make('quizaccess_seb', 'configkey'); - } - - /** - * Return SEB config cache instance. - * - * @return \cache_application - */ - private static function get_config_cache(): \cache_application { - return \cache::make('quizaccess_seb', 'config'); - } - - /** - * Return quiz settings cache object, - * - * @return \cache_application - */ - private static function get_quiz_settings_cache(): \cache_application { - return \cache::make('quizaccess_seb', 'quizsettings'); - } - - /** - * Adds the new record to the cache. - */ - protected function after_create() { - $this->after_save(); - } - - /** - * Updates the cache record. - * - * @param bool $result - */ - protected function after_update($result) { - $this->after_save(); - } - - /** - * 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); - } - - /** - * Removes unnecessary stuff from db. - */ - protected function before_delete() { - $key = $this->get('quizid'); - self::get_quiz_settings_cache()->delete($key); - self::get_config_cache()->delete($key); - self::get_config_key_cache()->delete($key); - } - /** * Validate the browser exam keys string. * diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php index f7a8ffdaa10f4..69da7e73bfb3e 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_donotrequire($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_donotrequire($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 not require SEB for a quiz. + * + * @param \context $context Context to check access in. + * @return bool + */ + public static function can_donotrequire(\context $context): bool { + return has_capability('quizaccess/seb:manage_seb_donotrequiresafeexambrowser', $context); + } + + /** + * Check if the current user can not require SEB for a quiz in the override menu. + * + * @param \context $context Context to check access in. + * @return bool + */ + public static function can_override_donotrequire(\context $context): bool { + return has_capability('quizaccess/seb:override_seb_donotrequiresafeexambrowser', $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..8c48a0a4f8671 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_donotrequiresafeexambrowser' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + 'quizaccess/seb:override_seb_donotrequiresafeexambrowser' => [ + '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/db/caches.php b/mod/quiz/accessrule/seb/db/caches.php deleted file mode 100644 index 4a93bc9fbf7c5..0000000000000 --- a/mod/quiz/accessrule/seb/db/caches.php +++ /dev/null @@ -1,47 +0,0 @@ -. - -/** - * Plugin cache definitions. - * - * @package quizaccess_seb - * @author Dmitrii Metelkin - * @copyright 2020 Catalyst IT - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -$definitions = [ - 'quizsettings' => [ - 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, - 'simpledata' => true, - 'staticacceleration' => true, - ], - 'config' => [ - 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, - 'simpledata' => true, - 'staticacceleration' => true, - ], - 'configkey' => [ - 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, - 'simpledata' => true, - 'staticacceleration' => true, - ], -]; diff --git a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php index c340f295a2f2d..3270f53c8d9ef 100644 --- a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php +++ b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php @@ -30,9 +30,6 @@ $string['addtemplate'] = 'Add new template'; $string['allowedbrowserkeysdistinct'] = 'The keys must all be different.'; $string['allowedbrowserkeyssyntax'] = 'A key should be a 64-character hex string.'; -$string['cachedef_config'] = 'SEB config cache'; -$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...'; @@ -106,6 +103,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_donotrequiresafeexambrowser'] = 'Change SEB quiz setting: Do not require Safe Exam Browser'; +$string['seb:override_seb_donotrequiresafeexambrowser'] = '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/quiz_settings_test.php b/mod/quiz/accessrule/seb/tests/quiz_settings_test.php index b70988aeb7a68..f0d3d7584e9e9 100644 --- a/mod/quiz/accessrule/seb/tests/quiz_settings_test.php +++ b/mod/quiz/accessrule/seb/tests/quiz_settings_test.php @@ -730,26 +730,6 @@ public function test_generates_config_values_as_null_when_expected(): void { $this->assertNotNull($quizsettings->get_config_key()); } - /** - * Test that quizsettings cache exists after creation. - */ - public function test_quizsettings_cache_exists_after_creation(): void { - $expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); - $this->assertEquals($expected->to_record(), \cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); - } - - /** - * Test that quizsettings cache gets deleted after deletion. - */ - public function test_quizsettings_cache_purged_after_deletion(): void { - $this->assertNotEmpty(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); - - $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); - $quizsettings->delete(); - - $this->assertFalse(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id)); - } - /** * Test that we can get seb_quiz_settings by quiz id. */ @@ -762,7 +742,7 @@ public function test_get_quiz_settings_by_quiz_id(): void { $expected->set('showsebtaskbar', 0); $this->assertNotEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); - // Now save and check that cached as been updated. + // Now save and check for update. $expected->save(); $this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record()); @@ -770,25 +750,6 @@ public function test_get_quiz_settings_by_quiz_id(): void { $this->assertFalse(seb_quiz_settings::get_by_quiz_id(7777777)); } - /** - * Test that SEB config cache exists after creation of the quiz. - */ - public function test_config_cache_exists_after_creation(): void { - $this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); - } - - /** - * Test that SEB config cache gets deleted after deletion. - */ - public function test_config_cache_purged_after_deletion(): void { - $this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); - - $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); - $quizsettings->delete(); - - $this->assertFalse(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id)); - } - /** * Test that we can get SEB config by quiz id. */ @@ -810,25 +771,6 @@ public function test_get_config_by_quiz_id(): void { $this->assertNull(seb_quiz_settings::get_config_by_quiz_id(7777777)); } - /** - * Test that SEB config key cache exists after creation of the quiz. - */ - public function test_config_key_cache_exists_after_creation(): void { - $this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); - } - - /** - * Test that SEB config key cache gets deleted after deletion. - */ - public function test_config_key_cache_purged_after_deletion(): void { - $this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); - - $quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); - $quizsettings->delete(); - - $this->assertFalse(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id)); - } - /** * Test that we can get SEB config key by quiz id. */ diff --git a/mod/quiz/accessrule/seb/tests/settings_provider_test.php b/mod/quiz/accessrule/seb/tests/settings_provider_test.php index 821d1c9e36aad..bc00c3e69163e 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_donotrequiresafeexambrowser', 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..2383dfae16bc7 100644 --- a/mod/quiz/classes/form/edit_override_form.php +++ b/mod/quiz/classes/form/edit_override_form.php @@ -23,6 +23,8 @@ use moodle_url; use moodleform; use stdClass; +use quizaccess_seb\seb_quiz_settings; +use quizaccess_seb\settings_provider; defined('MOODLE_INTERNAL') || die(); @@ -59,6 +61,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 +85,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 +230,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 +248,188 @@ 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_donotrequire($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); + $default = match (true) { + isset($this->sebdata['seb_showsebdownloadlink']) => $this->sebdata['seb_showsebdownloadlink'], + isset($this->quiz->seb_showsebdownloadlink) => $this->quiz->seb_showsebdownloadlink, + default => 1, + }; + $mform->setDefault('seb_showsebdownloadlink', $default); + $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); + $default = match (true) { + isset($this->sebdata['seb_showsebdownloadlink']) => $this->sebdata['seb_showsebdownloadlink'], + isset($this->quiz->seb_showsebdownloadlink) => $this->quiz->seb_showsebdownloadlink, + default => 1, + }; + $mform->setDefault('seb_showsebdownloadlink', $default); + $mform->disabledIf($name, 'enableseboverride'); + + if (isset($defaults[$name])) { + $default = match (true) { + isset($this->sebdata[$name]) => $this->sebdata[$name], + isset($this->quiz->{$name}) => $this->quiz->{$name}, + default => $defaults[$name], + }; + $mform->setDefault($name, $default); + } + + 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 827cdeac5e11a..ef9a76502a68f 100644 --- a/mod/quiz/classes/local/override_manager.php +++ b/mod/quiz/classes/local/override_manager.php @@ -33,7 +33,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 @@ -57,6 +84,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. * @@ -216,7 +252,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); @@ -239,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(['enableseboverride', ...self::OVERRIDEABLE_QUIZ_SEB_SETTINGS])); + $sebdata = serialize($sebdata); + + $datatoset['sebdata'] = $sebdata; $datatoset['quiz'] = $this->quiz->id; // Validate the data is OK. @@ -573,6 +615,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; @@ -593,9 +640,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 492fb2901892c..28f4523572b9d 100644 --- a/mod/quiz/overrideedit.php +++ b/mod/quiz/overrideedit.php @@ -75,6 +75,25 @@ if (!$manager->can_view_override($override, $course, $cm)) { throw new \moodle_exception('invalidoverrideid', 'quiz'); } + + // 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'); + } + } else { + if (!groups_user_groups_visible($course, $override->userid, $cm)) { + throw new \moodle_exception('invalidoverrideid', 'quiz'); + } + } } else { // Creating a new override. $data = new stdClass(); 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 = [];