first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -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;
}
}
@@ -0,0 +1,127 @@
<?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\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 seb_access_manager $accessmanager Access manager.
* @param string $reason Reason that access was prevented.
* @param string|null $configkey A Safe Exam Browser config key.
* @param string|null $browserexamkey A Safe Exam Browser browser exam key.
* @return base
*/
public static function create_strict(seb_access_manager $accessmanager, string $reason,
?string $configkey = null, ?string $browserexamkey = null): base {
global $USER;
$other = [];
$other['reason'] = $reason;
$other['savedconfigkey'] = $accessmanager->get_valid_config_key();
$other['receivedconfigkey'] = !empty($configkey) ? $configkey : $accessmanager->get_received_config_key();
$other['receivedbrowserexamkey'] = !empty($browserexamkey) ? $browserexamkey
: $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 ['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']
];
}
}
@@ -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 ['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 [];
}
}
@@ -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 ['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 [];
}
}
@@ -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 ['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 [];
}
}
@@ -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 ['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 [];
}
}
@@ -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 ['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 [];
}
}
@@ -0,0 +1,150 @@
<?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/>.
namespace quizaccess_seb\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use invalid_parameter_exception;
use mod_quiz\quiz_settings;
use quizaccess_seb\event\access_prevented;
use quizaccess_seb\seb_access_manager;
/**
* Validate browser exam key and config key.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class validate_quiz_keys extends external_api {
/**
* External function parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module ID',
VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
'url' => new external_value(PARAM_URL, 'Page URL to check',
VALUE_REQUIRED, null, NULL_NOT_ALLOWED),
'configkey' => new external_value(PARAM_ALPHANUMEXT, 'SEB config key',
VALUE_DEFAULT, null),
'browserexamkey' => new external_value(PARAM_ALPHANUMEXT, 'SEB browser exam key',
VALUE_DEFAULT, null),
]);
}
/**
* Validate a SEB config key or browser exam key.
*
* @param string $cmid Course module ID.
* @param string $url URL of the page on which the SEB JS API generated the keys.
* @param string|null $configkey A SEB config key hash. Includes URL in the hash.
* @param string|null $browserexamkey A SEB browser exam key hash. Includes the URL in the hash.
* @return array
*/
public static function execute(string $cmid, string $url, ?string $configkey = null, ?string $browserexamkey = null): array {
list(
'cmid' => $cmid,
'url' => $url,
'configkey' => $configkey,
'browserexamkey' => $browserexamkey
) = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'url' => $url,
'configkey' => $configkey,
'browserexamkey' => $browserexamkey,
]);
self::validate_context(\context_module::instance($cmid));
// At least one SEB key must be provided.
if (empty($configkey) && empty($browserexamkey)) {
throw new invalid_parameter_exception(get_string('error:ws:nokeyprovided', 'quizaccess_seb'));
}
// Check quiz exists corresponding to cmid.
if (($quizid = self::get_quiz_id($cmid)) === 0) {
throw new invalid_parameter_exception(get_string('error:ws:quiznotexists', 'quizaccess_seb', $cmid));
}
$result = ['configkey' => true, 'browserexamkey' => true];
$accessmanager = new seb_access_manager(quiz_settings::create($quizid));
// Check if there is a valid config key.
if (!$accessmanager->validate_config_key($configkey, $url)) {
access_prevented::create_strict($accessmanager, get_string('invalid_config_key', 'quizaccess_seb'),
$configkey, $browserexamkey)->trigger();
$result['configkey'] = false;
}
// Check if there is a valid browser exam key.
if (!$accessmanager->validate_browser_exam_key($browserexamkey, $url)) {
access_prevented::create_strict($accessmanager, get_string('invalid_browser_key', 'quizaccess_seb'),
$configkey, $browserexamkey)->trigger();
$result['browserexamkey'] = false;
}
if ($result['configkey'] && $result['browserexamkey']) {
// Set the state of the access for this Moodle session.
$accessmanager->set_session_access(true);
}
return $result;
}
/**
* External function returns.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'configkey' => new external_value(PARAM_BOOL, 'Is a provided config key valid?',
VALUE_REQUIRED, 0, NULL_NOT_ALLOWED),
'browserexamkey' => new external_value(PARAM_BOOL, 'Is a provided browser exam key valid?',
VALUE_REQUIRED, 0, NULL_NOT_ALLOWED)
]);
}
/**
* Check if there is a valid quiz corresponding to a course module it.
*
* @param string $cmid Course module ID.
* @return int Returns quiz id if cmid matches valid quiz, or 0 if there is no match.
*/
private static function get_quiz_id(string $cmid): int {
$quizid = 0;
$coursemodule = get_coursemodule_from_id('quiz', $cmid);
if (!empty($coursemodule)) {
$quizid = $coursemodule->instance;
}
return $quizid;
}
}
+146
View File
@@ -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 = []) {
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 = seb_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);
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -0,0 +1,297 @@
<?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\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 {modules} m ON cm.module = m.id AND m.name = :modulename
JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :context
WHERE qs.usermodified = :userid
GROUP BY c.id";
$params = [
'context' => CONTEXT_MODULE,
'modulename' => 'quiz',
'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 {modules} m ON cm.module = m.id AND m.name = :modulename
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);
$params['modulename'] = 'quiz';
// 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
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
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'),
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
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
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
JOIN {modules} m ON cm.module = m.id AND m.name = ?
WHERE cm.id = ?";
$params = ['quiz', $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);
}
}
@@ -0,0 +1,404 @@
<?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 \Collator;
use \DateTime;
/**
* 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.
*
* @link https://safeexambrowser.org/developer/seb-config-key.html
*
* @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:
//
// 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.
if ($this->is_associative_array($array)) {
// Note this is a pragmatic solution as none of the native PHP *sort method appear to sort strings that
// differ only in case (e.g. ["allowWLAN", "allowWlan"] is expected to have the lower version first).
$keys = array_keys($array);
(new Collator('root'))->asort($keys); // Use Unicode Collation Algorithm (UCA).
$original = $array;
$array = [];
foreach ($keys as $key) {
$array[$key] = $original[$key];
}
}
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);
}
}
@@ -0,0 +1,398 @@
<?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 mod_quiz\quiz_settings;
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 seb_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_settings $quiz A quiz object containing all information pertaining to current quiz. */
private $quiz;
/** @var seb_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_settings $quiz The details of the quiz.
*/
public function __construct(quiz_settings $quiz) {
$this->quiz = $quiz;
$this->context = context_module::instance($quiz->get_cmid());
$this->quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->get_quizid());
$this->validconfigkey = seb_quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());
}
/**
* Validate browser exam key. It will validate a provided browser exam key if provided, then will fall back to checking
* the header.
*
* @param string|null $browserexamkey Optional. Can validate a provided key, or will fall back to checking header.
* @param string|null $url Optionally provide URL of page to validate.
* @return bool
*/
public function validate_browser_exam_key(?string $browserexamkey = null, ?string $url = null): bool {
if (!$this->should_validate_browser_exam_key()) {
// Browser exam key should not be checked, so do not prevent access.
return true;
}
if (!$this->is_allowed_browser_examkeys_configured()) {
return true; // If no browser exam keys, no check required.
}
if (empty($browserexamkey)) {
$browserexamkey = $this->get_received_browser_exam_key();
}
$validbrowserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
// If the Browser Exam Key header isn't present, prevent access.
if (is_null($browserexamkey)) {
return false;
}
return $this->check_browser_exam_keys($validbrowserexamkeys, $browserexamkey, $url);
}
/**
* Validate a config key. It will check a provided config key if provided then will fall back to checking config
* key in header.
*
* @param string|null $configkey Optional. Can validate a provided key, or will fall back to checking header.
* @param string|null $url URL of page to validate.
* @return bool
*/
public function validate_config_key(?string $configkey = null, ?string $url = null): bool {
if (!$this->should_validate_config_key()) {
// Config key should not be checked, so do not prevent access.
return true;
}
// 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($configkey)) {
$configkey = $this->get_received_config_key();
}
if (empty($this->validconfigkey)) {
return false; // No config key has been saved.
}
if (is_null($configkey)) {
return false;
}
// Check if there is a valid config key supplied in the header.
return $this->check_key($this->validconfigkey, $configkey, $url);
}
/**
* 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->should_validate_basic_header()) {
// Config key should not be checked, so do not prevent access.
return true;
}
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 \mod_quiz\quiz_settings
*/
public function get_quiz(): quiz_settings {
return $this->quiz;
}
/**
* Check that at least one browser exam key exists in the quiz settings.
*
* @return bool True if one or more keys are set in quiz settings.
*/
private function is_allowed_browser_examkeys_configured(): bool {
return !empty($this->quizsettings->get('allowedbrowserexamkeys'));
}
/**
* 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.
* @param string|null $url URL of page to validate.
* @return bool True if the hash matches.
*/
private function check_browser_exam_keys(array $keys, string $header, ?string $url = null): bool {
foreach ($keys as $key) {
if ($this->check_key($key, $header, $url)) {
return true;
}
}
return false;
}
/**
* Check the hash from the request header against a single permitted key.
*
* @param string $validkey An allowed key.
* @param string $key The value of X-SafeExamBrowser-RequestHash, X-SafeExamBrowser-ConfigKeyHash or a provided key to check.
* @param string|null $url URL of page to validate.
* @return bool True if the hash matches.
*/
private function check_key(string $validkey, string $key, ?string $url = null): bool {
if (empty($url)) {
$url = $this->get_this_page_url();
}
return hash('sha256', $url . $validkey) === $key;
}
/**
* Returns Safe Exam Browser Config Key hash.
*
* @return string|null
*/
public function get_received_config_key(): ?string {
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(): ?string {
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,
]);
}
/**
* Set session access for quiz.
*
* @param bool $accessallowed
*/
public function set_session_access(bool $accessallowed): void {
global $SESSION;
if (!isset($SESSION->quizaccess_seb_access)) {
$SESSION->quizaccess_seb_access = [];
}
$SESSION->quizaccess_seb_access[$this->quiz->get_cmid()] = $accessallowed;
}
/**
* Check session access for quiz if already set.
*
* @return bool
*/
public function validate_session_access(): bool {
global $SESSION;
return !empty($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]);
}
/**
* Unset the global session access variable for this quiz.
*/
public function clear_session_access(): void {
global $SESSION;
unset($SESSION->quizaccess_seb_access[$this->quiz->get_cmid()]);
}
/**
* Redirect to SEB config link. This will force Safe Exam Browser to be reconfigured.
*/
public function redirect_to_seb_config_link(): void {
global $PAGE;
$seblink = \quizaccess_seb\link_generator::get_link($this->quiz->get_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
*/
public function should_redirect_to_seb_config_link(): bool {
// We check if there is an existing config key header. If there is none, we assume that
// the SEB application is not using header verification so auto redirect should not proceed.
$haskeyinheader = !is_null($this->get_received_config_key());
return $this->is_using_seb()
&& get_config('quizaccess_seb', 'autoreconfigureseb')
&& $haskeyinheader;
}
}
@@ -0,0 +1,671 @@
<?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 seb_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\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->raw_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->plist->set_or_update_value('allowPreferencesWindow', 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_bool_setting('allowuserquitseb');
$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_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 modern WebView and JS API if the SEB version supports it.
// Documentation: https://safeexambrowser.org/developer/seb-config-key.html .
// "Set the key browserWindowWebView to the policy "Prefer Modern" (value 3)".
$this->plist->set_or_update_value('browserWindowWebView', new CFNumber(3));
}
/**
* 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])) {
$this->process_bool_setting($setting);
}
}
}
/**
* Process provided single bool setting.
*
* @param string $name Setting name matching one from self::get_bool_seb_setting_map.
*/
private function process_bool_setting(string $name) {
$map = $this->get_bool_seb_setting_map();
if (!isset($map[$name])) {
throw new \coding_exception('Provided setting name can not be found in known bool settings');
}
$enabled = $this->raw_get($name) == 1 ? true : false;
$this->plist->set_or_update_value($map[$name], 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));
} else if (!is_null($this->plist->get_element_value('hashedQuitPassword'))) {
$this->plist->delete_element('hashedQuitPassword');
}
}
/**
* Sets the quitURL if found in the seb_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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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 = seb_quiz_settings::get_records(['templateid' => $this->get('id')]);
$result = empty($settings);
}
return $result;
}
}
@@ -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();
}
}