Skip to content

Commit

Permalink
MDL-80945 quizaccess_seb: Implement hooks and functions to override S…
Browse files Browse the repository at this point in the history
…EB settings.
  • Loading branch information
Michael Kotlyar committed Sep 5, 2024
1 parent 93828ef commit af2159e
Show file tree
Hide file tree
Showing 14 changed files with 1,126 additions and 80 deletions.
11 changes: 11 additions & 0 deletions mod/quiz/accessrule/seb/classes/privacy/provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
[
Expand Down
114 changes: 101 additions & 13 deletions mod/quiz/accessrule/seb/classes/seb_quiz_settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,73 @@ protected static function define_properties(): array {
* @return false|\quizaccess_seb\seb_quiz_settings
*/
public static function get_by_quiz_id(int $quizid) {
if ($data = self::get_quiz_settings_cache()->get($quizid)) {
if ($data = self::get_quiz_settings_cache()->get(self::get_cache_key($quizid))) {
return new static(0, $data);
}

return self::get_record(['quizid' => $quizid]);
$sebquizsetting = self::get_settings(['quizid' => $quizid]);

// Overwrite settings if available.
if ($override = 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;
}

/**
* 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 false|\quizaccess_seb\seb_quiz_settings
*/
public static function get_settings(array $filters = []) {
$sebquizsetting = self::get_record($filters);

if ($sebquizsetting || $filters['quizid']) {
$quizid = $filters['quizid'] ?? $sebquizsetting->get('quizid');

// Overwrite settings if available.
if ($override = 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_enable) && (bool) $override->seb_enable) {
$prefix = 'seb_';
foreach (array_keys(self::properties_definition()) as $key) {
if (isset($quizsetting->{$prefix.$key})) {
$sebquizsetting->set($key, $quizsetting->{$prefix.$key});
}
}
}
}
}

return $sebquizsetting;
}

/**
Expand All @@ -210,7 +272,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;
Expand All @@ -219,7 +282,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;
Expand All @@ -232,7 +295,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;
Expand All @@ -241,7 +305,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;
Expand Down Expand Up @@ -294,19 +358,30 @@ 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'.
*/
public static function delete_cache($cachekey) {
self::get_quiz_settings_cache()->delete($cachekey);
self::get_config_cache()->delete($cachekey);
self::get_config_key_cache()->delete($cachekey);
}

/**
Expand Down Expand Up @@ -437,6 +512,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 = get_quiz_override($quizid)) {
return "$quizid-{$override->id}";
}
return $quizid;
}

/**
* Case for USE_SEB_NO.
*/
Expand Down
39 changes: 35 additions & 4 deletions mod/quiz/accessrule/seb/classes/settings_provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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())) {
Expand Down Expand Up @@ -549,6 +551,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;
}

Expand All @@ -559,7 +566,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');
Expand All @@ -584,7 +595,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) {
Expand Down Expand Up @@ -801,6 +812,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.
*
Expand Down
16 changes: 16 additions & 0 deletions mod/quiz/accessrule/seb/db/access.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:manage_seb_donotrequiresafeexambrowser_override' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
],
],
'quizaccess/seb:manage_seb_templateid' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
Expand Down
6 changes: 3 additions & 3 deletions mod/quiz/accessrule/seb/db/caches.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
Expand Down
39 changes: 39 additions & 0 deletions mod/quiz/accessrule/seb/db/install.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,45 @@
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="quizaccess_seb_override" COMMENT="Stores the quiz level Safe Exam Browser override configuration.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="overrideid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quiz_override.id."/>
<FIELD NAME="templateid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quizaccess_seb_template.id."/>
<FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to enable/disable seb override."/>
<FIELD NAME="requiresafeexambrowser" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Bool whether to require SEB."/>
<FIELD NAME="showsebtaskbar" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show SEB task bar"/>
<FIELD NAME="showwificontrol" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to allow user to control networking."/>
<FIELD NAME="showreloadbutton" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show reload button."/>
<FIELD NAME="showtime" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show the clock."/>
<FIELD NAME="showkeyboardlayout" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show keyboard layout."/>
<FIELD NAME="allowuserquitseb" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show quit button."/>
<FIELD NAME="quitpassword" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Quit password to exit SEB."/>
<FIELD NAME="linkquitseb" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Link to exit SEB."/>
<FIELD NAME="userconfirmquit" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether confirm quit popup should appear."/>
<FIELD NAME="enableaudiocontrol" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show volume and audio controls."/>
<FIELD NAME="muteonstartup" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether browser starts muted."/>
<FIELD NAME="allowspellchecking" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether spell checking will happen in SEB."/>
<FIELD NAME="allowreloadinexam" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether user can reload."/>
<FIELD NAME="activateurlfiltering" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether URLs will be filtered."/>
<FIELD NAME="filterembeddedcontent" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool wither embedded content will be filtered"/>
<FIELD NAME="expressionsallowed" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma or newline separated list of allowed expressions"/>
<FIELD NAME="regexallowed" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Regex of allowed URLs"/>
<FIELD NAME="expressionsblocked" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma or newline separated list of blocked expressions"/>
<FIELD NAME="regexblocked" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Regex of blocked URLs"/>
<FIELD NAME="allowedbrowserexamkeys" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="List of allowed browser exam keys."/>
<FIELD NAME="showsebdownloadlink" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether SEB download link should appear"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="overrideid" TYPE="foreign" FIELDS="overrideid" REFTABLE="quiz_overrides" REFFIELDS="id"/>
<KEY NAME="templateid" TYPE="foreign" FIELDS="templateid" REFTABLE="quizaccess_seb_template" REFFIELDS="id"/>
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="quizaccess_seb_template" COMMENT="Templates for Safe Exam Browser configuration.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
Expand Down
Loading

0 comments on commit af2159e

Please sign in to comment.