MDL-67850 quizaccess: add Safe Exam Browser access rule plugin
authorAndrew Madden <andrewmadden@catalyst-au.net>
Thu, 27 Feb 2020 01:01:54 +0000 (12:01 +1100)
committerDmitrii Metelkin <dmitriim@catalyst-au.net>
Wed, 29 Apr 2020 04:28:54 +0000 (14:28 +1000)
Co-authored-by: Nicholas Hoobin <nicholashoobin@catalyst-au.net>
Co-authored-by: Dmitrii Metelkin <dmitriim@catalyst-au.net>
60 files changed:
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/src/managetemplates.js [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/access_manager.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/config_key.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/access_prevented.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_created.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_deleted.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_disabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_enabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_updated.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/helper.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/hideif_rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/link_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/form/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/table/template_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/privacy/provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/property_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/quiz_settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/settings_provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template_controller.php [new file with mode: 0644]
mod/quiz/accessrule/seb/config.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/access.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/caches.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.xml [new file with mode: 0644]
mod/quiz/accessrule/seb/db/upgrade.php [new file with mode: 0644]
mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php [new file with mode: 0644]
mod/quiz/accessrule/seb/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/behat/edit_form.feature [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/base.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/event_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/helper_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/property_list_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/quiz_settings_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/JSON_unencrypted_mac_001.txt [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/encrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencryptedwithoutoriginator.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_mac_001.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_win_223.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/settings_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/template_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/version.php [new file with mode: 0644]

diff --git a/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js
new file mode 100644 (file)
index 0000000..1a1bf50
Binary files /dev/null and b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js differ
diff --git a/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map
new file mode 100644 (file)
index 0000000..0e721e5
Binary files /dev/null and b/mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map differ
diff --git a/mod/quiz/accessrule/seb/amd/src/managetemplates.js b/mod/quiz/accessrule/seb/amd/src/managetemplates.js
new file mode 100644 (file)
index 0000000..1885a48
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Template management code.
+ *
+ * @module quizaccess_seb/managetemplates
+ * @class managetemplates
+ * @package quizaccess_seb
+ * @copyright  2020 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ */
+define(
+    ['jquery', 'core/ajax', 'core/str', 'core/notification'],
+    function($, ajax, str, notification) {
+        var manager = {
+            /**
+             * Confirm removal of the specified template.
+             *
+             * @method removeTemplate
+             * @param {EventFacade} e The EventFacade
+             */
+            removeTemplate: function(e) {
+                e.preventDefault();
+                var targetUrl = $(e.currentTarget).attr('href');
+                str.get_strings([
+                    {
+                        key:        'confirmtemplateremovaltitle',
+                        component:  'quizaccess_seb'
+                    },
+                    {
+                        key:        'confirmtemplateremovalquestion',
+                        component:  'quizaccess_seb'
+                    },
+                    {
+                        key:        'yes',
+                        component:  'moodle'
+                    },
+                    {
+                        key:        'no',
+                        component:  'moodle'
+                    }
+                ])
+                .then(function(s) {
+                    notification.confirm(s[0], s[1], s[2], s[3], function() {
+                        window.location = targetUrl;
+                    });
+
+                    return;
+                })
+                .catch();
+            },
+
+            /**
+             * Setup the template management UI.
+             *
+             * @method setup
+             */
+            setup: function() {
+                $('body').delegate('[data-action="delete"]', 'click', manager.removeTemplate);
+            }
+        };
+
+        return /** @alias module:quizaccess_seb/managetemplates */ {
+            /**
+             * Setup the template management UI.
+             *
+             * @method setup
+             */
+            setup: manager.setup
+        };
+    });
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
new file mode 100644 (file)
index 0000000..bc57ed8
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Backup instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @package    quizaccess_seb
+ * @category   backup
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_mod_quiz_access_subplugin.class.php');
+
+/**
+ * Backup instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin {
+
+    /**
+     * Stores the data related to the Safe Exam Browser quiz settings and management for a particular quiz.
+     *
+     * @return backup_subplugin_element
+     */
+    protected function define_quiz_subplugin_structure() {
+        parent::define_quiz_subplugin_structure();
+        $quizid = backup::VAR_ACTIVITYID;
+
+        $subplugin = $this->get_subplugin_element();
+        $subpluginwrapper = new backup_nested_element($this->get_recommended_name());
+
+        $template = new \quizaccess_seb\template();
+        $blanktemplatearray = (array) $template->to_record();
+        unset($blanktemplatearray['usermodified']);
+        unset($blanktemplatearray['timemodified']);
+
+        $templatekeys = array_keys($blanktemplatearray);
+
+        $subplugintemplatesettings = new backup_nested_element('quizaccess_seb_template', null, $templatekeys);
+
+        // Get quiz settings keys to save.
+        $settings = new \quizaccess_seb\quiz_settings();
+        $blanksettingsarray = (array) $settings->to_record();
+        unset($blanksettingsarray['id']); // We don't need to save reference to settings record in current instance.
+        // We don't need to save the data about who last modified the settings as they will be overwritten on restore. Also
+        // means we don't have to think about user data for the backup.
+        unset($blanksettingsarray['usermodified']);
+        unset($blanksettingsarray['timemodified']);
+
+        $settingskeys = array_keys($blanksettingsarray);
+
+        // Save the settings.
+        $subpluginquizsettings = new backup_nested_element('quizaccess_seb_quizsettings', null, $settingskeys);
+
+        // Connect XML elements into the tree.
+        $subplugin->add_child($subpluginwrapper);
+        $subpluginwrapper->add_child($subpluginquizsettings);
+        $subpluginquizsettings->add_child($subplugintemplatesettings);
+
+        // Set source to populate the settings data by referencing the ID of quiz being backed up.
+        $subpluginquizsettings->set_source_table(quizaccess_seb\quiz_settings::TABLE, ['quizid' => $quizid]);
+
+        $subpluginquizsettings->annotate_files('quizaccess_seb', 'filemanager_sebconfigfile', null);
+
+        $params = ['id' => '../templateid'];
+        $subplugintemplatesettings->set_source_table(\quizaccess_seb\template::TABLE, $params);
+
+        return $subplugin;
+    }
+}
\ No newline at end of file
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
new file mode 100644 (file)
index 0000000..928fb42
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Restore instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @package    quizaccess_seb
+ * @category   backup
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\quiz_settings;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_mod_quiz_access_subplugin.class.php');
+
+/**
+ * Restore instructions for the seb (Safe Exam Browser) quiz access subplugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin {
+
+    /**
+     * Provides path structure required to restore data for seb quiz access plugin.
+     *
+     * @return array
+     */
+    protected function define_quiz_subplugin_structure() {
+        $paths = [];
+
+        // Quiz settings.
+        $path = $this->get_pathfor('/quizaccess_seb_quizsettings'); // Subplugin root path.
+        $paths[] = new restore_path_element('quizaccess_seb_quizsettings', $path);
+
+        // Template settings.
+        $path = $this->get_pathfor('/quizaccess_seb_quizsettings/quizaccess_seb_template');
+        $paths[] = new restore_path_element('quizaccess_seb_template', $path);
+
+        return $paths;
+    }
+
+    /**
+     * Process the restored data for the quizaccess_seb_quizsettings table.
+     *
+     * @param stdClass $data Data for quizaccess_seb_quizsettings retrieved from backup xml.
+     */
+    public function process_quizaccess_seb_quizsettings($data) {
+        global $DB, $USER;
+
+        // Process quizsettings.
+        $data = (object) $data;
+        $data->quizid = $this->get_new_parentid('quiz'); // Update quizid with new reference.
+        $data->cmid = $this->task->get_moduleid();
+
+        unset($data->id);
+        $data->timecreated = $data->timemodified = time();
+        $data->usermodified = $USER->id;
+        $DB->insert_record(quizaccess_seb\quiz_settings::TABLE, $data);
+
+        // Process attached files.
+        $this->add_related_files('quizaccess_seb', 'filemanager_sebconfigfile', null);
+    }
+
+    /**
+     * Process the restored data for the quizaccess_seb_template table.
+     *
+     * @param stdClass $data Data for quizaccess_seb_template retrieved from backup xml.
+     */
+    public function process_quizaccess_seb_template($data) {
+        global $DB;
+
+        $data = (object) $data;
+
+        $quizid = $this->get_new_parentid('quiz');
+
+        $template = null;
+        if ($this->task->is_samesite()) {
+            $template = \quizaccess_seb\template::get_record(['id' => $data->id]);
+        } else {
+            // In a different site, try to find existing template with the same name and content.
+            $candidates = \quizaccess_seb\template::get_records(['name' => $data->name]);
+            foreach ($candidates as $candidate) {
+                if ($candidate->get('content') == $data->content) {
+                    $template = $candidate;
+                    break;
+                }
+            }
+        }
+
+        if (empty($template)) {
+            unset($data->id);
+            $template = new \quizaccess_seb\template(0, $data);
+            $template->save();
+        }
+
+        // Update the restored quiz settings to use restored template.
+        $DB->set_field(\quizaccess_seb\quiz_settings::TABLE, 'templateid', $template->get('id'), ['quizid' => $quizid]);
+    }
+
+}
+
diff --git a/mod/quiz/accessrule/seb/classes/access_manager.php b/mod/quiz/accessrule/seb/classes/access_manager.php
new file mode 100644 (file)
index 0000000..6a6bfc9
--- /dev/null
@@ -0,0 +1,300 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Manage the access to the quiz.
+ *
+ * @package    quizaccess_seb
+ * @author     Tim Hunt
+ * @author     Luca Bösch <luca.boesch@bfh.ch>
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use context_module;
+use quiz;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Manage the access to the quiz.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class access_manager {
+
+    /** Header sent by Safe Exam Browser containing the Config Key hash. */
+    private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH';
+
+    /** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */
+    private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH';
+
+    /** @var quiz $quiz A quiz object containing all information pertaining to current quiz. */
+    private $quiz;
+
+    /** @var quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */
+    private $quizsettings;
+
+    /** @var context_module $context Context of this quiz activity. */
+    private $context;
+
+    /** @var string|null $validconfigkey Expected valid SEB config key.
+     */
+    private $validconfigkey = null;
+
+    /**
+     * The access_manager constructor.
+     *
+     * @param quiz $quiz The details of the quiz.
+     */
+    public function __construct(quiz $quiz) {
+        $this->quiz = $quiz;
+        $this->context = context_module::instance($quiz->get_cmid());
+        $this->quizsettings = quiz_settings::get_by_quiz_id($quiz->get_quizid());
+        $this->validconfigkey = quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());
+    }
+
+    /**
+     * Check if the browser exam key hash in header matches one of the listed browser exam keys from quiz settings.
+     *
+     * @return bool True if header key matches one of the saved keys.
+     */
+    public function validate_browser_exam_keys() : bool {
+        // If browser exam keys are entered in settings, check they match the header.
+        $browserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
+        if (empty($browserexamkeys)) {
+            return true; // If no browser exam keys, no check required.
+        }
+
+        // If the Browser Exam Key header isn't present, prevent access.
+        if (is_null($this->get_received_browser_exam_key())) {
+            return false;
+        }
+
+        return $this->check_browser_exam_keys($browserexamkeys, $this->get_received_browser_exam_key());
+    }
+
+    /**
+     * Check if the config key hash in header matches quiz settings.
+     *
+     * @return bool True if header key matches saved key.
+     */
+    public function validate_config_key() : bool {
+        // If using client config, or with no requirement, then no check required.
+        $requiredtype = $this->get_seb_use_type();
+        if ($requiredtype == settings_provider::USE_SEB_NO
+                || $requiredtype == settings_provider::USE_SEB_CLIENT_CONFIG) {
+            return true;
+        }
+
+        if (empty($this->validconfigkey)) {
+            return false; // No config key has been saved.
+        }
+
+        // If the Config Key header isn't present, prevent access.
+        if (is_null($this->get_received_config_key())) {
+            return false;
+        }
+
+        return $this->check_key($this->validconfigkey, $this->get_received_config_key());
+    }
+
+    /**
+     * Check if Safe Exam Browser is required to access quiz.
+     * If quizsettings do not exist, then there is no requirement for using SEB.
+     *
+     * @return bool If required.
+     */
+    public function seb_required() : bool {
+        if (!$this->quizsettings) {
+            return false;
+        } else {
+            return $this->get_seb_use_type() != settings_provider::USE_SEB_NO;
+        }
+    }
+
+    /**
+     * This is the basic check for the Safe Exam Browser previously used in the quizaccess_safebrowser plugin that
+     * managed basic Moodle interactions with SEB.
+     *
+     * @return bool
+     */
+    public function validate_basic_header() : bool {
+        if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) {
+            return $this->is_using_seb();
+        }
+        return true;
+    }
+
+    /**
+     * Check if using Safe Exam Browser.
+     *
+     * @return bool
+     */
+    public function is_using_seb() : bool {
+        if (isset($_SERVER['HTTP_USER_AGENT'])) {
+            return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if user has any capability to bypass the Safe Exam Browser requirement.
+     *
+     * @return bool True if user can bypass check.
+     */
+    public function can_bypass_seb() : bool {
+        return has_capability('quizaccess/seb:bypassseb', $this->context);
+    }
+
+    /**
+     * Return the full URL that was used to request the current page, which is
+     * what we need for verifying the X-SafeExamBrowser-RequestHash header.
+     */
+    private function get_this_page_url() : string {
+        global $CFG, $FULLME;
+        // If $FULLME not set fall back to wwwroot.
+        if ($FULLME == null) {
+            return $CFG->wwwroot;
+        }
+        return $FULLME;
+    }
+
+    /**
+     * Return expected SEB config key.
+     *
+     * @return string|null
+     */
+    public function get_valid_config_key() : ?string {
+        return $this->validconfigkey;
+    }
+
+    /**
+     * Getter for the quiz object.
+     *
+     * @return quiz
+     */
+    public function get_quiz() : quiz {
+        return $this->quiz;
+    }
+
+    /**
+     * Check the hash from the request header against the permitted browser exam keys.
+     *
+     * @param array $keys Allowed browser exam keys.
+     * @param string $header The value of the X-SafeExamBrowser-RequestHash to check.
+     * @return bool True if the hash matches.
+     */
+    private function check_browser_exam_keys(array $keys, string $header) : bool {
+        foreach ($keys as $key) {
+            if ($this->check_key($key, $header)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check the hash from the request header against a single permitted key.
+     *
+     * @param string $key an allowed key.
+     * @param string $header the value of the X-SafeExamBrowser-RequestHash or X-SafeExamBrowser-ConfigKeyHash to check.
+     * @return bool true if the hash matches.
+     */
+    private function check_key($key, $header) : bool {
+        return hash('sha256', $this->get_this_page_url() . $key) === $header;
+    }
+
+    /**
+     * Returns Safe Exam Browser Config Key hash.
+     *
+     * @return string|null
+     */
+    public function get_received_config_key() {
+        if (isset($_SERVER[self::CONFIG_KEY_HEADER])) {
+            return trim($_SERVER[self::CONFIG_KEY_HEADER]);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the Browser Exam Key hash.
+     *
+     * @return string|null
+     */
+    public function get_received_browser_exam_key() {
+        if (isset($_SERVER[self::BROWSER_EXAM_KEY_HEADER])) {
+            return trim($_SERVER[self::BROWSER_EXAM_KEY_HEADER]);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get type of SEB usage for the quiz.
+     *
+     * @return int
+     */
+    public function get_seb_use_type() : int {
+        if (empty($this->quizsettings)) {
+            return settings_provider::USE_SEB_NO;
+        } else {
+            return $this->quizsettings->get('requiresafeexambrowser');
+        }
+    }
+
+    /**
+     * Should validate basic header?
+     *
+     * @return bool
+     */
+    public function should_validate_basic_header() : bool {
+        return in_array($this->get_seb_use_type(), [
+            settings_provider::USE_SEB_CLIENT_CONFIG,
+        ]);
+    }
+
+    /**
+     * Should validate SEB config key?
+     * @return bool
+     */
+    public function should_validate_config_key() : bool {
+        return in_array($this->get_seb_use_type(), [
+            settings_provider::USE_SEB_CONFIG_MANUALLY,
+            settings_provider::USE_SEB_TEMPLATE,
+            settings_provider::USE_SEB_UPLOAD_CONFIG,
+        ]);
+    }
+
+    /**
+     * Should validate browser exam key?
+     *
+     * @return bool
+     */
+    public function should_validate_browser_exam_key() : bool {
+        return in_array($this->get_seb_use_type(), [
+            settings_provider::USE_SEB_UPLOAD_CONFIG,
+            settings_provider::USE_SEB_CLIENT_CONFIG,
+        ]);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/config_key.php b/mod/quiz/accessrule/seb/classes/config_key.php
new file mode 100644 (file)
index 0000000..53a5802
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for generating and representing a Safe Exam Browser config key.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for generating and representing a Safe Exam Browser config key.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class config_key {
+
+    /** @var string $hash The Config Key hash. */
+    private $hash;
+
+    /**
+     * The config_key constructor.
+     *
+     * @param string $hash The Config Key hash.
+     */
+    public function __construct(string $hash) {
+        $this->hash = $hash;
+    }
+
+    /**
+     * Generate the Config Key hash from an SEB Config XML string.
+     *
+     * See  https://safeexambrowser.org/developer/seb-config-key.html for more information about the process.
+     *
+     * @param string $xml A PList XML string, representing SEB config.
+     * @return config_key This config key instance.
+     */
+    public static function generate(string $xml) : config_key {
+        if (!empty($xml) && !helper::is_valid_seb_config($xml)) {
+            throw new \invalid_parameter_exception('Invalid a PList XML string, representing SEB config');
+        }
+
+        $plist = new property_list($xml);
+        // Remove the key "originatorVersion" first. This key is exempted from the SEB-JSON hash (it's a special key
+        // which doesn't have any functionality, it's just meta data indicating which SEB version saved the config file).
+        $plist->delete_element('originatorVersion');
+        // Convert the plist XML of a decrypted/unencrypted SEB config file to a ordered JSON-like "SEB-JSON" object.
+        $hash = $plist->to_json();
+        // Hash the JSON with SHA256. Defaults to required Base16 encoding.
+        $hash = hash('SHA256', $hash);
+
+        return new self($hash);
+    }
+
+    /**
+     * Get the Config Key hash.
+     *
+     * @return string The Config Key hash
+     */
+    public function get_hash() : string {
+        return $this->hash;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/access_prevented.php b/mod/quiz/accessrule/seb/classes/event/access_prevented.php
new file mode 100644 (file)
index 0000000..c8c36bd
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when access to a quiz is prevented by this subplugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use core\event\base;
+use quizaccess_seb\access_manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when access to a quiz is prevented by this subplugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class access_prevented extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param access_manager $accessmanager Access manager.
+     * @param string $reason Reason that access was prevented.
+     * @return base
+     */
+    public static function create_strict(access_manager $accessmanager, string $reason) : base {
+        global $USER;
+
+        $other = [];
+        $other['reason'] = $reason;
+        $other['savedconfigkey'] = $accessmanager->get_valid_config_key();
+        $other['receivedconfigkey'] = $accessmanager->get_received_config_key();
+        $other['receivedbrowserexamkey'] = $accessmanager->get_received_browser_exam_key();
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $accessmanager->get_quiz()->get_quizid(),
+            'courseid' => $accessmanager->get_quiz()->get_courseid(),
+            'context' => $accessmanager->get_quiz()->get_context(),
+            'other' => $other,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quiz';
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:accessprevented', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        $description = "The user with id '$this->userid' has been prevented from accessing quiz with id '$this->objectid' by the "
+                . "Safe Exam Browser access plugin. The reason was '{$this->other['reason']}'. "
+            . "Expected config key: '{$this->other['savedconfigkey']}'. "
+            . "Received config key: '{$this->other['receivedconfigkey']}'. "
+            . "Received browser exam key: '{$this->other['receivedbrowserexamkey']}'.";
+
+        return $description;
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quiz', 'restore' => 'quiz');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [
+            'cmid' => ['db' => 'course_modules', 'restore' => 'course_modules']
+        ];
+    }
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/classes/event/template_created.php b/mod/quiz/accessrule/seb/classes/event/template_created.php
new file mode 100644 (file)
index 0000000..9f3bb35
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is created.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is created.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_created extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templatecreated', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has created a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_deleted.php b/mod/quiz/accessrule/seb/classes/event/template_deleted.php
new file mode 100644 (file)
index 0000000..84379ed
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is deleted.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is deleted.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_deleted extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param string $id The id of the template
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(string $id, context_system $context) : base {
+        global $USER;
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $id,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templatedeleted', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has deleted a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_disabled.php b/mod/quiz/accessrule/seb/classes/event/template_disabled.php
new file mode 100644 (file)
index 0000000..45a6ca9
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is disabled.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is disabled.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_disabled extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templatedisabled', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has disabled a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_enabled.php b/mod/quiz/accessrule/seb/classes/event/template_enabled.php
new file mode 100644 (file)
index 0000000..728c3b2
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is enabled.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is enabled.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_enabled extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templateenabled', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has enabled a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/event/template_updated.php b/mod/quiz/accessrule/seb/classes/event/template_updated.php
new file mode 100644 (file)
index 0000000..28498d5
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event for when a template is updated.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\event;
+
+use context_system;
+use core\event\base;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event for when a template is updated.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_updated extends base {
+
+    /**
+     * Create event with strict parameters.
+     *
+     * Define strict parameters to create event with instead of relying on internal validation of array. Better code practice.
+     * Easier for consumers of this class to know what data must be supplied and observers can have more trust in event data.
+     *
+     * @param template $template SEB template.
+     * @param context_system $context Context system.
+     * @return base
+     */
+    public static function create_strict(template $template, context_system $context) : base {
+        global $USER;
+        $tid = $template->get('id');
+
+        return self::create([
+            'userid' => $USER->id,
+            'objectid' => $tid,
+            'context' => $context,
+        ]);
+    }
+
+    /**
+     * Initialize the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'quizaccess_seb_template';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Get the name of the event.
+     *
+     * @return string Name of event.
+     */
+    public static function get_name() {
+        return get_string('event:templateupdated', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $params = [
+            'id' => $this->objectid,
+            'action' => 'edit',
+        ];
+        return new \moodle_url('/mod/quiz/accessrule/seb/template.php', $params);
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string Description.
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has updated a template with id '$this->objectid'.";
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the objectid to it's new value in the new course.
+     *
+     * @return array Mapping of object id.
+     */
+    public static function get_objectid_mapping() : array {
+        return array('db' => 'quizaccess_seb_template', 'restore' => 'quizaccess_seb_template');
+    }
+
+    /**
+     * This is used when restoring course logs where it is required that we
+     * map the information in 'other' to it's new value in the new course.
+     *
+     * @return array List of mapping of other ids.
+     */
+    public static function get_other_mapping() : array {
+        return [];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/helper.php b/mod/quiz/accessrule/seb/classes/helper.php
new file mode 100644 (file)
index 0000000..2ef33ce
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper class.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+
+use CFPropertyList\CFPropertyList;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+    /**
+     * Get a filler icon for display in the actions column of a table.
+     *
+     * @param string $url The URL for the icon.
+     * @param string $icon The icon identifier.
+     * @param string $alt The alt text for the icon.
+     * @param string $iconcomponent The icon component.
+     * @param array $options Display options.
+     * @return string
+     */
+    public static function format_icon_link($url, $icon, $alt, $iconcomponent = 'moodle', $options = array()) {
+        global $OUTPUT;
+
+        return $OUTPUT->action_icon(
+            $url,
+            new \pix_icon($icon, $alt, $iconcomponent, [
+                'title' => $alt,
+            ]),
+            null,
+            $options
+        );
+    }
+
+    /**
+     * Validate seb config string.
+     *
+     * @param string $sebconfig
+     * @return bool
+     */
+    public static function is_valid_seb_config(string $sebconfig) : bool {
+        $result = true;
+
+        set_error_handler(function($errno, $errstr, $errfile, $errline ){
+            throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
+        });
+
+        $plist = new CFPropertyList();
+        try {
+            $plist->parse($sebconfig);
+        } catch (\ErrorException $e) {
+            $result = false;
+        } catch (\Exception $e) {
+            $result = false;
+        }
+
+        restore_error_handler();
+
+        return $result;
+    }
+
+    /**
+     * A helper function to get a list of seb config file headers.
+     *
+     * @param int|null $expiretime  Unix timestamp
+     * @return array
+     */
+    public static function get_seb_file_headers(int $expiretime = null) : array {
+        if (is_null($expiretime)) {
+            $expiretime = time();
+        }
+        $headers = [];
+        $headers[] = 'Cache-Control: private, max-age=1, no-transform';
+        $headers[] = 'Expires: '. gmdate('D, d M Y H:i:s', $expiretime) .' GMT';
+        $headers[] = 'Pragma: no-cache';
+        $headers[] = 'Content-Disposition: attachment; filename=config.seb';
+        $headers[] = 'Content-Type: application/seb';
+
+        return $headers;
+    }
+
+    /**
+     * Get seb config content for a particular quiz. This method checks caps.
+     *
+     * @param string $cmid The course module ID for a quiz with config.
+     * @return string SEB config string.
+     */
+    public static function get_seb_config_content(string $cmid) : string {
+        // Try and get the course module.
+        $cm = get_coursemodule_from_id('quiz', $cmid, 0, false, MUST_EXIST);
+
+        // Make sure the user is logged in and has access to the module.
+        require_login($cm->course, false, $cm);
+
+        // Retrieve the config for quiz.
+        $config = quiz_settings::get_config_by_quiz_id($cm->instance);
+        if (empty($config)) {
+            throw new \moodle_exception('noconfigfound', 'quizaccess_seb', '', $cm->id);
+        }
+        return $config;
+    }
+
+    /**
+     * Serve a file to browser for download.
+     *
+     * @param string $contents Contents of file.
+     */
+    public static function send_seb_config_file(string $contents) {
+        // We can now send the file back to the browser.
+        foreach (self::get_seb_file_headers() as $header) {
+            header($header);
+        }
+
+        echo($contents);
+    }
+
+}
+
diff --git a/mod/quiz/accessrule/seb/classes/hideif_rule.php b/mod/quiz/accessrule/seb/classes/hideif_rule.php
new file mode 100644 (file)
index 0000000..ed29150
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class to store data for "hide if" rules for the settings form.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class to store data for "hide if" rules for the settings form.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class hideif_rule {
+
+    /**
+     * Name of the element to hide.
+     * @var string
+     */
+    protected $element;
+
+    /**
+     * Name of the element that $element is dependant on.
+     * @var string
+     */
+    protected $dependantname;
+
+    /**
+     * Condition. E.g. 'eq', 'noteq' and etc.
+     * @var string
+     */
+    protected $condition;
+
+    /**
+     * Value to check the $condition against.
+     * @var string
+     */
+    protected $dependantvalue;
+
+    /**
+     * Constructor.
+     *
+     * @param string $element Name of the element to hide.
+     * @param string $dependantname Name of the element that $element is dependant on.
+     * @param string $condition Condition. E.g. 'eq', 'noteq' and etc.
+     * @param string $dependantvalue Value to check the $condition against.
+     */
+    public function __construct(string $element, string $dependantname, string $condition, string $dependantvalue) {
+        $this->element = $element;
+        $this->dependantname = $dependantname;
+        $this->condition = $condition;
+        $this->dependantvalue = $dependantvalue;
+    }
+
+    /**
+     * Return name of the element to hide.
+     * @return string
+     */
+    public function get_element(): string {
+        return $this->element;
+    }
+
+    /**
+     * Returns name of the element that $element is dependant on.
+     * @return string
+     */
+    public function get_dependantname(): string {
+        return $this->dependantname;
+    }
+
+    /**
+     * Returns condition. E.g. 'eq', 'noteq' and etc
+     * @return string
+     */
+    public function get_condition(): string {
+        return $this->condition;
+    }
+
+    /**
+     * Returns value to check the $condition against.
+     * @return string
+     */
+    public function get_dependantvalue(): string {
+        return $this->dependantvalue;
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/classes/link_generator.php b/mod/quiz/accessrule/seb/classes/link_generator.php
new file mode 100644 (file)
index 0000000..05f204a
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Generate the links to open/download the Safe Exam Browser with correct settings.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Generate the links to open/download the Safe Exam Browser with correct settings.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class link_generator {
+
+    /**
+     * Get a link to force the download of the file over https or sebs protocols.
+     *
+     * @param string $cmid Course module ID.
+     * @param bool $seb Whether to use a seb:// scheme or fall back to http:// scheme.
+     * @param bool $secure Whether to use HTTPS or HTTP protocol.
+     * @return string A URL.
+     */
+    public static function get_link(string $cmid, bool $seb = false, bool $secure = true) : string {
+        // Check if course module exists.
+        get_coursemodule_from_id('quiz', $cmid, 0, false, MUST_EXIST);
+
+        $url = new moodle_url('/mod/quiz/accessrule/seb/config.php?cmid=' . $cmid);
+        if ($seb) {
+            $secure ? $url->set_scheme('sebs') : $url->set_scheme('seb');
+        } else {
+            $secure ? $url->set_scheme('https') : $url->set_scheme('http');
+        }
+        return $url->out();
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/local/form/template.php b/mod/quiz/accessrule/seb/classes/local/form/template.php
new file mode 100644 (file)
index 0000000..f1a6c8e
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form for manipulating with the template records.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\local\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Form for manipulating with the template records.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template extends \core\form\persistent {
+
+    /** @var string Persistent class name. */
+    protected static $persistentclass = 'quizaccess_seb\\template';
+
+    /**
+     * Form definition.
+     */
+    protected function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('text', 'name', get_string('name', 'quizaccess_seb'));
+        $mform->addRule('name', get_string('required'), 'required', null, 'client');
+        $mform->setType('name', PARAM_TEXT);
+
+        $mform->addElement('textarea', 'description', get_string('description', 'quizaccess_seb'));
+        $mform->setType('description', PARAM_TEXT);
+
+        if ($this->get_persistent()->get('id')) {
+            $mform->addElement('textarea', 'content', get_string('content', 'quizaccess_seb'), ['rows' => 20, 'cols' => 60]);
+            $mform->addRule('content', get_string('required'), 'required');
+        } else {
+            $mform->addElement('filepicker', 'content', get_string('content', 'quizaccess_seb'));
+            $mform->addRule('content', get_string('required'), 'required');
+        }
+
+        $mform->addElement('selectyesno', 'enabled', get_string('enabled', 'quizaccess_seb'));
+        $mform->setType('enabled', PARAM_INT);
+
+        $this->add_action_buttons();
+
+        if (!empty($this->get_persistent()) && !$this->get_persistent()->can_delete()) {
+            $mform->hardFreezeAllVisibleExcept([]);
+            $mform->addElement('cancel');
+        }
+    }
+
+    /**
+     * Filter out the foreign fields of the persistent.
+     *
+     * @param \stdClass $data The data to filter the fields out of.
+     * @return \stdClass.
+     */
+    protected function filter_data_for_persistent($data) {
+        // Uploading a new template file.
+        if (empty($this->get_persistent()->get('id'))) {
+            $files = $this->get_draft_files('content');
+            if ($files) {
+                $file = reset($files);
+                $data->content = $file->get_content();
+            } else {
+                // No file found. Remove content data and let persistent to return an error.
+                unset($data->content);
+            }
+        }
+
+        return parent::filter_data_for_persistent($data);
+    }
+
+    /**
+     * Extra validation.
+     *
+     * @param  \stdClass $data Data to validate.
+     * @param  array $files Array of files.
+     * @param  array $errors Currently reported errors.
+     * @return array of additional errors, or overridden errors.
+     */
+    protected function extra_validation($data, $files, array &$errors) {
+        $newerrors = [];
+
+        // Check name.
+        if (empty($data->name)) {
+            $newerrors['name'] = get_string('namerequired', 'quizaccess_seb');
+        }
+
+        return $newerrors;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/local/table/template_list.php b/mod/quiz/accessrule/seb/classes/local/table/template_list.php
new file mode 100644 (file)
index 0000000..a405b69
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Templates table.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\local\table;
+
+use quizaccess_seb\helper;
+use quizaccess_seb\template;
+use quizaccess_seb\template_controller;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/tablelib.php');
+
+/**
+ * Templates table.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_list extends \flexible_table {
+
+    /**
+     * @var int Autogenerated id.
+     */
+    private static $autoid = 0;
+
+    /**
+     * Constructor
+     *
+     * @param string|null $id to be used by the table, autogenerated if null.
+     */
+    public function __construct($id = null) {
+        global $PAGE;
+
+        $id = (is_null($id) ? self::$autoid++ : $id);
+        parent::__construct('quizaccess_seb' . $id);
+
+        $this->define_baseurl($PAGE->url);
+        $this->set_attribute('class', 'generaltable admintable');
+
+        // Column definition.
+        $this->define_columns([
+            'name',
+            'description',
+            'enabled',
+            'used',
+            'actions',
+        ]);
+
+        $this->define_headers([
+            get_string('name', 'quizaccess_seb'),
+            get_string('description', 'quizaccess_seb'),
+            get_string('enabled', 'quizaccess_seb'),
+            get_string('used', 'quizaccess_seb'),
+            get_string('actions'),
+        ]);
+
+        $this->setup();
+    }
+
+    /**
+     * Display name column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_name(template $data) : string {
+        return \html_writer::link(
+            new \moodle_url(template_controller::get_base_url(), [
+                'id' => $data->get('id'),
+                'action' => template_controller::ACTION_EDIT,
+            ]),
+            $data->get('name')
+        );
+    }
+
+    /**
+     * Display description column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_description(template $data) : string {
+        return $data->get('description');
+    }
+
+    /**
+     * Display enabled column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_enabled(template $data): string {
+        return empty($data->get('enabled')) ? get_string('no') : get_string('yes');
+    }
+
+    /**
+     * Display if a template is being used.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_used(template $data): string {
+        return $data->can_delete() ? get_string('no') : get_string('yes');
+    }
+
+    /**
+     * Display actions column.
+     *
+     * @param \quizaccess_seb\template $data Template for this row.
+     * @return string
+     */
+    protected function col_actions(template $data) : string {
+        $actions = [];
+
+        $actions[] = helper::format_icon_link(
+            new \moodle_url(template_controller::get_base_url(), [
+                'id'        => $data->get('id'),
+                'action'    => template_controller::ACTION_EDIT,
+            ]),
+            't/edit',
+            get_string('edit')
+        );
+
+        $actions[] = helper::format_icon_link(
+            new \moodle_url(template_controller::get_base_url(), [
+                'id'        => $data->get('id'),
+                'action'    => template_controller::ACTION_DELETE,
+                'sesskey'   => sesskey(),
+            ]),
+            't/delete',
+            get_string('delete'),
+            null,
+            [
+            'data-action' => 'delete',
+            'data-id' => $data->get('id'),
+            ]
+        );
+
+        return implode('&nbsp;', $actions);
+    }
+
+    /**
+     * Sets the data of the table.
+     *
+     * @param \quizaccess_seb\template[] $records An array with records.
+     */
+    public function display(array $records) {
+        foreach ($records as $record) {
+            $this->add_data_keyed($this->format_row($record));
+        }
+
+        $this->finish_output();
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/classes/privacy/provider.php b/mod/quiz/accessrule/seb/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..be9b65c
--- /dev/null
@@ -0,0 +1,290 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for quizaccess_seb.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\privacy;
+
+use context;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\userlist;
+use core_privacy\local\request\writer;
+use quizaccess_seb\quiz_settings;
+use quizaccess_seb\template;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem implementation for quizaccess_seb.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\core_userlist_provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Retrieve the user metadata stored by plugin.
+     *
+     * @param collection $collection Collection of metadata.
+     * @return collection Collection of metadata.
+     */
+    public static function get_metadata(collection $collection): collection {
+        $collection->add_database_table(
+            'quizaccess_seb_quizsettings',
+             [
+                 'quizid' => 'privacy:metadata:quizaccess_seb_quizsettings:quizid',
+                 'usermodified' => 'privacy:metadata:quizaccess_seb_quizsettings:usermodified',
+                 'timecreated' => 'privacy:metadata:quizaccess_seb_quizsettings:timecreated',
+                 'timemodified' => 'privacy:metadata:quizaccess_seb_quizsettings:timemodified',
+             ],
+            'privacy:metadata:quizaccess_seb_quizsettings'
+        );
+
+        $collection->add_database_table(
+            'quizaccess_seb_template',
+            [
+                'usermodified' => 'privacy:metadata:quizaccess_seb_template:usermodified',
+                'timecreated' => 'privacy:metadata:quizaccess_seb_template:timecreated',
+                'timemodified' => 'privacy:metadata:quizaccess_seb_template:timemodified',
+            ],
+            'privacy:metadata:quizaccess_seb_template'
+        );
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist A list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid): contextlist {
+        $contextlist = new contextlist();
+
+        // The data is associated at the module context level, so retrieve the quiz context id.
+        $sql = "SELECT c.id
+                  FROM {quizaccess_seb_quizsettings} qs
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                  JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context
+                 WHERE qs.usermodified = :userid
+              GROUP BY c.id";
+
+        $params = [
+            'context' => CONTEXT_MODULE,
+            'userid' => $userid
+        ];
+
+        $contextlist->add_from_sql($sql, $params);
+
+        $sql = "SELECT c.id
+                  FROM {quizaccess_seb_template} tem
+                  JOIN {quizaccess_seb_quizsettings} qs ON qs.templateid = tem.id
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                  JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context
+                 WHERE qs.usermodified = :userid
+              GROUP BY c.id";
+
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Get all cmids that correspond to the contexts for a user.
+        $cmids = [];
+        foreach ($contextlist->get_contexts() as $context) {
+            if ($context->contextlevel === CONTEXT_MODULE) {
+                $cmids[] = $context->instanceid;
+            }
+        }
+
+        // Do nothing if no matching quiz settings are found for the user.
+        if (empty($cmids)) {
+            return;
+        }
+
+        list($insql, $params) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
+
+        // SEB quiz settings.
+        $sql = "SELECT qs.id as id,
+                       qs.quizid as quizid,
+                       qs.usermodified as usermodified,
+                       qs.timecreated as timecreated,
+                       qs.timemodified as timemodified
+                  FROM {quizaccess_seb_quizsettings} qs
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                 WHERE cm.id {$insql}";
+
+        $quizsettingslist = $DB->get_records_sql($sql, $params);
+        $index = 0;
+        foreach ($quizsettingslist as $quizsettings) {
+            // Data export is organised in: {Context}/{Plugin Name}/{Table name}/{index}/data.json.
+            $index++;
+            $subcontext = [
+                get_string('pluginname', 'quizaccess_seb'),
+                quiz_settings::TABLE,
+                $index
+            ];
+
+            $data = (object) [
+                'quizid' => $quizsettings->quizid,
+                'usermodified' => $quizsettings->usermodified,
+                'timecreated' => transform::datetime($quizsettings->timecreated),
+                'timemodified' => transform::datetime($quizsettings->timemodified)
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $data);
+        }
+
+        // SEB template settings.
+        $sql = "SELECT tem.id as id,
+                       qs.quizid as quizid,
+                       tem.usermodified as usermodified,
+                       tem.timecreated as timecreated,
+                       tem.timemodified as timemodified
+                  FROM {quizaccess_seb_template} tem
+                  JOIN {quizaccess_seb_quizsettings} qs ON qs.templateid = tem.id
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                 WHERE cm.id {$insql}";
+
+        $templatesettingslist = $DB->get_records_sql($sql, $params);
+        $index = 0;
+        foreach ($templatesettingslist as $templatesetting) {
+            // Data export is organised in: {Context}/{Plugin Name}/{Table name}/{index}/data.json.
+            $index++;
+            $subcontext = [
+                get_string('pluginname', 'quizaccess_seb'),
+                template::TABLE,
+                $index
+            ];
+
+            $data = (object) [
+                'templateid' => $templatesetting->id,
+                'quizid' => $templatesetting->quizid,
+                'usermodified' => $templatesetting->usermodified,
+                'timecreated' => transform::datetime($templatesetting->timecreated),
+                'timemodified' => transform::datetime($templatesetting->timemodified)
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $data);
+        }
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param context $context The specific context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        // Sanity check that context is at the module context level, then get the quizid.
+        if ($context->contextlevel !== CONTEXT_MODULE) {
+            return;
+        }
+
+        $cmid = $context->instanceid;
+        $quizid = $DB->get_field('course_modules', 'instance', ['id' => $cmid]);
+
+        $params['quizid'] = $quizid;
+        $select = "id IN (SELECT templateid FROM {quizaccess_seb_quizsettings} qs WHERE qs.quizid = :quizid)";
+        $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "quizid = :quizid", $params);
+        $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, $select, $params);
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        global $DB;
+
+        // If the user has data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+
+        $params['usermodified'] = $contextlist->get_user()->id;
+        $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "usermodified = :usermodified", $params);
+        $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, "usermodified = :usermodified", $params);
+    }
+
+    /**
+     * Get the list of users who have data within a context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        $context = $userlist->get_context();
+
+        if (!$context instanceof \context_module) {
+            return;
+        }
+
+        // The data is associated at the quiz module context level, so retrieve the user's context id.
+        $sql = "SELECT qs.usermodified AS userid
+                  FROM {quizaccess_seb_quizsettings} qs
+                  JOIN {course_modules} cm ON cm.instance = qs.quizid
+                 WHERE cm.id = ?";
+        $params = [$context->instanceid];
+        $userlist->add_from_sql('userid', $sql, $params);
+    }
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        global $DB;
+        $context = $userlist->get_context();
+
+        // Sanity check that context is at the Module context level.
+        if ($context->contextlevel !== CONTEXT_MODULE) {
+            return;
+        }
+
+        $userids = $userlist->get_userids();
+        list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+
+        $DB->set_field_select('quizaccess_seb_quizsettings', 'usermodified', 0, "usermodified {$insql}", $inparams);
+        $DB->set_field_select('quizaccess_seb_template', 'usermodified', 0, "usermodified {$insql}", $inparams);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/property_list.php b/mod/quiz/accessrule/seb/classes/property_list.php
new file mode 100644 (file)
index 0000000..57525e6
--- /dev/null
@@ -0,0 +1,389 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Wrapper for CFPropertyList to handle low level iteration.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use CFPropertyList\CFArray;
+use CFPropertyList\CFBoolean;
+use CFPropertyList\CFData;
+use CFPropertyList\CFDate;
+use CFPropertyList\CFDictionary;
+use CFPropertyList\CFNumber;
+use CFPropertyList\CFPropertyList;
+use CFPropertyList\CFString;
+use CFPropertyList\CFType;
+use \DateTime;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Wrapper for CFPropertyList to handle low level iteration.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class property_list {
+
+    /** A random 4 character unicode string to replace backslashes during json_encode. */
+    private const BACKSLASH_SUBSTITUTE = "ؼҷҍԴ";
+
+    /** @var CFPropertyList $cfpropertylist */
+    private $cfpropertylist;
+
+    /**
+     * property_list constructor.
+     *
+     * @param string $xml A Plist XML string.
+     */
+    public function __construct(string $xml = '') {
+        $this->cfpropertylist = new CFPropertyList();
+
+        if (empty($xml)) {
+            // If xml not provided, create a blank PList with root dictionary set up.
+            $this->cfpropertylist->add(new CFDictionary([]));
+        } else {
+            // Parse the XML into a PList object.
+            $this->cfpropertylist->parse($xml, CFPropertyList::FORMAT_XML);
+        }
+    }
+
+    /**
+     * Add a new element to the root dictionary element.
+     *
+     * @param string $key Key to assign to new element.
+     * @param CFType $element The new element. May be a collection such as an array.
+     */
+    public function add_element_to_root(string $key, CFType $element) {
+        // Get the PList's root dictionary and add new element.
+        $this->cfpropertylist->getValue()->add($key, $element);
+    }
+
+    /**
+     * Get value of element identified by key.
+     *
+     * @param string $key Key of element.
+     * @return mixed Value of element found, or null if none found.
+     */
+    public function get_element_value(string $key) {
+        $result = null;
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key, &$result) {
+            // Convert date to iso 8601 if date object.
+            if ($key === $elkey) {
+                $result = $elvalue->getValue();
+            }
+        }, $this->cfpropertylist->getValue());
+
+        if (is_array($result)) {
+            // Turn CFType elements in PHP elements.
+            $result = $this->array_serialize_cftypes($result);
+        }
+        return $result;
+    }
+
+    /**
+     * Update the value of any element with matching key.
+     *
+     * Only allow string, number and boolean elements to be updated.
+     *
+     * @param string $key Key of element to update.
+     * @param mixed $value Value to update element with.
+     */
+    public function update_element_value(string $key, $value) {
+        if (is_array($value)) {
+            throw new \invalid_parameter_exception('Use update_element_array to update a collection.');
+        }
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key, $value) {
+            // Set new value.
+            if ($key === $elkey) {
+                $element = $parent->get($elkey);
+                // Limit update to boolean and strings types, and check value matches expected type.
+                if (($element instanceof CFString && is_string($value))
+                        || ($element instanceof CFNumber && is_numeric($value))
+                        || ($element instanceof CFBoolean && is_bool($value))) {
+                    $element->setValue($value);
+                } else {
+                    throw new \invalid_parameter_exception(
+                            'Only string, number and boolean elements can be updated, or value type does not match element type: '
+                            . get_class($element));
+                }
+            }
+        }, $this->cfpropertylist->getValue());
+    }
+
+    /**
+     * Update the array of any dict or array element with matching key.
+     *
+     * Will replace array.
+     *
+     * @param string $key Key of element to update.
+     * @param array $value Array to update element with.
+     */
+    public function update_element_array(string $key, array $value) {
+        // Validate new array.
+        foreach ($value as $element) {
+            // If any element is not a CFType instance, then throw exception.
+            if (!($element instanceof CFType)) {
+                throw new \invalid_parameter_exception('New array must only contain CFType objects.');
+            }
+        }
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key, $value) {
+            if ($key === $elkey) {
+                $element = $parent->get($elkey);
+                // Replace existing element with new element and array but same key.
+                if ($element instanceof CFDictionary) {
+                    $parent->del($elkey);
+                    $parent->add($elkey, new CFDictionary($value));
+                } else if ($element instanceof CFArray) {
+                    $parent->del($elkey);
+                    $parent->add($elkey, new CFArray($value));
+                }
+            }
+        }, $this->cfpropertylist->getValue());
+    }
+
+    /**
+     * Delete any element with a matching key.
+     *
+     * @param string $key Key of element to delete.
+     */
+    public function delete_element(string $key) {
+        $this->plist_map( function($elvalue, $elkey, $parent) use ($key) {
+            // Convert date to iso 8601 if date object.
+            if ($key === $elkey) {
+                $parent->del($key);
+            }
+        }, $this->cfpropertylist->getValue());
+    }
+
+    /**
+     * Helper function to either set or update a CF type value to the plist.
+     *
+     * @param string $key
+     * @param CFType $input
+     */
+    public function set_or_update_value(string $key, CFType $input) {
+        $value = $this->get_element_value($key);
+        if (empty($value)) {
+            $this->add_element_to_root($key, $input);
+        } else {
+            $this->update_element_value($key, $input->getValue());
+        }
+    }
+
+    /**
+     * Convert the PList to XML.
+     *
+     * @return string XML ready for creating an XML file.
+     */
+    public function to_xml() : string {
+        return $this->cfpropertylist->toXML();
+    }
+
+    /**
+     * Return a JSON representation of the PList. The JSON is constructed to be used to generate a SEB Config Key.
+     *
+     * See the developer documention for SEB for more information on the requirements on generating a SEB Config Key.
+     * https://safeexambrowser.org/developer/seb-config-key.html
+     *
+     * 1. Don't add any whitespace or line formatting to the SEB-JSON string.
+     * 2. Don't add character escaping (also backshlashes "\" as found in URL filter rules should not be escaped).
+     * 3. All <dict> elements from the plist XML must be ordered (alphabetically sorted) by their key names. Use a
+     * recursive method to apply ordering also to nested dictionaries contained in the root-level dictionary and in
+     * arrays. Use non-localized (culture invariant), non-ASCII value based case insensitive ordering. For example the
+     * key <key>allowWlan</key> comes before <key>allowWLAN</key>. Cocoa/Obj-C and .NET/C# usually use this case
+     * insensitive ordering as default, but PHP for example doesn't.
+     * 4. Remove empty <dict> elements (key/value). Current versions of SEB clients should anyways not generate empty
+     * dictionaries, but this was possible with outdated versions. If config files have been generated that time, such
+     * elements might still be around.
+     * 5. All string elements must be UTF8 encoded.
+     * 6. Base16 strings should use lower-case a-f characters, even though this isn't relevant in the current
+     * implementation of the Config Key calculation.
+     * 7. <data> plist XML elements must be converted to Base64 strings.
+     * 8. <date> plist XML elements must be converted to ISO 8601 formatted strings.
+     *
+     * @return string A json encoded string.
+     */
+    public function to_json() : string {
+        // Create a clone of the PList, so main list isn't mutated.
+        $jsonplist = new CFPropertyList();
+        $jsonplist->parse($this->cfpropertylist->toXML(), CFPropertyList::FORMAT_XML);
+
+        // Pass root dict to recursively convert dates to ISO 8601 format, encode strings to UTF-8,
+        // lock data to Base 64 encoding and remove empty dictionaries.
+        $this->prepare_plist_for_json_encoding($jsonplist->getValue());
+
+        // Serialize PList to array.
+        $plistarray = $jsonplist->toArray();
+
+        // Sort array alphabetically by key using case insensitive, natural sorting. See point 3 for more information.
+        $plistarray = $this->array_sort($plistarray);
+
+        // Encode in JSON with following rules from SEB docs.
+        // 1. Don't add any whitespace or line formatting to the SEB-JSON string.
+        // 2. Don't add unicode or slash escaping.
+        $json = json_encode($plistarray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+
+        // There is no way to prevent json_encode from escaping backslashes. We replace each backslash with a unique string
+        // prior to encoding in prepare_plist_for_json_encoding(). We can then replace the substitute with a single backslash.
+        $json = str_replace(self::BACKSLASH_SUBSTITUTE, "\\", $json);
+        return $json;
+    }
+
+    /**
+     * Recursively convert PList date values from unix to iso 8601 format, and ensure strings are UTF 8 encoded.
+     *
+     * This will mutate the PList.
+     */
+
+    /**
+     * Recursively convert PList date values from unix to iso 8601 format, and ensure strings are UTF 8 encoded.
+     *
+     * This will mutate the PList.
+     * @param \Iterator $root The root element of the PList. Must be a dictionary or array.
+     */
+    private function prepare_plist_for_json_encoding($root) {
+        $this->plist_map( function($value, $key, $parent) {
+            // Convert date to ISO 8601 if date object.
+            if ($value instanceof CFDate) {
+                $date = DateTime::createFromFormat('U', $value->getValue());
+                $date->setTimezone(new \DateTimeZone('UTC')); // Zulu timezone a.k.a. UTC+00.
+                $isodate = $date->format('c');
+                $value->setValue($isodate);
+            }
+            // Make sure strings are UTF 8 encoded.
+            if ($value instanceof CFString) {
+                // As literal backslashes will be lost during encoding, we must replace them with a unique substitute to be
+                // reverted after JSON encoding.
+                $string = str_replace("\\", self::BACKSLASH_SUBSTITUTE, $value->getValue());
+                $value->setValue(mb_convert_encoding($string, 'UTF-8'));
+            }
+            // Data should remain base 64 encoded, so convert to base encoded string for export. Otherwise
+            // CFData will decode the data when serialized.
+            if ($value instanceof CFData) {
+                $data = trim($value->getCodedValue());
+                $parent->del($key);
+                $parent->add($key, new CFString($data));
+            }
+            // Empty dictionaries should be removed.
+            if ($value instanceof CFDictionary && empty($value->getValue())) {
+                $parent->del($key);
+            }
+        }, $root);
+
+    }
+
+    /**
+     * Iterate through the PList elements, and call the callback on each.
+     *
+     * @param callable $callback A callback function called for every element.
+     * @param \Iterator $root The root element of the PList. Must be a dictionary or array.
+     * @param bool $recursive Whether the function should traverse dicts and arrays recursively.
+     */
+    private function plist_map(callable $callback, \Iterator $root, bool $recursive = true) {
+        $root->rewind();
+        while ($root->valid()) {
+            $value = $root->current();
+            $key = $root->key();
+
+            // Recursively traverse all dicts and arrays if flag is true.
+            if ($recursive && $value instanceof \Iterator) {
+                $this->plist_map($callback, $value);
+            }
+
+            // Callback function called for every element.
+            $callback($value, $key, $root);
+
+            $root->next();
+        }
+    }
+
+    /**
+     * Recursively sort array alphabetically by key.
+     *
+     * @param array $array Top level array to process.
+     * @return array Processed array.
+     */
+    private function array_sort(array $array) {
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $array[$key] = $this->array_sort($array[$key]);
+            }
+        }
+        // Sort assoc array. From SEB docs - "Use non-localized (culture invariant), non-ASCII value based case
+        // insensitive ordering."
+        if ($this->is_associative_array($array)) {
+            ksort($array, SORT_STRING | SORT_FLAG_CASE);
+        }
+
+        return $array;
+    }
+
+    /**
+     * Recursively remove empty arrays.
+     *
+     * @param array $array Top level array to process.
+     * @return array Processed array.
+     */
+    private function array_remove_empty_arrays(array $array) {
+        foreach ($array as $key => $value) {
+            if (is_array($value)) {
+                $array[$key] = $this->array_remove_empty_arrays($array[$key]);
+            }
+
+            // Remove empty arrays.
+            if (is_array($array[$key]) && empty($array[$key])) {
+                unset($array[$key]);
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * If an array contains CFType objects, wrap array in a CFDictionary to allow recursive serialization of data
+     * into a standard PHP array.
+     *
+     * @param array $array Array containing CFType objects.
+     * @return array Standard PHP array.
+     */
+    private function array_serialize_cftypes(array $array) : array {
+        $array = new CFDictionary($array); // Convert back to CFDictionary so serialization is recursive.
+        return $array->toArray(); // Serialize.
+    }
+
+    /**
+     * Check if an array is associative or sequential.
+     *
+     * @param array $array Array to check.
+     * @return bool False if not associative.
+     */
+    private function is_associative_array(array $array) {
+        if (empty($array)) {
+            return false;
+        }
+        // Check that all keys are not sequential integers starting from 0 (Which is what PHP arrays have behind the scenes.)
+        return array_keys($array) !== range(0, count($array) - 1);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/quiz_settings.php b/mod/quiz/accessrule/seb/classes/quiz_settings.php
new file mode 100644 (file)
index 0000000..9511efe
--- /dev/null
@@ -0,0 +1,648 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Entity model representing quiz settings for the seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use CFPropertyList\CFArray;
+use CFPropertyList\CFBoolean;
+use CFPropertyList\CFDictionary;
+use CFPropertyList\CFNumber;
+use CFPropertyList\CFString;
+use core\persistent;
+use lang_string;
+use moodle_exception;
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Entity model representing quiz settings for the seb plugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_settings extends persistent {
+
+    /** Table name for the persistent. */
+    const TABLE = 'quizaccess_seb_quizsettings';
+
+    /** @var property_list $plist The SEB config represented as a Property List object. */
+    private $plist;
+
+    /** @var string $config The SEB config represented as a string. */
+    private $config;
+
+    /** @var string $configkey The SEB config key represented as a string. */
+    private $configkey;
+
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() : array {
+        return [
+            'quizid' => [
+                'type' => PARAM_INT,
+            ],
+            'cmid' => [
+                'type' => PARAM_INT,
+            ],
+            'templateid' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+            'requiresafeexambrowser' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+            'showsebtaskbar' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'showwificontrol' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'showreloadbutton' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'showtime' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'showkeyboardlayout' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowuserquitseb' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'quitpassword' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'linkquitseb' => [
+                'type' => PARAM_URL,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'userconfirmquit' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'enableaudiocontrol' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'muteonstartup' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowspellchecking' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowreloadinexam' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'activateurlfiltering' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'filterembeddedcontent' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+                'null' => NULL_ALLOWED,
+            ],
+            'expressionsallowed' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'regexallowed' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'expressionsblocked' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'regexblocked' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+            'showsebdownloadlink' => [
+                'type' => PARAM_INT,
+                'default' => 1,
+                'null' => NULL_ALLOWED,
+            ],
+            'allowedbrowserexamkeys' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED,
+            ],
+        ];
+    }
+
+    /**
+     * 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\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);
+        }
+
+        return self::get_record(['quizid' => $quizid]);
+    }
+
+    /**
+     * Return cached 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 = self::get_config_cache()->get($quizid);
+
+        if ($config !== false) {
+            return $config;
+        }
+
+        $config = null;
+        if ($settings = self::get_by_quiz_id($quizid)) {
+            $config = $settings->get_config();
+            self::get_config_cache()->set($quizid, $config);
+        }
+
+        return $config;
+    }
+
+    /**
+     * Return cached 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.
+     *
+     * @param string $keys Newline separated browser exam keys.
+     * @return true|lang_string If there is an error, an error string is returned.
+     */
+    protected function validate_allowedbrowserexamkeys($keys) {
+        $keys = $this->split_keys($keys);
+        foreach ($keys as $i => $key) {
+            if (!preg_match('~^[a-f0-9]{64}$~', $key)) {
+                return new lang_string('allowedbrowserkeyssyntax', 'quizaccess_seb');
+            }
+        }
+        if (count($keys) != count(array_unique($keys))) {
+            return new lang_string('allowedbrowserkeysdistinct', 'quizaccess_seb');
+        }
+        return true;
+    }
+
+    /**
+     * Get the browser exam keys as a pre-split array instead of just as a string.
+     *
+     * @return array
+     */
+    protected function get_allowedbrowserexamkeys() : array {
+        $keysstring = $this->raw_get('allowedbrowserexamkeys');
+        $keysstring = empty($keysstring) ? '' : $keysstring;
+        return $this->split_keys($keysstring);
+    }
+
+    /**
+     * Hook to execute before an update.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_update() {
+        $this->before_save();
+    }
+
+    /**
+     * Hook to execute before a create.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_create() {
+        $this->before_save();
+    }
+
+    /**
+     * As there is no hook for before both create and update, this function is called by both hooks.
+     */
+    private function before_save() {
+        // Set template to 0 if using anything different to template.
+        if ($this->get('requiresafeexambrowser') != settings_provider::USE_SEB_TEMPLATE) {
+            $this->set('templateid', 0);
+        }
+
+        // Process configs to make sure that all data is set correctly.
+        $this->process_configs();
+    }
+
+    /**
+     * Before validate hook.
+     */
+    protected function before_validate() {
+        // Template can't be null.
+        if (is_null($this->get('templateid'))) {
+            $this->set('templateid', 0);
+        }
+    }
+
+    /**
+     * Create or update the config string based on the current quiz settings.
+     */
+    private function process_configs() {
+        switch ($this->get('requiresafeexambrowser')) {
+            case settings_provider::USE_SEB_NO:
+                $this->process_seb_config_no();
+                break;
+
+            case settings_provider::USE_SEB_CONFIG_MANUALLY:
+                $this->process_seb_config_manually();
+                break;
+
+            case settings_provider::USE_SEB_TEMPLATE:
+                $this->process_seb_template();
+                break;
+
+            case settings_provider::USE_SEB_UPLOAD_CONFIG:
+                $this->process_seb_upload_config();
+                break;
+
+            default: // Also settings_provider::USE_SEB_CLIENT_CONFIG.
+                $this->process_seb_client_config();
+        }
+
+        // Generate config key based on given SEB config.
+        if (!empty($this->config)) {
+            $this->configkey = config_key::generate($this->config)->get_hash();
+        } else {
+            $this->configkey = null;
+        }
+    }
+
+    /**
+     * Return SEB config key.
+     *
+     * @return string|null
+     */
+    public function get_config_key() : ?string {
+        $this->process_configs();
+
+        return $this->configkey;
+    }
+
+    /**
+     * Return string representation of the config.
+     *
+     * @return string|null
+     */
+    public function get_config() : ?string {
+        $this->process_configs();
+
+        return $this->config;
+    }
+
+    /**
+     * Case for USE_SEB_NO.
+     */
+    private function process_seb_config_no() {
+        $this->config = null;
+    }
+
+    /**
+     * Case for USE_SEB_CONFIG_MANUALLY. This creates a plist and applies all settings from the posted form, along with
+     * some defaults.
+     */
+    private function process_seb_config_manually() {
+        // If at any point a configuration file has been uploaded and parsed, clear the settings.
+        $this->plist = new property_list();
+
+        $this->process_bool_settings();
+        $this->process_quit_password_settings();
+        $this->process_quit_url_from_settings();
+        $this->process_url_filters();
+        $this->process_required_enforced_settings();
+
+        // One of the requirements for USE_SEB_CONFIG_MANUALLY is setting examSessionClearCookiesOnStart to false.
+        $this->plist->set_or_update_value('examSessionClearCookiesOnStart', new CFBoolean(false));
+        $this->config = $this->plist->to_xml();
+    }
+
+    /**
+     * Case for USE_SEB_TEMPLATE. This creates a plist from the template uploaded, then applies the quit password
+     * setting and some defaults.
+     */
+    private function process_seb_template() {
+        $template = template::get_record(['id' => $this->get('templateid')]);
+        $this->plist = new property_list($template->get('content'));
+
+        $this->process_quit_password_settings();
+        $this->process_quit_url_from_template_or_config();
+        $this->process_required_enforced_settings();
+
+        $this->config = $this->plist->to_xml();
+    }
+
+    /**
+     * Case for USE_SEB_UPLOAD_CONFIG. This creates a plist from an uploaded configuration file, then applies the quiz
+     * password settings and some defaults.
+     */
+    private function process_seb_upload_config() {
+        $file = settings_provider::get_module_context_sebconfig_file($this->get('cmid'));
+
+        // If there was no file, create an empty plist so the rest of this wont explode.
+        if (empty($file)) {
+            throw new moodle_exception('noconfigfilefound', 'quizaccess_seb', '', $this->get('cmid'));
+        } else {
+            $this->plist = new property_list($file->get_content());
+        }
+
+        $this->process_quit_password_settings();
+        $this->process_quit_url_from_template_or_config();
+        $this->process_required_enforced_settings();
+
+        $this->config = $this->plist->to_xml();
+    }
+
+    /**
+     * Case for USE_SEB_CLIENT_CONFIG. This creates an empty plist to remove the config stored.
+     */
+    private function process_seb_client_config() {
+        $this->config = null;
+    }
+
+    /**
+     * Sets or updates some sensible default settings, these are the items 'startURL' and 'sendBrowserExamKey'.
+     */
+    private function process_required_enforced_settings() {
+        global $CFG;
+
+        $quizurl = new moodle_url($CFG->wwwroot . "/mod/quiz/view.php", ['id' => $this->get('cmid')]);
+        $this->plist->set_or_update_value('startURL', new CFString($quizurl->out(true)));
+        $this->plist->set_or_update_value('sendBrowserExamKey', new CFBoolean(true));
+    }
+
+    /**
+     * Use the boolean map to add Moodle boolean setting to config PList.
+     */
+    private function process_bool_settings() {
+        $settings = $this->to_record();
+        $map = $this->get_bool_seb_setting_map();
+        foreach ($settings as $setting => $value) {
+            if (isset($map[$setting])) {
+                $enabled = $value == 1 ? true : false;
+                $this->plist->add_element_to_root($map[$setting], new CFBoolean($enabled));
+            }
+        }
+    }
+
+    /**
+     * Turn hashed quit password and quit link into PList strings and add to config PList.
+     */
+    private function process_quit_password_settings() {
+        $settings = $this->to_record();
+        if (!empty($settings->quitpassword) && is_string($settings->quitpassword)) {
+            // Hash quit password.
+            $hashedpassword = hash('SHA256', $settings->quitpassword);
+            $this->plist->add_element_to_root('hashedQuitPassword', new CFString($hashedpassword));
+        }
+    }
+
+    /**
+     * Sets the quitURL if found in the quiz_settings.
+     */
+    private function process_quit_url_from_settings() {
+        $settings = $this->to_record();
+        if (!empty($settings->linkquitseb) && is_string($settings->linkquitseb)) {
+            $this->plist->set_or_update_value('quitURL', new CFString($settings->linkquitseb));
+        }
+    }
+
+    /**
+     * Sets the quiz_setting's linkquitseb if a quitURL value was found in a template or uploaded config.
+     */
+    private function process_quit_url_from_template_or_config() {
+        // Does the plist (template or config file) have an existing quitURL?
+        $quiturl = $this->plist->get_element_value('quitURL');
+        if (!empty($quiturl)) {
+            $this->set('linkquitseb', $quiturl);
+        }
+    }
+
+    /**
+     * Turn return separated strings for URL filters into a PList array and add to config PList.
+     */
+    private function process_url_filters() {
+        $settings = $this->to_record();
+        // Create rules to each expression provided and add to config.
+        $urlfilterrules = [];
+        // Get all rules separated by newlines and remove empty rules.
+        $expallowed = array_filter(explode(PHP_EOL, $settings->expressionsallowed));
+        $expblocked = array_filter(explode(PHP_EOL, $settings->expressionsblocked));
+        $regallowed = array_filter(explode(PHP_EOL, $settings->regexallowed));
+        $regblocked = array_filter(explode(PHP_EOL, $settings->regexblocked));
+        foreach ($expallowed as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, true, false);
+        }
+        foreach ($expblocked as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, false, false);
+        }
+        foreach ($regallowed as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, true, true);
+        }
+        foreach ($regblocked as $rulestring) {
+            $urlfilterrules[] = $this->create_filter_rule($rulestring, false, true);
+        }
+        $this->plist->add_element_to_root('URLFilterRules', new CFArray($urlfilterrules));
+    }
+
+    /**
+     * Create a CFDictionary represeting a URL filter rule.
+     *
+     * @param string $rulestring The expression to filter with.
+     * @param bool $allowed Allowed or blocked.
+     * @param bool $isregex Regex or simple.
+     * @return CFDictionary A PList dictionary.
+     */
+    private function create_filter_rule(string $rulestring, bool $allowed, bool $isregex) : CFDictionary {
+        $action = $allowed ? 1 : 0;
+        return new CFDictionary([
+                    'action' => new CFNumber($action),
+                    'active' => new CFBoolean(true),
+                    'expression' => new CFString(trim($rulestring)),
+                    'regex' => new CFBoolean($isregex),
+                    ]);
+    }
+
+    /**
+     * Map the settings that are booleans to the Safe Exam Browser config keys.
+     *
+     * @return array Moodle setting as key, SEB setting as value.
+     */
+    private function get_bool_seb_setting_map() : array {
+        return [
+            'activateurlfiltering' => 'URLFilterEnable',
+            'allowspellchecking' => 'allowSpellCheck',
+            'allowreloadinexam' => 'browserWindowAllowReload',
+            'allowuserquitseb' => 'allowQuit',
+            'enableaudiocontrol' => 'audioControlEnabled',
+            'filterembeddedcontent' => 'URLFilterEnableContentFilter',
+            'muteonstartup' => 'audioMute',
+            'showkeyboardlayout' => 'showInputLanguage',
+            'showreloadbutton' => 'showReloadButton',
+            'showsebtaskbar' => 'showTaskBar',
+            'showtime' => 'showTime',
+            'showwificontrol' => 'allowWlan',
+            'userconfirmquit' => 'quitURLConfirm',
+        ];
+    }
+
+    /**
+     * This helper method takes list of browser exam keys in a string and splits it into an array of separate keys.
+     *
+     * @param string|null $keys the allowed keys.
+     * @return array of string, the separate keys.
+     */
+    private function split_keys($keys) : array {
+        $keys = preg_split('~[ \t\n\r,;]+~', $keys, -1, PREG_SPLIT_NO_EMPTY);
+        foreach ($keys as $i => $key) {
+            $keys[$i] = strtolower($key);
+        }
+        return $keys;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/settings_provider.php b/mod/quiz/accessrule/seb/classes/settings_provider.php
new file mode 100644 (file)
index 0000000..86ef593
--- /dev/null
@@ -0,0 +1,1119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for providing quiz settings, to make setting up quiz form manageable.
+ *
+ * To make sure there are no inconsistencies between data sets, run tests in tests/phpunit/settings_provider_test.php.
+ *
+ * @package    quizaccess_seb
+ * @author     Luca Bösch <luca.boesch@bfh.ch>
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use context_module;
+use context_user;
+use lang_string;
+use stdClass;
+use stored_file;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class for providing quiz settings, to make setting up quiz form manageable.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class settings_provider {
+
+    /**
+     * No SEB should be used.
+     */
+    const USE_SEB_NO = 0;
+
+    /**
+     * Use SEB and configure it manually.
+     */
+    const USE_SEB_CONFIG_MANUALLY = 1;
+
+    /**
+     * Use SEB config from pre configured template.
+     */
+    const USE_SEB_TEMPLATE = 2;
+
+    /**
+     * Use SEB config from uploaded config file.
+     */
+    const USE_SEB_UPLOAD_CONFIG = 3;
+
+    /**
+     * Use client config. Not SEB config is required.
+     */
+    const USE_SEB_CLIENT_CONFIG = 4;
+
+    /**
+     * Insert form element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param \HTML_QuickForm_element $element Element to insert.
+     * @param string $before Insert element before.
+     */
+    protected static function insert_element(\mod_quiz_mod_form $quizform,
+                                             \MoodleQuickForm $mform, \HTML_QuickForm_element $element, $before = 'security') {
+        $mform->insertElementBefore($element, $before);
+    }
+
+    /**
+     * Remove element from the form.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     */
+    protected static function remove_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string  $elementname) {
+        if ($mform->elementExists($elementname)) {
+            $mform->removeElement($elementname);
+            $mform->setDefault($elementname, null);
+        }
+    }
+
+    /**
+     * Add help button to the element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     */
+    protected static function add_help_button(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) {
+        if ($mform->elementExists($elementname)) {
+            $mform->addHelpButton($elementname, $elementname, 'quizaccess_seb');
+        }
+    }
+
+    /**
+     * Set default value for the element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     * @param mixed $value Default value.
+     */
+    protected static function set_default(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string  $elementname, $value) {
+        $mform->setDefault($elementname, $value);
+    }
+
+    /**
+     * Set element type.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     * @param string $type Type of the form element.
+     */
+    protected static function set_type(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname, string $type) {
+        $mform->setType($elementname, $type);
+    }
+
+    /**
+     * Freeze form element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @param string $elementname Element name.
+     */
+    protected static function freeze_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) {
+        if ($mform->elementExists($elementname)) {
+            $mform->freeze($elementname);
+        }
+    }
+
+    /**
+     * Add SEB header element to  the form.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_header_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        global  $OUTPUT;
+
+        $element = $mform->createElement('header', 'seb', get_string('seb', 'quizaccess_seb'));
+        self::insert_element($quizform, $mform, $element);
+
+        // Display notification about locked settings.
+        if (self::is_seb_settings_locked($quizform->get_instance())) {
+            $notify = new \core\output\notification(
+                get_string('settingsfrozen', 'quizaccess_seb'),
+                \core\output\notification::NOTIFY_WARNING
+            );
+
+            $notifyelement = $mform->createElement('html', $OUTPUT->render($notify));
+            self::insert_element($quizform, $mform, $notifyelement);
+        }
+
+        if (self::is_conflicting_permissions($quizform->get_context())) {
+            $notify = new \core\output\notification(
+                get_string('conflictingsettings', 'quizaccess_seb'),
+                \core\output\notification::NOTIFY_WARNING
+            );
+
+            $notifyelement = $mform->createElement('html', $OUTPUT->render($notify));
+            self::insert_element($quizform, $mform, $notifyelement);
+        }
+    }
+
+    /**
+     * Add SEB usage element with all available options.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_usage_options(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        $element = $mform->createElement(
+            'select',
+            'seb_requiresafeexambrowser',
+            get_string('seb_requiresafeexambrowser', 'quizaccess_seb'),
+            self::get_requiresafeexambrowser_options($quizform->get_context())
+        );
+
+        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::add_help_button($quizform, $mform, 'seb_requiresafeexambrowser');
+
+        if (self::is_conflicting_permissions($quizform->get_context())) {
+            self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser');
+        }
+    }
+
+    /**
+     * Add Templates element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_templates(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_use_seb_template($quizform->get_context()) || self::is_conflicting_permissions($quizform->get_context())) {
+            $element = $mform->createElement(
+                'select',
+                'seb_templateid',
+                get_string('seb_templateid', 'quizaccess_seb'),
+                self::get_template_options()
+            );
+        } else {
+            $element = $mform->createElement('hidden', 'seb_templateid');
+        }
+
+        self::insert_element($quizform, $mform, $element);
+        self::set_type($quizform, $mform, 'seb_templateid', PARAM_INT);
+        self::set_default($quizform, $mform, 'seb_templateid', 0);
+        self::add_help_button($quizform, $mform, 'seb_templateid');
+
+        // In case if the user can't use templates, but the quiz is configured to use them,
+        // we'd like to display template, but freeze it.
+        if (self::is_conflicting_permissions($quizform->get_context())) {
+            self::freeze_element($quizform, $mform, 'seb_templateid');
+        }
+    }
+
+    /**
+     * Add upload config file element.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_config_file(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        $itemid = 0;
+
+        $draftitemid = 0;
+        file_prepare_draft_area(
+            $draftitemid,
+            $quizform->get_context()->id,
+            'quizaccess_seb',
+            'filemanager_sebconfigfile',
+            $itemid
+        );
+
+        if (self::can_upload_seb_file($quizform->get_context())) {
+            $element = $mform->createElement(
+                'filemanager',
+                'filemanager_sebconfigfile',
+                get_string('filemanager_sebconfigfile', 'quizaccess_seb'),
+                null,
+                self::get_filemanager_options()
+            );
+        } else {
+            $element = $mform->createElement('hidden', 'filemanager_sebconfigfile');
+        }
+
+        self::insert_element($quizform, $mform, $element);
+        self::set_type($quizform, $mform, 'filemanager_sebconfigfile', PARAM_RAW);
+        self::set_default($quizform, $mform, 'filemanager_sebconfigfile', $draftitemid);
+        self::add_help_button($quizform, $mform, 'filemanager_sebconfigfile');
+    }
+
+    /**
+     * Add Show Safe Exam Browser download button.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_show_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_change_seb_showsebdownloadlink($quizform->get_context())) {
+            $element = $mform->createElement('selectyesno',
+                'seb_showsebdownloadlink',
+                get_string('seb_showsebdownloadlink', 'quizaccess_seb')
+            );
+            self::insert_element($quizform, $mform, $element);
+            self::set_type($quizform, $mform, 'seb_showsebdownloadlink', PARAM_BOOL);
+            self::set_default($quizform, $mform, 'seb_showsebdownloadlink', 1);
+            self::add_help_button($quizform, $mform, 'seb_showsebdownloadlink');
+        }
+    }
+
+    /**
+     * Add Allowed Browser Exam Keys setting.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_allowedbrowserexamkeys(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_change_seb_allowedbrowserexamkeys($quizform->get_context())) {
+            $element = $mform->createElement('textarea',
+                'seb_allowedbrowserexamkeys',
+                get_string('seb_allowedbrowserexamkeys', 'quizaccess_seb')
+            );
+            self::insert_element($quizform, $mform, $element);
+            self::set_type($quizform, $mform, 'seb_allowedbrowserexamkeys', PARAM_RAW);
+            self::set_default($quizform, $mform, 'seb_allowedbrowserexamkeys', '');
+            self::add_help_button($quizform, $mform, 'seb_allowedbrowserexamkeys');
+        }
+    }
+
+    /**
+     * Add SEB config elements.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function add_seb_config_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        $defaults = self::get_seb_config_element_defaults();
+        $types = self::get_seb_config_element_types();
+
+        foreach (self::get_seb_config_elements() as $name => $type) {
+            if (!self::can_manage_seb_config_setting($name, $quizform->get_context())) {
+                $type = 'hidden';
+            }
+
+            $element = $mform->createElement($type, $name, get_string($name, 'quizaccess_seb'));
+            self::insert_element($quizform, $mform, $element);
+            unset($element); // We need to make sure each &element only references the current element in loop.
+
+            self::add_help_button($quizform, $mform, $name);
+
+            if (isset($defaults[$name])) {
+                self::set_default($quizform, $mform, $name, $defaults[$name]);
+            }
+
+            if (isset($types[$name])) {
+                self::set_type($quizform, $mform, $name, $types[$name]);
+            }
+        }
+    }
+
+    /**
+     * Add setting fields.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    public static function add_seb_settings_fields(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::can_configure_seb($quizform->get_context())) {
+            self::add_seb_header_element($quizform, $mform);
+            self::add_seb_usage_options($quizform, $mform);
+            self::add_seb_templates($quizform, $mform);
+            self::add_seb_config_file($quizform, $mform);
+            self::add_seb_show_download_link($quizform, $mform);
+            self::add_seb_config_elements($quizform, $mform);
+            self::add_seb_allowedbrowserexamkeys($quizform, $mform);
+            self::hide_seb_elements($quizform, $mform);
+            self::lock_seb_elements($quizform, $mform);
+        }
+    }
+
+    /**
+     * Hide SEB elements if required.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function hide_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        foreach (self::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 SEB elements if required.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    protected static function lock_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
+        if (self::is_seb_settings_locked($quizform->get_instance()) || self::is_conflicting_permissions($quizform->get_context())) {
+            // Freeze common quiz settings.
+            self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser');
+            self::freeze_element($quizform, $mform, 'seb_templateid');
+            self::freeze_element($quizform, $mform, 'seb_showsebdownloadlink');
+            self::freeze_element($quizform, $mform, 'seb_allowedbrowserexamkeys');
+
+            $quizsettings = quiz_settings::get_by_quiz_id((int) $quizform->get_instance());
+
+            // If the file has been uploaded, then replace it with the link to download the file.
+            if (!empty($quizsettings) && $quizsettings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) {
+                self::remove_element($quizform, $mform, 'filemanager_sebconfigfile');
+                if ($link = self::get_uploaded_seb_file_download_link($quizform, $mform)) {
+                    $element = $mform->createElement(
+                        'static',
+                        'filemanager_sebconfigfile',
+                        get_string('filemanager_sebconfigfile', 'quizaccess_seb'),
+                        $link
+                    );
+                    self::insert_element($quizform, $mform, $element, 'seb_showsebdownloadlink');
+                }
+            }
+
+            // Remove template ID if not using template for this quiz.
+            if (empty($quizsettings) || $quizsettings->get('requiresafeexambrowser') != self::USE_SEB_TEMPLATE) {
+                $mform->removeElement('seb_templateid');
+            }
+
+            // Freeze all SEB specific settings.
+            foreach (self::get_seb_config_elements() as $element => $type) {
+                self::freeze_element($quizform, $mform, $element);
+            }
+        }
+    }
+
+    /**
+     * Return uploaded SEB config file link.
+     *
+     * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     * @return string
+     */
+    protected static function get_uploaded_seb_file_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) : string {
+        $link = '';
+        $file = self::get_module_context_sebconfig_file($quizform->get_coursemodule()->id);
+
+        if ($file) {
+            $url = \moodle_url::make_pluginfile_url(
+                $file->get_contextid(),
+                $file->get_component(),
+                $file->get_filearea(),
+                $file->get_itemid(),
+                $file->get_filepath(),
+                $file->get_filename(),
+                true
+            );
+            $link = \html_writer::link($url, get_string('downloadsebconfig', 'quizaccess_seb'));
+        }
+
+        return $link;
+    }
+
+    /**
+     * Get the type of element for each of the form elements in quiz settings.
+     *
+     * Contains all setting elements. Array key is name of 'form element'/'database column (excluding prefix)'.
+     *
+     * @return array All quiz form elements to be added and their types.
+     */
+    public static function get_seb_config_elements() : array {
+        return [
+            'seb_linkquitseb' => 'text',
+            'seb_userconfirmquit' => 'selectyesno',
+            'seb_allowuserquitseb' => 'selectyesno',
+            'seb_quitpassword' => 'passwordunmask',
+            'seb_allowreloadinexam' => 'selectyesno',
+            'seb_showsebtaskbar' => 'selectyesno',
+            'seb_showreloadbutton' => 'selectyesno',
+            'seb_showtime' => 'selectyesno',
+            'seb_showkeyboardlayout' => 'selectyesno',
+            'seb_showwificontrol' => 'selectyesno',
+            'seb_enableaudiocontrol' => 'selectyesno',
+            'seb_muteonstartup' => 'selectyesno',
+            'seb_allowspellchecking' => 'selectyesno',
+            'seb_activateurlfiltering' => 'selectyesno',
+            'seb_filterembeddedcontent' => 'selectyesno',
+            'seb_expressionsallowed' => 'textarea',
+            'seb_regexallowed' => 'textarea',
+            'seb_expressionsblocked' => 'textarea',
+            'seb_regexblocked' => 'textarea',
+        ];
+    }
+
+
+    /**
+     * Get the types of the quiz settings elements.
+     * @return array List of types for the setting elements.
+     */
+    public static function get_seb_config_element_types() : array {
+        return [
+            'seb_linkquitseb' => PARAM_RAW,
+            'seb_userconfirmquit' => PARAM_BOOL,
+            'seb_allowuserquitseb' => PARAM_BOOL,
+            'seb_quitpassword' => PARAM_RAW,
+            'seb_allowreloadinexam' => PARAM_BOOL,
+            'seb_showsebtaskbar' => PARAM_BOOL,
+            'seb_showreloadbutton' => PARAM_BOOL,
+            'seb_showtime' => PARAM_BOOL,
+            'seb_showkeyboardlayout' => PARAM_BOOL,
+            'seb_showwificontrol' => PARAM_BOOL,
+            'seb_enableaudiocontrol' => PARAM_BOOL,
+            'seb_muteonstartup' => PARAM_BOOL,
+            'seb_allowspellchecking' => PARAM_BOOL,
+            'seb_activateurlfiltering' => PARAM_BOOL,
+            'seb_filterembeddedcontent' => PARAM_BOOL,
+            'seb_expressionsallowed' => PARAM_RAW,
+            'seb_regexallowed' => PARAM_RAW,
+            'seb_expressionsblocked' => PARAM_RAW,
+            'seb_regexblocked' => PARAM_RAW,
+        ];
+    }
+
+    /**
+     * Check that we have conflicting permissions.
+     *
+     * In Some point we can have settings save by the person who use specific
+     * type of SEB usage (e.g. use templates). But then another person who can't
+     * use template (but still can update other settings) edit the same quiz. This is
+     * conflict of permissions and we'd like to build the settings form having this in
+     * mind.
+     *
+     * @param \context $context Context used with capability checking.
+     *
+     * @return bool
+     */
+    public static function is_conflicting_permissions(\context $context) {
+        if ($context instanceof \context_course) {
+            return false;
+        }
+
+        $settings = quiz_settings::get_record(['cmid' => (int) $context->instanceid]);
+
+        if (empty($settings)) {
+            return false;
+        }
+
+        if (!self::can_use_seb_template($context) &&
+            $settings->get('requiresafeexambrowser') == self::USE_SEB_TEMPLATE) {
+            return true;
+        }
+
+        if (!self::can_upload_seb_file($context) &&
+            $settings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) {
+            return true;
+        }
+
+        if (!self::can_configure_manually($context) &&
+            $settings->get('requiresafeexambrowser') == self::USE_SEB_CONFIG_MANUALLY) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a list of all options of SEB usage.
+     *
+     * @param \context $context Context used with capability checking selection options.
+     * @return array
+     */
+    public static function get_requiresafeexambrowser_options(\context $context) : array {
+        $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');
+        }
+
+        if (self::can_use_seb_template($context) || self::is_conflicting_permissions($context)) {
+            if (!empty(self::get_template_options())) {
+                $options[self::USE_SEB_TEMPLATE] = get_string('seb_use_template', 'quizaccess_seb');
+            }
+        }
+
+        if (self::can_upload_seb_file($context) || self::is_conflicting_permissions($context)) {
+            $options[self::USE_SEB_UPLOAD_CONFIG] = get_string('seb_use_upload', 'quizaccess_seb');
+        }
+
+        $options[self::USE_SEB_CLIENT_CONFIG] = get_string('seb_use_client', 'quizaccess_seb');
+
+        return $options;
+    }
+
+    /**
+     * Returns a list of templates.
+     * @return array
+     */
+    protected static function get_template_options() : array {
+        $templates = [];
+        $records = template::get_records(['enabled' => 1], 'name');
+        if ($records) {
+            foreach ($records as $record) {
+                $templates[$record->get('id')] = $record->get('name');
+            }
+        }
+
+        return $templates;
+    }
+
+    /**
+     * Returns a list of options for the file manager element.
+     * @return array
+     */
+    public static function get_filemanager_options() : array {
+        return [
+            'subdirs' => 0,
+            'maxfiles' => 1,
+            'accepted_types' => ['.seb']
+        ];
+    }
+
+    /**
+     * Get the default values of the quiz settings.
+     *
+     * Array key is name of 'form element'/'database column (excluding prefix)'.
+     *
+     * @return array List of settings and their defaults.
+     */
+    public static function get_seb_config_element_defaults() : array {
+        return [
+            'seb_linkquitseb' => '',
+            'seb_userconfirmquit' => 1,
+            'seb_allowuserquitseb' => 1,
+            'seb_quitpassword' => '',
+            'seb_allowreloadinexam' => 1,
+            'seb_showsebtaskbar' => 1,
+            'seb_showreloadbutton' => 1,
+            'seb_showtime' => 1,
+            'seb_showkeyboardlayout' => 1,
+            'seb_showwificontrol' => 0,
+            'seb_enableaudiocontrol' => 0,
+            'seb_muteonstartup' => 0,
+            'seb_allowspellchecking' => 0,
+            'seb_activateurlfiltering' => 0,
+            'seb_filterembeddedcontent' => 0,
+            'seb_expressionsallowed' => '',
+            'seb_regexallowed' => '',
+            'seb_expressionsblocked' => '',
+            'seb_regexblocked' => '',
+        ];
+    }
+
+    /**
+     * Validate that if a file has been uploaded by current user, that it is a valid PLIST XML file.
+     * This function is only called if requiresafeexambrowser == settings_provider::USE_SEB_UPLOAD_CONFIG.
+     *
+     * @param string $itemid Item ID of file in user draft file area.
+     * @return void|lang_string
+     */
+    public static function validate_draftarea_configfile($itemid) {
+        // When saving the settings, this value will be null.
+        if (is_null($itemid)) {
+            return;
+        }
+        // If there is a config file uploaded, make sure it is a PList XML file.
+        $file = self::get_current_user_draft_file($itemid);
+
+        // If we require an SEB config uploaded, and the file exists, parse it.
+        if ($file) {
+            if (!helper::is_valid_seb_config($file->get_content())) {
+                return new lang_string('fileparsefailed', 'quizaccess_seb');
+            }
+        }
+
+        // If we require an SEB config uploaded, and the file does not exist, error.
+        if (!$file) {
+            return new lang_string('filenotpresent', 'quizaccess_seb');
+        }
+    }
+
+    /**
+     * Try and get a file in the user draft filearea by itemid.
+     *
+     * @param string $itemid Item ID of the file.
+     * @return stored_file|null Returns null if no file is found.
+     */
+    public static function get_current_user_draft_file(string $itemid) : ?stored_file {
+        global $USER;
+        $context = context_user::instance($USER->id);
+        $fs = get_file_storage();
+        if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $itemid, 'id DESC', false)) {
+            return null;
+        }
+        return reset($files);
+    }
+
+    /**
+     * Get the file that is stored in the course module file area.
+     *
+     * @param string $cmid The course module id which is used as an itemid reference.
+     * @return stored_file|null Returns null if no file is found.
+     */
+    public static function get_module_context_sebconfig_file(string $cmid) : ?stored_file {
+        $fs = new \file_storage();
+        $context = context_module::instance($cmid);
+
+        if (!$files = $fs->get_area_files($context->id, 'quizaccess_seb', 'filemanager_sebconfigfile', 0,
+            'id DESC', false)) {
+            return null;
+        }
+
+        return reset($files);
+    }
+
+    /**
+     * Saves filemanager_sebconfigfile files to the moodle storage backend.
+     *
+     * @param string $draftitemid The id of the draft area to use.
+     * @param string $cmid The cmid of for the quiz.
+     * @return bool Always true
+     */
+    public static function save_filemanager_sebconfigfile_draftarea(string $draftitemid, string $cmid) : bool {
+        if ($draftitemid) {
+            $context = context_module::instance($cmid);
+            file_save_draft_area_files($draftitemid, $context->id, 'quizaccess_seb', 'filemanager_sebconfigfile',
+                0, []);
+        }
+
+        return true;
+    }
+
+    /**
+     * Cleanup function to delete the saved config when it has not been specified.
+     * This will be called when settings_provider::USE_SEB_UPLOAD_CONFIG is not true.
+     *
+     * @param string $cmid The cmid of for the quiz.
+     * @return bool Always true or exception if error occurred
+     */
+    public static function delete_uploaded_config_file(string $cmid) : bool {
+        $file = self::get_module_context_sebconfig_file($cmid);
+
+        if (!empty($file)) {
+            return $file->delete();
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if the current user can configure SEB.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_configure_seb(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', $context);
+    }
+
+    /**
+     * Check if the current user can use preconfigured templates.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_use_seb_template(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_templateid', $context);
+    }
+
+    /**
+     * Check if the current user can upload own SEB config file.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_upload_seb_file(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context);
+    }
+
+    /**
+     * Check if the current user can change Show Safe Exam Browser download button setting.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_change_seb_showsebdownloadlink(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_showsebdownloadlink', $context);
+    }
+
+    /**
+     * Check if the current user can change Allowed Browser Exam Keys setting.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_change_seb_allowedbrowserexamkeys(\context $context) : bool {
+        return has_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', $context);
+    }
+
+    /**
+     * Check if the current user can config SEB manually.
+     *
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_configure_manually(\context $context) : bool {
+        foreach (self::get_seb_config_elements() as $name => $type) {
+            if (self::can_manage_seb_config_setting($name, $context)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if the current user can manage provided SEB setting.
+     *
+     * @param string $settingname Name of the setting.
+     * @param \context $context Context to check access in.
+     * @return bool
+     */
+    public static function can_manage_seb_config_setting(string $settingname, \context $context) : bool {
+        $capsttocheck = [];
+
+        foreach (self::get_seb_settings_map() as $type => $settings) {
+            $capsttocheck = self::build_config_capabilities_to_check($settingname, $settings);
+            if (!empty($capsttocheck)) {
+                break;
+            }
+        }
+
+        foreach ($capsttocheck as $capability) {
+            // Capability must exist.
+            if (!$capinfo = get_capability_info($capability)) {
+                throw new \coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
+            }
+        }
+
+        return has_all_capabilities($capsttocheck, $context);
+    }
+
+    /**
+     * Helper method to build a list of capabilities to check.
+     *
+     * @param string $settingname Given setting name to build caps for.
+     * @param array $settings A list of settings to go through.
+     * @return array
+     */
+    protected static function build_config_capabilities_to_check(string $settingname, array $settings) : array {
+        $capsttocheck = [];
+
+        foreach ($settings as $setting => $children) {
+            if ($setting == $settingname) {
+                $capsttocheck[$setting] = self::build_setting_capability_name($setting);
+                break; // Found what we need exit the loop.
+            }
+
+            // Recursively check all children.
+            $capsttocheck = self::build_config_capabilities_to_check($settingname, $children);
+            if (!empty($capsttocheck)) {
+                // Matching child found, add the parent capability to the list of caps to check.
+                $capsttocheck[$setting] = self::build_setting_capability_name($setting);
+                break; // Found what we need exit the loop.
+            }
+        }
+
+        return $capsttocheck;
+    }
+
+    /**
+     * Helper method to return a map of all settings.
+     *
+     * @return array
+     */
+    public static function get_seb_settings_map() : array {
+        return [
+            self::USE_SEB_NO => [
+
+            ],
+            self::USE_SEB_CONFIG_MANUALLY => [
+                'seb_showsebdownloadlink' => [],
+                'seb_linkquitseb' => [],
+                'seb_userconfirmquit' => [],
+                'seb_allowuserquitseb' => [
+                    'seb_quitpassword' => []
+                ],
+                'seb_allowreloadinexam' => [],
+                'seb_showsebtaskbar' => [
+                    'seb_showreloadbutton' => [],
+                    'seb_showtime' => [],
+                    'seb_showkeyboardlayout' => [],
+                    'seb_showwificontrol' => [],
+                ],
+                'seb_enableaudiocontrol' => [
+                    'seb_muteonstartup' => [],
+                ],
+                'seb_allowspellchecking' => [],
+                'seb_activateurlfiltering' => [
+                    'seb_filterembeddedcontent' => [],
+                    'seb_expressionsallowed' => [],
+                    'seb_regexallowed' => [],
+                    'seb_expressionsblocked' => [],
+                    'seb_regexblocked' => [],
+                ],
+            ],
+            self::USE_SEB_TEMPLATE => [
+                'seb_templateid' => [],
+                'seb_showsebdownloadlink' => [],
+                'seb_allowuserquitseb' => [
+                    'seb_quitpassword' => [],
+                ],
+            ],
+            self::USE_SEB_UPLOAD_CONFIG => [
+                'filemanager_sebconfigfile' => [],
+                'seb_showsebdownloadlink' => [],
+                'seb_allowuserquitseb' => [
+                    'seb_quitpassword' => [],
+                ],
+                'seb_allowedbrowserexamkeys' => [],
+            ],
+            self::USE_SEB_CLIENT_CONFIG => [
+                'seb_showsebdownloadlink' => [],
+                'seb_allowedbrowserexamkeys' => [],
+            ],
+        ];
+    }
+
+    /**
+     * Get allowed settings for provided SEB usage type.
+     *
+     * @param int $requiresafeexambrowser SEB usage type.
+     * @return array
+     */
+    private static function get_allowed_settings(int $requiresafeexambrowser) : array {
+        $result = [];
+        $map = self::get_seb_settings_map();
+
+        if (!key_exists($requiresafeexambrowser, $map)) {
+            return $result;
+        }
+
+        return self::build_allowed_settings($map[$requiresafeexambrowser]);
+    }
+
+    /**
+     * Recursive method to build a list of allowed settings.
+     *
+     * @param array $settings A list of settings from settings map.
+     * @return array
+     */
+    private static function build_allowed_settings(array $settings) : array {
+        $result = [];
+
+        foreach ($settings as $name => $children) {
+            $result[] = $name;
+            foreach ($children as $childname => $child) {
+                $result[] = $childname;
+                $result = array_merge($result, self::build_allowed_settings($child));
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Get the conditions that an element should be hid in the form. Expects matching using 'eq'.
+     *
+     * Array key is name of 'form element'/'database column (excluding prefix)'.
+     * Values are instances of hideif_rule class.
+     *
+     * @return array List of rules per element.
+     */
+    public static function get_quiz_hideifs() : array {
+        $hideifs = [];
+
+        // We are building rules based on the settings map, that means children will be dependant on parent.
+        // In most cases it's all pretty standard.
+        // However it could be some specific cases for some fields, which will be overridden later.
+        foreach (self::get_seb_settings_map() as $type => $settings) {
+            foreach ($settings as $setting => $children) {
+                $hideifs[$setting][] = new hideif_rule($setting, 'seb_requiresafeexambrowser', 'noteq', $type);
+
+                foreach ($children as $childname => $child) {
+                    $hideifs[$childname][] = new hideif_rule($childname, 'seb_requiresafeexambrowser', 'noteq', $type);
+                    $hideifs[$childname][] = new hideif_rule($childname, $setting, 'eq', 0);
+                }
+            }
+        }
+
+        // Specific case for "Enable quitting of SEB". It should available for Manual, Template and Uploaded config.
+        $hideifs['seb_allowuserquitseb'] = [
+            new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
+            new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG),
+        ];
+
+        // Specific case for "Quit password". It should be available for Manual, Template and Uploaded config. As it's parent.
+        $hideifs['seb_quitpassword'] = [
+            new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
+            new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG),
+            new hideif_rule('seb_quitpassword', 'seb_allowuserquitseb', 'eq', 0),
+        ];
+
+        // Specific case for "Show Safe Exam Browser download button". It should be available for all cases, except No Seb.
+        $hideifs['seb_showsebdownloadlink'] = [
+            new hideif_rule('seb_showsebdownloadlink', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO)
+        ];
+
+        // Specific case for "Allowed Browser Exam Keys". It should be available for Template and Browser config.
+        $hideifs['seb_allowedbrowserexamkeys'] = [
+            new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
+            new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CONFIG_MANUALLY),
+            new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_TEMPLATE),
+        ];
+
+        return $hideifs;
+    }
+
+    /**
+     * Build a capability name for the provided SEB setting.
+     *
+     * @param string $settingname Name of the setting.
+     * @return string
+     */
+    public static function build_setting_capability_name(string $settingname) : string {
+        if (!key_exists($settingname, self::get_seb_config_elements())) {
+            throw new \coding_exception('Incorrect SEB quiz setting ' . $settingname);
+        }
+
+        return 'quizaccess/seb:manage_' . $settingname;
+    }
+
+    /**
+     * Check if settings is locked.
+     *
+     * @param int $quizid Quiz ID.
+     * @return bool
+     */
+    public static function is_seb_settings_locked($quizid) : bool {
+        if (empty($quizid)) {
+            return false;
+        }
+
+        return quiz_has_attempts($quizid);
+    }
+
+    /**
+     * Filter a standard class by prefix.
+     *
+     * @param stdClass $settings Quiz settings object.
+     * @return stdClass Filtered object.
+     */
+    private static function filter_by_prefix(\stdClass $settings): stdClass {
+        $newsettings = new \stdClass();
+        foreach ($settings as $name => $setting) {
+            // Only add it, if not there.
+            if (strpos($name, "seb_") === 0) {
+                $newsettings->$name = $setting; // Add new key.
+            }
+        }
+        return $newsettings;
+    }
+
+    /**
+     * Filter settings based on the setting map. Set value of not allowed settings to null.
+     *
+     * @param stdClass $settings Quiz settings.
+     * @return \stdClass
+     */
+    private static function filter_by_settings_map(stdClass $settings) : stdClass {
+        if (!isset($settings->seb_requiresafeexambrowser)) {
+            return $settings;
+        }
+
+        $newsettings = new \stdClass();
+        $newsettings->seb_requiresafeexambrowser = $settings->seb_requiresafeexambrowser;
+        $allowedsettings = self::get_allowed_settings((int)$newsettings->seb_requiresafeexambrowser);
+        unset($settings->seb_requiresafeexambrowser);
+
+        foreach ($settings as $name => $value) {
+            if (!in_array($name, $allowedsettings)) {
+                $newsettings->$name = null;
+            } else {
+                $newsettings->$name = $value;
+            }
+        }
+
+        return $newsettings;
+    }
+
+    /**
+     * Filter quiz settings for this plugin only.
+     *
+     * @param stdClass $settings Quiz settings.
+     * @return stdClass Filtered settings.
+     */
+    public static function filter_plugin_settings(stdClass $settings) : stdClass {
+        $settings = self::filter_by_prefix($settings);
+        $settings = self::filter_by_settings_map($settings);
+
+        return self::strip_all_prefixes($settings);
+    }
+
+    /**
+     * Strip the seb_ prefix from each setting key.
+     *
+     * @param \stdClass $settings Object containing settings.
+     * @return \stdClass The modified settings object.
+     */
+    private static function strip_all_prefixes(\stdClass $settings): stdClass {
+        $newsettings = new \stdClass();
+        foreach ($settings as $name => $setting) {
+            $newname = preg_replace("/^seb_/", "", $name);
+            $newsettings->$newname = $setting; // Add new key.
+        }
+        return $newsettings;
+    }
+
+    /**
+     * Add prefix to string.
+     *
+     * @param string $name String to add prefix to.
+     * @return string String with prefix.
+     */
+    public static function add_prefix(string $name): string {
+        if (strpos($name, 'seb_') !== 0) {
+            $name = 'seb_' . $name;
+        }
+        return $name;
+    }
+}
diff --git a/mod/quiz/accessrule/seb/classes/template.php b/mod/quiz/accessrule/seb/classes/template.php
new file mode 100644 (file)
index 0000000..708bf2a
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Entity model representing template settings for the seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Nicholas Hoobin <nicholashoobin@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use core\persistent;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Entity model representing template settings for the seb plugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template extends persistent {
+
+    /** Table name for the persistent. */
+    const TABLE = 'quizaccess_seb_template';
+
+    /** @var property_list $plist The SEB config represented as a Property List object. */
+    private $plist;
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'name' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+            ],
+            'description' => [
+                'type' => PARAM_TEXT,
+                'default' => '',
+            ],
+            'content' => [
+                'type' => PARAM_RAW,
+            ],
+            'enabled' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+            'sortorder' => [
+                'type' => PARAM_INT,
+                'default' => 0,
+            ],
+        ];
+    }
+
+    /**
+     * Hook to execute before an update.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_update() {
+        $this->before_save();
+    }
+
+    /**
+     * Hook to execute before a create.
+     *
+     * Please note that at this stage the data has already been validated and therefore
+     * any new data being set will not be validated before it is sent to the database.
+     */
+    protected function before_create() {
+        $this->before_save();
+    }
+
+    /**
+     * As there is no hook for before both create and update, this function is called by both hooks.
+     */
+    private function before_save() {
+        $this->plist = new property_list($this->get('content'));
+        $this->set('content', $this->plist->to_xml());
+    }
+
+    /**
+     * Validate template content.
+     *
+     * @param string $content Content string to validate.
+     *
+     * @return bool|\lang_string
+     */
+    protected function validate_content(string $content) {
+        if (helper::is_valid_seb_config($content)) {
+            return true;
+        } else {
+            return new \lang_string('invalidtemplate', 'quizaccess_seb');
+        }
+    }
+
+    /**
+     * Check if we can delete the template.
+     *
+     * @return bool
+     */
+    public function can_delete() : bool {
+        $result = true;
+
+        if ($this->get('id')) {
+            $settings = quiz_settings::get_records(['templateid' => $this->get('id')]);
+            $result = empty($settings);
+        }
+
+        return $result;
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/classes/template_controller.php b/mod/quiz/accessrule/seb/classes/template_controller.php
new file mode 100644 (file)
index 0000000..7613a5b
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for manipulating with the template records.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb;
+
+use core\notification;
+use quizaccess_seb\local\table\template_list;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for manipulating with the template records.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_controller {
+    /**
+     * View action.
+     */
+    const ACTION_VIEW = 'view';
+
+    /**
+     * Add action.
+     */
+    const ACTION_ADD = 'add';
+
+    /**
+     * Edit action.
+     */
+    const ACTION_EDIT = 'edit';
+
+    /**
+     * Delete action.
+     */
+    const ACTION_DELETE = 'delete';
+
+    /**
+     * Hide action.
+     */
+    const ACTION_HIDE = 'hide';
+
+    /**
+     * Show action.
+     */
+    const ACTION_SHOW = 'show';
+
+
+    /**
+     * Locally cached $OUTPUT object.
+     * @var \bootstrap_renderer
+     */
+    protected $output;
+
+    /**
+     * region_manager constructor.
+     */
+    public function __construct() {
+        global $OUTPUT;
+
+        $this->output = $OUTPUT;
+    }
+
+    /**
+     * Execute required action.
+     *
+     * @param string $action Action to execute.
+     */
+    public function execute($action) {
+
+        $this->set_external_page();
+
+        switch($action) {
+            case self::ACTION_ADD:
+            case self::ACTION_EDIT:
+                $this->edit($action, optional_param('id', null, PARAM_INT));
+                break;
+
+            case self::ACTION_DELETE:
+                $this->delete(required_param('id', PARAM_INT));
+                break;
+
+            case self::ACTION_HIDE:
+                $this->hide(required_param('id', PARAM_INT));
+                break;
+
+            case self::ACTION_SHOW:
+                $this->show(required_param('id', PARAM_INT));
+                break;
+
+            case self::ACTION_VIEW:
+            default:
+                $this->view();
+                break;
+        }
+    }
+
+    /**
+     * Set external page for the manager.
+     */
+    protected function set_external_page() {
+        admin_externalpage_setup('quizaccess_seb/template');
+    }
+
+    /**
+     * Return record instance.
+     *
+     * @param int $id
+     * @param \stdClass|null $data
+     *
+     * @return \quizaccess_seb\template
+     */
+    protected function get_instance($id = 0, \stdClass $data = null) {
+        return new template($id, $data);
+    }
+
+    /**
+     * Print out all records in a table.
+     */
+    protected function display_all_records() {
+        $records = template::get_records([], 'id');
+
+        $table = new template_list();
+        $table->display($records);
+    }
+
+    /**
+     * Returns a text for create new record button.
+     * @return string
+     */
+    protected function get_create_button_text() : string {
+        return get_string('addtemplate', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns form for the record.
+     *
+     * @param \quizaccess_seb\template|null $instance
+     *
+     * @return \quizaccess_seb\local\form\template
+     */
+    protected function get_form($instance) : \quizaccess_seb\local\form\template {
+        global $PAGE;
+
+        return new \quizaccess_seb\local\form\template($PAGE->url->out(false), ['persistent' => $instance]);
+    }
+
+    /**
+     * View page heading string.
+     * @return string
+     */
+    protected function get_view_heading() : string {
+        return get_string('managetemplates', 'quizaccess_seb');
+    }
+
+    /**
+     * New record heading string.
+     * @return string
+     */
+    protected function get_new_heading() : string {
+        return get_string('newtemplate', 'quizaccess_seb');
+    }
+
+    /**
+     * Edit record heading string.
+     * @return string
+     */
+    protected function get_edit_heading() : string {
+        return get_string('edittemplate', 'quizaccess_seb');
+    }
+
+    /**
+     * Returns base URL for the manager.
+     * @return string
+     */
+    public static function get_base_url() : string {
+        return '/mod/quiz/accessrule/seb/template.php';
+    }
+
+    /**
+     * Execute edit action.
+     *
+     * @param string $action Could be edit or create.
+     * @param null|int $id Id of the region or null if creating a new one.
+     */
+    protected function edit($action, $id = null) {
+        global $PAGE;
+
+        $PAGE->set_url(new \moodle_url(static::get_base_url(), ['action' => $action, 'id' => $id]));
+        $instance = null;
+
+        if ($id) {
+            $instance = $this->get_instance($id);
+        }
+
+        $form = $this->get_form($instance);
+
+        if ($form->is_cancelled()) {
+            redirect(new \moodle_url(static::get_base_url()));
+        } else if ($data = $form->get_data()) {
+            unset($data->submitbutton);
+            try {
+                if (empty($data->id)) {
+                    $data->content = $form->get_file_content('content');
+                    $persistent = $this->get_instance(0, $data);
+                    $persistent->create();
+
+                    \quizaccess_seb\event\template_created::create_strict(
+                        $persistent,
+                        \context_system::instance()
+                    )->trigger();
+                    $this->trigger_enabled_event($persistent);
+                } else {
+                    $instance->from_record($data);
+                    $instance->update();
+
+                    \quizaccess_seb\event\template_updated::create_strict(
+                        $instance,
+                        \context_system::instance()
+                    )->trigger();
+                    $this->trigger_enabled_event($instance);
+                }
+                notification::success(get_string('changessaved'));
+            } catch (\Exception $e) {
+                notification::error($e->getMessage());
+            }
+            redirect(new \moodle_url(static::get_base_url()));
+        } else {
+            if (empty($instance)) {
+                $this->header($this->get_new_heading());
+            } else {
+                if (!$instance->can_delete()) {
+                    notification::warning(get_string('cantedit', 'quizaccess_seb'));
+                }
+                $this->header($this->get_edit_heading());
+            }
+        }
+
+        $form->display();
+        $this->footer();
+    }
+
+    /**
+     * Execute delete action.
+     *
+     * @param int $id ID of the region.
+     */
+    protected function delete($id) {
+        require_sesskey();
+        $instance = $this->get_instance($id);
+
+        if ($instance->can_delete()) {
+            $instance->delete();
+            notification::success(get_string('deleted'));
+
+            \quizaccess_seb\event\template_deleted::create_strict(
+                $id,
+                \context_system::instance()
+            )->trigger();
+
+            redirect(new \moodle_url(static::get_base_url()));
+        } else {
+            notification::warning(get_string('cantdelete', 'quizaccess_seb'));
+            redirect(new \moodle_url(static::get_base_url()));
+        }
+    }
+
+    /**
+     * Execute view action.
+     */
+    protected function view() {
+        global $PAGE;
+
+        $this->header($this->get_view_heading());
+        $this->print_add_button();
+        $this->display_all_records();
+
+        // JS for Template management.
+        $PAGE->requires->js_call_amd('quizaccess_seb/managetemplates', 'setup');
+
+        $this->footer();
+    }
+
+    /**
+     * Show the template.
+     *
+     * @param int $id The ID of the template to show.
+     */
+    protected function show(int $id) {
+        $this->show_hide($id, 1);
+    }
+
+    /**
+     * Hide the template.
+     *
+     * @param int $id The ID of the template to hide.
+     */
+    protected function hide($id) {
+        $this->show_hide($id, 0);
+    }
+
+    /**
+     * Show or Hide the template.
+     *
+     * @param int $id The ID of the template to hide.
+     * @param int $visibility The intended visibility.
+     */
+    protected function show_hide(int $id, int $visibility) {
+        require_sesskey();
+        $template = $this->get_instance($id);
+        $template->set('enabled', $visibility);
+        $template->save();
+
+        $this->trigger_enabled_event($template);
+
+        redirect(new \moodle_url(self::get_base_url()));
+    }
+
+    /**
+     * Print out add button.
+     */
+    protected function print_add_button() {
+        echo $this->output->single_button(
+            new \moodle_url(static::get_base_url(), ['action' => self::ACTION_ADD]),
+            $this->get_create_button_text()
+        );
+    }
+
+    /**
+     * Print out page header.
+     * @param string $title Title to display.
+     */
+    protected function header($title) {
+        echo $this->output->header();
+        echo $this->output->heading($title);
+    }
+
+    /**
+     * Print out the page footer.
+     *
+     * @return void
+     */
+    protected function footer() {
+        echo $this->output->footer();
+    }
+
+    /**
+     * Helper function to fire off an event that informs of if a template is enabled or not.
+     *
+     * @param template $template The template persistent object.
+     */
+    private function trigger_enabled_event(template $template) {
+        $eventstring = ($template->get('enabled') == 0 ? 'disabled' : 'enabled');
+
+        $func = '\quizaccess_seb\event\template_' . $eventstring;
+        $func::create_strict(
+            $template,
+            \context_system::instance()
+        )->trigger();
+    }
+
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/config.php b/mod/quiz/accessrule/seb/config.php
new file mode 100644 (file)
index 0000000..522d57b
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Serves an encrypted/unencrypted string as a file for download.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../config.php');
+
+$cmid = required_param('cmid', PARAM_RAW);
+
+$config = \quizaccess_seb\helper::get_seb_config_content($cmid);
+\quizaccess_seb\helper::send_seb_config_file($config);
diff --git a/mod/quiz/accessrule/seb/db/access.php b/mod/quiz/accessrule/seb/db/access.php
new file mode 100644 (file)
index 0000000..c857fa9
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Define capabilities for plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+    'quizaccess/seb:managetemplates' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+        )
+    ),
+    'quizaccess/seb:bypassseb' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_requiresafeexambrowser' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_templateid' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_filemanager_sebconfigfile' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showsebdownloadlink' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowedbrowserexamkeys' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_linkquitseb' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_userconfirmquit' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowuserquitseb' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_quitpassword' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowreloadinexam' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showsebtaskbar' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showreloadbutton' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showtime' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_showkeyboardlayout' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+
+    'quizaccess/seb:manage_seb_showwificontrol' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_enableaudiocontrol' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_muteonstartup' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_allowspellchecking' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_activateurlfiltering' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_filterembeddedcontent' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_expressionsallowed' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_regexallowed' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_expressionsblocked' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+    'quizaccess/seb:manage_seb_regexblocked' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
+);
diff --git a/mod/quiz/accessrule/seb/db/caches.php b/mod/quiz/accessrule/seb/db/caches.php
new file mode 100644 (file)
index 0000000..4a93bc9
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin cache definitions.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @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/db/install.php b/mod/quiz/accessrule/seb/db/install.php
new file mode 100644 (file)
index 0000000..466eb74
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Install script for plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot  . '/mod/quiz/accessrule/seb/lib.php');
+
+/**
+ * Custom code to be run on installing the plugin.
+ */
+function xmldb_quizaccess_seb_install() {
+    global $DB;
+
+    // Reconfigure all existing quizzes to use a new quizaccess_seb.
+    $params = ['browsersecurity' => 'safebrowser'];
+
+    $total = $DB->count_records('quiz', $params);
+    $rs = $DB->get_recordset('quiz', $params);
+
+    $i = 0;
+    $pbar = new progress_bar('updatequizrecords', 500, true);
+
+    foreach ($rs as $quiz) {
+        if (!$DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $quiz->id])) {
+            $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
+
+            $sebsettings = new stdClass();
+
+            $sebsettings->quizid = $quiz->id;
+            $sebsettings->cmid = $cm->id;
+            $sebsettings->templateid = 0;
+            $sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG;
+            $sebsettings->showsebtaskbar = null;
+            $sebsettings->showwificontrol = null;
+            $sebsettings->showreloadbutton = null;
+            $sebsettings->showtime = null;
+            $sebsettings->showkeyboardlayout = null;
+            $sebsettings->allowuserquitseb = null;
+            $sebsettings->quitpassword = null;
+            $sebsettings->linkquitseb = null;
+            $sebsettings->userconfirmquit = null;
+            $sebsettings->enableaudiocontrol = null;
+            $sebsettings->muteonstartup = null;
+            $sebsettings->allowspellchecking = null;
+            $sebsettings->allowreloadinexam = null;
+            $sebsettings->activateurlfiltering = null;
+            $sebsettings->filterembeddedcontent = null;
+            $sebsettings->expressionsallowed = null;
+            $sebsettings->regexallowed = null;
+            $sebsettings->expressionsblocked = null;
+            $sebsettings->regexblocked = null;
+            $sebsettings->allowedbrowserexamkeys = null;
+            $sebsettings->showsebdownloadlink = 1;
+            $sebsettings->usermodified = get_admin()->id;
+            $sebsettings->timecreated = time();
+            $sebsettings->timemodified = time();
+
+            $DB->insert_record('quizaccess_seb_quizsettings', $sebsettings);
+
+            $quiz->browsersecurity = '-';
+            $DB->update_record('quiz', $quiz);
+        }
+
+        $i++;
+        $pbar->update($i, $total, "Reconfiguring existing quizzes to use a new SEB plugin - $i/$total.");
+    }
+
+    $rs->close();
+
+    return true;
+}
diff --git a/mod/quiz/accessrule/seb/db/install.xml b/mod/quiz/accessrule/seb/db/install.xml
new file mode 100644 (file)
index 0000000..1969ad3
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="mod/quiz/accessrule/seb/db" VERSION="2019120400" COMMENT="XMLDB file for Moodle mod/quiz/accessrule/seb"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd">
+  <TABLES>
+    <TABLE NAME="quizaccess_seb_quizsettings" COMMENT="Stores the quiz level Safe Exam Browser configuration.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quiz id."/>
+        <FIELD NAME="cmid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to course module id."/>
+        <FIELD NAME="templateid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quizaccess_seb_template.id."/>
+        <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="quizid" TYPE="foreign-unique" FIELDS="quizid" REFTABLE="quiz" REFFIELDS="id"/>
+        <KEY NAME="cmid" TYPE="foreign-unique" FIELDS="cmid" REFTABLE="course_modules" REFFIELDS="id"/>
+        <KEY NAME="templateid" TYPE="foreign" FIELDS="templateid" REFTABLE="quizacces_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"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Name of the template"/>
+        <FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="content" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Content of the template"/>
+        <FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <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="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+  </TABLES>
+</XMLDB>
diff --git a/mod/quiz/accessrule/seb/db/upgrade.php b/mod/quiz/accessrule/seb/db/upgrade.php
new file mode 100644 (file)
index 0000000..8ff2370
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Upgrade script for plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot  . '/mod/quiz/accessrule/seb/lib.php');
+
+/**
+ * Function to upgrade quizaccess_seb plugin.
+ *
+ * @param int $oldversion The version we are upgrading from.
+ * @return bool Result.
+ */
+function xmldb_quizaccess_seb_upgrade($oldversion) {
+    global $DB;
+    $dbman = $DB->get_manager();
+
+    return true;
+}
diff --git a/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php b/mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php
new file mode 100644 (file)
index 0000000..1ec6f72
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Luca Bösch <luca.boesch@bfh.ch>
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$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'] = 'Template can\'t be deleted as it has been used for one or more quizzes';
+$string['cantedit'] = 'Template can\'t be edited as it has been used for one or more quizzes';
+$string['clientrequiresseb'] = 'This quiz has been configured to use the Safe Exam Browser with client configuration.';
+$string['confirmtemplateremovalquestion'] = 'Are you sure you want to remove this template?';
+$string['confirmtemplateremovaltitle'] = 'Confirm template removal?';
+$string['conflictingsettings'] = 'You don\'t have permissions to update existing Safe Exam Browser settings';
+$string['content'] = 'Template';
+$string['description'] = 'Description';
+$string['disabledsettings'] = 'Disabled settings.';
+$string['disabledsettings_help'] = 'Safe Exam Browser quiz settings are unable to be updated if there is one or more existing quiz attempts. To re-enable settings, all quiz attempts must be deleted.';
+$string['downloadsebconfig'] = 'Download SEB config file';
+$string['duplicatetemplate'] = "Template with the same name already exists";
+$string['edittemplate'] = 'Edit template';
+$string['enabled'] = 'Enabled';
+$string['event:accessprevented'] = "Quiz access was prevented";
+$string['event:templatecreated'] = "SEB Template was created";
+$string['event:templatedeleted'] = "SEB Template was deleted";
+$string['event:templatedisabled'] = "SEB Template was disabled";
+$string['event:templateenabled'] = "SEB Template was enabled";
+$string['event:templateupdated'] = "SEB Template was updated";
+$string['exitsebbutton'] = 'Exit Safe Exam Browser';
+$string['filemanager_sebconfigfile'] = 'Upload Safe Exam Browser config file';
+$string['filemanager_sebconfigfile_help'] = 'Please upload your own Safe Exam Browser config file for this quiz.';
+$string['filenotpresent'] = 'Please upload a SEB config file.';
+$string['fileparsefailed'] = 'The uploaded file could not be saved as a SEB config file.';
+$string['httplinkbutton'] = 'Download Configuration';
+$string['invalid_browser_key'] = "Invalid SEB browser key";
+$string['invalid_config_key'] = "Invalid SEB config key";
+$string['invalidkeys'] = "The config key or browser exam keys could not be validated. Please ensure you are using the Safe Exam Browser with correct configuration file.";
+$string['invalidtemplate'] = "Invalid SEB config template";
+$string['manage_templates'] = 'Safe Exam Browser templates';
+$string['managetemplates'] = 'Manage templates';
+$string['missingrequiredsettings'] = 'Config settings not provided all required settings.';
+$string['name'] = 'Name';
+$string['newtemplate'] = 'New template';
+$string['noconfigfilefound'] = 'No uploaded SEB config file could be found for quiz with cmid: {$a}';
+$string['noconfigfound'] = 'No SEB config could be found for quiz with cmid: {$a}';
+$string['not_seb'] = "No SEB browser is being used";
+$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 rule';
+$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.';
+$string['privacy:metadata:quizaccess_seb_quizsettings:timemodified'] = 'Unix time that the settings were last modified.';
+$string['privacy:metadata:quizaccess_seb_quizsettings:usermodified'] = 'ID of user who last created or modified the settings.';
+$string['privacy:metadata:quizaccess_seb_template'] = 'Safe Exam Browser template settings. This includes the ID of the last user to create or modify the template.';
+$string['privacy:metadata:quizaccess_seb_template:timecreated'] = 'Unix time that the template was created.';
+$string['privacy:metadata:quizaccess_seb_template:timemodified'] = 'Unix time that the template was last modified.';
+$string['privacy:metadata:quizaccess_seb_template:usermodified'] = 'ID of user who last created or modified the template.';
+$string['quizsettings'] = 'Quiz settings';
+$string['restoredfrom'] = '{$a->name} (restored via cmid {$a->cmid})';
+$string['seb'] = 'Safe Exam Browser';
+$string['seb:bypassseb'] = 'Bypass the requirement to view quiz in Safe Exam Browser.';
+$string['seb:manage_filemanager_sebconfigfile'] = 'Modify Safe Exam Browser quiz setting: Select SEB config file.';
+$string['seb:manage_seb_activateurlfiltering'] = 'Modify Safe Exam Browser quiz setting: Activate URL filtering.';
+$string['seb:manage_seb_allowedbrowserexamkeys'] = 'Modify Safe Exam Browser quiz setting: Modify Allowed Browser Exam Keys.';
+$string['seb:manage_seb_allowreloadinexam'] = 'Modify Safe Exam Browser quiz setting: Allow reload.';
+$string['seb:manage_seb_allowspellchecking'] = 'Modify Safe Exam Browser quiz setting: Enable spell checking.';
+$string['seb:manage_seb_allowuserquitseb'] = 'Modify Safe Exam Browser quiz setting: Allow quit.';
+$string['seb:manage_seb_enableaudiocontrol'] = 'Modify Safe Exam Browser quiz setting: Enable audio control.';
+$string['seb:manage_seb_expressionsallowed'] = 'Modify Safe Exam Browser quiz setting: Simple expressions allowed.';
+$string['seb:manage_seb_expressionsblocked'] = 'Modify Safe Exam Browser quiz setting: Simple expressions blocked.';
+$string['seb:manage_seb_filterembeddedcontent'] = 'Modify Safe Exam Browser quiz setting: Filter embedded content.';
+$string['seb:manage_seb_linkquitseb'] = 'Modify Safe Exam Browser quiz setting: Quit link.';
+$string['seb:manage_seb_muteonstartup'] = 'Modify Safe Exam Browser quiz setting: Mute on startup.';
+$string['seb:manage_seb_quitpassword'] = 'Modify Safe Exam Browser quiz setting: Quit password.';
+$string['seb:manage_seb_regexallowed'] = 'Modify Safe Exam Browser quiz setting: Regex expressions allowed.';
+$string['seb:manage_seb_regexblocked'] = 'Modify Safe Exam Browser quiz setting: Regex expressions blocked.';
+$string['seb:manage_seb_requiresafeexambrowser'] = 'Modify Safe Exam Browser quiz setting: Require Safe Exam Browser.';
+$string['seb:manage_seb_showkeyboardlayout'] = 'Modify Safe Exam Browser quiz setting: Show keyboard layout.';
+$string['seb:manage_seb_showreloadbutton'] = 'Modify Safe Exam Browser quiz setting: Show reload button.';
+$string['seb:manage_seb_showsebtaskbar'] = 'Modify Safe Exam Browser quiz setting: Show task bar.';
+$string['seb:manage_seb_showtime'] = 'Modify Safe Exam Browser quiz setting: Show time.';
+$string['seb:manage_seb_showwificontrol'] = 'Modify Safe Exam Browser quiz setting: Show Wi-Fi control.';
+$string['seb:manage_seb_showsebdownloadlink'] = 'Modify Safe Exam Browser quiz setting: Show download link.';
+$string['seb:manage_seb_templateid'] = 'Modify Safe Exam Browser quiz setting: Select SEB template.';
+$string['seb:manage_seb_userconfirmquit'] = 'Modify Safe Exam Browser quiz setting: Confirm on quit.';
+$string['seb:managetemplates'] = 'Manage Safe Exam Browser configuration templates.';
+$string['seb_activateurlfiltering'] = 'Enable URL filtering';
+$string['seb_activateurlfiltering_help'] = 'If enabled, URLs will be filtered when loading web pages. The filter set has to be defined below.';
+$string['seb_allowedbrowserexamkeys'] = 'Allowed Browser Exam Keys';
+$string['seb_allowedbrowserexamkeys_help'] = 'In this field you can enter the allowed Browser Exam Keys for versions of Safe Exam Browser that are permitted to access this quiz. If no keys are entered, then Moodle does not check Browser Exam Keys.';
+$string['seb_allowreloadinexam'] = 'Enable reload in exam';
+$string['seb_allowreloadinexam_help'] = 'If enabled, page reload is allowed (reload button in SEB task bar, browser tool bar, iOS side slider menu, keyboard shortcut F5/cmd+R). Offline caching might break when the user tries to reload a page without internet connection.';
+$string['seb_allowspellchecking'] = 'Enable spell checking';
+$string['seb_allowspellchecking_help'] = 'If enabled, spell checking in the SEB browser is allowed.';
+$string['seb_allowuserquitseb'] = 'Enable quitting of SEB';
+$string['seb_allowuserquitseb_help'] = 'If enabled, users can quit SEB with the "Quit" button in the SEB task bar or by pressing the keys Ctrl-Q or by clicking the main browser window close button.';
+$string['seb_enableaudiocontrol'] = 'Enable audio controls';
+$string['seb_enableaudiocontrol_help'] = 'If enabled, the audio control icon is shown in the SEB task bar.';
+$string['seb_expressionsallowed'] = 'Expressions allowed';
+$string['seb_expressionsallowed_help'] = 'A text field which contains the allowed filtering expressions for the allowed URLs. Use of the wildcard char \'\*\' is possible. Examples for expressions: \'example.com\' or \'example.com/stuff/\*\'. \'example.com\' matches \'example.com\', \'www.example.com\' and \'www.mail.example.com\'. \'example.com/stuff/\*\' matches all requests to any subdomain of \'example.com\' that have \'stuff\' as the first segment of the path.';
+$string['seb_expressionsblocked'] = 'Expressions blocked';
+$string['seb_expressionsblocked_help'] = 'A text field which contains the filtering expressions for the blocked URLs. Use of the wildcard char \'\*\' is possible. Examples for expressions: \'example.com\' or \'example.com/stuff/\*\'. \'example.com\' matches \'example.com\', \'www.example.com\' and \'www.mail.example.com\'. \'example.com/stuff/\*\' matches all requests to any subdomain of \'example.com\' that have \'stuff\' as the first segment of the path.';
+$string['seb_filterembeddedcontent'] = 'Filter also embedded content';
+$string['seb_filterembeddedcontent_help'] = 'If enabled, also all embedded resources will be filtered using the filter set.';
+$string['seb_help'] = 'Setup quiz to use the Safe Exam Browser.';
+$string['seb_linkquitseb'] = 'Show Exit Safe Exam Browser button, configured with this quit link';
+$string['seb_linkquitseb_help'] = 'In this field you can enter the link to quit SEB. It will be used on an "Exit Safe Exam Browser" button on the page that appears after the exam is submitted. When clicking the button or the link placed wherever you want to put it, it is possible to quit SEB without having to enter a quit password. If no link is entered, then the "Exit Safe Exam Browser" button does not appear and there is no link set to quit SEB.';
+$string['seb_managetemplates'] = 'Manage Safe Exam Browser templates';
+$string['seb_muteonstartup'] = 'Mute on startup';
+$string['seb_muteonstartup_help'] = 'If enabled, audio is initially muted when starting SEB.';
+$string['seb_quitpassword'] = 'Quit password';
+$string['seb_quitpassword_help'] = 'This password is prompted when users try to quit SEB with the "Quit" button, Ctrl-Q or the close button in the main browser window. If no quit password is set, then SEB just prompts "Are you sure you want to quit SEB?".';
+$string['seb_regexallowed'] = 'Regex allowed';
+$string['seb_regexallowed_help'] = 'A text field which contains the filtering expressions for allowed URLs in a regular expression (Regex) format.';
+$string['seb_regexblocked'] = 'Regex blocked';
+$string['seb_regexblocked_help'] = 'A text field which contains the filtering expressions for blocked URLs in a regular expression (Regex) format.';
+$string['seb_requiresafeexambrowser'] = 'Require the use of Safe Exam Browser';
+$string['seb_requiresafeexambrowser_help'] = "If enabled, students can only attempt the quiz using the Safe Exam Browser.
+The available options are:
+
+* No
+<br/>Safe Exam Browser is not required to attempt the quiz.
+* Yes – Use an existing template
+<br/>A template for the configuration of Safe Exam Browser can be used. Templates are managed by the Moodle Administrator. Your manual settings overwrite the settings in the template.
+* Yes – Configure manually
+<br/>No template for the configuration of Safe Exam Browser will be used. You can configure Safe Exam Browser manually.
+* Yes – Upload my own config
+<br/>You can upload your own Safe Exam Browser configuration file. All manual settings and the use of templates will be disabled.
+* Yes – Use SEB client config
+<br/>No configurations of Safe Exam Browser are on Moodle side. The quiz can be attempted with any configuration of Safe Exam Browser.";
+$string['seb_showkeyboardlayout'] = 'Show keyboard layout';
+$string['seb_showkeyboardlayout_help'] = 'If enabled, the current keyboard layout is shown in the SEB task bar. It allows you to switch to other keyboard layouts, which have been enabled in the operating system.';
+$string['seb_showreloadbutton'] = 'Show reload button';
+$string['seb_showreloadbutton_help'] = 'If enabled, a reload button appears in the SEB task bar. This button allows to reload the current web page.';
+$string['seb_showsebtaskbar'] = 'Show SEB task bar';
+$string['seb_showsebtaskbar_help'] = 'If enabled, a task bar appears at the bottom of the SEB browser window. In case you like to show the Wi-Fi control, the reload button, the time or the keyboard layout to your students, you have to activate the task bar. The task bar is also needed when you permit third party applications, which are displayed as icons in the task bar.';
+$string['seb_showtime'] = 'Show time';
+$string['seb_showtime_help'] = 'If enabled, the current time is displayed in the SEB task bar.';
+$string['seb_showwificontrol'] = 'Show Wi-Fi control';
+$string['seb_showwificontrol_help'] = 'If enabled, a Wi-Fi control button appears in the SEB task bar. The button allows to reconnect to Wi-Fi networks which have previously been connected to.';
+$string['seb_showsebdownloadlink'] = 'Show Safe Exam Browser download button';
+$string['seb_showsebdownloadlink_help'] = 'If enabled, a button for Safe Exam Browser download will be shown on the quiz start page.';
+$string['seb_templateid'] = 'Safe Exam Browser config template';
+$string['seb_templateid_help'] = 'The settings in the selected config template will be used for the configuration of the Safe Exam Browser while attempting the quiz. You may overwrite the settings in the template with your manual settings.';
+$string['seb_use_client'] = 'Yes – Use SEB client config';
+$string['seb_use_manually'] = 'Yes – Configure manually';
+$string['seb_use_template'] = 'Yes – Use an existing template';
+$string['seb_use_upload'] = 'Yes – Upload my own config';
+$string['seb_userconfirmquit'] = 'Ask user to confirm quitting';
+$string['seb_userconfirmquit_help'] = 'If enabled, users have to confirm quitting of SEB when a quit link is detected.';
+$string['sebdownloadbutton'] = 'Download Safe Exam Browser';
+$string['seblinkbutton'] = 'Launch Safe Exam Browser';
+$string['sebrequired'] = "This quiz has been configured so that students may only attempt it using the Safe Exam Browser.";
+$string['setting:autoreconfigureseb'] = 'Auto configure SEB';
+$string['setting:autoreconfigureseb_desc'] = 'If enabled, users who navigate to the quiz using the Safe Exam Browser will be automatically forced to reconfigure their Safe Exam Browser.';
+$string['setting:displayblocksbeforestart'] = 'Display blocks before quiz started';
+$string['setting:displayblocksbeforestart_desc'] = 'If enabled, blocks will be displayed before students start a quiz.';
+$string['setting:displayblockswhenfinished'] = 'Display blocks after quiz finished';
+$string['setting:displayblockswhenfinished_desc'] = 'If enabled, blocks will be displayed after students finished a quiz.';
+$string['setting:downloadlink'] = 'Safe Exam Browser download link';
+$string['setting:downloadlink_desc'] = 'URL for downloading the Safe Exam Browser application.';
+$string['setting:quizpasswordrequired'] = 'Quiz password required';
+$string['setting:quizpasswordrequired_desc'] = 'If enabled, all quizzes that require the Safe Exam Browser must have a quiz password set.';
+$string['setting:showhttplink'] = 'Show http:// link';
+$string['setting:showseblink'] = 'Show seb:// link';
+$string['setting:showseblinks'] = 'Show Safe Exam Browser config links';
+$string['setting:showseblinks_desc'] = 'Decide whether to show links for the user to access the Safe Exam Browser configuration file when access to quiz is prevented. Note that seb:// links may not work for every browser.';
+$string['setting:supportedversions'] = 'Please note that the following minimum versions of the Safe Exam Browser client are required for use of the Config Key feature: macOS -  2.1.5pre2, Windows - 2.4, iOS - 2.1.14';
+$string['settingsfrozen'] = 'Due to there being at least one quiz attempt, the Safe Exam Browser settings can no longer be updated.';
+$string['unknown_reason'] = "Unknown reason";
+$string['used'] = 'In use';
diff --git a/mod/quiz/accessrule/seb/lib.php b/mod/quiz/accessrule/seb/lib.php
new file mode 100644 (file)
index 0000000..50236e4
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Main library of plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Serve the files.
+ *
+ * @param stdClass $course the course object
+ * @param stdClass $cm the course module object
+ * @param \context $context the context
+ * @param string $filearea the name of the file area
+ * @param array $args extra arguments (itemid, path)
+ * @param bool $forcedownload whether or not force download
+ * @param array $options additional options affecting the file serving
+ * @return bool false if the file not found, just send the file otherwise and do not return anything
+ */
+function quizaccess_seb_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
+    if ($context->contextlevel != CONTEXT_MODULE) {
+        return false;
+    }
+
+    if ($filearea !== 'filemanager_sebconfigfile') {
+        return false;
+    }
+
+    require_login($course, true, $cm);
+    if (!has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context)) {
+        return false;
+    }
+
+    $itemid = array_shift($args);
+    $filename = array_pop($args);
+
+    if (!$args) {
+        $filepath = '/';
+    } else {
+        $filepath = '/' .implode('/', $args) . '/';
+    }
+
+    $fs = get_file_storage();
+    $file = $fs->get_file($context->id, 'quizaccess_seb', $filearea, $itemid, $filepath, $filename);
+    if (!$file) {
+        return false;
+    }
+
+    send_stored_file($file, 0, 0, $forcedownload, $options);
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/rule.php b/mod/quiz/accessrule/seb/rule.php
new file mode 100644 (file)
index 0000000..c4c75c2
--- /dev/null
@@ -0,0 +1,590 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Implementation of the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\access_manager;
+use quizaccess_seb\quiz_settings;
+use quizaccess_seb\settings_provider;
+use \quizaccess_seb\event\access_prevented;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/accessrule/accessrulebase.php');
+
+/**
+ * Implementation of the quizaccess_seb plugin.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb extends quiz_access_rule_base {
+
+    /** @var access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */
+    private $accessmanager;
+
+    /**
+     * Create an instance of this rule for a particular quiz.
+     *
+     * @param quiz $quizobj information about the quiz in question.
+     * @param int $timenow the time that should be considered as 'now'.
+     * @param access_manager $accessmanager the quiz accessmanager.
+     */
+    public function __construct(quiz $quizobj, int $timenow, access_manager $accessmanager) {
+        parent::__construct($quizobj, $timenow);
+        $this->accessmanager = $accessmanager;
+    }
+
+    /**
+     * Return an appropriately configured instance of this rule, if it is applicable
+     * to the given quiz, otherwise return null.
+     *
+     * @param quiz $quizobj information about the quiz in question.
+     * @param int $timenow the time that should be considered as 'now'.
+     * @param bool $canignoretimelimits whether the current user is exempt from
+     *      time limits by the mod/quiz:ignoretimelimits capability.
+     * @return quiz_access_rule_base|null the rule, if applicable, else null.
+     */
+    public static function make (quiz $quizobj, $timenow, $canignoretimelimits) {
+        $accessmanager = new access_manager($quizobj);
+        // If Safe Exam Browser is not required, this access rule is not applicable.
+        if (!$accessmanager->seb_required()) {
+            return null;
+        }
+
+        return new self($quizobj, $timenow, $accessmanager);
+    }
+
+    /**
+     * Add any fields that this rule requires to the quiz settings form. This
+     * method is called from {@link mod_quiz_mod_form::definition()}, while the
+     * security section is being built.
+     *
+     * @param mod_quiz_mod_form $quizform the quiz settings form that is being built.
+     * @param MoodleQuickForm $mform the wrapped MoodleQuickForm.
+     */
+    public static function add_settings_form_fields(mod_quiz_mod_form $quizform, MoodleQuickForm $mform) {
+        settings_provider::add_seb_settings_fields($quizform, $mform);
+    }
+
+    /**
+     * Validate the data from any form fields added using {@link add_settings_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 mod_quiz_mod_form $quizform the quiz form object.
+     * @return array $errors the updated $errors array.
+     */
+    public static function validate_settings_form_fields(array $errors,
+                                                         array $data, $files, mod_quiz_mod_form $quizform) : array {
+
+        $quizid = $data['instance'];
+        $cmid = $data['coursemodule'];
+        $context = $quizform->get_context();
+
+        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 quiz_settings())->from_record($settings);
+        // Set non-form fields.
+        $quizsettings->set('quizid', $quizid);
+        $quizsettings->set('cmid', $cmid);
+        $quizsettings->validate();
+
+        // Add any errors to list.
+        foreach ($quizsettings->get_errors() as $name => $error) {
+            $name = settings_provider::add_prefix($name); // Re-add prefix to match form element.
+            $errors[$name] = $error->out();
+        }
+
+        // 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 settings form is submitted. This
+     * is called from {@link quiz_after_add_or_update()} in lib.php.
+     *
+     * @param object $quiz the data from the quiz form, including $quiz->id
+     *      which is the id of the quiz being saved.
+     */
+    public static function save_settings($quiz) {
+        $context = context_module::instance($quiz->coursemodule);
+
+        if (!settings_provider::can_configure_seb($context)) {
+            return;
+        }
+
+        if (settings_provider::is_seb_settings_locked($quiz->id)) {
+            return;
+        }
+
+        if (settings_provider::is_conflicting_permissions($context)) {
+            return;
+        }
+
+        $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course, false, MUST_EXIST);
+
+        $settings = settings_provider::filter_plugin_settings($quiz);
+        $settings->quizid = $quiz->id;
+        $settings->cmid = $cm->id;
+
+        // Get existing settings or create new settings if none exist.
+        $quizsettings = quiz_settings::get_by_quiz_id($quiz->id);
+        if (empty($quizsettings)) {
+            $quizsettings = new quiz_settings(0, $settings);
+        } else {
+            $settings->id = $quizsettings->get('id');
+            $quizsettings->from_record($settings);
+        }
+
+        // Process uploaded files if required.
+        if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) {
+            $draftitemid = file_get_submitted_draft_itemid('filemanager_sebconfigfile');
+            settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $cm->id);
+        } else {
+            settings_provider::delete_uploaded_config_file($cm->id);
+        }
+
+        // Save or delete settings.
+        if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) {
+            $quizsettings->save();
+        } else if ($quizsettings->get('id')) {
+            $quizsettings->delete();
+        }
+    }
+
+    /**
+     * Delete any rule-specific settings when the quiz is deleted. This is called
+     * from {@link quiz_delete_instance()} in lib.php.
+     *
+     * @param object $quiz the data from the database, including $quiz->id
+     *      which is the id of the quiz being deleted.
+     */
+    public static function delete_settings($quiz) {
+        $quizsettings = quiz_settings::get_by_quiz_id($quiz->id);
+        // Check that there are existing settings.
+        if ($quizsettings !== false) {
+            $quizsettings->delete();
+        }
+    }
+
+    /**
+     * Return the bits of SQL needed to load all the settings from all the access
+     * plugins in one DB query. The easiest way to understand what you need to do
+     * here is probalby to read the code of {@link quiz_access_manager::load_settings()}.
+     *
+     * If you have some settings that cannot be loaded in this way, then you can
+     * use the {@link get_extra_settings()} method instead, but that has
+     * performance implications.
+     *
+     * @param int $quizid the id of the quiz we are loading settings for. This
+     *     can also be accessed as quiz.id in the SQL. (quiz is a table alisas for {quiz}.)
+     * @return array with three elements:
+     *     1. fields: any fields to add to the select list. These should be alised
+     *        if neccessary so that the field name starts the name of the plugin.
+     *     2. joins: any joins (should probably be LEFT JOINS) with other tables that
+     *        are needed.
+     *     3. params: array of placeholder values that are needed by the SQL. You must
+     *        used named placeholders, and the placeholder names should start with the
+     *        plugin name, to avoid collisions.
+     */
+    public static function get_settings_sql($quizid) : array {
+        return [
+                'seb.requiresafeexambrowser AS seb_requiresafeexambrowser, '
+                . 'seb.showsebtaskbar AS seb_showsebtaskbar, '
+                . 'seb.showwificontrol AS seb_showwificontrol, '
+                . 'seb.showreloadbutton AS seb_showreloadbutton, '
+                . 'seb.showtime AS seb_showtime, '
+                . 'seb.showkeyboardlayout AS seb_showkeyboardlayout, '
+                . 'seb.allowuserquitseb AS seb_allowuserquitseb, '
+                . 'seb.quitpassword AS seb_quitpassword, '
+                . 'seb.linkquitseb AS seb_linkquitseb, '
+                . 'seb.userconfirmquit AS seb_userconfirmquit, '
+                . 'seb.enableaudiocontrol AS seb_enableaudiocontrol, '
+                . 'seb.muteonstartup AS seb_muteonstartup, '
+                . 'seb.allowspellchecking AS seb_allowspellchecking, '
+                . 'seb.allowreloadinexam AS seb_allowreloadinexam, '
+                . 'seb.activateurlfiltering AS seb_activateurlfiltering, '
+                . 'seb.filterembeddedcontent AS seb_filterembeddedcontent, '
+                . 'seb.expressionsallowed AS seb_expressionsallowed, '
+                . 'seb.regexallowed AS seb_regexallowed, '
+                . 'seb.expressionsblocked AS seb_expressionsblocked, '
+                . 'seb.regexblocked AS seb_regexblocked, '
+                . 'seb.allowedbrowserexamkeys AS seb_allowedbrowserexamkeys, '
+                . 'seb.showsebdownloadlink AS seb_showsebdownloadlink, '
+                . 'sebtemplate.id AS seb_templateid '
+                , 'LEFT JOIN {quizaccess_seb_quizsettings} seb ON seb.quizid = quiz.id '
+                . 'LEFT JOIN {quizaccess_seb_template} sebtemplate ON seb.templateid = sebtemplate.id '
+                , []
+        ];
+    }
+
+    /**
+     * Whether the user should be blocked from starting a new attempt or continuing
+     * an attempt now.
+     *
+     * @return string false if access should be allowed, a message explaining the
+     *      reason if access should be prevented.
+     */
+    public function prevent_access() {
+        global $PAGE;
+
+        if (!$this->accessmanager->seb_required()) {
+            return false;
+        }
+
+        if ($this->accessmanager->can_bypass_seb()) {
+            return false;
+        }
+
+        // If the rule is active, enforce a secure view whilst taking the quiz.
+        $PAGE->set_pagelayout('secure');
+        $this->prevent_display_blocks();
+
+        if ($this->accessmanager->should_validate_basic_header() && !$this->accessmanager->validate_basic_header()) {
+            access_prevented::create_strict($this->accessmanager, $this->get_reason_text('not_seb'))->trigger();
+            return $this->get_require_seb_error_message();
+        }
+
+        if ($this->accessmanager->should_validate_config_key() && !$this->accessmanager->validate_config_key()) {
+            if ($this->should_redirect_to_seb_config_link()) {
+                $this->redirect_to_seb_config_link();
+            }
+
+            access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_config_key'))->trigger();
+            return $this->get_invalid_key_error_message();
+        }
+
+        if ($this->accessmanager->should_validate_browser_exam_key() && !$this->accessmanager->validate_browser_exam_keys()) {
+            access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_browser_key'))->trigger();
+            return $this->get_invalid_key_error_message();
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a list of finished attempts for the current user.
+     *
+     * @return array
+     */
+    private function get_user_finished_attempts() : array {
+        global $USER;
+
+        return quiz_get_user_attempts(
+            $this->quizobj->get_quizid(),
+            $USER->id,
+            quiz_attempt::FINISHED,
+            false
+        );
+    }
+
+    /**
+     * Prevent block displaying as configured.
+     */
+    private function prevent_display_blocks() {
+        global $PAGE;
+
+        if ($PAGE->has_set_url() && $PAGE->url == $this->quizobj->view_url()) {
+            $attempts = $this->get_user_finished_attempts();
+
+            // Don't display blocks before starting an attempt.
+            if (empty($attempts) && !get_config('quizaccess_seb', 'displayblocksbeforestart')) {
+                $PAGE->blocks->show_only_fake_blocks();
+            }
+
+            // Don't display blocks after finishing an attempt.
+            if (!empty($attempts) && !get_config('quizaccess_seb', 'displayblockswhenfinished')) {
+                $PAGE->blocks->show_only_fake_blocks();
+            }
+        }
+    }
+
+    /**
+     * Returns reason for access prevention as a text.
+     *
+     * @param string $identifier Reason string identifier.
+     * @return string
+     */
+    private function get_reason_text(string $identifier) : string {
+        if (in_array($identifier, ['not_seb', 'invalid_config_key', 'invalid_browser_key'])) {
+            return get_string($identifier, 'quizaccess_seb');
+        }
+
+        return get_string('unknown_reason', 'quizaccess_seb');
+    }
+
+    /**
+     * Return error message when a SEB key is not valid.
+     *
+     * @return string
+     */
+    private function get_invalid_key_error_message() : string {
+        // Return error message with download link and links to get the seb config.
+        return get_string('invalidkeys', 'quizaccess_seb')
+            . $this->display_buttons($this->get_action_buttons());
+    }
+
+    /**
+     * Return error message when a SEB browser is not used.
+     *
+     * @return string
+     */
+    private function get_require_seb_error_message() : string {
+        $message = get_string('clientrequiresseb', 'quizaccess_seb');
+
+        if ($this->should_display_download_seb_link()) {
+            $message .= $this->display_buttons($this->get_download_seb_button());
+        }
+
+        // Return error message with download link.
+        return $message;
+    }
+
+    /**
+     * Helper function to display an Exit Safe Exam Browser button if configured to do so and attempts are > 0.
+     *
+     * @return string empty or a button which has the configured seb quit link.
+     */
+    private function get_quit_button() : string {
+        $quitbutton = '';
+
+        if (empty($this->get_user_finished_attempts())) {
+            return $quitbutton;
+        }
+
+        // Only display if the link has been configured and attempts are greater than 0.
+        if (!empty($this->quiz->seb_linkquitseb)) {
+            $quitbutton = html_writer::link(
+                $this->quiz->seb_linkquitseb,
+                get_string('exitsebbutton', 'quizaccess_seb'),
+                ['class' => 'btn btn-secondary']
+            );
+        }
+
+        return $quitbutton;
+    }
+
+    /**
+     * Information, such as might be shown on the quiz view page, relating to this restriction.
+     * There is no obligation to return anything. If it is not appropriate to tell students
+     * about this rule, then just return ''.
+     *
+     * @return mixed a message, or array of messages, explaining the restriction
+     *         (may be '' if no message is appropriate).
+     */
+    public function description() : array {
+        $messages = [get_string('sebrequired', 'quizaccess_seb')];
+
+        // Display download SEB config link for those who can bypass using SEB.
+        if ($this->accessmanager->can_bypass_seb() && $this->accessmanager->should_validate_config_key()) {
+            $messages[] = $this->display_buttons($this->get_download_config_button());
+        }
+
+        // Those with higher level access will be able to see the button if they've made an attempt.
+        if (!$this->prevent_access()) {
+            $messages[] = $this->display_buttons($this->get_quit_button());
+        }
+
+        return $messages;
+    }
+
+    /**
+     * Sets up the attempt (review or summary) page with any special extra
+     * properties required by this rule.
+     *
+     * @param moodle_page $page the page object to initialise.
+     */
+    public function setup_attempt_page($page) {
+        $page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title);
+        $page->set_cacheable(false);
+        $page->set_popup_notification_allowed(false); // Prevent message notifications.
+        $page->set_heading($page->title);
+        $page->set_pagelayout('secure');
+    }
+
+    /**
+     * Prepare buttons HTML code for being displayed on the screen.
+     *
+     * @param string $buttonshtml Html string of the buttons.
+     * @param string $class Optional CSS class (or classes as space-separated list)
+     * @param array $attributes Optional other attributes as array
+     *
+     * @return string HTML code of the provided buttons.
+     */
+    private function display_buttons(string $buttonshtml, $class = '', array $attributes = null) : string {
+        $html = '';
+
+        if (!empty($buttonshtml)) {
+            $html = html_writer::div($buttonshtml, $class, $attributes);
+        }
+
+        return $html;
+    }
+
+    /**
+     * Get buttons to prompt user to download SEB or config file or launch SEB.
+     *
+     * @return string Html block of all action buttons.
+     */
+    private function get_action_buttons() : string {
+        $buttons = '';
+
+        if ($this->should_display_download_seb_link()) {
+            $buttons .= $this->get_download_seb_button();
+        }
+
+        // Get config for displaying links.
+        $linkconfig = explode(',', get_config('quizaccess_seb', 'showseblinks'));
+
+        // Display links to download config/launch SEB only if required.
+        if ($this->accessmanager->should_validate_config_key()) {
+            if (in_array('seb', $linkconfig)) {
+                $buttons .= $this->get_launch_seb_button();
+            }
+
+            if (in_array('http', $linkconfig)) {
+                $buttons .= $this->get_download_config_button();
+            }
+        }
+
+        return $buttons;
+    }
+
+    /**
+     * Get a button to download SEB.
+     *
+     * @return string A link to download SafeExam Browser.
+     */
+    private function get_download_seb_button() : string {
+        global $OUTPUT;
+
+        $button = '';
+
+        if (!empty($this->get_seb_download_url())) {
+            $button = $OUTPUT->single_button($this->get_seb_download_url(), get_string('sebdownloadbutton', 'quizaccess_seb'));
+        }
+
+        return $button;
+    }
+
+    /**
+     * Get a button to launch Safe Exam Browser.
+     *
+     * @return string A link to launch Safe Exam Browser.
+     */
+    private function get_launch_seb_button() : string {
+        global $OUTPUT;
+
+        $seblink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, true, is_https());
+
+        return $OUTPUT->single_button($seblink, get_string('seblinkbutton', 'quizaccess_seb'), 'get');
+    }
+
+    /**
+     * Get a button to download Safe Exam Browser config.
+     *
+     * @return string A link to launch Safe Exam Browser.
+     */
+    private function get_download_config_button() : string {
+        global $OUTPUT;
+
+        $httplink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, false, is_https());
+
+        return $OUTPUT->single_button($httplink, get_string('httplinkbutton', 'quizaccess_seb'), 'get');
+    }
+
+    /**
+     * Returns SEB download URL.
+     *
+     * @return string
+     */
+    private function get_seb_download_url() : string {
+        return get_config('quizaccess_seb', 'downloadlink');
+    }
+
+    /**
+     * Check if we should display a link to download Safe Exam Browser.
+     *
+     * @return bool
+     */
+    private function should_display_download_seb_link() : bool {
+        return !empty($this->quiz->seb_showsebdownloadlink);
+    }
+
+    /**
+     * Redirect to SEB config link. This will force Safe Exam Browser to be reconfigured.
+     */
+    private function redirect_to_seb_config_link() {
+        global $PAGE;
+
+        $seblink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, true, is_https());
+        $PAGE->requires->js_amd_inline("document.location.replace('" . $seblink . "')");
+    }
+
+    /**
+     * Check if we need to redirect to SEB config link.
+     * @return bool
+     */
+    private function should_redirect_to_seb_config_link() : bool {
+        return $this->accessmanager->is_using_seb() && get_config('quizaccess_seb', 'autoreconfigureseb');
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/settings.php b/mod/quiz/accessrule/seb/settings.php
new file mode 100644 (file)
index 0000000..7e38f62
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Global configuration settings for the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+global $ADMIN;
+
+if ($hassiteconfig) {
+
+    $settings->add(new admin_setting_heading(
+        'quizaccess_seb/supportedversions',
+        '',
+        $OUTPUT->notification(get_string('setting:supportedversions', 'quizaccess_seb'), 'warning')));
+
+    $settings->add(new admin_setting_configcheckbox('quizaccess_seb/autoreconfigureseb',
+        get_string('setting:autoreconfigureseb', 'quizaccess_seb'),
+        get_string('setting:autoreconfigureseb_desc', 'quizaccess_seb'),
+        '1'));
+
+    $links = [
+        'seb' => get_string('setting:showseblink', 'quizaccess_seb'),
+        'http' => get_string('setting:showhttplink', 'quizaccess_seb')
+    ];
+    $settings->add(new admin_setting_configmulticheckbox('quizaccess_seb/showseblinks',
+        get_string('setting:showseblinks', 'quizaccess_seb'),
+        get_string('setting:showseblinks_desc', 'quizaccess_seb'),
+        $links, $links));
+
+    $settings->add(new admin_setting_configtext('quizaccess_seb/downloadlink',
+        get_string('setting:downloadlink', 'quizaccess_seb'),
+        get_string('setting:downloadlink_desc', 'quizaccess_seb'),
+        'https://safeexambrowser.org/download_en.html',
+        PARAM_URL));
+
+    $settings->add(new admin_setting_configcheckbox('quizaccess_seb/quizpasswordrequired',
+        get_string('setting:quizpasswordrequired', 'quizaccess_seb'),
+        get_string('setting:quizpasswordrequired_desc', 'quizaccess_seb'),
+        '0'));
+
+    $settings->add(new admin_setting_configcheckbox('quizaccess_seb/displayblocksbeforestart',
+        get_string('setting:displayblocksbeforestart', 'quizaccess_seb'),
+        get_string('setting:displayblocksbeforestart_desc', 'quizaccess_seb'),
+        '0'));
+
+    $settings->add(new admin_setting_configcheckbox('quizaccess_seb/displayblockswhenfinished',
+        get_string('setting:displayblockswhenfinished', 'quizaccess_seb'),
+        get_string('setting:displayblockswhenfinished_desc', 'quizaccess_seb'),
+        '1'));
+}
+
+if (has_capability('quizaccess/seb:managetemplates', context_system::instance())) {
+    $ADMIN->add('modsettingsquizcat',
+        new admin_externalpage(
+            'quizaccess_seb/template',
+            get_string('manage_templates', 'quizaccess_seb'),
+            new moodle_url('/mod/quiz/accessrule/seb/template.php'),
+            'quizaccess/seb:managetemplates'
+        )
+    );
+}
diff --git a/mod/quiz/accessrule/seb/template.php b/mod/quiz/accessrule/seb/template.php
new file mode 100644 (file)
index 0000000..0565baf
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Page to manipulate templates.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+$action = optional_param('action', 'view', PARAM_ALPHANUMEXT);
+
+$PAGE->set_context(context_system::instance());
+
+$manager = new \quizaccess_seb\template_controller();
+$manager->execute($action);
diff --git a/mod/quiz/accessrule/seb/tests/behat/edit_form.feature b/mod/quiz/accessrule/seb/tests/behat/edit_form.feature
new file mode 100644 (file)
index 0000000..ea9af1d
--- /dev/null
@@ -0,0 +1,210 @@
+@javascript @mod_quiz @quizaccess @quizaccess_seb
+Feature: Safe Exam Browser settings in quiz edit form
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I turn editing mode on
+
+  Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types, except "Use an existing template".
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Configure manually"
+    And the "Require the use of Safe Exam Browser" select box should not contain "Yes – Use an existing template"
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Upload my own config"
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use SEB client config"
+    And the field "Require the use of Safe Exam Browser" matches value "No"
+
+  Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types if at least one template has been added.
+    Given the following "quizaccess_seb > seb templates" exist:
+      | name       |
+      | Template 1 |
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Configure manually"
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use an existing template"
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Upload my own config"
+    And the "Require the use of Safe Exam Browser" select box should contain "Yes – Use SEB client config"
+    And the field "Require the use of Safe Exam Browser" matches value "No"
+
+  Scenario: SEB settings if using No SEB
+    Given the following "quizaccess_seb > seb templates" exist:
+      | name       |
+      | Template 1 |
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And I set the field "Require the use of Safe Exam Browser" to "No"
+    Then I should not see "Upload Safe Exam Browser config file"
+    Then I should not see "Safe Exam Browser config template"
+    Then I should not see "Template 1"
+    Then I should not see "Show Safe Exam Browser download button"
+    Then I should not see "Enable quitting of SEB"
+    Then I should not see "Quit password"
+    Then I should not see "Allowed Browser Exam Keys"
+    Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
+    Then I should not see "Ask user to confirm quitting"
+    Then I should not see "Enable reload in exam"
+    Then I should not see "Show SEB task bar"
+    Then I should not see "Show reload button"
+    Then I should not see "Show time"
+    Then I should not see "Show keyboard layout"
+    Then I should not see "Show Wi-Fi control"
+    Then I should not see "Enable audio controls"
+    Then I should not see "Mute on startup"
+    Then I should not see "Enable spell checking"
+    Then I should not see "Enable URL filtering"
+    Then I should not see "Filter also embedded content"
+    Then I should not see "Expressions allowed"
+    Then I should not see "Regex allowed"
+    Then I should not see "Expressions blocked"
+    Then I should not see "Regex blocked"
+
+  Scenario: SEB settings if using Use SEB client config
+    Given the following "quizaccess_seb > seb templates" exist:
+      | name       |
+      | Template 1 |
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And I set the field "Require the use of Safe Exam Browser" to "Yes – Use SEB client config"
+    Then I should see "Show Safe Exam Browser download button"
+    Then I should see "Allowed Browser Exam Keys"
+    Then I should not see "Upload Safe Exam Browser config file"
+    Then I should not see "Safe Exam Browser config template"
+    Then I should not see "Template 1"
+    Then I should not see "Enable quitting of SEB"
+    Then I should not see "Quit password"
+    Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
+    Then I should not see "Ask user to confirm quitting"
+    Then I should not see "Enable reload in exam"
+    Then I should not see "Show SEB task bar"
+    Then I should not see "Show reload button"
+    Then I should not see "Show time"
+    Then I should not see "Show keyboard layout"
+    Then I should not see "Show Wi-Fi control"
+    Then I should not see "Enable audio controls"
+    Then I should not see "Mute on startup"
+    Then I should not see "Enable spell checking"
+    Then I should not see "Enable URL filtering"
+    Then I should not see "Filter also embedded content"
+    Then I should not see "Expressions allowed"
+    Then I should not see "Regex allowed"
+    Then I should not see "Expressions blocked"
+    Then I should not see "Regex blocked"
+
+  Scenario: SEB settings if using Upload my own config
+    Given the following "quizaccess_seb > seb templates" exist:
+      | name       |
+      | Template 1 |
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And I set the field "Require the use of Safe Exam Browser" to "Yes – Upload my own config"
+    Then I should see "Upload Safe Exam Browser config file"
+    Then I should see "Show Safe Exam Browser download button"
+    Then I should see "Enable quitting of SEB"
+    Then I should see "Quit password"
+    Then I should see "Allowed Browser Exam Keys"
+    Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
+    Then I should not see "Ask user to confirm quitting"
+    Then I should not see "Enable reload in exam"
+    Then I should not see "Show SEB task bar"
+    Then I should not see "Show reload button"
+    Then I should not see "Show time"
+    Then I should not see "Show keyboard layout"
+    Then I should not see "Show Wi-Fi control"
+    Then I should not see "Enable audio controls"
+    Then I should not see "Mute on startup"
+    Then I should not see "Enable spell checking"
+    Then I should not see "Enable URL filtering"
+    Then I should not see "Filter also embedded content"
+    Then I should not see "Expressions allowed"
+    Then I should not see "Regex allowed"
+    Then I should not see "Expressions blocked"
+    Then I should not see "Regex blocked"
+    Then I should not see "Safe Exam Browser config template"
+    Then I should not see "Template 1"
+    And I set the field "Enable quitting of SEB" to "No"
+    Then I should not see "Quit password"
+
+  Scenario: SEB settings if using Use an existing template
+    Given the following "quizaccess_seb > seb templates" exist:
+      | name       |
+      | Template 1 |
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And I set the field "Require the use of Safe Exam Browser" to "Yes – Use an existing template"
+    Then I should see "Safe Exam Browser config template"
+    Then I should see "Template 1"
+    Then I should see "Show Safe Exam Browser download button"
+    Then I should see "Enable quitting of SEB"
+    Then I should see "Quit password"
+    Then I should not see "Allowed Browser Exam Keys"
+    Then I should not see "Upload Safe Exam Browser config file"
+    Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
+    Then I should not see "Ask user to confirm quitting"
+    Then I should not see "Enable reload in exam"
+    Then I should not see "Show SEB task bar"
+    Then I should not see "Show reload button"
+    Then I should not see "Show time"
+    Then I should not see "Show keyboard layout"
+    Then I should not see "Show Wi-Fi control"
+    Then I should not see "Enable audio controls"
+    Then I should not see "Mute on startup"
+    Then I should not see "Enable spell checking"
+    Then I should not see "Enable URL filtering"
+    Then I should not see "Filter also embedded content"
+    Then I should not see "Expressions allowed"
+    Then I should not see "Regex allowed"
+    Then I should not see "Expressions blocked"
+    Then I should not see "Regex blocked"
+    And I set the field "Enable quitting of SEB" to "No"
+    Then I should not see "Quit password"
+
+  Scenario: SEB settings if using Configure manually
+    Given the following "quizaccess_seb > seb templates" exist:
+      | name       |
+      | Template 1 |
+    When I add a "Quiz" to section "1"
+    And I expand all fieldsets
+    And I set the field "Require the use of Safe Exam Browser" to "Yes – Configure manually"
+    Then I should see "Show Safe Exam Browser download button"
+    Then I should see "Enable quitting of SEB"
+    Then I should see "Quit password"
+    Then I should see "Show Exit Safe Exam Browser button, configured with this quit link"
+    Then I should see "Ask user to confirm quitting"
+    Then I should see "Enable reload in exam"
+    Then I should see "Show SEB task bar"
+    Then I should see "Show reload button"
+    Then I should see "Show time"
+    Then I should see "Show keyboard layout"
+    Then I should see "Show Wi-Fi control"
+    Then I should see "Enable audio controls"
+    Then I should not see "Mute on startup"
+    Then I should see "Enable spell checking"
+    Then I should see "Enable URL filtering"
+    Then I should not see "Filter also embedded content"
+    Then I should not see "Expressions allowed"
+    Then I should not see "Regex allowed"
+    Then I should not see "Expressions blocked"
+    Then I should not see "Regex blocked"
+    And I set the field "Enable quitting of SEB" to "No"
+    Then I should not see "Quit password"
+    And I set the field "Show SEB task bar" to "No"
+    Then I should not see "Show reload button"
+    Then I should not see "Show time"
+    Then I should not see "Show keyboard layout"
+    Then I should not see "Show Wi-Fi control"
+    And I set the field "Enable audio controls" to "Yes"
+    Then I should see "Mute on startup"
+    And I set the field "Enable URL filtering" to "Yes"
+    Then I should see "Filter also embedded content"
+    Then I should see "Expressions allowed"
+    Then I should see "Regex allowed"
+    Then I should see "Expressions blocked"
+    Then I should see "Regex blocked"
+    Then I should not see "Upload Safe Exam Browser config file"
+    Then I should not see "Allowed Browser Exam Keys"
+    Then I should not see "Safe Exam Browser config template"
+    Then I should not see "Template 1"
diff --git a/mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php b/mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php
new file mode 100644 (file)
index 0000000..08e19a3
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Behat data generator the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Behat data generator for quizaccess_seb.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_quizaccess_seb_generator extends behat_generator_base {
+
+    /**
+     * Get a list of the entities that can be created.
+
+     * @return array entity name => information about how to generate.
+     */
+    protected function get_creatable_entities(): array {
+        return [
+            'seb templates' => [
+                'datagenerator' => 'template',
+                'required' => ['name'],
+            ],
+        ];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/tests/generator/lib.php b/mod/quiz/accessrule/seb/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..351f60e
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data generator the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Data generator the quizaccess_seb plugin.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_generator extends component_generator_base {
+
+    /**
+     * Create SEB template.
+     *
+     * @param array $data Template data.
+     * @return \quizaccess_seb\template
+     */
+    public function create_template(array $data) {
+        global $CFG;
+
+        if (!isset($data['name'])) {
+            $data['name'] = 'test';
+        }
+
+        if (!isset($data['content'])) {
+            $data['content'] = file_get_contents(
+                $CFG->dirroot . '/mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted.seb'
+            );
+        }
+
+        if (!isset($data['enabled'])) {
+            $data['enabled'] = 1;
+        }
+
+        $template = new \quizaccess_seb\template();
+        $template->set('content', $data['content']);
+        $template->set('name', $data['name']);
+        $template->set('enabled', $data['enabled']);
+        $template->save();
+
+        return $template;
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php b/mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php
new file mode 100644 (file)
index 0000000..9e7ed71
--- /dev/null
@@ -0,0 +1,450 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit tests for the access manager.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\access_manager;
+use quizaccess_seb\quiz_settings;
+use quizaccess_seb\settings_provider;
+use quizaccess_seb\tests\phpunit\quizaccess_seb_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/base.php');
+
+/**
+ * PHPUnit tests for the access manager.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizacces_seb_access_manager_testcase extends quizaccess_seb_testcase {
+
+    /**
+     * Called before every test.
+     */
+    public function setUp() {
+        parent::setUp();
+        $this->setAdminUser();
+    }
+
+    /**
+     * Test access_manager private property quizsettings is null.
+     */
+    public function test_access_manager_quizsettings_null() {
+        $this->quiz = $this->create_test_quiz($this->course);
+
+        $accessmanager = $this->get_access_manager();
+
+        $this->assertFalse($accessmanager->seb_required());
+
+        $reflection = new \ReflectionClass('\quizaccess_seb\access_manager');
+        $property = $reflection->getProperty('quizsettings');
+        $property->setAccessible(true);
+
+        $this->assertFalse($property->getValue($accessmanager));
+    }
+
+    /**
+     * Test that SEB is not required.
+     */
+    public function test_seb_required_false() {
+        $this->quiz = $this->create_test_quiz($this->course);
+
+        $accessmanager = $this->get_access_manager();
+        $this->assertFalse($accessmanager->seb_required());
+    }
+
+    /**
+     * Test that SEB is required.
+     */
+    public function test_seb_required_true() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $accessmanager = $this->get_access_manager();
+        $this->assertTrue($accessmanager->seb_required());
+    }
+
+    /**
+     * Test that user has capability to bypass SEB check.
+     */
+    public function test_user_can_bypass_seb_check() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Set the bypass SEB check capability to $USER.
+        $this->assign_user_capability('quizaccess/seb:bypassseb', context_module::instance($this->quiz->cmid)->id);
+
+        $accessmanager = $this->get_access_manager();
+        $this->assertTrue($accessmanager->can_bypass_seb());
+    }
+
+    /**
+     * Test user does not have capability to bypass SEB check.
+     */
+    public function test_user_cannot_bypass_seb_check() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $accessmanager = $this->get_access_manager();
+        $this->assertFalse($accessmanager->can_bypass_seb());
+    }
+
+    /**
+     * Test we can detect SEB usage.
+     */
+    public function test_is_using_seb() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $accessmanager = $this->get_access_manager();
+
+        $this->assertFalse($accessmanager->is_using_seb());
+
+        $_SERVER['HTTP_USER_AGENT'] = 'Test';
+        $this->assertFalse($accessmanager->is_using_seb());
+
+        $_SERVER['HTTP_USER_AGENT'] = 'SEB';
+        $this->assertTrue($accessmanager->is_using_seb());
+    }
+
+    /**
+     * Test that the quiz Config Key matches the incoming request header.
+     */
+    public function test_access_keys_validate_with_config_key() {
+        global $FULLME;
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $accessmanager = $this->get_access_manager();
+
+        $configkey = quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key();
+
+        // Set up dummy request.
+        $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
+        $expectedhash = hash('sha256', $FULLME . $configkey);
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
+
+        $this->assertTrue($accessmanager->validate_browser_exam_keys());
+        $this->assertTrue($accessmanager->validate_config_key());
+    }
+
+    /**
+     * Test that the quiz Config Key does not match the incoming request header.
+     */
+    public function test_access_keys_fail_to_validate_with_config_key() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+        $accessmanager = $this->get_access_manager();
+
+        $this->assertFalse($accessmanager->validate_config_key());
+        $this->assertTrue($accessmanager->validate_browser_exam_keys());
+    }
+
+    /**
+     * Test that config key is not checked when using client configuration with SEB.
+     */
+    public function test_config_key_not_checked_if_client_requirement_is_selected() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+        $accessmanager = $this->get_access_manager();
+        $this->assertTrue($accessmanager->validate_config_key());
+        $this->assertTrue($accessmanager->validate_browser_exam_keys());
+    }
+
+    /**
+     * Test that if there are no browser exam keys for quiz, check is skipped.
+     */
+    public function test_no_browser_exam_keys_cause_check_to_be_skipped() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+
+        $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $settings->set('allowedbrowserexamkeys', '');
+        $settings->save();
+        $accessmanager = $this->get_access_manager();
+        $this->assertTrue($accessmanager->validate_config_key());
+        $this->assertTrue($accessmanager->validate_browser_exam_keys());
+    }
+
+    /**
+     * Test that access fails if there is no hash in header.
+     */
+    public function test_access_keys_fail_if_browser_exam_key_header_does_not_exist() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+
+        $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
+        $settings->save();
+        $accessmanager = $this->get_access_manager();
+        $this->assertTrue($accessmanager->validate_config_key());
+        $this->assertFalse($accessmanager->validate_browser_exam_keys());
+    }
+
+    /**
+     * Test that access fails if browser exam key doesn't match hash in header.
+     */
+    public function test_access_keys_fail_if_browser_exam_key_header_does_not_match_provided_hash() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+
+        $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
+        $settings->save();
+        $accessmanager = $this->get_access_manager();
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = hash('sha256', 'notwhatyouwereexpectinghuh');
+        $this->assertTrue($accessmanager->validate_config_key());
+        $this->assertFalse($accessmanager->validate_browser_exam_keys());
+    }
+
+    /**
+     * Test that browser exam key matches hash in header.
+     */
+    public function test_browser_exam_keys_match_header_hash() {
+        global $FULLME;
+
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+        $settings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $browserexamkey = hash('sha256', 'browserexamkey');
+        $settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK.
+        $settings->save();
+        $accessmanager = $this->get_access_manager();
+
+        // Set up dummy request.
+        $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
+        $expectedhash = hash('sha256', $FULLME . $browserexamkey);
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash;
+        $this->assertTrue($accessmanager->validate_config_key());
+        $this->assertTrue($accessmanager->validate_browser_exam_keys());
+    }
+
+    /**
+     * Test can get received config key.
+     */
+    public function test_get_received_config_key() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+        $accessmanager = $this->get_access_manager();
+
+        $this->assertNull($accessmanager->get_received_config_key());
+
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Test key';
+        $this->assertEquals('Test key', $accessmanager->get_received_config_key());
+    }
+
+    /**
+     * Test can get received browser key.
+     */
+    public function get_received_browser_exam_key() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+        $accessmanager = $this->get_access_manager();
+
+        $this->assertNull($accessmanager->get_received_browser_exam_key());
+
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Test browser key';
+        $this->assertEquals('Test browser key', $accessmanager->get_received_browser_exam_key());
+    }
+
+    /**
+     * Test can correctly get type of SEB usage for the quiz.
+     */
+    public function test_get_seb_use_type() {
+        // No SEB.
+        $this->quiz = $this->create_test_quiz($this->course);
+        $accessmanager = $this->get_access_manager();
+        $this->assertEquals(settings_provider::USE_SEB_NO, $accessmanager->get_seb_use_type());
+
+        // Manually.
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+        $accessmanager = $this->get_access_manager();
+        $this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $accessmanager->get_seb_use_type());
+
+        // Use template.
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+        $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
+        $quizsettings->set('templateid', $this->create_template()->get('id'));
+        $quizsettings->save();
+        $accessmanager = $this->get_access_manager();
+        $this->assertEquals(settings_provider::USE_SEB_TEMPLATE, $accessmanager->get_seb_use_type());
+
+        // Use uploaded config.
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+        $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header.
+        $xml = file_get_contents(__DIR__ . '/sample_data/unencrypted.seb');
+        $this->create_module_test_file($xml, $this->quiz->cmid);
+        $quizsettings->save();
+        $accessmanager = $this->get_access_manager();
+        $this->assertEquals(settings_provider::USE_SEB_UPLOAD_CONFIG, $accessmanager->get_seb_use_type());
+
+        // Use client config.
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
+        $accessmanager = $this->get_access_manager();
+        $this->assertEquals(settings_provider::USE_SEB_CLIENT_CONFIG, $accessmanager->get_seb_use_type());
+    }
+
+    /**
+     * Data provider for self::test_should_validate_basic_header.
+     *
+     * @return array
+     */
+    public function should_validate_basic_header_data_provider() {
+        return [
+            [settings_provider::USE_SEB_NO, false],
+            [settings_provider::USE_SEB_CONFIG_MANUALLY, false],
+            [settings_provider::USE_SEB_TEMPLATE, false],
+            [settings_provider::USE_SEB_UPLOAD_CONFIG, false],
+            [settings_provider::USE_SEB_CLIENT_CONFIG, true],
+        ];
+    }
+
+    /**
+     * Test we know when we should validate basic header.
+     *
+     * @param int $type Type of SEB usage.
+     * @param bool $expected Expected result.
+     *
+     * @dataProvider should_validate_basic_header_data_provider
+     */
+    public function test_should_validate_basic_header($type, $expected) {
+        $accessmanager = $this->getMockBuilder(access_manager::class)
+            ->disableOriginalConstructor()
+            ->setMethods(['get_seb_use_type'])
+            ->getMock();
+        $accessmanager->method('get_seb_use_type')->willReturn($type);
+
+        $this->assertEquals($expected, $accessmanager->should_validate_basic_header());
+
+    }
+
+    /**
+     * Data provider for self::test_should_validate_config_key.
+     *
+     * @return array
+     */
+    public function should_validate_config_key_data_provider() {
+        return [
+            [settings_provider::USE_SEB_NO, false],
+            [settings_provider::USE_SEB_CONFIG_MANUALLY, true],
+            [settings_provider::USE_SEB_TEMPLATE, true],
+            [settings_provider::USE_SEB_UPLOAD_CONFIG, true],
+            [settings_provider::USE_SEB_CLIENT_CONFIG, false],
+        ];
+    }
+
+    /**
+     * Test we know when we should validate config key.
+     *
+     * @param int $type Type of SEB usage.
+     * @param bool $expected Expected result.
+     *
+     * @dataProvider should_validate_config_key_data_provider
+     */
+    public function test_should_validate_config_key($type, $expected) {
+        $accessmanager = $this->getMockBuilder(access_manager::class)
+            ->disableOriginalConstructor()
+            ->setMethods(['get_seb_use_type'])
+            ->getMock();
+        $accessmanager->method('get_seb_use_type')->willReturn($type);
+
+        $this->assertEquals($expected, $accessmanager->should_validate_config_key());
+    }
+
+    /**
+     * Data provider for self::test_should_validate_browser_exam_key.
+     *
+     * @return array
+     */
+    public function should_validate_browser_exam_key_data_provider() {
+        return [
+            [settings_provider::USE_SEB_NO, false],
+            [settings_provider::USE_SEB_CONFIG_MANUALLY, false],
+            [settings_provider::USE_SEB_TEMPLATE, false],
+            [settings_provider::USE_SEB_UPLOAD_CONFIG, true],
+            [settings_provider::USE_SEB_CLIENT_CONFIG, true],
+        ];
+    }
+
+    /**
+     * Test we know when we should browser exam key.
+     *
+     * @param int $type Type of SEB usage.
+     * @param bool $expected Expected result.
+     *
+     * @dataProvider should_validate_browser_exam_key_data_provider
+     */
+    public function test_should_validate_browser_exam_key($type, $expected) {
+        $accessmanager = $this->getMockBuilder(access_manager::class)
+            ->disableOriginalConstructor()
+            ->setMethods(['get_seb_use_type'])
+            ->getMock();
+        $accessmanager->method('get_seb_use_type')->willReturn($type);
+
+        $this->assertEquals($expected, $accessmanager->should_validate_browser_exam_key());
+    }
+
+    /**
+     * Test that access manager uses cached Config Key.
+     */
+    public function test_access_manager_uses_cached_config_key() {
+        global $FULLME;
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $accessmanager = $this->get_access_manager();
+
+        $configkey = $accessmanager->get_valid_config_key();
+
+        // Set up dummy request.
+        $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
+        $expectedhash = hash('sha256', $FULLME . $configkey);
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
+
+        $this->assertTrue($accessmanager->validate_config_key());
+
+        // Change settings (but don't save) and check that still can validate config key.
+        $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $quizsettings->set('showsebtaskbar', 0);
+        $this->assertNotEquals($quizsettings->get_config_key(), $configkey);
+        $this->assertTrue($accessmanager->validate_config_key());
+
+        // Now save settings which should purge caches but access manager still has config key.
+        $quizsettings->save();
+        $this->assertNotEquals($quizsettings->get_config_key(), $configkey);
+        $this->assertTrue($accessmanager->validate_config_key());
+
+        // Initialise a new access manager. Now validation should fail.
+        $accessmanager = $this->get_access_manager();
+        $this->assertFalse($accessmanager->validate_config_key());
+    }
+
+    /**
+     * Check that valid SEB config key is null if quiz doesn't have SEB settings.
+     */
+    public function test_valid_config_key_is_null_if_no_settings() {
+        $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
+        $accessmanager = $this->get_access_manager();
+
+        $this->assertEmpty(quiz_settings::get_record(['quizid' => $this->quiz->id]));
+        $this->assertNull($accessmanager->get_valid_config_key());
+
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php b/mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php
new file mode 100644 (file)
index 0000000..f8d1d1a
--- /dev/null
@@ -0,0 +1,285 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit tests for backup and restore functionality.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\tests\phpunit\quizaccess_seb_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/base.php');
+
+/**
+ * PHPUnit tests for backup and restore functionality.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_backup_restore_testcase extends quizaccess_seb_testcase {
+
+    /** @var \quizaccess_seb\template $template A test template. */
+    protected $template;
+
+    /**
+     * Called before every test.
+     */
+    public function setUp() {
+        global $USER;
+
+        parent::setUp();
+        $this->setAdminUser();
+        $this->template = $this->create_template();
+        $this->user = $USER;
+    }
+
+    /**
+     * A helper method to create a quiz with template usage of SEB.
+     *
+     * @return \quizaccess_seb\quiz_settings
+     */
+    protected function create_quiz_with_template() {
+        $this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+        $quizsettings = \quizaccess_seb\quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $quizsettings->set('requiresafeexambrowser', \quizaccess_seb\settings_provider::USE_SEB_TEMPLATE);
+        $quizsettings->set('templateid', $this->template->get('id'));
+        $quizsettings->save();
+
+        return $quizsettings;
+    }
+
+    /**
+     * A helper method to emulate backup and restore of the quiz.
+     *
+     * @return \cm_info|null
+     */
+    protected function backup_and_restore_quiz() {
+        return duplicate_module($this->course, get_fast_modinfo($this->course)->get_cm($this->quiz->cmid));
+    }
+
+    /**
+     * A helper method to backup test quiz.
+     *
+     * @return mixed A backup ID ready to be restored.
+     */
+    protected function backup_quiz() {
+        global $CFG;
+
+        // Get the necessary files to perform backup and restore.
+        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+        $backupid = 'test-seb-backup-restore';
+
+        $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->quiz->coursemodule, backup::FORMAT_MOODLE,
+            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->user->id);
+        $bc->execute_plan();
+
+        $results = $bc->get_results();
+        $file = $results['backup_destination'];
+        $fp = get_file_packer('application/vnd.moodle.backup');
+        $filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
+        $file->extract_to_pathname($fp, $filepath);
+        $bc->destroy();
+
+        return $backupid;
+    }
+
+    /**
+     * A helper method to restore provided backup.
+     *
+     * @param string $backupid Backup ID to restore.
+     */
+    protected function restore_quiz($backupid) {
+        $rc = new restore_controller($backupid, $this->course->id,
+            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->user->id, backup::TARGET_CURRENT_ADDING);
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+    }
+
+    /**
+     * A helper method to emulate restoring to a different site.
+     */
+    protected function change_site() {
+        set_config('siteidentifier', random_string(32) . 'not the same site');
+    }
+
+    /**
+     * A helper method to validate backup and restore results.
+     *
+     * @param cm_info $newcm Restored course_module object.
+     */
+    protected function validate_backup_restore(cm_info $newcm) {
+        $this->assertEquals(2, quizaccess_seb\quiz_settings::count_records());
+        $actual = \quizaccess_seb\quiz_settings::get_record(['quizid' => $newcm->instance]);
+
+        $expected = \quizaccess_seb\quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $this->assertEquals($expected->get('templateid'), $actual->get('templateid'));
+        $this->assertEquals($expected->get('requiresafeexambrowser'), $actual->get('requiresafeexambrowser'));
+        $this->assertEquals($expected->get('showsebdownloadlink'), $actual->get('showsebdownloadlink'));
+        $this->assertEquals($expected->get('allowuserquitseb'), $actual->get('allowuserquitseb'));
+        $this->assertEquals($expected->get('quitpassword'), $actual->get('quitpassword'));
+        $this->assertEquals($expected->get('allowedbrowserexamkeys'), $actual->get('allowedbrowserexamkeys'));
+
+        // Validate specific SEB config settings.
+        foreach (\quizaccess_seb\settings_provider::get_seb_config_elements() as $name => $notused) {
+            $name = preg_replace("/^seb_/", "", $name);
+            $this->assertEquals($expected->get($name), $actual->get($name));
+        }
+    }
+
+    /**
+     * Test backup and restore when no seb.
+     */
+    public function test_backup_restore_no_seb() {
+        $this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_NO);
+        $this->assertEquals(0, quizaccess_seb\quiz_settings::count_records());
+
+        $this->backup_and_restore_quiz();
+        $this->assertEquals(0, quizaccess_seb\quiz_settings::count_records());
+    }
+
+    /**
+     * Test backup and restore when manually configured.
+     */
+    public function test_backup_restore_manual_config() {
+        $this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $expected = \quizaccess_seb\quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $expected->set('showsebdownloadlink', 0);
+        $expected->set('quitpassword', '123');
+        $expected->save();
+
+        $this->assertEquals(1, quizaccess_seb\quiz_settings::count_records());
+
+        $newcm = $this->backup_and_restore_quiz();
+        $this->validate_backup_restore($newcm);
+    }
+
+    /**
+     * Test backup and restore when using template.
+     */
+    public function test_backup_restore_template_config() {
+        $this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $expected = \quizaccess_seb\quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $template = $this->create_template();
+        $expected->set('requiresafeexambrowser', \quizaccess_seb\settings_provider::USE_SEB_TEMPLATE);
+        $expected->set('templateid', $template->get('id'));
+        $expected->save();
+
+        $this->assertEquals(1, quizaccess_seb\quiz_settings::count_records());
+
+        $newcm = $this->backup_and_restore_quiz();
+        $this->validate_backup_restore($newcm);
+    }
+
+    /**
+     * Test backup and restore when using uploaded file.
+     */
+    public function test_backup_restore_uploaded_config() {
+        $this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $expected = \quizaccess_seb\quiz_settings::get_record(['quizid' => $this->quiz->id]);
+        $expected->set('requiresafeexambrowser', \quizaccess_seb\settings_provider::USE_SEB_UPLOAD_CONFIG);
+        $xml = file_get_contents(__DIR__ . '/sample_data/unencrypted.seb');
+        $this->create_module_test_file($xml, $this->quiz->cmid);
+        $expected->save();
+
+        $this->assertEquals(1, quizaccess_seb\quiz_settings::count_records());
+
+        $newcm = $this->backup_and_restore_quiz();
+        $this->validate_backup_restore($newcm);
+
+        $expectedfile = \quizaccess_seb\settings_provider::get_module_context_sebconfig_file($this->quiz->cmid);
+        $actualfile = \quizaccess_seb\settings_provider::get_module_context_sebconfig_file($newcm->id);
+
+        $this->assertEquals($expectedfile->get_content(), $actualfile->get_content());
+    }
+
+    /**
+     * No new template should be restored if restoring to a different site,
+     * but the template with  the same name and content exists..
+     */
+    public function test_restore_template_to_a_different_site_when_the_same_template_exists() {
+        $this->create_quiz_with_template();
+        $backupid = $this->backup_quiz();
+
+        $this->assertEquals(1, quizaccess_seb\quiz_settings::count_records());
+        $this->assertEquals(1, quizaccess_seb\template::count_records());
+
+        $this->change_site();
+        $this->restore_quiz($backupid);
+
+        // Should see additional setting record, but no new template record.
+        $this->assertEquals(2, quizaccess_seb\quiz_settings::count_records());
+        $this->assertEquals(1, quizaccess_seb\template::count_records());
+    }
+
+    /**
+     * A new template should be restored if restoring to a different site, but existing template
+     * has the same content, but different name.
+     */
+    public function test_restore_template_to_a_different_site_when_the_same_content_but_different_name() {
+        $this->create_quiz_with_template();
+        $backupid = $this->backup_quiz();
+
+        $this->assertEquals(1, quizaccess_seb\quiz_settings::count_records());
+        $this->assertEquals(1, quizaccess_seb\template::count_records());
+
+        $this->template->set('name', 'New name for template');
+        $this->template->save();
+
+        $this->change_site();
+        $this->restore_quiz($backupid);
+
+        // Should see additional setting record, and new template record.
+        $this->assertEquals(2, quizaccess_seb\quiz_settings::count_records());
+        $this->assertEquals(2, quizaccess_seb\template::count_records());
+    }
+
+    /**
+     * A new template should be restored if restoring to a different site, but existing template
+     * has the same name, but different content.
+     */
+    public function test_restore_template_to_a_different_site_when_the_same_name_but_different_content() {
+        global $CFG;
+
+        $this->create_quiz_with_template();
+        $backupid = $this->backup_quiz();
+
+        $this->assertEquals(1, quizaccess_seb\quiz_settings::count_records());
+        $this->assertEquals(1, quizaccess_seb\template::count_records());
+
+        $newxml = file_get_contents($CFG->dirroot . '/mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencrypted.seb');
+        $this->template->set('content', $newxml);
+        $this->template->save();
+
+        $this->change_site();
+        $this->restore_quiz($backupid);
+
+        // Should see additional setting record, and new template record.
+        $this->assertEquals(2, quizaccess_seb\quiz_settings::count_records());
+        $this->assertEquals(2, quizaccess_seb\template::count_records());
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/base.php b/mod/quiz/accessrule/seb/tests/phpunit/base.php
new file mode 100644 (file)
index 0000000..18f391f
--- /dev/null
@@ -0,0 +1,321 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for tests. All tests should extend it to get common helper methods.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace quizaccess_seb\tests\phpunit;
+
+use quizaccess_seb\access_manager;
+use quizaccess_seb\settings_provider;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . "/mod/quiz/accessrule/seb/rule.php"); // Include plugin rule class.
+require_once($CFG->dirroot . "/mod/quiz/mod_form.php"); // Include plugin rule class.
+
+/**
+ * Base class for tests. All tests should extend it to get common helper methods.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class quizaccess_seb_testcase extends \advanced_testcase {
+
+    /** @var \stdClass $course Test course to contain quiz. */
+    protected $course;
+
+    /** @var \stdClass $quiz A test quiz. */
+    protected $quiz;
+
+    /** @var \stdClass $user A test logged-in user. */
+    protected $user;
+
+    /**
+     * Called before every test.
+     */
+    public function setUp() {
+        parent::setUp();
+
+        $this->resetAfterTest();
+        $this->course = $this->getDataGenerator()->create_course();
+    }
+
+    /**
+     * Assign a capability to $USER
+     * The function creates a student $USER if $USER->id is empty
+     *
+     * @param string $capability Capability name.
+     * @param int $contextid Context ID.
+     * @param int $roleid Role ID.
+     * @return int The role id - mainly returned for creation, so calling function can reuse it.
+     */
+    protected function assign_user_capability($capability, $contextid, $roleid = null) {
+        global $USER;
+
+        // Create a new student $USER if $USER doesn't exist.
+        if (empty($USER->id)) {
+            $user = $this->getDataGenerator()->create_user();
+            $this->setUser($user);
+        }
+
+        if (empty($roleid)) {
+            $roleid = \create_role('Dummy role', 'dummyrole', 'dummy role description');
+        }
+
+        \assign_capability($capability, CAP_ALLOW, $roleid, $contextid);
+
+        \role_assign($roleid, $USER->id, $contextid);
+
+        \accesslib_clear_all_caches_for_unit_testing();
+
+        return $roleid;
+    }
+
+    /**
+     * Strip the seb_ prefix from each setting key.
+     *
+     * @param \stdClass $settings Object containing settings.
+     * @return \stdClass The modified settings object.
+     */
+    protected function strip_all_prefixes(\stdClass $settings) : \stdClass {
+        $newsettings = new \stdClass();
+        foreach ($settings as $name => $setting) {
+            $newname = preg_replace("/^seb_/", "", $name);
+            $newsettings->$newname = $setting; // Add new key.
+        }
+        return $newsettings;
+    }
+
+    /**
+     * Creates a file in the user draft area.
+     *
+     * @param string $xml
+     * @return int The user draftarea id
+     */
+    protected function create_test_draftarea_file(string $xml) : int {
+        global $USER;
+
+        $itemid = 0;
+        $usercontext = \context_user::instance($USER->id);
+        $filerecord = [
+            'contextid' => \context_user::instance($USER->id)->id,
+            'component' => 'user',
+            'filearea' => 'draft',
+            'itemid' => $itemid,
+            'filepath' => '/',
+            'filename' => 'test.xml'
+        ];
+
+        $fs = get_file_storage();
+        $fs->create_file_from_string($filerecord, $xml);
+
+        $draftitemid = 0;
+        file_prepare_draft_area($draftitemid, $usercontext->id, 'user', 'draft', 0);
+
+        return $draftitemid;
+    }
+
+    /**
+     * Create a file in a modules filearea.
+     *
+     * @param string $xml XML content of the file.
+     * @param string $cmid Course module id.
+     * @return int Item ID of file.
+     */
+    protected function create_module_test_file(string $xml, string $cmid) : int {
+        $itemid = 0;
+        $fs = get_file_storage();
+        $filerecord = [
+            'contextid' => \context_module::instance($cmid)->id,
+            'component' => 'quizaccess_seb',
+            'filearea' => 'filemanager_sebconfigfile',
+            'itemid' => $itemid,
+            'filepath' => '/',
+            'filename' => 'test.xml'
+        ];
+        $fs->create_file_from_string($filerecord, $xml);
+        return $itemid;
+    }
+
+    /**
+     * Create a test quiz for the specified course.
+     *
+     * @param \stdClass $course
+     * @param int $requiresafeexambrowser How to use SEB for this quiz?
+     * @return  array
+     */
+    protected function create_test_quiz($course, $requiresafeexambrowser = settings_provider::USE_SEB_NO) {
+        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+
+        $quiz = $quizgenerator->create_instance([
+            'course' => $course->id,
+            'questionsperpage' => 0,
+            'grade' => 100.0,
+            'sumgrades' => 2,
+            'seb_requiresafeexambrowser' => $requiresafeexambrowser,
+        ]);
+        $quiz->seb_showsebdownloadlink = 1;
+        $quiz->coursemodule = $quiz->cmid;
+
+        // Create a couple of questions.
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $questiongenerator->create_question_category();
+
+        $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
+        quiz_add_quiz_question($saq->id, $quiz);
+        $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+        quiz_add_quiz_question($numq->id, $quiz);
+
+        return $quiz;
+    }
+
+    /**
+     * Answer questions for a quiz + user.
+     *
+     * @param \stdClass $quiz Quiz to attempt.
+     * @param \stdClass $user A user to attempt the quiz.
+     * @return  array
+     */
+    protected function attempt_quiz($quiz, $user) {
+        $this->setUser($user);
+
+        $starttime = time();
+        $quizobj = \quiz::create($quiz->id, $user->id);
+
+        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        // Start the attempt.
+        $attempt = quiz_create_attempt($quizobj, 1, false, $starttime, false, $user->id);
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $starttime);
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+        // Answer the questions.
+        $attemptobj = \quiz_attempt::create($attempt->id);
+
+        $tosubmit = [
+            1 => ['answer' => 'frog'],
+            2 => ['answer' => '3.14'],
+        ];
+
+        $attemptobj->process_submitted_actions($starttime, false, $tosubmit);
+
+        // Finish the attempt.
+        $attemptobj = \quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($starttime, false);
+
+        $this->setUser();
+
+        return [$quizobj, $quba, $attemptobj];
+    }
+
+    /**
+     * Create test template.
+     *
+     * @param string|null $xml Template content.
+     * @return \quizaccess_seb\template Just created template.
+     */
+    public function create_template(string $xml = null) {
+        $data = [];
+
+        if (!is_null($xml)) {
+            $data['content'] = $xml;
+        }
+
+        return $this->getDataGenerator()->get_plugin_generator('quizaccess_seb')->create_template($data);
+    }
+
+    /**
+     * Get access manager for testing.
+     *
+     * @return \quizaccess_seb\access_manager
+     */
+    protected function get_access_manager() {
+        return new access_manager(new \quiz($this->quiz,
+            get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course));
+    }
+
+    /**
+     * A helper method to make the rule form the currently created quiz and  course.
+     *
+     * @return \quiz_access_rule_base|null
+     */
+    protected function make_rule() {
+        return \quizaccess_seb::make(
+            new \quiz($this->quiz, get_coursemodule_from_id('quiz', $this->quiz->cmid), $this->course),
+            0,
+            true
+        );
+    }
+
+    /**
+     * A helper method to set up quiz view page.
+     */
+    protected function set_up_quiz_view_page() {
+        global $PAGE;
+
+        $page = new \moodle_page();
+        $page->set_context(\context_module::instance($this->quiz->cmid));
+        $page->set_course($this->course);
+        $page->set_pagelayout('standard');
+        $page->set_pagetype("mod-quiz-view");
+        $page->set_url('/mod/quiz/view.php?id=' . $this->quiz->cmid);
+
+        $PAGE = $page;
+    }
+
+    /**
+     * Get a test object containing mock test settings.
+     *
+     * @return \stdClass Settings.
+     */
+    protected function get_test_settings() : \stdClass {
+        return (object) [
+            'quizid' => 1,
+            'cmid' => 1,
+            'requiresafeexambrowser' => '1',
+            'showsebtaskbar' => '1',
+            'showwificontrol' => '0',
+            'showreloadbutton' => '1',
+            'showtime' => '0',
+            'showkeyboardlayout' => '1',
+            'allowuserquitseb' => '1',
+            'quitpassword' => 'test',
+            'linkquitseb' => '',
+            'userconfirmquit' => '1',
+            'enableaudiocontrol' => '1',
+            'muteonstartup' => '0',
+            'allowspellchecking' => '0',
+            'allowreloadinexam' => '1',
+            'activateurlfiltering' => '1',
+            'filterembeddedcontent' => '0',
+            'expressionsallowed' => 'test.com',
+            'regexallowed' => '',
+            'expressionsblocked' => '',
+            'regexblocked' => '',
+            'showsebdownloadlink' => '1',
+        ];
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php b/mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php
new file mode 100644 (file)
index 0000000..b60f798
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit Tests for config_key class.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\config_key;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * PHPUnit Tests for config_key class.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_config_key_testcase extends advanced_testcase {
+
+    /**
+     * Test that trying to generate the hash key with bad xml will result in an error.
+     */
+    public function test_config_key_not_generated_with_bad_xml() {
+        $this->expectException(invalid_parameter_exception::class);
+        $this->expectExceptionMessage("Invalid a PList XML string, representing SEB config");
+        config_key::generate("<?xml This is some bad xml for sure.");
+    }
+
+    /**
+     * Test that a config key is generated with empty configuration. SEB would be using defaults for all settings.
+     */
+    public function test_config_key_hash_generated_with_empty_string() {
+        $hash = config_key::generate('')->get_hash();
+        $this->assertEquals('4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', $hash);
+    }
+
+    /**
+     * Check that the Config Key hash is not altered if the originatorVersion is present in the XML or not.
+     */
+    public function test_presence_of_originator_version_does_not_effect_hash() {
+        $xmlwithoriginatorversion = file_get_contents(__DIR__ . '/sample_data/simpleunencrypted.seb');
+        $xmlwithoutoriginatorversion = file_get_contents(__DIR__ . '/sample_data/simpleunencryptedwithoutoriginator.seb');
+        $hashwithorigver = config_key::generate($xmlwithoriginatorversion)->get_hash();
+        $hashwithoutorigver = config_key::generate($xmlwithoutoriginatorversion)->get_hash();
+        $this->assertEquals($hashwithorigver, $hashwithoutorigver);
+    }
+
+    /**
+     * Provide a seb file, the expected Config Key and a password if encrypted.
+     *
+     * @return array
+     */
+    public function real_ck_hash_provider() : array {
+        return [
+            'unencrypted_mac2.1.4' => ['unencrypted_mac_001.seb',
+                    '4fa9af8ec8759eb7c680752ef4ee5eaf1a860628608fccae2715d519849f9292', ''],
+            'unencrypted_win2.2.3' => ['unencrypted_win_223.seb',
+                    'fc6f4ea5922717760f4d6d536c23b8d19bf20b52aa97940f5427a76e20f49026', ''],
+        ];
+    }
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/event_test.php b/mod/quiz/accessrule/seb/tests/phpunit/event_test.php
new file mode 100644 (file)
index 0000000..980be06
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit tests for all plugin events.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\tests\phpunit\quizaccess_seb_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/base.php');
+
+/**
+ * PHPUnit tests for all plugin events.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_event_testcase extends quizaccess_seb_testcase {
+
+    /**
+     * Test creating the access_prevented event.
+     */
+    public function test_event_access_prevented() {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+        $accessmanager = new \quizaccess_seb\access_manager(new quiz($quiz,
+            get_coursemodule_from_id('quiz', $quiz->cmid), $this->course));
+
+        // Set up event with data.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'configkey';
+        $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'browserexamkey';
+
+        $event = \quizaccess_seb\event\access_prevented::create_strict($accessmanager, 'Because I said so.');
+
+        // Create an event sink, trigger event and retrieve event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertEquals(1, count($events));
+        $event = reset($events);
+
+        $expectedconfigkey = $accessmanager->get_valid_config_key();
+
+        // Test that the event data is as expected.
+        $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
+        $this->assertEquals('Quiz access was prevented', $event->get_name());
+        $this->assertEquals(
+            "The user with id '$user->id' has been prevented from accessing quiz with id '$quiz->id' by the "
+            . "Safe Exam Browser access plugin. The reason was 'Because I said so.'. "
+            . "Expected config key: '$expectedconfigkey'. "
+            . "Received config key: 'configkey'. Received browser exam key: 'browserexamkey'.",
+            $event->get_description());
+        $this->assertEquals(context_module::instance($quiz->cmid), $event->get_context());
+        $this->assertEquals($user->id, $event->userid);
+        $this->assertEquals($quiz->id, $event->objectid);
+        $this->assertEquals($this->course->id, $event->courseid);
+        $this->assertEquals('Because I said so.', $event->other['reason']);
+        $this->assertEquals($expectedconfigkey, $event->other['savedconfigkey']);
+        $this->assertEquals('configkey', $event->other['receivedconfigkey']);
+        $this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']);
+    }
+
+    /**
+     * Test creating the template_created event.
+     */
+    public function test_event_create_template() {
+        $this->resetAfterTest();
+        // Set up event with data.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $template = $this->create_template();
+
+        $event = \quizaccess_seb\event\template_created::create_strict(
+            $template,
+            context_system::instance());
+
+        // Create an event sink, trigger event and retrieve event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $this->assertEquals(1, count($events));
+        $event = reset($events);
+
+        // Test that the event data is as expected.
+        $this->assertInstanceOf('\quizaccess_seb\event\template_created', $event);
+        $this->assertEquals('SEB Template was created', $event->get_name());
+        $this->assertEquals(
+            "The user with id '$user->id' has created a template with id '{$template->get('id')}'.",
+            $event->get_description()
+        );
+        $this->assertEquals(context_system::instance(), $event->get_context());
+        $this->assertEquals($user->id, $event->userid);
+        $this->assertEquals($template->get('id'), $event->objectid);
+    }
+}
\ No newline at end of file
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/helper_test.php b/mod/quiz/accessrule/seb/tests/phpunit/helper_test.php
new file mode 100644 (file)
index 0000000..d0c746a
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit tests for helper class.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\tests\phpunit\quizaccess_seb_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/base.php');
+
+/**
+ * PHPUnit tests for helper class.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_helper_testcase extends quizaccess_seb_testcase {
+
+    /**
+     * Test that we can check valid seb string.
+     */
+    public function test_is_valid_seb_config() {
+        $validseb = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
+<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
+            . "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
+            . "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
+            . "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
+            . "<key>URLFilterEnableContentFilter</key><false/><key>hashedQuitPassword</key>"
+            . "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
+            . "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
+            . "<string>test.com</string><key>regex</key><false/></dict></array>"
+            . "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
+        $invalidseb = 'Invalid seb';
+        $emptyseb = '';
+
+        $this->assertTrue(\quizaccess_seb\helper::is_valid_seb_config($validseb));
+        $this->assertFalse(\quizaccess_seb\helper::is_valid_seb_config($invalidseb));
+        $this->assertFalse(\quizaccess_seb\helper::is_valid_seb_config($emptyseb));
+    }
+
+    /**
+     * Test that we can get seb file headers.
+     */
+    public function test_get_seb_file_headers() {
+        $expiretime = 1582767914;
+        $headers = \quizaccess_seb\helper::get_seb_file_headers($expiretime);
+
+        $this->assertCount(5, $headers);
+        $this->assertEquals('Cache-Control: private, max-age=1, no-transform', $headers[0]);
+        $this->assertEquals('Expires: Thu, 27 Feb 2020 01:45:14 GMT', $headers[1]);
+        $this->assertEquals('Pragma: no-cache', $headers[2]);
+        $this->assertEquals('Content-Disposition: attachment; filename=config.seb', $headers[3]);
+        $this->assertEquals('Content-Type: application/seb', $headers[4]);
+    }
+
+
+    /**
+     * Test that the course module must exist to get a seb config file content.
+     */
+    public function test_can_not_get_config_content_with_invalid_cmid() {
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->setUser($user); // Log user in.
+
+        $this->expectException(dml_exception::class);
+        $this->expectExceptionMessage("Can't find data record in database. (SELECT cm.*, m.name, md.name AS modname \n"
+            . "              FROM {course_modules} cm\n"
+            . "                   JOIN {modules} md ON md.id = cm.module\n"
+            . "                   JOIN {quiz} m ON m.id = cm.instance\n"
+            . "                   \n"
+            . "             WHERE cm.id = :cmid AND md.name = :modulename\n"
+            . "                   \n"
+            . "[array (\n"
+            . "  'cmid' => '999',\n"
+            . "  'modulename' => 'quiz',\n"
+            .')])');
+        \quizaccess_seb\helper::get_seb_config_content('999');
+    }
+
+    /**
+     * Test that the user must be enrolled to get seb config content.
+     */
+    public function test_can_not_get_config_content_when_user_not_enrolled_in_course() {
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user); // Log user in.
+
+        $this->expectException(moodle_exception::class);
+        $this->expectExceptionMessage('Unsupported redirect detected, script execution terminated');
+        \quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
+    }
+
+    /**
+     * Test that if SEB quiz settings can't be found, a seb config content won't be provided.
+     */
+    public function test_can_not_get_config_content_if_config_not_found_for_cmid() {
+        global $DB;
+
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->create_test_quiz($course);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->setUser($user); // Log user in.
+
+        $this->expectException(moodle_exception::class);
+        $this->expectExceptionMessage("No SEB config could be found for quiz with cmid: $quiz->cmid");
+        \quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
+    }
+
+    /**
+     * That that if config is empty for a quiz, a seb config content won't be provided.
+     */
+    public function test_can_not_get_config_content_if_config_empty() {
+        global $DB;
+
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_NO);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->setUser($user); // Log user in.
+
+        $this->expectException(moodle_exception::class);
+        $this->expectExceptionMessage("No SEB config could be found for quiz with cmid: $quiz->cmid");
+        \quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
+    }
+
+    /**
+     * Test config content is provided successfully.
+     */
+    public function test_config_provided() {
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->setUser($user); // Log user in.
+
+        $config = \quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
+
+        $url = new moodle_url("/mod/quiz/view.php", ['id' => $quiz->cmid]);
+
+        $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            . "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
+            . "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key>"
+            . "<false/><key>showReloadButton</key><true/><key>showTime</key><true/><key>showInputLanguage</key>"
+            . "<true/><key>allowQuit</key><true/><key>quitURLConfirm</key><true/><key>audioControlEnabled</key>"
+            . "<false/><key>audioMute</key><false/><key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key>"
+            . "<true/><key>URLFilterEnable</key><false/><key>URLFilterEnableContentFilter</key><false/>"
+            . "<key>URLFilterRules</key><array/><key>startURL</key><string>$url</string>"
+            . "<key>sendBrowserExamKey</key><true/><key>examSessionClearCookiesOnStart</key><false/>"
+            . "</dict></plist>\n", $config);
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php b/mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php
new file mode 100644 (file)
index 0000000..72d1b7d
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit tests for hideif_rule.
+ *
+ * @package    quizaccess_seb
+ * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\hideif_rule;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * PHPUnit tests for hideif_rule.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_hideif_rule_testcase extends advanced_testcase {
+
+    /**
+     * Test that can get rule data.
+     */
+    public function test_can_get_what_set_in_constructor() {
+        $rule = new hideif_rule('Element', 'Dependant', 'eq', 'Value');
+        $this->assertEquals('Element', $rule->get_element());
+        $this->assertEquals('Dependant', $rule->get_dependantname());
+        $this->assertEquals('eq', $rule->get_condition());
+        $this->assertEquals('Value', $rule->get_dependantvalue());
+    }
+
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php b/mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php
new file mode 100644 (file)
index 0000000..aa2091e
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * PHPUnit tests for link_generator.
+ *
+ * @package    quizaccess_seb
+ * @author     Andrew Madden <andrewmadden@catalyst-au.net>
+ * @copyright  2019 Catalyst IT
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use quizaccess_seb\link_generator;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * PHPUnit tests for link_generator.
+ *
+ * @copyright  2020 Catalyst IT
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quizaccess_seb_link_generator_testcase extends advanced_testcase {
+
+    /**
+     * Called before every test.
+     */
+    public function setUp() {
+        parent::setUp();
+        $this->resetAfterTest();
+    }
+
+    /**
+     * Test that a http link is generated correctly.
+     */
+    public function test_http_link_generated() {
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
+
+        $this->assertEquals(
+            "http://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
+            link_generator::get_link($quiz->cmid, false, false));
+    }
+
+    /**
+     * Test that a http link is generated correctly.
+     */
+    public function test_https_link_generated() {
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
+
+        $this->assertEquals(
+            "https://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
+            link_generator::get_link($quiz->cmid, false));
+    }
+
+    /**
+     * Test that a seb link is generated correctly.
+     */
+    public function test_seb_link_generated() {
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
+
+        $this->assertEquals(
+            "seb://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
+            link_generator::get_link($quiz->cmid, true, false));
+    }
+
+    /**
+     * Test that a sebs link is generated correctly.
+     */
+    public function test_sebs_link_generated() {
+        $course = $this->getDataGenerator()->create_course();
+        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
+
+        $this->assertEquals(
+            "sebs://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
+            link_generator::get_link($quiz->cmid, true));
+    }
+
+    /**
+     * Test that link_generator can't not be instantiated with fake course module.
+     */
+    public function test_course_module_does_not_exist() {
+        $this->expectException(dml_exception::class);
+        $this->expectExceptionMessageRegExp("/^Can't find data record in database.*/");
+        $generator = link_generator::get_link(123456, false);
+    }
+}
diff --git a/mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php b/mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..197f0c6
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public&nb