diff --git a/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php index 0d8ba022e666e..3f3241686568b 100644 --- a/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php +++ b/mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php @@ -71,10 +71,43 @@ protected function define_quiz_subplugin_structure() { // Save the settings. $subpluginquizsettings = new backup_nested_element('quizaccess_seb_quizsettings', null, $settingskeys); + // Save the overrides. + $overridekeys = [ + 'id', + 'overrideid', + 'templateid', + 'enabled', + 'requiresafeexambrowser', + 'showsebtaskbar', + 'showwificontrol', + 'showreloadbutton', + 'showtime', + 'showkeyboardlayout', + 'allowuserquitseb', + 'quitpassword', + 'linkquitseb', + 'userconfirmquit', + 'enableaudiocontrol', + 'muteonstartup', + 'allowspellchecking', + 'allowreloadinexam', + 'activateurlfiltering', + 'filterembeddedcontent', + 'expressionsallowed', + 'regexallowed', + 'expressionsblocked', + 'regexblocked', + 'allowedbrowserexamkeys', + 'showsebdownloadlink', + 'timecreated', + ]; + $subpluginoverrides = new backup_nested_element('quizaccess_seb_override', null, $overridekeys); + // Connect XML elements into the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($subpluginquizsettings); $subpluginquizsettings->add_child($subplugintemplatesettings); + $subpluginwrapper->add_child($subpluginoverrides); // Set source to populate the settings data by referencing the ID of quiz being backed up. $subpluginquizsettings->set_source_table(quizaccess_seb\seb_quiz_settings::TABLE, ['quizid' => $quizid]); @@ -84,6 +117,15 @@ protected function define_quiz_subplugin_structure() { $params = ['id' => '../templateid']; $subplugintemplatesettings->set_source_table(\quizaccess_seb\template::TABLE, $params); + // Set source to populate the override data by referencing the ID of quiz being backed up. + $sql = "SELECT qso.* + FROM {quizaccess_seb_override} qso + JOIN {quiz_overrides} qo + ON qo.id = qso.overrideid + AND qo.quiz = ?"; + $params = [$quizid]; + $subpluginoverrides->set_source_sql($sql, $params); + return $subplugin; } } diff --git a/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php b/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php index 929c0043107b2..d54fc7bbf3722 100644 --- a/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php +++ b/mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php @@ -39,6 +39,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin { + /** + * A list of quizaccess_seb_override records to be restored. + * @var array + */ + protected $overrides = []; /** * Provides path structure required to restore data for seb quiz access plugin. @@ -56,6 +61,10 @@ protected function define_quiz_subplugin_structure() { $path = $this->get_pathfor('/quizaccess_seb_quizsettings/quizaccess_seb_template'); $paths[] = new restore_path_element('quizaccess_seb_template', $path); + // Overrides. + $path = $this->get_pathfor('/quizaccess_seb_override'); + $paths[] = new restore_path_element('quizaccess_seb_override', $path); + return $paths; } @@ -128,5 +137,43 @@ public function process_quizaccess_seb_template($data) { } } -} + /** + * Process the restored data for the quizaccess_seb_override table. + * + * @param stdClass $data Data for quizaccess_seb_override retrieved from backup xml. + */ + public function process_quizaccess_seb_override($data) { + global $DB, $USER; + // Process quizsettings. + $data = (object) $data; + unset($data->id); + $data->timecreated = $data->timemodified = time(); + $data->usermodified = $USER->id; + + // Do not use template if it is no longer enabled. + if ($data->requiresafeexambrowser == settings_provider::USE_SEB_TEMPLATE && + !$DB->record_exists(template::TABLE, ['id' => $data->templateid, 'enabled' => '1'])) { + $data->templateid = 0; + $data->requiresafeexambrowser = settings_provider::USE_SEB_NO; + } + + // We wait until the quiz is complete before we restore as we need to get the new quiz_override IDs. + $this->overrides[] = $data; + } + + /** + * Maps the new override IDs to the quizaccess_seb_override entries. + * + * @return void + */ + public function after_restore_quiz() { + global $DB; + foreach ($this->overrides as $data) { + $newoverrideid = $this->get_mappingid('quiz_override', $data->overrideid); + $data->overrideid = $newoverrideid; + $DB->insert_record('quizaccess_seb_override', $data); + } + } + +} diff --git a/mod/quiz/accessrule/seb/classes/privacy/provider.php b/mod/quiz/accessrule/seb/classes/privacy/provider.php index d6a33b36ffcaa..706b6128a4978 100644 --- a/mod/quiz/accessrule/seb/classes/privacy/provider.php +++ b/mod/quiz/accessrule/seb/classes/privacy/provider.php @@ -56,6 +56,17 @@ class provider implements * @return collection Collection of metadata. */ public static function get_metadata(collection $collection): collection { + $collection->add_database_table( + 'quizaccess_seb_override', + [ + 'overrideid' => 'privacy:metadata:quizaccess_seb_override:overrideid', + 'usermodified' => 'privacy:metadata:quizaccess_seb_override:usermodified', + 'timecreated' => 'privacy:metadata:quizaccess_seb_override:timecreated', + 'timemodified' => 'privacy:metadata:quizaccess_seb_override:timemodified', + ], + 'privacy:metadata:quizaccess_seb_override' + ); + $collection->add_database_table( 'quizaccess_seb_quizsettings', [ diff --git a/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php b/mod/quiz/accessrule/seb/classes/seb_quiz_settings.php index 1e338b497869a..c02082181f541 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\local\override_manager; defined('MOODLE_INTERNAL') || die(); @@ -193,14 +194,52 @@ protected static function define_properties(): array { * This method gets data from cache before doing any DB calls. * * @param int $quizid Quiz id. - * @return false|\quizaccess_seb\seb_quiz_settings + * @return false|seb_quiz_settings */ public static function get_by_quiz_id(int $quizid) { - if ($data = self::get_quiz_settings_cache()->get($quizid)) { + if ($data = self::get_quiz_settings_cache()->get(self::get_cache_key($quizid))) { return new static(0, $data); } - return self::get_record(['quizid' => $quizid]); + return self::get_settings(['quizid' => $quizid]) ?: false; + } + + /** + * Return an instance by quiz id and apply an override if available. Similar to self::get_records. + * + * @param array $filters conditions used to query seb_quiz_settings. + * @return ?seb_quiz_settings + */ + public static function get_settings(array $filters = []) { + $sebquizsetting = self::get_record($filters) ?: null; + + if ($sebquizsetting || $filters['quizid']) { + $quizid = $filters['quizid'] ?? $sebquizsetting->get('quizid'); + + // Overwrite settings if available. + if ($override = override_manager::get_quiz_override($quizid)) { + // Create blank seb_quiz_settings instance if none exists. + if (!$sebquizsetting) { + $record = (object)[ + 'quizid' => $quizid, + 'cmid' => get_coursemodule_from_instance('quiz', $quizid)->id, + ]; + $sebquizsetting = new self(0, $record); + } + + // Overwrite settings if enabled. + if (isset($override->seb_enabled) && (bool) $override->seb_enabled) { + $prefix = 'seb_'; + foreach (array_keys(self::properties_definition()) as $key) { + if (isset($override->{$prefix.$key})) { + $sebquizsetting->set($key, $override->{$prefix.$key}); + } + } + } + } + } + + return $sebquizsetting; } /** @@ -210,7 +249,8 @@ 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); + $cachekey = self::get_cache_key($quizid); + $config = self::get_config_cache()->get($cachekey); if ($config !== false) { return $config; @@ -219,7 +259,7 @@ 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); + self::get_config_cache()->set($cachekey, $config); } return $config; @@ -232,7 +272,8 @@ 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); + $cachekey = self::get_cache_key($quizid); + $configkey = self::get_config_key_cache()->get($cachekey); if ($configkey !== false) { return $configkey; @@ -241,7 +282,7 @@ public static function get_config_key_by_quiz_id(int $quizid): ?string { $configkey = null; if ($settings = self::get_by_quiz_id($quizid)) { $configkey = $settings->get_config_key(); - self::get_config_key_cache()->set($quizid, $configkey); + self::get_config_key_cache()->set($cachekey, $configkey); } return $configkey; @@ -294,19 +335,31 @@ 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); + $quizid = $this->get('quizid'); + $cachekey = self::get_cache_key($quizid); + self::get_quiz_settings_cache()->set($cachekey, $this->to_record()); + self::get_config_cache()->set($cachekey, $this->config); + self::get_config_key_cache()->set($cachekey, $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); + $cachekey = self::get_cache_key($this->get('quizid')); + self::delete_cache($cachekey); + } + + /** + * Removes cached SEB data. + * + * @param string $cachekey The ID of of a quiz, or combined with the ID of an override e.g. '12' or '12-1'. + * @return void + */ + public static function delete_cache($cachekey): void { + self::get_quiz_settings_cache()->delete($cachekey); + self::get_config_cache()->delete($cachekey); + self::get_config_key_cache()->delete($cachekey); } /** @@ -437,6 +490,19 @@ public function get_config(): ?string { return $this->config; } + /** + * Return key to index cache. Takes override into account. + * + * @param int $quizid Quiz id. + * @return string + */ + private static function get_cache_key($quizid) { + if ($override = override_manager::get_quiz_override($quizid)) { + return "$quizid-{$override->id}"; + } + return $quizid; + } + /** * Case for USE_SEB_NO. */ diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php index f374a4bb82b46..d5e9ab0a2b5a8 100644 --- a/mod/quiz/accessrule/seb/classes/settings_provider.php +++ b/mod/quiz/accessrule/seb/classes/settings_provider.php @@ -188,16 +188,18 @@ 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()) + 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())) { @@ -556,6 +558,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; } @@ -566,7 +573,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'); @@ -593,7 +604,7 @@ public static function get_requiresafeexambrowser_options(\context $context): ar * Returns a list of templates. * @return array */ - protected static function get_template_options($cmid): array { + public static function get_template_options($cmid): array { $templates = []; $templatetable = template::TABLE; $sebquizsettingstable = seb_quiz_settings::TABLE; @@ -832,6 +843,26 @@ public static function can_configure_manually(\context $context): bool { return false; } + /** + * 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:manage_seb_donotrequiresafeexambrowser_override', $context); + } + /** * Check if the current user can manage provided SEB setting. * diff --git a/mod/quiz/accessrule/seb/db/access.php b/mod/quiz/accessrule/seb/db/access.php index 2972b868892d6..ec89c32989fed 100644 --- a/mod/quiz/accessrule/seb/db/access.php +++ b/mod/quiz/accessrule/seb/db/access.php @@ -57,6 +57,14 @@ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], + ], + 'quizaccess/seb:manage_seb_donotrequiresafeexambrowser' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], 'clonepermissionsfrom' => 'quizaccess/seb:manage_seb_requiresafeexambrowser', ], // Ability to select "Yes – Use SEB client config" as an option for "Require the use of Safe Exam Browser". @@ -67,6 +75,14 @@ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], + ], + 'quizaccess/seb:manage_seb_donotrequiresafeexambrowser_override' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], 'clonepermissionsfrom' => 'quizaccess/seb:manage_seb_requiresafeexambrowser', ], 'quizaccess/seb:manage_seb_templateid' => [ diff --git a/mod/quiz/accessrule/seb/db/caches.php b/mod/quiz/accessrule/seb/db/caches.php index 4a93bc9fbf7c5..79fd4b9436fc7 100644 --- a/mod/quiz/accessrule/seb/db/caches.php +++ b/mod/quiz/accessrule/seb/db/caches.php @@ -28,19 +28,19 @@ $definitions = [ 'quizsettings' => [ 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, + 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, ], 'config' => [ 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, + 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, ], 'configkey' => [ 'mode' => cache_store::MODE_APPLICATION, - 'simplekeys' => true, + 'simplekeys' => false, 'simpledata' => true, 'staticacceleration' => true, ], diff --git a/mod/quiz/accessrule/seb/db/install.xml b/mod/quiz/accessrule/seb/db/install.xml index 17caa56b3c3bf..a6e22e064e977 100644 --- a/mod/quiz/accessrule/seb/db/install.xml +++ b/mod/quiz/accessrule/seb/db/install.xml @@ -1,5 +1,5 @@ - @@ -44,6 +44,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/mod/quiz/accessrule/seb/db/upgrade.php b/mod/quiz/accessrule/seb/db/upgrade.php index 2a1b7c178b92c..5c2dc62d5ce77 100644 --- a/mod/quiz/accessrule/seb/db/upgrade.php +++ b/mod/quiz/accessrule/seb/db/upgrade.php @@ -48,6 +48,58 @@ function xmldb_quizaccess_seb_upgrade($oldversion) { // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. + global $DB; + $dbman = $DB->get_manager(); + + if ($oldversion < 2024110700) { + // Define table quizaccess_seb_override to be created. + $table = new xmldb_table('quizaccess_seb_override'); + + // Adding fields to table quizaccess_seb_override. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('overrideid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('templateid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('enabled', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('requiresafeexambrowser', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('showsebtaskbar', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('showwificontrol', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('showreloadbutton', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('showtime', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('showkeyboardlayout', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('allowuserquitseb', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('quitpassword', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('linkquitseb', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('userconfirmquit', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('enableaudiocontrol', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('muteonstartup', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('allowspellchecking', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('allowreloadinexam', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('activateurlfiltering', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('filterembeddedcontent', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('expressionsallowed', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('regexallowed', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('expressionsblocked', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('regexblocked', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('allowedbrowserexamkeys', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('showsebdownloadlink', XMLDB_TYPE_INTEGER, '1', null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table quizaccess_seb_override. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('overrideid', XMLDB_KEY_FOREIGN, ['overrideid'], 'quiz_overrides', ['id']); + $table->add_key('templateid', XMLDB_KEY_FOREIGN, ['templateid'], 'quizaccess_seb_template', ['id']); + $table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']); + + // Conditionally launch create table for quizaccess_seb_override. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_plugin_savepoint(true, 2024110700, 'quizaccess', 'seb'); + } return true; } diff --git a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php index c6e307f3da033..537374dc7aba2 100644 --- a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php +++ b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php @@ -77,6 +77,11 @@ $string['notemplate'] = 'No template'; $string['passwordnotset'] = 'Current settings require quizzes using the Safe Exam Browser to have a quiz password set.'; $string['pluginname'] = 'Safe Exam Browser access rules'; +$string['privacy:metadata:quizaccess_seb_override'] = 'Safe Exam Browser settings for a quiz override. This includes the ID of the last user to create or modify the settings and the id of the quiz override.'; +$string['privacy:metadata:quizaccess_seb_override:overrideid'] = 'ID of the override the settings are attached to.'; +$string['privacy:metadata:quizaccess_seb_override:timecreated'] = 'Unix time that the template was created.'; +$string['privacy:metadata:quizaccess_seb_override:timemodified'] = 'Unix time that the template was last modified.'; +$string['privacy:metadata:quizaccess_seb_override:usermodified'] = 'ID of user who last created or modified the template.'; $string['privacy:metadata:quizaccess_seb_quizsettings'] = 'Safe Exam Browser settings for a quiz. This includes the ID of the last user to create or modify the settings.'; $string['privacy:metadata:quizaccess_seb_quizsettings:quizid'] = 'ID of the quiz the settings exist for.'; $string['privacy:metadata:quizaccess_seb_quizsettings:timecreated'] = 'Unix time that the settings were created.'; @@ -97,6 +102,8 @@ $string['seb:manage_seb_allowspellchecking'] = 'Change SEB quiz setting: Enable spell checking'; $string['seb:manage_seb_allowuserquitseb'] = 'Change SEB quiz setting: Allow quit'; $string['seb:manage_seb_configuremanually'] = 'Change SEB quiz setting: Select manual configuration'; +$string['seb:manage_seb_donotrequiresafeexambrowser'] = 'Change SEB quiz setting: Do not require Safe Exam Browser'; +$string['seb:manage_seb_donotrequiresafeexambrowser_override'] = 'Override SEB quiz setting: Do not require Safe Exam Browser'; $string['seb:manage_seb_enableaudiocontrol'] = 'Change SEB quiz setting: Enable audio control'; $string['seb:manage_seb_expressionsallowed'] = 'Change SEB quiz setting: Simple expressions allowed'; $string['seb:manage_seb_expressionsblocked'] = 'Change SEB quiz setting: Simple expressions blocked'; diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php index b419f703b23b6..232dca6b201bc 100644 --- a/mod/quiz/accessrule/seb/rule.php +++ b/mod/quiz/accessrule/seb/rule.php @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use mod_quiz\form\edit_override_form; use mod_quiz\local\access_rule_base; +use mod_quiz\local\access_rule_overridable; use mod_quiz\quiz_attempt; use quizaccess_seb\seb_access_manager; use quizaccess_seb\seb_quiz_settings; @@ -30,7 +32,7 @@ * @copyright 2019 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class quizaccess_seb extends access_rule_base { +class quizaccess_seb extends access_rule_base implements access_rule_overridable { /** @var seb_access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */ private $accessmanager; @@ -269,6 +271,472 @@ public static function get_settings_sql($quizid): array { ]; } + /** + * Fetches the best suited default value for a field. If there is an override value set, use this. + * If there's no override value, check if the quiz had SEB settings and use this value instead. + * Otherwise, use the default value defined. + * + * @param string $field The field key to search $default and $override. + * @param string $default The default form value. + * @param \stdClass|null $override The override data object. + * @param \stdClass $quiz The quiz data object. + * @param boolean $removeprefix Remove 'seb_' from the field key. + * @return string + */ + protected static function get_override_default_field( + string $field, string $default, ?\stdClass $override, \stdClass $quiz, bool $removeprefix = false): string { + if ($removeprefix) { + $field = substr($field, 4); + } + return match(true) { + isset($override->field) => $override->field, + isset($quiz->$field) => $quiz->$field, + default => $default, + }; + } + + /** + * Override form section header. If this is false, the plugin's override form fields will not appear. + * + * Array must have two keys. The 'name' key for the name of the heading element, and the 'title' key for the + * text to display in the heading. + * + * @return array (name, title, expand) + */ + public static function get_override_form_section_header(): array { + return ['name' => 'seb', 'title' => get_string('seb', 'quizaccess_seb')]; + } + + /** + * Determine whether the rule section should be expanded or not in the override form. + * + * @param edit_override_form $quizform the quiz override settings form being built. + * @return bool set true if section should be expanded. + */ + public static function get_override_form_section_expand(edit_override_form $quizform): bool { + $quizid = $quizform->get_quiz()->id; + return seb_quiz_settings::record_exists_select('quizid = ?', [$quizid]); + } + + /** + * Add fields to the quiz override form. + * + * @param edit_override_form $quizform the quiz override settings form being built. + * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. + * @return void + */ + public static function add_override_form_fields(edit_override_form $quizform, MoodleQuickForm $mform): void { + global $DB; + $override = $DB->get_record('quizaccess_seb_override', ['overrideid' => $quizform->get_overrideid()]) ?: null; + $context = $quizform->get_context(); + $quiz = $quizform->get_quiz(); + $templateoptions = settings_provider::get_template_options($quiz->cmid); + + $mform->addElement('checkbox', 'seb_enabled', get_string('enabled', 'quizaccess_seb')); + $mform->setDefault('seb_enabled', self::get_override_default_field('enabled', false, $override, $quiz)); + + // ... "Require the use of Safe Exam Browser" + if (settings_provider::can_override_donotrequire($context)) { + $requireseboptions[settings_provider::USE_SEB_NO] = get_string('no'); + } + + if (settings_provider::can_configure_manually($context) || + settings_provider::is_conflicting_permissions($context)) { + $requireseboptions[settings_provider::USE_SEB_CONFIG_MANUALLY] = get_string('seb_use_manually', 'quizaccess_seb'); + } + + if (settings_provider::can_use_seb_template($context) || + settings_provider::is_conflicting_permissions($context)) { + if (!empty($templateoptions)) { + $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', + self::get_override_default_field('requiresafeexambrowser', 0, $override, $quiz), + ); + $mform->addHelpButton('seb_requiresafeexambrowser', 'seb_requiresafeexambrowser', 'quizaccess_seb'); + $mform->disabledIf('seb_requiresafeexambrowser', 'enableseboverride'); + + if (settings_provider::is_conflicting_permissions($context)) { + $mform->freeze('seb_requiresafeexambrowser'); + } + + // ... "Safe Exam Browser config template" + if (settings_provider::can_use_seb_template($context) || + settings_provider::is_conflicting_permissions($context)) { + $element = $mform->addElement( + 'select', + 'seb_templateid', + get_string('seb_templateid', 'quizaccess_seb'), + $templateoptions, + ); + } else { + $element = $mform->addElement('hidden', 'seb_templateid'); + } + + $mform->setType('seb_templateid', PARAM_INT); + $mform->setDefault('seb_templateid', self::get_override_default_field('templateid', 0, $override, $quiz)); + $mform->addHelpButton('seb_templateid', 'seb_templateid', 'quizaccess_seb'); + $mform->disabledIf('seb_templateid', 'enableseboverride'); + + if (settings_provider::is_conflicting_permissions($context)) { + $mform->freeze('seb_templateid'); + } + + // ... "Show Safe Exam browser download button" + if (settings_provider::can_change_seb_showsebdownloadlink($context)) { + $mform->addElement('selectyesno', + 'seb_showsebdownloadlink', + get_string('seb_showsebdownloadlink', 'quizaccess_seb') + ); + + $mform->setType('seb_showsebdownloadlink', PARAM_BOOL); + $mform->setDefault('seb_showsebdownloadlink', + self::get_override_default_field('showsebdownloadlink', 1, $override, $quiz)); + $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, $context)) { + $type = 'hidden'; + } + + $mform->addElement($type, $name, get_string($name, 'quizaccess_seb')); + + $mform->addHelpButton($name, $name, 'quizaccess_seb'); + $mform->setType($name, PARAM_BOOL); + $mform->setDefault($name, + self::get_override_default_field($name, 1, $override, $quiz)); + $mform->disabledIf($name, 'enableseboverride'); + + if (isset($defaults[$name])) { + $mform->setDefault($name, + self::get_override_default_field($name, $defaults[$name], $override, $quiz, true)); + } + + if (isset($types[$name])) { + $mform->setType($name, $types[$name]); + } + } + + if (settings_provider::can_change_seb_allowedbrowserexamkeys($context)) { + $mform->addElement('textarea', + 'seb_allowedbrowserexamkeys', + get_string('seb_allowedbrowserexamkeys', 'quizaccess_seb'), + ); + + $mform->setType('seb_allowedbrowserexamkeys', PARAM_RAW); + $mform->setDefault('seb_allowedbrowserexamkeys', + self::get_override_default_field('allowedbrowserexamkeys', '', $override, $quiz)); + $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($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) $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); + } + } + } + } + + + /** + * Validate the data from any form fields added using {@see add_override_form_fields()}. + * + * @param array $errors the errors found so far. + * @param array $data the submitted form data. + * @param array $files information about any uploaded files. + * @param edit_override_form $quizform the quiz override form object. + * @return array $errors the updated $errors array. + */ + public static function validate_override_form_fields( + array $errors, array $data, array $files, edit_override_form $quizform): array { + $context = $quizform->get_context(); + $cmid = $context->instanceid; + $quizid = get_module_from_cmid($cmid)[0]->id; + + if (!settings_provider::can_configure_seb($context)) { + return $errors; + } + + if (settings_provider::is_seb_settings_locked($quizid)) { + return $errors; + } + + if (settings_provider::is_conflicting_permissions($context)) { + return $errors; + } + + $settings = settings_provider::filter_plugin_settings((object) $data); + + // Validate basic settings using persistent class. + $quizsettings = (new seb_quiz_settings())->from_record($settings); + $quizsettings->set('cmid', $cmid); + $quizsettings->set('quizid', $quizid); + + // Edge case for filemanager_sebconfig. + if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) { + $errorvalidatefile = settings_provider::validate_draftarea_configfile($data['filemanager_sebconfigfile']); + if (!empty($errorvalidatefile)) { + $errors['filemanager_sebconfigfile'] = $errorvalidatefile; + } + } + + // Edge case to force user to select a template. + if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_TEMPLATE) { + if (empty($data['seb_templateid'])) { + $errors['seb_templateid'] = get_string('invalidtemplate', 'quizaccess_seb'); + } + } + + if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) { + // Global settings may be active which require a quiz password to be set if using SEB. + if (!empty(get_config('quizaccess_seb', 'quizpasswordrequired')) && empty($data['quizpassword'])) { + $errors['quizpassword'] = get_string('passwordnotset', 'quizaccess_seb'); + } + } + + return $errors; + } + + /** + * Save any submitted settings when the quiz override settings form is submitted. + * + * @param array $override data from the override form. + * @return void + */ + public static function save_override_settings($override): void { + global $DB, $USER; + + $defaults = [ + 'seb_enabled' => 0, + 'seb_requiresafeexambrowser' => 0, + 'seb_templateid' => 0, + 'seb_allowedbrowserexamkeys' => '', + 'seb_showsebdownloadlink' => 1, + ]; + $defaults += settings_provider::get_seb_config_element_defaults(); + + foreach ($defaults as $key => $default) { + if (!isset($override[$key])) { + $override[$key] = $default; + } + } + + $seboverride = (object)[ + 'overrideid' => $override['overrideid'], + 'enabled' => $override['seb_enabled'], + 'templateid' => $override['seb_templateid'], + 'requiresafeexambrowser' => $override['seb_requiresafeexambrowser'], + 'showsebtaskbar' => $override['seb_showsebtaskbar'], + 'showwificontrol' => $override['seb_showwificontrol'], + 'showreloadbutton' => $override['seb_showreloadbutton'], + 'showtime' => $override['seb_showtime'], + 'showkeyboardlayout' => $override['seb_showkeyboardlayout'], + 'allowuserquitseb' => $override['seb_allowuserquitseb'], + 'quitpassword' => $override['seb_quitpassword'], + 'linkquitseb' => $override['seb_linkquitseb'], + 'userconfirmquit' => $override['seb_userconfirmquit'], + 'enableaudiocontrol' => $override['seb_enableaudiocontrol'], + 'muteonstartup' => $override['seb_muteonstartup'], + 'allowspellchecking' => $override['seb_allowspellchecking'], + 'allowreloadinexam' => $override['seb_allowreloadinexam'], + 'activateurlfiltering' => $override['seb_activateurlfiltering'], + 'filterembeddedcontent' => $override['seb_filterembeddedcontent'], + 'expressionsallowed' => $override['seb_expressionsallowed'], + 'regexallowed' => $override['seb_regexallowed'], + 'expressionsblocked' => $override['seb_expressionsblocked'], + 'regexblocked' => $override['seb_regexblocked'], + 'allowedbrowserexamkeys' => $override['seb_allowedbrowserexamkeys'], + 'showsebdownloadlink' => $override['seb_showsebdownloadlink'], + 'usermodified' => $USER->id, + 'timemodified' => time(), + ]; + + if ($seboverrideid = $DB->get_field('quizaccess_seb_override', 'id', ['overrideid' => $override['overrideid']])) { + $seboverride->id = $seboverrideid; + $DB->update_record('quizaccess_seb_override', $seboverride); + } else { + $seboverride->timecreated = time(); + $DB->insert_record('quizaccess_seb_override', $seboverride); + } + + // Delete cache. + $quizid = $DB->get_field('quiz_overrides', 'quiz', ['id' => $override['overrideid']]); + seb_quiz_settings::delete_cache("$quizid-{$override['overrideid']}"); + } + + /** + * Delete any rule-specific override settings when the quiz override is deleted. + * + * @param int $quizid all overrides being deleted should belong to the same quiz. + * @param array $overrides an array of override objects to be deleted. + * @return void + */ + public static function delete_override_settings($quizid, $overrides): void { + global $DB; + $ids = array_column($overrides, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids); + $DB->delete_records_select('quizaccess_seb_override', "id $insql", $inparams); + + foreach ($overrides as $override) { + $key = "{$quizid}-{$override->id}"; + seb_quiz_settings::delete_cache($key); + } + } + + /** + * Provide form field keys in the override form as a string array + * e.g. ['rule_enabled', 'rule_password']. + * + * @return array + */ + public static function get_override_setting_keys(): array { + return [ + 'seb_enabled', + 'seb_templateid', + 'seb_requiresafeexambrowser', + 'seb_showsebtaskbar', + 'seb_showwificontrol', + 'seb_showreloadbutton', + 'seb_showtime', + 'seb_showkeyboardlayout', + 'seb_allowuserquitseb', + 'seb_quitpassword', + 'seb_linkquitseb', + 'seb_userconfirmquit', + 'seb_enableaudiocontrol', + 'seb_muteonstartup', + 'seb_allowspellchecking', + 'seb_allowreloadinexam', + 'seb_activateurlfiltering', + 'seb_filterembeddedcontent', + 'seb_expressionsallowed', + 'seb_regexallowed', + 'seb_expressionsblocked', + 'seb_regexblocked', + 'seb_allowedbrowserexamkeys', + 'seb_showsebdownloadlink', + ]; + } + + /** + * Provide required form field keys in the override form as a string array + * e.g. ['rule_enabled']. + * + * @return array + */ + public static function get_override_required_setting_keys(): array { + return ['seb_enabled']; + } + + /** + * Get components of the SQL query to fetch the access rule components' override + * settings. To be used as part of a quiz_override query to reference. + * + * @param string $overridetablename Name of the table to reference for joins. + * @return array 'selects', 'joins' and 'params'. + */ + public static function get_override_settings_sql($overridetablename): array { + $selects = implode(', ', [ + 'seb.enabled seb_enabled', + 'seb.templateid seb_templateid', + 'seb.requiresafeexambrowser seb_requiresafeexambrowser', + 'seb.showsebtaskbar seb_showsebtaskbar', + 'seb.showwificontrol seb_showwificontrol', + 'seb.showreloadbutton seb_showreloadbutton', + 'seb.showtime seb_showtime', + 'seb.showkeyboardlayout seb_showkeyboardlayout', + 'seb.allowuserquitseb seb_allowuserquitseb', + 'seb.quitpassword seb_quitpassword', + 'seb.linkquitseb seb_linkquitseb', + 'seb.userconfirmquit seb_userconfirmquit', + 'seb.enableaudiocontrol seb_enableaudiocontrol', + 'seb.muteonstartup seb_muteonstartup', + 'seb.allowspellchecking seb_allowspellchecking', + 'seb.allowreloadinexam seb_allowreloadinexam', + 'seb.activateurlfiltering seb_activateurlfiltering', + 'seb.filterembeddedcontent seb_filterembeddedcontent', + 'seb.expressionsallowed seb_expressionsallowed', + 'seb.regexallowed seb_regexallowed', + 'seb.expressionsblocked seb_expressionsblocked', + 'seb.regexblocked seb_regexblocked', + 'seb.allowedbrowserexamkeys seb_allowedbrowserexamkeys', + 'seb.showsebdownloadlink seb_showsebdownloadlink', + ]); + return [ + $selects, + "LEFT JOIN {quizaccess_seb_override} seb ON seb.overrideid = {$overridetablename}.id", + [], + ]; + } + + /** + * Update fields and values of the override table using the override settings. + * + * @param object $override the override data to use to update the $fields and $values. + * @param array $fields the fields to populate. + * @param array $values the fields to populate. + * @param context $context the context of which the override is being applied to. + * @return array + */ + public static function add_override_table_fields($override, $fields, $values, $context): array { + if (isset($override->seb_enabled) && !empty($override->seb_enabled)) { + $fields[] = get_string('seb_requiresafeexambrowser', 'quizaccess_seb'); + $values[] = settings_provider::get_requiresafeexambrowser_options($context)[$override->seb_requiresafeexambrowser]; + } + return [$fields, $values]; + } + /** * Whether the user should be blocked from starting a new attempt or continuing * an attempt now. diff --git a/mod/quiz/accessrule/seb/tests/backup_restore_test.php b/mod/quiz/accessrule/seb/tests/backup_restore_test.php index 78300a240faa7..69724b87c9043 100644 --- a/mod/quiz/accessrule/seb/tests/backup_restore_test.php +++ b/mod/quiz/accessrule/seb/tests/backup_restore_test.php @@ -18,6 +18,9 @@ defined('MOODLE_INTERNAL') || die(); +global $CFG; + +require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); require_once(__DIR__ . '/test_helper_trait.php'); /** @@ -28,7 +31,7 @@ * @copyright 2020 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -final class backup_restore_test extends \advanced_testcase { +final class backup_restore_test extends \restore_date_testcase { use \quizaccess_seb_test_helper_trait; @@ -324,4 +327,111 @@ public function test_restore_template_to_a_different_site_when_the_same_name_but $this->assertEquals(2, template::count_records()); } + /** + * Test backup and restore seb settings with an override. + * + * @covers \backup_quizaccess_seb_subplugin::define_quiz_subplugin_structure + * @covers \restore_quizaccess_seb_subplugin::process_quizaccess_seb_override + * @covers \restore_quizaccess_seb_subplugin::after_restore_quiz + */ + public function test_backup_restore_override(): void { + global $DB; + $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); + $this->user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id); + + // Create SEB settings for quiz. + $seb = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]); + $seb->set('showsebdownloadlink', 0); + $seb->set('quitpassword', '123'); + $seb->save(); + + $this->assertEquals(1, seb_quiz_settings::count_records()); + $this->assertEquals(0, $DB->count_records('quiz_overrides')); + $this->assertEquals(0, $DB->count_records('quizaccess_seb_override')); + + // Create an override. + $overrideid = $this->save_override($this->user); + + $this->assertEquals(1, seb_quiz_settings::count_records()); + $this->assertEquals(1, $DB->count_records('quiz_overrides')); + $this->assertEquals(1, $DB->count_records('quizaccess_seb_override')); + + // Backup and count override records. + $this->backup_and_restore($this->course); + + $this->assertEquals(2, seb_quiz_settings::count_records()); + $this->assertEquals(2, $DB->count_records('quiz_overrides')); + $this->assertEquals(2, $DB->count_records('quizaccess_seb_override')); + + // Check values are as expected. + $override = $DB->get_record('quiz_overrides', ['id' => $overrideid]); + $seboverride = $DB->get_record('quizaccess_seb_override', ['overrideid' => $overrideid]); + $restoredoverride = $DB->get_record_sql("SELECT * FROM {quiz_overrides} WHERE id <> ?", [$overrideid]); + $restoredseboverride = $DB->get_record_sql("SELECT * FROM {quizaccess_seb_override} WHERE overrideid <> ?", [$overrideid]); + + $this->assertEquals($override->id, $seboverride->overrideid); + $this->assertEquals($restoredoverride->id, $restoredseboverride->overrideid); + $this->assertNotEquals($override->id, $restoredoverride->id); + + // Compare override settings to make sure nothing is lost. + // Exclude comparing the following values as they are expected to differ. + $exclude = ['id', 'overrideid', 'usermodified', 'timecreated', 'timemodified']; + $keys = array_diff(array_keys(get_object_vars($seboverride)), $exclude); + foreach ($keys as $key) { + $this->assertEquals($seboverride->{$key}, $restoredseboverride->{$key}); + } + } + + + /** + * Test backup and restore course and quiz with only an SEB override. + * + * @covers \backup_quizaccess_seb_subplugin::define_quiz_subplugin_structure + * @covers \restore_quizaccess_seb_subplugin::process_quizaccess_seb_override + * @covers \restore_quizaccess_seb_subplugin::after_restore_quiz + */ + public function test_backup_restore_override_no_seb(): void { + global $DB; + $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); + $this->user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id); + + $this->assertEquals(1, seb_quiz_settings::count_records()); + $this->assertEquals(0, $DB->count_records('quiz_overrides')); + $this->assertEquals(0, $DB->count_records('quizaccess_seb_override')); + + // Create an override. + $overrideid = $this->save_override($this->user); + + $this->assertEquals(1, seb_quiz_settings::count_records()); + $this->assertEquals(1, $DB->count_records('quiz_overrides')); + $this->assertEquals(1, $DB->count_records('quizaccess_seb_override')); + + // Backup and count override records. + $this->backup_and_restore($this->course); + + $this->assertEquals(2, seb_quiz_settings::count_records()); + $this->assertEquals(2, $DB->count_records('quiz_overrides')); + $this->assertEquals(2, $DB->count_records('quizaccess_seb_override')); + + // Check values are as expected. + $override = $DB->get_record('quiz_overrides', ['id' => $overrideid]); + $seboverride = $DB->get_record('quizaccess_seb_override', ['overrideid' => $overrideid]); + $restoredoverride = $DB->get_record_sql("SELECT * FROM {quiz_overrides} WHERE id <> ?", [$overrideid]); + $restoredseboverride = $DB->get_record_sql("SELECT * FROM {quizaccess_seb_override} WHERE overrideid <> ?", [$overrideid]); + + $this->assertEquals($override->id, $seboverride->overrideid); + $this->assertEquals($restoredoverride->id, $restoredseboverride->overrideid); + $this->assertNotEquals($override->id, $restoredoverride->id); + + // Compare override settings to make sure nothing is lost. + // Exclude comparing the following values as they are expected to differ. + $exclude = ['id', 'overrideid', 'usermodified', 'timecreated', 'timemodified']; + $keys = array_diff(array_keys(get_object_vars($seboverride)), $exclude); + foreach ($keys as $key) { + $this->assertEquals($seboverride->{$key}, $restoredseboverride->{$key}); + } + } + } diff --git a/mod/quiz/accessrule/seb/tests/override_test.php b/mod/quiz/accessrule/seb/tests/override_test.php new file mode 100644 index 0000000000000..bc72722784690 --- /dev/null +++ b/mod/quiz/accessrule/seb/tests/override_test.php @@ -0,0 +1,379 @@ +. + +namespace quizaccess_seb; + +use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; +use quizaccess_seb\helper; +use mod_quiz_external; +use core_external\external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once(__DIR__ . '/test_helper_trait.php'); +require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php'); + +/** + * Tests for Safe Exam Browser access rules + * + * @package quizaccess_seb + * @copyright 2024 Michael Kotlyar + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class override_test extends \advanced_testcase { + + use \quizaccess_seb_test_helper_trait; + + /** @var \stdClass $user A test logged-in user to override settings for. */ + protected $overrideuser; + + /** + * Set up method. + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(true); + $this->setAdminUser(); + + // Create course and users. + $this->course = $this->getDataGenerator()->create_course(); + $this->user = $this->getDataGenerator()->create_user(); + $this->overrideuser = $this->getDataGenerator()->create_user(); + + // Enrol users to course. + $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id); + $this->getDataGenerator()->enrol_user($this->overrideuser->id, $this->course->id); + } + + /** + * Test we are able to fetch the override settings for the right user. + * + * In this test, we are performing multiple tasks to make sure they don't disrupt eachother: + * - First, we create a quiz with no SEB settings and override one of the users + * - Second, we add SEB settings to the quiz + * - Third, we then remove the override + * - Fourth, we remove the SEB settings + * + * @covers \quizaccess_seb\helper::get_seb_config_content + * @covers \quizaccess_seb\seb_quiz_settings::delete + * @covers \quizaccess_seb\seb_quiz_settings::save + */ + public function test_override_settings(): void { + $users = [$this->user, $this->overrideuser]; + + // Create a quiz with no SEB access rules. + $this->quiz = $this->create_test_quiz($this->course); + + // Create an override for overrideuser. + $this->setAdminUser(); + $overrideid = $this->save_override($this->overrideuser); + + // Check overrideuser is overridden. + $this->setUser($this->overrideuser); + $config = helper::get_seb_config_content($this->quiz->cmid); + $this->assertNotEmpty($config); + + // Confirm there are no SEB settings for user. + $this->setUser($this->user); + $raised = false; + try { + helper::get_seb_config_content($this->quiz->cmid); + } catch (\moodle_exception $e) { + $raised = true; + $this->assertMatchesRegularExpression( + '@' . 'No SEB config could be found for quiz with cmid: ' . $this->quiz->cmid . '@', + $e->getMessage() + ); + } + $this->assertTrue($raised); + + // Add SEB settings to quiz. + $settings = $this->get_test_settings(['quizid' => $this->quiz->id]); + $quizsettings = new seb_quiz_settings(0, $settings); + $quizsettings->save(); + + // Check both users have settings. + $configs = []; + foreach ($users as $user) { + $this->setUser($user); + $config = helper::get_seb_config_content($this->quiz->cmid); + $this->assertNotEmpty($config); + $configs[] = $config; + } + + // Check that settings are not equal. + $this->assertNotEquals($configs[0], $configs[1]); + + // Remove override from override user. + quiz_settings::create($this->quiz->id) + ->get_override_manager() + ->delete_overrides_by_id([$overrideid]); + + // Check both users have settings. + $configs = []; + foreach ($users as $user) { + $this->setUser($user); + $config = helper::get_seb_config_content($this->quiz->cmid); + $this->assertNotEmpty($config); + $configs[] = $config; + } + + // Check that settings are now equal. + $this->assertEquals($configs[0], $configs[1]); + + // Remove settings. + $quizsettings->delete($this->quiz->id); + + // Check users no longer have SEB settings. + foreach ($users as $user) { + $this->setUser($user); + $raised = false; + try { + helper::get_seb_config_content($this->quiz->cmid); + } catch (\moodle_exception $e) { + $raised = true; + $this->assertMatchesRegularExpression( + '@' . 'No SEB config could be found for quiz with cmid: ' . $this->quiz->cmid . '@', + $e->getMessage() + ); + } + $this->assertTrue($raised); + } + } + + /** + * Test quiz override for SEB, checking the SEB values retrieved are correct. + * + * @covers \quizaccess_seb\seb_quiz_settings::get_by_quiz_id + * @covers \quizaccess_seb\seb_quiz_settings::get_config_key_by_quiz_id + */ + public function test_override_settings_values(): void { + // Create quiz and add SEB access rule. + $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY); + + // Override user seb settings. + $this->save_override($this->overrideuser, [ + 'seb_requiresafeexambrowser' => 1, + 'seb_showsebtaskbar' => 0, + 'seb_showwificontrol' => 1, + 'seb_showreloadbutton' => 0, + 'seb_showtime' => 0, + 'seb_showkeyboardlayout' => 0, + 'seb_allowuserquitseb' => 0, + 'seb_quitpassword' => 'test', + 'seb_linkquitseb' => 'https://example.com/quit', + 'seb_userconfirmquit' => 0, + 'seb_enableaudiocontrol' => 1, + 'seb_muteonstartup' => 1, + 'seb_allowspellchecking' => 1, + 'seb_allowreloadinexam' => 0, + 'seb_activateurlfiltering' => 1, + 'seb_filterembeddedcontent' => 1, + 'seb_expressionsallowed' => 'test.com', + 'seb_regexallowed' => '^allow$', + 'seb_expressionsblocked' => 'bad.com', + 'seb_regexblocked' => '^bad$', + 'seb_showsebdownloadlink' => 0, + ]); + + // Check we are retrieving overridden settings. + $this->setUser($this->overrideuser); + $sebconfig = seb_quiz_settings::get_by_quiz_id($this->quiz->id); + + $this->assertEquals(1, $sebconfig->get('requiresafeexambrowser')); + $this->assertEquals(0, $sebconfig->get('showsebtaskbar')); + $this->assertEquals(1, $sebconfig->get('showwificontrol')); + $this->assertEquals(0, $sebconfig->get('showreloadbutton')); + $this->assertEquals(0, $sebconfig->get('showtime')); + $this->assertEquals(0, $sebconfig->get('showkeyboardlayout')); + $this->assertEquals(0, $sebconfig->get('allowuserquitseb')); + $this->assertEquals('test', $sebconfig->get('quitpassword')); + $this->assertEquals('https://example.com/quit', $sebconfig->get('linkquitseb')); + $this->assertEquals(0, $sebconfig->get('userconfirmquit')); + $this->assertEquals(1, $sebconfig->get('enableaudiocontrol')); + $this->assertEquals(1, $sebconfig->get('muteonstartup')); + $this->assertEquals(1, $sebconfig->get('allowspellchecking')); + $this->assertEquals(0, $sebconfig->get('allowreloadinexam')); + $this->assertEquals(1, $sebconfig->get('activateurlfiltering')); + $this->assertEquals(1, $sebconfig->get('filterembeddedcontent')); + $this->assertEquals('test.com', $sebconfig->get('expressionsallowed')); + $this->assertEquals('^allow$', $sebconfig->get('regexallowed')); + $this->assertEquals('bad.com', $sebconfig->get('expressionsblocked')); + $this->assertEquals('^bad$', $sebconfig->get('regexblocked')); + $this->assertEquals(0, $sebconfig->get('showsebdownloadlink')); + $this->assertEquals([], $sebconfig->get('allowedbrowserexamkeys')); + + // Test normal user is not overridden. + $this->setUser($this->user); + $sebconfig = seb_quiz_settings::get_by_quiz_id($this->quiz->id); + + $this->assertEquals(0, $sebconfig->get('activateurlfiltering')); + $this->assertEquals([], $sebconfig->get('allowedbrowserexamkeys')); + $this->assertEquals(1, $sebconfig->get('allowreloadinexam')); + $this->assertEquals(0, $sebconfig->get('allowspellchecking')); + $this->assertEquals(1, $sebconfig->get('allowuserquitseb')); + $this->assertEquals(0, $sebconfig->get('enableaudiocontrol')); + $this->assertEquals('', $sebconfig->get('expressionsallowed')); + $this->assertEquals('', $sebconfig->get('expressionsblocked')); + $this->assertEquals(0, $sebconfig->get('filterembeddedcontent')); + $this->assertEquals('', $sebconfig->get('linkquitseb')); + $this->assertEquals(0, $sebconfig->get('muteonstartup')); + $this->assertEquals('', $sebconfig->get('quitpassword')); + $this->assertEquals('', $sebconfig->get('regexallowed')); + $this->assertEquals('', $sebconfig->get('regexblocked')); + $this->assertEquals(1, $sebconfig->get('requiresafeexambrowser')); + $this->assertEquals(1, $sebconfig->get('showkeyboardlayout')); + $this->assertEquals(1, $sebconfig->get('showreloadbutton')); + $this->assertEquals(1, $sebconfig->get('showsebdownloadlink')); + $this->assertEquals(1, $sebconfig->get('showsebtaskbar')); + $this->assertEquals(1, $sebconfig->get('showtime')); + $this->assertEquals(0, $sebconfig->get('showwificontrol')); + $this->assertEquals(1, $sebconfig->get('userconfirmquit')); + } + + /** + * Test quiz override settings for SEB are correctly cached. + * + * @covers \quizaccess_seb\seb_quiz_settings::get_by_quiz_id + * @covers \quizaccess_seb\seb_quiz_settings::get_config_key_by_quiz_id + */ + public function test_override_cache(): void { + $this->quiz = $this->create_test_quiz($this->course); + $settings = $this->get_test_settings(['quizid' => $this->quiz->id, 'muteonstartup' => '1']); + $quizsettings = new seb_quiz_settings(0, $settings); + $quizsettings->save(); + + // Retrieve SEB settings, triggering the cache. + $sebconfig = seb_quiz_settings::get_config_by_quiz_id($this->quiz->id); + $cachedsebconfig = \cache::make('quizaccess_seb', 'config')->get($this->quiz->id); + $this->assertNotEmpty($sebconfig); + $this->assertNotEmpty($cachedsebconfig); + $this->assertEquals($sebconfig, $cachedsebconfig); + + $sebkey = seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id); + $cachedsebkey = \cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id); + $this->assertNotEmpty($sebkey); + $this->assertNotEmpty($cachedsebkey); + $this->assertEquals($sebkey, $cachedsebkey); + + // Override the user. + $overrideid = $this->save_override($this->overrideuser); + + // Retrieve overridden SEB settings. + $this->setUser($this->overrideuser); + + $overridesebconfig = seb_quiz_settings::get_config_by_quiz_id($this->quiz->id); + $overridecachesebconfig = \cache::make('quizaccess_seb', 'config')->get("{$this->quiz->id}-$overrideid"); + $this->assertNotEmpty($overridesebconfig); + $this->assertNotEmpty($overridecachesebconfig); + $this->assertEquals($overridesebconfig, $overridecachesebconfig); + + $overridesebkey = seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id); + $overridecachedsebkey = \cache::make('quizaccess_seb', 'configkey')->get("{$this->quiz->id}-$overrideid"); + $this->assertNotEmpty($overridesebkey); + $this->assertNotEmpty($overridecachedsebkey); + $this->assertEquals($overridesebkey, $overridecachedsebkey); + + // Test overridden and original seb settings are different. + $this->assertNotEquals($overridesebkey, $sebkey); + $this->assertNotEquals($overridecachedsebkey, $cachedsebkey); + $this->assertNotEquals($overridesebconfig, $sebconfig); + $this->assertNotEquals($overridecachesebconfig, $cachedsebconfig); + + // Delete original settings. + $this->setAdminUser(); + $quizsettings->delete(); + + // Test cached settings are gone and cached override settings are unaffected. + $sebconfig = seb_quiz_settings::get_config_by_quiz_id($this->quiz->id); + $cachedsebconfig = \cache::make('quizaccess_seb', 'config')->get($this->quiz->id); + $this->assertEmpty($sebconfig); + $this->assertEmpty($cachedsebconfig); + + $sebkey = seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id); + $cachedsebkey = \cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id); + $this->assertEmpty($sebkey); + $this->assertEmpty($cachedsebkey); + + $this->setUser($this->overrideuser); + + $overridesebconfig = seb_quiz_settings::get_config_by_quiz_id($this->quiz->id); + $overridecachesebconfig = \cache::make('quizaccess_seb', 'config')->get("{$this->quiz->id}-$overrideid"); + $this->assertNotEmpty($overridesebconfig); + $this->assertNotEmpty($overridecachesebconfig); + $this->assertEquals($overridesebconfig, $overridecachesebconfig); + + $overridesebkey = seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id); + $overridecachedsebkey = \cache::make('quizaccess_seb', 'configkey')->get("{$this->quiz->id}-$overrideid"); + $this->assertNotEmpty($overridesebkey); + $this->assertNotEmpty($overridecachedsebkey); + $this->assertEquals($overridesebkey, $overridecachedsebkey); + + // Delete override settings. + quiz_settings::create($this->quiz->id) + ->get_override_manager() + ->delete_overrides_by_id([$overrideid]); + + // Test override settings are now empty. + $overridesebconfig = seb_quiz_settings::get_config_by_quiz_id($this->quiz->id); + $overridecachesebconfig = \cache::make('quizaccess_seb', 'config')->get("{$this->quiz->id}-$overrideid"); + $this->assertEmpty($overridesebconfig); + $this->assertEmpty($overridecachesebconfig); + + $overridesebkey = seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id); + $overridecachedsebkey = \cache::make('quizaccess_seb', 'configkey')->get("{$this->quiz->id}-$overrideid"); + $this->assertEmpty($overridesebkey); + $this->assertEmpty($overridecachedsebkey); + } + + /** + * Test get_quiz_access_information with override + * + * @covers \mod_quiz_external::get_quiz_access_information + * @covers \quizaccess_seb::description + */ + public function test_get_quiz_access_information_with_override(): void { + // Create a new quiz. + $this->quiz = $this->create_test_quiz($this->course); + + // Add SEB access rule. + $settings = $this->get_test_settings(['quizid' => $this->quiz->id, 'muteonstartup' => '1']); + $quizsettings = new seb_quiz_settings(0, $settings); + $quizsettings->save(); + + // Get access manager rule descriptions. + $cm = get_coursemodule_from_id('quiz', $this->quiz->cmid, $this->course->id, false, MUST_EXIST); + $quizsettings = new quiz_settings($this->quiz, $cm, $this->course); + $accessmanager = $quizsettings->get_access_manager(time()); + $expected = $accessmanager->describe_rules(); + + // Get information via external function. + $info = mod_quiz_external::get_quiz_access_information($this->quiz->id); + $result = $info['accessrules']; + + $this->assertEquals($expected, $result); + + // Override a user, make sure get_quiz_access_information is not affected. + $this->save_override($this->overrideuser); + + $info = mod_quiz_external::get_quiz_access_information($this->quiz->id); + $result = $info['accessrules']; + + $this->assertEquals($expected, $result); + } +} diff --git a/mod/quiz/accessrule/seb/tests/settings_provider_test.php b/mod/quiz/accessrule/seb/tests/settings_provider_test.php index f9d49445cd1ed..f60146e2b34ed 100644 --- a/mod/quiz/accessrule/seb/tests/settings_provider_test.php +++ b/mod/quiz/accessrule/seb/tests/settings_provider_test.php @@ -655,16 +655,14 @@ public function test_get_requiresafeexambrowser_options($settingcapability): voi $this->set_up_user_and_role(); $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->assertFalse(array_key_exists(settings_provider::USE_SEB_CLIENT_CONFIG, $options)); - $this->assertTrue(array_key_exists(settings_provider::USE_SEB_NO, $options)); + $this->assertCount(0, $options); assign_capability($settingcapability, CAP_ALLOW, $this->roleid, $this->context->id); $options = settings_provider::get_requiresafeexambrowser_options($this->context); + $this->assertCount(0, $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(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)); diff --git a/mod/quiz/accessrule/seb/tests/test_helper_trait.php b/mod/quiz/accessrule/seb/tests/test_helper_trait.php index 9f02428d73c57..962a8819ffab5 100644 --- a/mod/quiz/accessrule/seb/tests/test_helper_trait.php +++ b/mod/quiz/accessrule/seb/tests/test_helper_trait.php @@ -25,6 +25,7 @@ use mod_quiz\local\access_rule_base; use mod_quiz\quiz_attempt; +use mod_quiz\quiz_settings; use quizaccess_seb\seb_access_manager; use quizaccess_seb\settings_provider; @@ -308,4 +309,51 @@ protected function get_test_settings(array $settings = []): \stdClass { ], $settings); } + /** + * Create initial SEB settings data for quiz_override db table. + * + * @param bool|\stdClass $user User object used to for override to target. + * @param bool|Array $settings Override settings with this array. + * @return string + */ + protected function save_override($user = false, $settings = false) { + $user = $user ?: $this->user; + + $initialsettings = [ + 'seb_enabled' => '1', + 'seb_requiresafeexambrowser' => '1', + 'seb_showsebtaskbar' => '1', + 'seb_showwificontrol' => '0', + 'seb_showreloadbutton' => '1', + 'seb_showtime' => '0', + 'seb_showkeyboardlayout' => '1', + 'seb_allowuserquitseb' => '1', + 'seb_quitpassword' => 'test', + 'seb_linkquitseb' => '', + 'seb_userconfirmquit' => '1', + 'seb_enableaudiocontrol' => '1', + 'seb_muteonstartup' => '0', + 'seb_allowspellchecking' => '0', + 'seb_allowreloadinexam' => '1', + 'seb_activateurlfiltering' => '1', + 'seb_filterembeddedcontent' => '0', + 'seb_expressionsallowed' => 'test.com', + 'seb_regexallowed' => '', + 'seb_expressionsblocked' => '', + 'seb_regexblocked' => '', + 'seb_showsebdownloadlink' => '1', + ]; + + if (is_array($settings)) { + $initialsettings = array_merge($initialsettings, $settings); + } + + $quizobj = quiz_settings::create($this->quiz->id); + $manager = $quizobj->get_override_manager(); + return $manager->save_override([ + 'userid' => $user->id, + ...$initialsettings, + ]); + } + } diff --git a/mod/quiz/accessrule/seb/version.php b/mod/quiz/accessrule/seb/version.php index 4fb5bcbd8d917..2db3c4a6ddccb 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 = 2024100700; +$plugin->version = 2024110700; $plugin->requires = 2024100100; $plugin->component = 'quizaccess_seb'; $plugin->maturity = MATURITY_STABLE;