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
+27
View File
@@ -0,0 +1,27 @@
<?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/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
@@ -0,0 +1,46 @@
<?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_delaybetweenattempts.
*
* @package quizaccess_delaybetweenattempts
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quizaccess_delaybetweenattempts\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quizaccess_delaybetweenattempts implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,33 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_delaybetweenattempts plugin.
*
* @package quizaccess
* @subpackage delaybetweenattempts
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'Delay between attempts quiz access rule';
$string['privacy:metadata'] = 'The Delay between attempts quiz access rule plugin does not store any personal data.';
$string['youcannotwait'] = 'This quiz closes before you will be allowed to start another attempt.';
$string['youmustwait'] = 'You must wait before you may re-attempt this quiz. You will be allowed to start another attempt after {$a}.';
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_settings;
/**
* A rule imposing the delay between attempts settings.
*
* @package quizaccess_delaybetweenattempts
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_delaybetweenattempts extends access_rule_base {
public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
if (empty($quizobj->get_quiz()->delay1) && empty($quizobj->get_quiz()->delay2)) {
return null;
}
return new self($quizobj, $timenow);
}
public function prevent_new_attempt($numprevattempts, $lastattempt) {
if ($this->quiz->attempts > 0 && $numprevattempts >= $this->quiz->attempts) {
// No more attempts allowed anyway.
return false;
}
if ($this->quiz->timeclose != 0 && $this->timenow > $this->quiz->timeclose) {
// No more attempts allowed anyway.
return false;
}
$nextstarttime = $this->compute_next_start_time($numprevattempts, $lastattempt);
if ($this->timenow < $nextstarttime) {
if ($this->quiz->timeclose == 0 || $nextstarttime <= $this->quiz->timeclose) {
return get_string('youmustwait', 'quizaccess_delaybetweenattempts',
userdate($nextstarttime));
} else {
return get_string('youcannotwait', 'quizaccess_delaybetweenattempts');
}
}
return false;
}
/**
* Compute the next time a student would be allowed to start an attempt,
* according to this rule.
* @param int $numprevattempts number of previous attempts.
* @param stdClass $lastattempt information about the previous attempt.
* @return number the time.
*/
protected function compute_next_start_time($numprevattempts, $lastattempt) {
if ($numprevattempts == 0) {
return 0;
}
$lastattemptfinish = $lastattempt->timefinish;
if ($this->quiz->timelimit > 0) {
$lastattemptfinish = min($lastattemptfinish,
$lastattempt->timestart + $this->quiz->timelimit);
}
if ($numprevattempts == 1 && $this->quiz->delay1) {
return $lastattemptfinish + $this->quiz->delay1;
} else if ($numprevattempts > 1 && $this->quiz->delay2) {
return $lastattemptfinish + $this->quiz->delay2;
}
return 0;
}
public function is_finished($numprevattempts, $lastattempt) {
$nextstarttime = $this->compute_next_start_time($numprevattempts, $lastattempt);
return $this->timenow <= $nextstarttime &&
$this->quiz->timeclose != 0 && $nextstarttime >= $this->quiz->timeclose;
}
}
@@ -0,0 +1,286 @@
<?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_delaybetweenattempts;
use mod_quiz\quiz_settings;
use quizaccess_delaybetweenattempts;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/accessrule/delaybetweenattempts/rule.php');
/**
* Unit tests for the quizaccess_delaybetweenattempts plugin.
*
* @package quizaccess_delaybetweenattempts
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rule_test extends \basic_testcase {
public function test_just_first_delay(): void {
$quiz = new \stdClass();
$quiz->attempts = 3;
$quiz->timelimit = 0;
$quiz->delay1 = 1000;
$quiz->delay2 = 0;
$quiz->timeclose = 0;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->timefinish = 10000;
$rule = new quizaccess_delaybetweenattempts($quizobj, 10000);
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 9000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 9001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
}
public function test_just_second_delay(): void {
$quiz = new \stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 0;
$quiz->delay1 = 0;
$quiz->delay2 = 1000;
$quiz->timeclose = 0;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->timefinish = 10000;
$rule = new quizaccess_delaybetweenattempts($quizobj, 10000);
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertEquals($rule->prevent_new_attempt(3, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$attempt->timefinish = 9000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timefinish = 9001;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertEquals($rule->prevent_new_attempt(4, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
}
public function test_just_both_delays(): void {
$quiz = new \stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 0;
$quiz->delay1 = 2000;
$quiz->delay2 = 1000;
$quiz->timeclose = 0;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->timefinish = 10000;
$rule = new quizaccess_delaybetweenattempts($quizobj, 10000);
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(12000)));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertEquals($rule->prevent_new_attempt(3, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$attempt->timefinish = 8000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timefinish = 8001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(4, $attempt));
$attempt->timefinish = 9000;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timefinish = 9001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11001)));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertEquals($rule->prevent_new_attempt(4, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
}
public function test_with_close_date(): void {
$quiz = new \stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 0;
$quiz->delay1 = 2000;
$quiz->delay2 = 1000;
$quiz->timeclose = 15000;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->timefinish = 13000;
$rule = new quizaccess_delaybetweenattempts($quizobj, 10000);
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
$attempt->timefinish = 13000;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(15000)));
$attempt->timefinish = 13001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youcannotwait', 'quizaccess_delaybetweenattempts'));
$attempt->timefinish = 14000;
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(15000)));
$attempt->timefinish = 14001;
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youcannotwait', 'quizaccess_delaybetweenattempts'));
$rule = new quizaccess_delaybetweenattempts($quizobj, 15000);
$attempt->timefinish = 13000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$attempt->timefinish = 13001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youcannotwait', 'quizaccess_delaybetweenattempts'));
$attempt->timefinish = 14000;
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 14001;
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youcannotwait', 'quizaccess_delaybetweenattempts'));
$rule = new quizaccess_delaybetweenattempts($quizobj, 15001);
$attempt->timefinish = 13000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$attempt->timefinish = 13001;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$attempt->timefinish = 14000;
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 14001;
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
}
public function test_time_limit_and_overdue(): void {
$quiz = new \stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 100;
$quiz->delay1 = 2000;
$quiz->delay2 = 1000;
$quiz->timeclose = 0;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->timestart = 9900;
$attempt->timefinish = 10100;
$rule = new quizaccess_delaybetweenattempts($quizobj, 10000);
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(12000)));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertEquals($rule->prevent_new_attempt(3, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$attempt->timestart = 7950;
$attempt->timefinish = 8000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timestart = 7950;
$attempt->timefinish = 8001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(4, $attempt));
$attempt->timestart = 8950;
$attempt->timefinish = 9000;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timestart = 8950;
$attempt->timefinish = 9001;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11001)));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertEquals($rule->prevent_new_attempt(4, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$attempt->timestart = 8900;
$attempt->timefinish = 9100;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timestart = 8901;
$attempt->timefinish = 9100;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(11001)));
$this->assertEquals($rule->prevent_new_attempt(2, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
$this->assertEquals($rule->prevent_new_attempt(4, $attempt),
get_string('youmustwait', 'quizaccess_delaybetweenattempts', userdate(10001)));
}
}
@@ -0,0 +1,32 @@
<?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/>.
/**
* Version information for the quizaccess_delaybetweenattempts plugin.
*
* @package quizaccess
* @subpackage delaybetweenattempts
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quizaccess_delaybetweenattempts';
@@ -0,0 +1,46 @@
<?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_ipaddress.
*
* @package quizaccess_ipaddress
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quizaccess_ipaddress\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quizaccess_ipaddress implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_ipaddress plugin.
*
* @package quizaccess
* @subpackage ipaddress
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'IP address quiz access rule';
$string['privacy:metadata'] = 'The IP address quiz access rule plugin does not store any personal data.';
$string['subnetwrong'] = 'This quiz is only accessible from certain locations, and this computer is not on the allowed list.';
+44
View File
@@ -0,0 +1,44 @@
<?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/>.
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_settings;
/**
* A rule implementing the ipaddress check against the ->subnet setting.
*
* @package quizaccess_ipaddress
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_ipaddress extends access_rule_base {
public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
if (empty($quizobj->get_quiz()->subnet)) {
return null;
}
return new self($quizobj, $timenow);
}
public function prevent_access() {
if (address_in_subnet(getremoteaddr(), $this->quiz->subnet)) {
return false;
} else {
return get_string('subnetwrong', 'quizaccess_ipaddress');
}
}
}
@@ -0,0 +1,69 @@
<?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_ipaddress;
use mod_quiz\quiz_settings;
use quizaccess_ipaddress;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/accessrule/ipaddress/rule.php');
/**
* Unit tests for the quizaccess_ipaddress plugin.
*
* @package quizaccess_ipaddress
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rule_test extends \basic_testcase {
public function test_ipaddress_access_rule(): void {
$quiz = new \stdClass();
$attempt = new \stdClass();
$cm = new \stdClass();
$cm->id = 0;
// Test the allowed case by getting the user's IP address. However, this
// does not always work, for example using the mac install package on my laptop.
$quiz->subnet = getremoteaddr(null);
if (!empty($quiz->subnet)) {
$quizobj = new quiz_settings($quiz, $cm, null);
$rule = new quizaccess_ipaddress($quizobj, 0);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->description());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
$quiz->subnet = '0.0.0.0';
$quizobj = new quiz_settings($quiz, $cm, null);
$rule = new quizaccess_ipaddress($quizobj, 0);
$this->assertNotEmpty($rule->prevent_access());
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Version information for the quizaccess_ipaddress plugin.
*
* @package quizaccess
* @subpackage ipaddress
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quizaccess_ipaddress';
@@ -0,0 +1,46 @@
<?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_numattempts.
*
* @package quizaccess_numattempts
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quizaccess_numattempts\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quizaccess_numattempts implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_numattempts plugin.
*
* @package quizaccess
* @subpackage numattempts
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['attemptsallowedn'] = 'Attempts allowed: {$a}';
$string['pluginname'] = 'Number of attempts quiz access rule';
$string['privacy:metadata'] = 'The Number of attempts quiz access rule plugin does not store any personal data.';
+52
View File
@@ -0,0 +1,52 @@
<?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/>.
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_settings;
/**
* A rule controlling the number of attempts allowed.
*
* @package quizaccess_numattempts
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_numattempts extends access_rule_base {
public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
if ($quizobj->get_num_attempts_allowed() == 0) {
return null;
}
return new self($quizobj, $timenow);
}
public function description() {
return get_string('attemptsallowedn', 'quizaccess_numattempts', $this->quiz->attempts);
}
public function prevent_new_attempt($numprevattempts, $lastattempt) {
if ($numprevattempts >= $this->quiz->attempts) {
return get_string('nomoreattempts', 'quiz');
}
return false;
}
public function is_finished($numprevattempts, $lastattempt) {
return $numprevattempts >= $this->quiz->attempts;
}
}
@@ -0,0 +1,65 @@
<?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_numattempts;
use mod_quiz\quiz_settings;
use quizaccess_numattempts;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/accessrule/numattempts/rule.php');
/**
* Unit tests for the quizaccess_numattempts plugin.
*
* @package quizaccess_numattempts
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rule_test extends \basic_testcase {
public function test_num_attempts_access_rule(): void {
$quiz = new \stdClass();
$quiz->attempts = 3;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$rule = new quizaccess_numattempts($quizobj, 0);
$attempt = new \stdClass();
$this->assertEquals($rule->description(),
get_string('attemptsallowedn', 'quizaccess_numattempts', 3));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertEquals($rule->prevent_new_attempt(3, $attempt),
get_string('nomoreattempts', 'quiz'));
$this->assertEquals($rule->prevent_new_attempt(666, $attempt),
get_string('nomoreattempts', 'quiz'));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->is_finished(2, $attempt));
$this->assertTrue($rule->is_finished(3, $attempt));
$this->assertTrue($rule->is_finished(666, $attempt));
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
}
@@ -0,0 +1,32 @@
<?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/>.
/**
* Version information for the quizaccess_numattempts plugin.
*
* @package quizaccess
* @subpackage numattempts
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quizaccess_numattempts';
@@ -0,0 +1,46 @@
<?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_offlineattempts.
*
* @package quizaccess_offlineattempts
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quizaccess_offlineattempts\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quizaccess_offlineattempts implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_offlineattempts plugin.
*
* @package quizaccess_offlineattempts
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['allowofflineattempts'] = 'Allow quiz to be attempted offline using the mobile app';
$string['allowofflineattempts_help'] = 'If enabled, a mobile app user can download the quiz and attempt it offline.
Note: It is not possible for a quiz to be attempted offline if it has a time limit, or requires a network address, or uses any question behaviour other than deferred feedback (with or without CBM), or uses sequential navigation.';
$string['confirmdatasaved'] = 'I confirm that I do not have any unsaved work on a mobile device.';
$string['mobileapp'] = 'Mobile app';
$string['offlineattemptserror'] = 'It is not possible for a quiz to be attempted offline if it has a time limit, or requires a network address, or uses any question behaviour other than deferred feedback (with or without CBM), or uses sequential navigation.';
$string['offlinedatamessage'] = 'You have worked on this attempt using a mobile device. Data was last saved to this site {$a} ago.';
$string['pleaseconfirm'] = 'Please check and confirm that you do not have any unsaved work.';
$string['pluginname'] = 'Offline attempts access rule';
$string['privacy:metadata'] = 'The Offline attempts quiz access rule plugin does not store any personal data.';
@@ -0,0 +1,134 @@
<?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/>.
use mod_quiz\form\preflight_check_form;
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_settings;
/**
* A rule implementing the offlineattempts check.
*
* @package quizaccess_offlineattempts
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.2
*/
class quizaccess_offlineattempts extends access_rule_base {
public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
global $CFG;
// If mobile services are off, the user won't be able to use any external app.
if (empty($CFG->enablemobilewebservice) or empty($quizobj->get_quiz()->allowofflineattempts)) {
return null;
}
return new self($quizobj, $timenow);
}
public function is_preflight_check_required($attemptid) {
global $SESSION, $DB;
// First, check if the user did something offline.
if (!empty($attemptid)) {
$timemodifiedoffline = $DB->get_field('quiz_attempts', 'timemodifiedoffline', ['id' => $attemptid]);
if (empty($timemodifiedoffline)) {
return false;
}
return empty($SESSION->offlineattemptscheckedquizzes[$this->quiz->id]);
} else {
// Starting a new attempt, we don't have to check anything here.
return false;
}
}
public function add_preflight_check_form_fields(preflight_check_form $quizform,
MoodleQuickForm $mform, $attemptid) {
global $DB;
$timemodifiedoffline = $DB->get_field('quiz_attempts', 'timemodifiedoffline', ['id' => $attemptid]);
$lasttime = format_time(time() - $timemodifiedoffline);
$mform->addElement('header', 'offlineattemptsheader', get_string('mobileapp', 'quizaccess_offlineattempts'));
$mform->addElement('static', 'offlinedatamessage', '',
get_string('offlinedatamessage', 'quizaccess_offlineattempts', $lasttime));
$mform->addElement('advcheckbox', 'confirmdatasaved', null,
get_string('confirmdatasaved', 'quizaccess_offlineattempts'));
}
public function validate_preflight_check($data, $files, $errors, $attemptid) {
// The user confirmed that he doesn't have unsaved work.
if (!empty($data['confirmdatasaved'])) {
return $errors;
}
$errors['confirmdatasaved'] = get_string('pleaseconfirm', 'quizaccess_offlineattempts');
return $errors;
}
public function notify_preflight_check_passed($attemptid) {
global $SESSION;
$SESSION->offlineattemptscheckedquizzes[$this->quiz->id] = true;
}
public function current_attempt_finished() {
global $SESSION;
// Clear the flag in the session that says that the user has already agreed to the notice.
if (!empty($SESSION->offlineattemptscheckedquizzes[$this->quiz->id])) {
unset($SESSION->offlineattemptscheckedquizzes[$this->quiz->id]);
}
}
public static function add_settings_form_fields(
mod_quiz_mod_form $quizform, MoodleQuickForm $mform) {
global $CFG;
// Allow to enable the access rule only if the Mobile services are enabled.
if ($CFG->enablemobilewebservice) {
$mform->addElement('selectyesno', 'allowofflineattempts',
get_string('allowofflineattempts', 'quizaccess_offlineattempts'));
$mform->addHelpButton('allowofflineattempts', 'allowofflineattempts', 'quizaccess_offlineattempts');
$mform->setDefault('allowofflineattempts', 0);
$mform->setAdvanced('allowofflineattempts');
$mform->disabledIf('allowofflineattempts', 'timelimit[number]', 'neq', 0);
$mform->disabledIf('allowofflineattempts', 'subnet', 'neq', '');
$mform->disabledIf('allowofflineattempts', 'navmethod', 'eq', 'sequential');
}
}
public static function validate_settings_form_fields(array $errors,
array $data, $files, mod_quiz_mod_form $quizform) {
global $CFG;
if ($CFG->enablemobilewebservice) {
// Do not allow offline attempts if:
// - The quiz uses a timer.
// - The quiz is restricted by subnet.
// - The question behaviour is not deferred feedback or deferred feedback with CBM.
// - The quiz uses the sequential navigation.
if (!empty($data['allowofflineattempts']) &&
(!empty($data['timelimit']) || !empty($data['subnet']) ||
$data['navmethod'] === 'sequential' ||
($data['preferredbehaviour'] != 'deferredfeedback' && $data['preferredbehaviour'] != 'deferredcbm'))) {
$errors['allowofflineattempts'] = get_string('offlineattemptserror', 'quizaccess_offlineattempts');
}
}
return $errors;
}
}
@@ -0,0 +1,51 @@
<?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_offlineattempts;
use mod_quiz\quiz_settings;
use quizaccess_offlineattempts;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/accessrule/offlineattempts/rule.php');
/**
* Unit tests for the quizaccess_offlineattempts plugin.
*
* @package quizaccess_offlineattempts
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rule_test extends \basic_testcase {
public function test_offlineattempts_access_rule(): void {
$quiz = new \stdClass();
$quiz->allowofflineattempts = 1;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$rule = new quizaccess_offlineattempts($quizobj, 0);
$attempt = new \stdClass();
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
}
@@ -0,0 +1,29 @@
<?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/>.
/**
* Version information for the quizaccess_offlineattempts plugin.
*
* @package quizaccess_offlineattempts
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quizaccess_offlineattempts';
@@ -0,0 +1,46 @@
<?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_openclosedate.
*
* @package quizaccess_openclosedate
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quizaccess_openclosedate\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quizaccess_openclosedate implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,28 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_openclosedate plugin.
*
* @package quizaccess
* @subpackage openclosedate
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['notavailable'] = 'This quiz is currently not available.';
$string['pluginname'] = 'Open and close date access rule';
$string['privacy:metadata'] = 'The Open and close date quiz access rule plugin does not store any personal data.';
@@ -0,0 +1,85 @@
<?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/>.
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_settings;
/**
* A rule enforcing open and close dates.
*
* @package quizaccess_openclosedate
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_openclosedate extends access_rule_base {
public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
// This rule is always used, even if the quiz has no open or close date.
return new self($quizobj, $timenow);
}
public function prevent_access() {
$message = get_string('notavailable', 'quizaccess_openclosedate');
if ($this->timenow < $this->quiz->timeopen) {
return $message;
}
if (!$this->quiz->timeclose) {
return false;
}
if ($this->timenow <= $this->quiz->timeclose) {
return false;
}
if ($this->quiz->overduehandling != 'graceperiod') {
return $message;
}
if ($this->timenow <= $this->quiz->timeclose + $this->quiz->graceperiod) {
return false;
}
return $message;
}
public function is_finished($numprevattempts, $lastattempt) {
return $this->quiz->timeclose && $this->timenow > $this->quiz->timeclose;
}
public function end_time($attempt) {
if ($this->quiz->timeclose) {
return $this->quiz->timeclose;
}
return false;
}
public function time_left_display($attempt, $timenow) {
// If this is a teacher preview after the close date, do not show
// the time.
if ($attempt->preview && $timenow > $this->quiz->timeclose) {
return false;
}
// Otherwise, return to the time left until the close date, providing that is
// less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
$endtime = $this->end_time($attempt);
if ($endtime !== false && $timenow > $endtime - QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
return $endtime - $timenow;
}
return false;
}
}
@@ -0,0 +1,190 @@
<?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_openclosedate;
use mod_quiz\quiz_settings;
use quizaccess_openclosedate;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/accessrule/openclosedate/rule.php');
/**
* Unit tests for the quizaccess_openclosedate plugin.
*
* @package quizaccess_openclosedate
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rule_test extends \basic_testcase {
public function test_no_dates(): void {
$quiz = new \stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 0;
$quiz->overduehandling = 'autosubmit';
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->preview = 0;
$rule = new quizaccess_openclosedate($quizobj, 10000);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 10000));
$this->assertFalse($rule->time_left_display($attempt, 0));
$rule = new quizaccess_openclosedate($quizobj, 0);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
public function test_start_date(): void {
$quiz = new \stdClass();
$quiz->timeopen = 10000;
$quiz->timeclose = 0;
$quiz->overduehandling = 'autosubmit';
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->preview = 0;
$rule = new quizaccess_openclosedate($quizobj, 9999);
$this->assertEquals($rule->prevent_access(),
get_string('notavailable', 'quizaccess_openclosedate'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
$rule = new quizaccess_openclosedate($quizobj, 10000);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
public function test_close_date(): void {
$quiz = new \stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 20000;
$quiz->overduehandling = 'autosubmit';
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->preview = 0;
$rule = new quizaccess_openclosedate($quizobj, 20000);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertEquals($rule->end_time($attempt), 20000);
$this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
$this->assertEquals($rule->time_left_display($attempt, 19900), 100);
$this->assertEquals($rule->time_left_display($attempt, 20000), 0);
$this->assertEquals($rule->time_left_display($attempt, 20100), -100);
$rule = new quizaccess_openclosedate($quizobj, 20001);
$this->assertEquals($rule->prevent_access(),
get_string('notavailable', 'quizaccess_openclosedate'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertTrue($rule->is_finished(0, $attempt));
$this->assertEquals($rule->end_time($attempt), 20000);
$this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
$this->assertEquals($rule->time_left_display($attempt, 19900), 100);
$this->assertEquals($rule->time_left_display($attempt, 20000), 0);
$this->assertEquals($rule->time_left_display($attempt, 20100), -100);
}
public function test_both_dates(): void {
$quiz = new \stdClass();
$quiz->timeopen = 10000;
$quiz->timeclose = 20000;
$quiz->overduehandling = 'autosubmit';
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->preview = 0;
$rule = new quizaccess_openclosedate($quizobj, 9999);
$this->assertEquals($rule->prevent_access(),
get_string('notavailable', 'quizaccess_openclosedate'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$rule = new quizaccess_openclosedate($quizobj, 10000);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$rule = new quizaccess_openclosedate($quizobj, 20000);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$rule = new quizaccess_openclosedate($quizobj, 20001);
$this->assertEquals($rule->prevent_access(),
get_string('notavailable', 'quizaccess_openclosedate'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertTrue($rule->is_finished(0, $attempt));
$this->assertEquals($rule->end_time($attempt), 20000);
$this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
$this->assertEquals($rule->time_left_display($attempt, 19900), 100);
$this->assertEquals($rule->time_left_display($attempt, 20000), 0);
$this->assertEquals($rule->time_left_display($attempt, 20100), -100);
}
public function test_close_date_with_overdue(): void {
$quiz = new \stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 20000;
$quiz->overduehandling = 'graceperiod';
$quiz->graceperiod = 1000;
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$attempt = new \stdClass();
$attempt->preview = 0;
$rule = new quizaccess_openclosedate($quizobj, 20000);
$this->assertFalse($rule->prevent_access());
$rule = new quizaccess_openclosedate($quizobj, 20001);
$this->assertFalse($rule->prevent_access());
$rule = new quizaccess_openclosedate($quizobj, 21000);
$this->assertFalse($rule->prevent_access());
$rule = new quizaccess_openclosedate($quizobj, 21001);
$this->assertEquals($rule->prevent_access(),
get_string('notavailable', 'quizaccess_openclosedate'));
}
}
@@ -0,0 +1,32 @@
<?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/>.
/**
* Version information for the quizaccess_openclosedate plugin.
*
* @package quizaccess
* @subpackage openclosedate
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quizaccess_openclosedate';
@@ -0,0 +1,46 @@
<?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_password.
*
* @package quizaccess_password
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quizaccess_password\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quizaccess_password implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,33 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_password plugin.
*
* @package quizaccess
* @subpackage password
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['passworderror'] = 'The password entered was incorrect';
$string['pluginname'] = 'Password quiz access rule';
$string['privacy:metadata'] = 'The Password quiz access rule plugin does not store any personal data.';
$string['quizpassword'] = 'Quiz password';
$string['requirepasswordmessage'] = 'To attempt this quiz you need to know the quiz password';
+92
View File
@@ -0,0 +1,92 @@
<?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/>.
use mod_quiz\form\preflight_check_form;
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_settings;
/**
* A rule implementing the password check.
*
* @package quizaccess_password
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_password extends access_rule_base {
public static function make(quiz_settings $quizobj, $timenow, $canignoretimelimits) {
if (empty($quizobj->get_quiz()->password)) {
return null;
}
return new self($quizobj, $timenow);
}
public function description() {
return get_string('requirepasswordmessage', 'quizaccess_password');
}
public function is_preflight_check_required($attemptid) {
global $SESSION;
return empty($SESSION->passwordcheckedquizzes[$this->quiz->id]);
}
public function add_preflight_check_form_fields(preflight_check_form $quizform,
MoodleQuickForm $mform, $attemptid) {
$mform->addElement('header', 'passwordheader', get_string('password'));
$mform->addElement('static', 'passwordmessage', '',
get_string('requirepasswordmessage', 'quizaccess_password'));
// Don't use the 'proper' field name of 'password' since that get's
// Firefox's password auto-complete over-excited.
$mform->addElement('passwordunmask', 'quizpassword',
get_string('quizpassword', 'quizaccess_password'), ['autofocus' => 'true']);
}
public function validate_preflight_check($data, $files, $errors, $attemptid) {
$enteredpassword = $data['quizpassword'];
if (strcmp($this->quiz->password, $enteredpassword) === 0) {
return $errors; // Password is OK.
} else if (isset($this->quiz->extrapasswords)) {
// Group overrides may have additional passwords.
foreach ($this->quiz->extrapasswords as $password) {
if (strcmp($password, $enteredpassword) === 0) {
return $errors; // Password is OK.
}
}
}
$errors['quizpassword'] = get_string('passworderror', 'quizaccess_password');
return $errors;
}
public function notify_preflight_check_passed($attemptid) {
global $SESSION;
$SESSION->passwordcheckedquizzes[$this->quiz->id] = true;
}
public function current_attempt_finished() {
global $SESSION;
// Clear the flag in the session that says that the user has already
// entered the password for this quiz.
if (!empty($SESSION->passwordcheckedquizzes[$this->quiz->id])) {
unset($SESSION->passwordcheckedquizzes[$this->quiz->id]);
}
}
}
@@ -0,0 +1,54 @@
<?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_password;
use mod_quiz\quiz_settings;
use quizaccess_password;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/accessrule/password/rule.php');
/**
* Unit tests for the quizaccess_password plugin.
*
* @package quizaccess_password
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rule_test extends \basic_testcase {
public function test_password_access_rule(): void {
$quiz = new \stdClass();
$quiz->password = 'frog';
$cm = new \stdClass();
$cm->id = 0;
$quizobj = new quiz_settings($quiz, $cm, null);
$rule = new quizaccess_password($quizobj, 0);
$attempt = new \stdClass();
$this->assertFalse($rule->prevent_access());
$this->assertEquals($rule->description(),
get_string('requirepasswordmessage', 'quizaccess_password'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->end_time($attempt));
$this->assertFalse($rule->time_left_display($attempt, 0));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Version information for the quizaccess_password plugin.
*
* @package quizaccess
* @subpackage password
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quizaccess_password';
@@ -0,0 +1,3 @@
define("quizaccess_seb/managetemplates",["jquery","core/ajax","core/str","core/notification"],(function($,ajax,str,notification){var manager={removeTemplate:function(e){e.preventDefault();var targetUrl=$(e.currentTarget).attr("href");str.get_strings([{key:"confirmtemplateremovaltitle",component:"quizaccess_seb"},{key:"confirmtemplateremovalquestion",component:"quizaccess_seb"},{key:"yes",component:"moodle"},{key:"no",component:"moodle"}]).then((function(s){notification.confirm(s[0],s[1],s[2],s[3],(function(){window.location=targetUrl}))})).catch()},setup:function(){$("body").delegate('[data-action="delete"]',"click",manager.removeTemplate)}};return{setup:manager.setup}}));
//# sourceMappingURL=managetemplates.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"managetemplates.min.js","sources":["../src/managetemplates.js"],"sourcesContent":["/**\n * Template management code.\n *\n * @module quizaccess_seb/managetemplates\n * @copyright 2020 Dmitrii Metelkin <dmitriim@catalyst-au.net>\n */\ndefine(\n ['jquery', 'core/ajax', 'core/str', 'core/notification'],\n function($, ajax, str, notification) {\n var manager = {\n /**\n * Confirm removal of the specified template.\n *\n * @method removeTemplate\n * @param {EventFacade} e The EventFacade\n */\n removeTemplate: function(e) {\n e.preventDefault();\n var targetUrl = $(e.currentTarget).attr('href');\n str.get_strings([\n {\n key: 'confirmtemplateremovaltitle',\n component: 'quizaccess_seb'\n },\n {\n key: 'confirmtemplateremovalquestion',\n component: 'quizaccess_seb'\n },\n {\n key: 'yes',\n component: 'moodle'\n },\n {\n key: 'no',\n component: 'moodle'\n }\n ])\n .then(function(s) {\n notification.confirm(s[0], s[1], s[2], s[3], function() {\n window.location = targetUrl;\n });\n\n return;\n })\n .catch();\n },\n\n /**\n * Setup the template management UI.\n *\n * @method setup\n */\n setup: function() {\n $('body').delegate('[data-action=\"delete\"]', 'click', manager.removeTemplate);\n }\n };\n\n return /** @alias module:quizaccess_seb/managetemplates */ {\n /**\n * Setup the template management UI.\n *\n * @method setup\n */\n setup: manager.setup\n };\n });\n"],"names":["define","$","ajax","str","notification","manager","removeTemplate","e","preventDefault","targetUrl","currentTarget","attr","get_strings","key","component","then","s","confirm","window","location","catch","setup","delegate"],"mappings":"AAMAA,wCACI,CAAC,SAAU,YAAa,WAAY,sBACpC,SAASC,EAAGC,KAAMC,IAAKC,kBACfC,QAAU,CAOVC,eAAgB,SAASC,GACrBA,EAAEC,qBACEC,UAAYR,EAAEM,EAAEG,eAAeC,KAAK,QACxCR,IAAIS,YAAY,CACZ,CACIC,IAAY,8BACZC,UAAY,kBAEhB,CACID,IAAY,iCACZC,UAAY,kBAEhB,CACID,IAAY,MACZC,UAAY,UAEhB,CACID,IAAY,KACZC,UAAY,YAGnBC,MAAK,SAASC,GACXZ,aAAaa,QAAQD,EAAE,GAAIA,EAAE,GAAIA,EAAE,GAAIA,EAAE,IAAI,WACzCE,OAAOC,SAAWV,gBAKzBW,SAQLC,MAAO,WACHpB,EAAE,QAAQqB,SAAS,yBAA0B,QAASjB,QAAQC,wBAIX,CAMvDe,MAAOhB,QAAQgB"}
@@ -0,0 +1,11 @@
define("quizaccess_seb/validate_quiz_access",["exports","core/ajax","core/config","core/notification","quizaccess_seb/view"],(function(_exports,_ajax,_config,_notification,View){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Validate Safe Exam Browser access keys.
*
* @module quizaccess_seb/validate_quiz_access
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_config=_interopRequireDefault(_config),_notification=_interopRequireDefault(_notification),View=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(View),window.SafeExamBrowser=window.SafeExamBrowser||null;const safeExamBrowserKeysUpdated=function(cmid){let autoreconfigure=arguments.length>1&&void 0!==arguments[1]&&arguments[1];isQuizAccessValid(cmid).then((response=>(setTimeout(View.clearLoadingAlert,1e3),response.configkey&&response.browserexamkey?View.allowAccess():(!0===autoreconfigure&&!1===response.configkey&&reconfigureSafeExamBrowser(cmid),setTimeout(View.showValidationFailedModal,1e3)),response))).catch((err=>{_notification.default.exception(err)}))},isQuizAccessValid=cmid=>{const request={methodname:"quizaccess_seb_validate_quiz_keys",args:{cmid:cmid,url:window.location.href,configkey:window.SafeExamBrowser.security.configKey,browserexamkey:window.SafeExamBrowser.security.browserExamKey}};return _ajax.default.call([request])[0]},isKeyEmpty=key=>":"===key,reconfigureSafeExamBrowser=cmid=>{const redirecturl=_config.default.wwwroot.replace(/^http/i,"seb")+"/mod/quiz/accessrule/seb/config.php?cmid="+cmid;document.location.replace(redirecturl)};_exports.init=async function(cmid){let autoreconfigure=arguments.length>1&&void 0!==arguments[1]&&arguments[1];null!==window.SafeExamBrowser&&(await View.addLoadingAlert(),isKeyEmpty(window.SafeExamBrowser.security.configKey)&&isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)?window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated):safeExamBrowserKeysUpdated(cmid,autoreconfigure))}}));
//# sourceMappingURL=validate_quiz_access.min.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("quizaccess_seb/view",["exports","core/notification","core/templates","core/str","core/local/modal/alert"],(function(_exports,_notification,Templates,Str,_alert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Manage the quiz views.
*
* @module quizaccess_seb/view
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showValidationFailedModal=_exports.clearLoadingAlert=_exports.allowAccess=_exports.addLoadingAlert=void 0,_notification=_interopRequireDefault(_notification),Templates=_interopRequireWildcard(Templates),Str=_interopRequireWildcard(Str),_alert=_interopRequireDefault(_alert);const SELECTOR_MAIN="#region-main",SELECTOR_LOADING=".seb-loading",TEMPLATE_LOADING="quizaccess_seb/loading";_exports.allowAccess=()=>{window.location.reload()};_exports.addLoadingAlert=()=>Templates.render(TEMPLATE_LOADING,{}).then(((html,js)=>{const alertRegion=window.document.querySelector(SELECTOR_MAIN);return Templates.prependNodeContents(alertRegion,html,js)})).catch(_notification.default.exception);_exports.clearLoadingAlert=()=>{const alert=window.document.querySelector(SELECTOR_LOADING);alert&&Templates.replaceNode(alert,"","")};_exports.showValidationFailedModal=()=>{_alert.default.create({title:Str.get_string("sebkeysvalidationfailed","quizaccess_seb"),body:Str.get_string("invalidkeys","quizaccess_seb"),large:!1,show:!0}).catch(_notification.default.exception)}}));
//# sourceMappingURL=view.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"view.min.js","sources":["../src/view.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Manage the quiz views.\n *\n * @module quizaccess_seb/view\n * @author Andrew Madden <andrewmadden@catalyst-au.net>\n * @copyright 2021 Catalyst IT\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from \"core/notification\";\nimport * as Templates from \"core/templates\";\nimport * as Str from \"core/str\";\nimport ModalAlert from \"core/local/modal/alert\";\n\n/** @var SELECTOR List of CSS selectors. */\nconst SELECTOR = {\n MAIN: '#region-main',\n LOADING: '.seb-loading',\n};\n\n/** @var Template List of mustache templates. */\nconst TEMPLATE = {\n LOADING: 'quizaccess_seb/loading',\n};\n\n/**\n * Manages view when access has been granted.\n */\nexport const allowAccess = () => {\n window.location.reload();\n};\n\n/**\n * Add an alert to page to inform that Safe Exam Browser access is being checked.\n *\n * @return {Promise}\n */\nexport const addLoadingAlert = () => {\n return Templates.render(TEMPLATE.LOADING, {}).then((html, js) => {\n const alertRegion = window.document.querySelector(SELECTOR.MAIN);\n return Templates.prependNodeContents(alertRegion, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Remove the Safe Exam Browser access check alert from the page.\n */\nexport const clearLoadingAlert = () => {\n const alert = window.document.querySelector(SELECTOR.LOADING);\n if (alert) {\n Templates.replaceNode(alert, '', '');\n }\n};\n\n/**\n * Display validation failed modal.\n */\nexport const showValidationFailedModal = () => {\n ModalAlert.create({\n title: Str.get_string('sebkeysvalidationfailed', 'quizaccess_seb'),\n body: Str.get_string('invalidkeys', 'quizaccess_seb'),\n large: false,\n show: true,\n }).catch(Notification.exception);\n};\n"],"names":["SELECTOR","TEMPLATE","window","location","reload","Templates","render","then","html","js","alertRegion","document","querySelector","prependNodeContents","catch","Notification","exception","alert","replaceNode","create","title","Str","get_string","body","large","show"],"mappings":";;;;;;;;8VA8BMA,cACI,eADJA,iBAEO,eAIPC,iBACO,8CAMc,KACvBC,OAAOC,SAASC,mCAQW,IACpBC,UAAUC,OAAOL,iBAAkB,IAAIM,MAAK,CAACC,KAAMC,YAChDC,YAAcR,OAAOS,SAASC,cAAcZ,sBAC3CK,UAAUQ,oBAAoBH,YAAaF,KAAMC,OACzDK,MAAMC,sBAAaC,sCAMO,WACvBC,MAAQf,OAAOS,SAASC,cAAcZ,kBACxCiB,OACAZ,UAAUa,YAAYD,MAAO,GAAI,wCAOA,oBAC1BE,OAAO,CACdC,MAAOC,IAAIC,WAAW,0BAA2B,kBACjDC,KAAMF,IAAIC,WAAW,cAAe,kBACpCE,OAAO,EACPC,MAAM,IACPX,MAAMC,sBAAaC"}
@@ -0,0 +1,66 @@
/**
* Template management code.
*
* @module quizaccess_seb/managetemplates
* @copyright 2020 Dmitrii Metelkin <dmitriim@catalyst-au.net>
*/
define(
['jquery', 'core/ajax', 'core/str', 'core/notification'],
function($, ajax, str, notification) {
var manager = {
/**
* Confirm removal of the specified template.
*
* @method removeTemplate
* @param {EventFacade} e The EventFacade
*/
removeTemplate: function(e) {
e.preventDefault();
var targetUrl = $(e.currentTarget).attr('href');
str.get_strings([
{
key: 'confirmtemplateremovaltitle',
component: 'quizaccess_seb'
},
{
key: 'confirmtemplateremovalquestion',
component: 'quizaccess_seb'
},
{
key: 'yes',
component: 'moodle'
},
{
key: 'no',
component: 'moodle'
}
])
.then(function(s) {
notification.confirm(s[0], s[1], s[2], s[3], function() {
window.location = targetUrl;
});
return;
})
.catch();
},
/**
* Setup the template management UI.
*
* @method setup
*/
setup: function() {
$('body').delegate('[data-action="delete"]', 'click', manager.removeTemplate);
}
};
return /** @alias module:quizaccess_seb/managetemplates */ {
/**
* Setup the template management UI.
*
* @method setup
*/
setup: manager.setup
};
});
@@ -0,0 +1,121 @@
// 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/>.
/**
* Validate Safe Exam Browser access keys.
*
* @module quizaccess_seb/validate_quiz_access
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import Config from 'core/config';
import Notification from "core/notification";
import * as View from 'quizaccess_seb/view';
// SafeExamBrowser object will be automatically initialized if using the SafeExamBrowser application.
window.SafeExamBrowser = window.SafeExamBrowser || null;
/**
* Once the keys are fetched, action checking access.
*
* @param {init} cmid Value of course module id of the quiz.
* @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb.
*/
const safeExamBrowserKeysUpdated = (cmid, autoreconfigure = false) => {
// Action opening up the quiz.
isQuizAccessValid(cmid).then((response) => {
// Show the alert for an extra second to allow user to see it.
setTimeout(View.clearLoadingAlert, 1000);
if (response.configkey && response.browserexamkey) {
View.allowAccess();
} else {
// If autoreconfigureseb is enabled, attempt to reconfigure page with quiz settings.
if (autoreconfigure === true && response.configkey === false) {
reconfigureSafeExamBrowser(cmid);
}
setTimeout(View.showValidationFailedModal, 1000);
}
return response;
}).catch(err => {
Notification.exception(err);
});
};
/**
* Validate keys in Moodle backend.
*
* @param {init} cmid Value of course module id of the quiz.
* @return {Promise}
*/
const isQuizAccessValid = (cmid) => {
const request = {
methodname: 'quizaccess_seb_validate_quiz_keys',
args: {
cmid: cmid,
url: window.location.href,
configkey: window.SafeExamBrowser.security.configKey,
browserexamkey: window.SafeExamBrowser.security.browserExamKey
},
};
return Ajax.call([request])[0];
};
/**
* Check if the key is not yet set.
*
* @param {string} key config key or browser exam key.
* @return {boolean}
*/
const isKeyEmpty = (key) => {
// If the SafeExamBrowser object is defined, the default 'empty' value of the configKey and browserExamKey is ':'.
return key === ":";
};
/**
* Reload Safe Exam Browser with current quiz configuration.
*
* @param {init} cmid Value of course module id of the quiz.
*/
const reconfigureSafeExamBrowser = (cmid) => {
const domain = Config.wwwroot.replace(/^http/i, 'seb');
const redirecturl = domain + '/mod/quiz/accessrule/seb/config.php?cmid=' + cmid;
document.location.replace(redirecturl);
};
/**
* Initialize the process of fetching the keys.
*
* @param {init} cmid Value of course module id of the quiz.
* @param {boolean} autoreconfigure Value of Moodle setting: quizaccess_seb/autoreconfigureseb.
*/
export const init = async(cmid, autoreconfigure = false) => {
// If the SafeExamBrowser object is instantiated, try and use it to fetch the access keys.
if (window.SafeExamBrowser !== null) {
await View.addLoadingAlert();
// If the SEB keys are already set, we can call our callback directly.
if (!isKeyEmpty(window.SafeExamBrowser.security.configKey) || !isKeyEmpty(window.SafeExamBrowser.security.browserExamKey)) {
safeExamBrowserKeysUpdated(cmid, autoreconfigure);
} else {
window.SafeExamBrowser.security.updateKeys(safeExamBrowserKeysUpdated);
}
}
};
+80
View File
@@ -0,0 +1,80 @@
// 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 quiz views.
*
* @module quizaccess_seb/view
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2021 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Notification from "core/notification";
import * as Templates from "core/templates";
import * as Str from "core/str";
import ModalAlert from "core/local/modal/alert";
/** @var SELECTOR List of CSS selectors. */
const SELECTOR = {
MAIN: '#region-main',
LOADING: '.seb-loading',
};
/** @var Template List of mustache templates. */
const TEMPLATE = {
LOADING: 'quizaccess_seb/loading',
};
/**
* Manages view when access has been granted.
*/
export const allowAccess = () => {
window.location.reload();
};
/**
* Add an alert to page to inform that Safe Exam Browser access is being checked.
*
* @return {Promise}
*/
export const addLoadingAlert = () => {
return Templates.render(TEMPLATE.LOADING, {}).then((html, js) => {
const alertRegion = window.document.querySelector(SELECTOR.MAIN);
return Templates.prependNodeContents(alertRegion, html, js);
}).catch(Notification.exception);
};
/**
* Remove the Safe Exam Browser access check alert from the page.
*/
export const clearLoadingAlert = () => {
const alert = window.document.querySelector(SELECTOR.LOADING);
if (alert) {
Templates.replaceNode(alert, '', '');
}
};
/**
* Display validation failed modal.
*/
export const showValidationFailedModal = () => {
ModalAlert.create({
title: Str.get_string('sebkeysvalidationfailed', 'quizaccess_seb'),
body: Str.get_string('invalidkeys', 'quizaccess_seb'),
large: false,
show: true,
}).catch(Notification.exception);
};
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Backup instructions for the seb (Safe Exam Browser) quiz access subplugin.
*
* @package quizaccess_seb
* @category backup
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_mod_quiz_access_subplugin.class.php');
/**
* Backup instructions for the seb (Safe Exam Browser) quiz access subplugin.
*
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_quizaccess_seb_subplugin extends backup_mod_quiz_access_subplugin {
/**
* Stores the data related to the Safe Exam Browser quiz settings and management for a particular quiz.
*
* @return backup_subplugin_element
*/
protected function define_quiz_subplugin_structure() {
parent::define_quiz_subplugin_structure();
$quizid = backup::VAR_ACTIVITYID;
$subplugin = $this->get_subplugin_element();
$subpluginwrapper = new backup_nested_element($this->get_recommended_name());
$template = new \quizaccess_seb\template();
$blanktemplatearray = (array) $template->to_record();
unset($blanktemplatearray['usermodified']);
unset($blanktemplatearray['timemodified']);
$templatekeys = array_keys($blanktemplatearray);
$subplugintemplatesettings = new backup_nested_element('quizaccess_seb_template', null, $templatekeys);
// Get quiz settings keys to save.
$settings = new \quizaccess_seb\seb_quiz_settings();
$blanksettingsarray = (array) $settings->to_record();
unset($blanksettingsarray['id']); // We don't need to save reference to settings record in current instance.
// We don't need to save the data about who last modified the settings as they will be overwritten on restore. Also
// means we don't have to think about user data for the backup.
unset($blanksettingsarray['usermodified']);
unset($blanksettingsarray['timemodified']);
$settingskeys = array_keys($blanksettingsarray);
// Save the settings.
$subpluginquizsettings = new backup_nested_element('quizaccess_seb_quizsettings', null, $settingskeys);
// Connect XML elements into the tree.
$subplugin->add_child($subpluginwrapper);
$subpluginwrapper->add_child($subpluginquizsettings);
$subpluginquizsettings->add_child($subplugintemplatesettings);
// Set source to populate the settings data by referencing the ID of quiz being backed up.
$subpluginquizsettings->set_source_table(quizaccess_seb\seb_quiz_settings::TABLE, ['quizid' => $quizid]);
$subpluginquizsettings->annotate_files('quizaccess_seb', 'filemanager_sebconfigfile', null);
$params = ['id' => '../templateid'];
$subplugintemplatesettings->set_source_table(\quizaccess_seb\template::TABLE, $params);
return $subplugin;
}
}
@@ -0,0 +1,119 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Restore instructions for the seb (Safe Exam Browser) quiz access subplugin.
*
* @package quizaccess_seb
* @category backup
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use quizaccess_seb\seb_quiz_settings;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_mod_quiz_access_subplugin.class.php');
/**
* Restore instructions for the seb (Safe Exam Browser) quiz access subplugin.
*
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_quizaccess_seb_subplugin extends restore_mod_quiz_access_subplugin {
/**
* Provides path structure required to restore data for seb quiz access plugin.
*
* @return array
*/
protected function define_quiz_subplugin_structure() {
$paths = [];
// Quiz settings.
$path = $this->get_pathfor('/quizaccess_seb_quizsettings'); // Subplugin root path.
$paths[] = new restore_path_element('quizaccess_seb_quizsettings', $path);
// Template settings.
$path = $this->get_pathfor('/quizaccess_seb_quizsettings/quizaccess_seb_template');
$paths[] = new restore_path_element('quizaccess_seb_template', $path);
return $paths;
}
/**
* Process the restored data for the quizaccess_seb_quizsettings table.
*
* @param stdClass $data Data for quizaccess_seb_quizsettings retrieved from backup xml.
*/
public function process_quizaccess_seb_quizsettings($data) {
global $DB, $USER;
// Process quizsettings.
$data = (object) $data;
$data->quizid = $this->get_new_parentid('quiz'); // Update quizid with new reference.
$data->cmid = $this->task->get_moduleid();
unset($data->id);
$data->timecreated = $data->timemodified = time();
$data->usermodified = $USER->id;
$DB->insert_record(quizaccess_seb\seb_quiz_settings::TABLE, $data);
// Process attached files.
$this->add_related_files('quizaccess_seb', 'filemanager_sebconfigfile', null);
}
/**
* Process the restored data for the quizaccess_seb_template table.
*
* @param stdClass $data Data for quizaccess_seb_template retrieved from backup xml.
*/
public function process_quizaccess_seb_template($data) {
global $DB;
$data = (object) $data;
$quizid = $this->get_new_parentid('quiz');
$template = null;
if ($this->task->is_samesite()) {
$template = \quizaccess_seb\template::get_record(['id' => $data->id]);
} else {
// In a different site, try to find existing template with the same name and content.
$candidates = \quizaccess_seb\template::get_records(['name' => $data->name]);
foreach ($candidates as $candidate) {
if ($candidate->get('content') == $data->content) {
$template = $candidate;
break;
}
}
}
if (empty($template)) {
unset($data->id);
$template = new \quizaccess_seb\template(0, $data);
$template->save();
}
// Update the restored quiz settings to use restored template.
$DB->set_field(\quizaccess_seb\seb_quiz_settings::TABLE, 'templateid', $template->get('id'), ['quizid' => $quizid]);
}
}
@@ -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();
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Serves an encrypted/unencrypted string as a file for download.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../config.php');
$cmid = required_param('cmid', PARAM_RAW);
$config = \quizaccess_seb\helper::get_seb_config_content($cmid);
\quizaccess_seb\helper::send_seb_config_file($config);
+237
View File
@@ -0,0 +1,237 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Define capabilities for plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'quizaccess/seb:managetemplates' => [
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'manager' => CAP_ALLOW,
]
],
'quizaccess/seb:bypassseb' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_requiresafeexambrowser' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_templateid' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_filemanager_sebconfigfile' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_showsebdownloadlink' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_allowedbrowserexamkeys' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_linkquitseb' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_userconfirmquit' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_allowuserquitseb' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_quitpassword' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_allowreloadinexam' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_showsebtaskbar' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_showreloadbutton' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_showtime' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_showkeyboardlayout' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_showwificontrol' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_enableaudiocontrol' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_muteonstartup' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_allowspellchecking' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_activateurlfiltering' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_filterembeddedcontent' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_expressionsallowed' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_regexallowed' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_expressionsblocked' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
'quizaccess/seb:manage_seb_regexblocked' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'manager' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
]
],
];
+47
View File
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Plugin cache definitions.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$definitions = [
'quizsettings' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
'staticacceleration' => true,
],
'config' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
'staticacceleration' => true,
],
'configkey' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
'staticacceleration' => true,
],
];
+95
View File
@@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Install script for plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/accessrule/seb/lib.php');
/**
* Custom code to be run on installing the plugin.
*/
function xmldb_quizaccess_seb_install() {
global $DB;
// Reconfigure all existing quizzes to use a new quizaccess_seb.
$params = ['browsersecurity' => 'safebrowser'];
$total = $DB->count_records('quiz', $params);
if ($total > 0) {
$rs = $DB->get_recordset('quiz', $params);
$i = 0;
$pbar = new progress_bar('updatequizrecords', 500, true);
foreach ($rs as $quiz) {
if (!$DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $quiz->id])) {
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
$sebsettings = new stdClass();
$sebsettings->quizid = $quiz->id;
$sebsettings->cmid = $cm->id;
$sebsettings->templateid = 0;
$sebsettings->requiresafeexambrowser = \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG;
$sebsettings->showsebtaskbar = null;
$sebsettings->showwificontrol = null;
$sebsettings->showreloadbutton = null;
$sebsettings->showtime = null;
$sebsettings->showkeyboardlayout = null;
$sebsettings->allowuserquitseb = null;
$sebsettings->quitpassword = null;
$sebsettings->linkquitseb = null;
$sebsettings->userconfirmquit = null;
$sebsettings->enableaudiocontrol = null;
$sebsettings->muteonstartup = null;
$sebsettings->allowspellchecking = null;
$sebsettings->allowreloadinexam = null;
$sebsettings->activateurlfiltering = null;
$sebsettings->filterembeddedcontent = null;
$sebsettings->expressionsallowed = null;
$sebsettings->regexallowed = null;
$sebsettings->expressionsblocked = null;
$sebsettings->regexblocked = null;
$sebsettings->allowedbrowserexamkeys = null;
$sebsettings->showsebdownloadlink = 1;
$sebsettings->usermodified = get_admin()->id;
$sebsettings->timecreated = time();
$sebsettings->timemodified = time();
$DB->insert_record('quizaccess_seb_quizsettings', $sebsettings);
$quiz->browsersecurity = '-';
$DB->update_record('quiz', $quiz);
}
$i++;
$pbar->update($i, $total, "Reconfiguring existing quizzes to use a new SEB plugin - $i/$total.");
}
$rs->close();
}
return true;
}
+65
View File
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/quiz/accessrule/seb/db" VERSION="2019120400" COMMENT="XMLDB file for Moodle mod/quiz/accessrule/seb"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="quizaccess_seb_quizsettings" COMMENT="Stores the quiz level Safe Exam Browser configuration.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quiz id."/>
<FIELD NAME="cmid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to course module id."/>
<FIELD NAME="templateid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key to quizaccess_seb_template.id."/>
<FIELD NAME="requiresafeexambrowser" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Bool whether to require SEB."/>
<FIELD NAME="showsebtaskbar" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show SEB task bar"/>
<FIELD NAME="showwificontrol" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to allow user to control networking."/>
<FIELD NAME="showreloadbutton" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show reload button."/>
<FIELD NAME="showtime" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show the clock."/>
<FIELD NAME="showkeyboardlayout" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show keyboard layout."/>
<FIELD NAME="allowuserquitseb" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show quit button."/>
<FIELD NAME="quitpassword" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Quit password to exit SEB."/>
<FIELD NAME="linkquitseb" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Link to exit SEB."/>
<FIELD NAME="userconfirmquit" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether confirm quit popup should appear."/>
<FIELD NAME="enableaudiocontrol" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool to show volume and audio controls."/>
<FIELD NAME="muteonstartup" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether browser starts muted."/>
<FIELD NAME="allowspellchecking" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether spell checking will happen in SEB."/>
<FIELD NAME="allowreloadinexam" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether user can reload."/>
<FIELD NAME="activateurlfiltering" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether URLs will be filtered."/>
<FIELD NAME="filterembeddedcontent" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool wither embedded content will be filtered"/>
<FIELD NAME="expressionsallowed" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma or newline separated list of allowed expressions"/>
<FIELD NAME="regexallowed" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Regex of allowed URLs"/>
<FIELD NAME="expressionsblocked" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma or newline separated list of blocked expressions"/>
<FIELD NAME="regexblocked" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Regex of blocked URLs"/>
<FIELD NAME="allowedbrowserexamkeys" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="List of allowed browser exam keys."/>
<FIELD NAME="showsebdownloadlink" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Bool whether SEB download link should appear"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="quizid" TYPE="foreign-unique" FIELDS="quizid" REFTABLE="quiz" REFFIELDS="id"/>
<KEY NAME="cmid" TYPE="foreign-unique" FIELDS="cmid" REFTABLE="course_modules" REFFIELDS="id"/>
<KEY NAME="templateid" TYPE="foreign" FIELDS="templateid" REFTABLE="quizaccess_seb_template" REFFIELDS="id"/>
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="quizaccess_seb_template" COMMENT="Templates for Safe Exam Browser configuration.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Name of the template"/>
<FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="content" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Content of the template"/>
<FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains mappings for classes that have been renamed.
*
* @package quizaccess_seb
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$renamedclasses = [
// Since Moodle 4.2.
'quizaccess_seb\quiz_settings' => 'quizaccess_seb\seb_quiz_settings',
'quizaccess_seb\access_manager' => 'quizaccess_seb\seb_access_manager',
];
+36
View File
@@ -0,0 +1,36 @@
<?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/>.
/**
* List web services and external functions for plugin.
*
* @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
*/
defined('MOODLE_INTERNAL') || die();
$functions = [
'quizaccess_seb_validate_quiz_keys' => [
'classname' => 'quizaccess_seb\external\validate_quiz_keys',
'methodname' => 'execute',
'description' => 'Validate a Safe Exam Browser config key or a browser exam key.',
'type' => 'read',
'ajax' => true,
],
];
+50
View File
@@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Upgrade script for plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/accessrule/seb/lib.php');
/**
* Function to upgrade quizaccess_seb plugin.
*
* @param int $oldversion The version we are upgrading from.
* @return bool Result.
*/
function xmldb_quizaccess_seb_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,203 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the quizaccess_seb plugin.
*
* @package quizaccess_seb
* @author Luca Bösch <luca.boesch@bfh.ch>
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['addtemplate'] = 'Add new template';
$string['allowedbrowserkeysdistinct'] = 'The keys must all be different.';
$string['allowedbrowserkeyssyntax'] = 'A key should be a 64-character hex string.';
$string['cachedef_config'] = 'SEB config cache';
$string['cachedef_configkey'] = 'SEB config key cache';
$string['cachedef_quizsettings'] = 'SEB quiz settings cache';
$string['cantdelete'] = 'The template can\'t be deleted as it has been used for one or more quizzes.';
$string['cantedit'] = 'The template can\'t be edited as it has been used for one or more quizzes.';
$string['checkingaccess'] = 'Checking access to Safe Exam Browser...';
$string['clientrequiresseb'] = 'This quiz has been configured to use the Safe Exam Browser with client configuration.';
$string['confirmtemplateremovalquestion'] = 'Are you sure you want to remove this template?';
$string['confirmtemplateremovaltitle'] = 'Confirm template removal?';
$string['conflictingsettings'] = 'You don\'t have permission to update existing Safe Exam Browser settings.';
$string['content'] = 'Template';
$string['description'] = 'Description';
$string['disabledsettings'] = 'Disabled settings.';
$string['disabledsettings_help'] = 'Safe Exam Browser quiz settings can\'t be changed if the quiz has been attempted. To change a setting, all quiz attempts must first be deleted.';
$string['downloadsebconfig'] = 'Download SEB config file';
$string['duplicatetemplate'] = 'A template with the same name already exists.';
$string['edittemplate'] = 'Edit template';
$string['enabled'] = 'Enabled';
$string['error:ws:nokeyprovided'] = 'At least one Safe Exam Browser key must be provided.';
$string['error:ws:quiznotexists'] = 'Quiz not found matching course module ID: {$a}';
$string['event:accessprevented'] = "Quiz access was prevented";
$string['event:templatecreated'] = 'SEB template was created';
$string['event:templatedeleted'] = 'SEB template was deleted';
$string['event:templatedisabled'] = 'SEB template was disabled';
$string['event:templateenabled'] = 'SEB template was enabled';
$string['event:templateupdated'] = 'SEB template was updated';
$string['exitsebbutton'] = 'Exit Safe Exam Browser';
$string['filemanager_sebconfigfile'] = 'Upload Safe Exam Browser config file';
$string['filemanager_sebconfigfile_help'] = 'Please upload your own Safe Exam Browser config file for this quiz.';
$string['filenotpresent'] = 'Please upload a SEB config file.';
$string['fileparsefailed'] = 'The uploaded file could not be saved as a SEB config file.';
$string['httplinkbutton'] = 'Download configuration';
$string['invalid_browser_key'] = "Invalid SEB browser key";
$string['invalid_config_key'] = "Invalid SEB config key";
$string['invalidkeys'] = 'The Safe Exam Browser keys could not be validated. Check that you\'re using Safe Exam Browser with the correct configuration file.';
$string['invalidtemplate'] = "Invalid SEB config template";
$string['manage_templates'] = 'Safe Exam Browser templates';
$string['managetemplates'] = 'Manage templates';
$string['missingrequiredsettings'] = 'Config settings are missing some required values.';
$string['name'] = 'Name';
$string['newtemplate'] = 'New template';
$string['noconfigfilefound'] = 'No uploaded SEB config file could be found for quiz with cmid: {$a}';
$string['noconfigfound'] = 'No SEB config could be found for quiz with cmid: {$a}';
$string['not_seb'] = 'No Safe Exam Browser is being used.';
$string['notemplate'] = 'No template';
$string['passwordnotset'] = 'Current settings require quizzes using the Safe Exam Browser to have a quiz password set.';
$string['pluginname'] = 'Safe Exam Browser access rules';
$string['privacy:metadata:quizaccess_seb_quizsettings'] = 'Safe Exam Browser settings for a quiz. This includes the ID of the last user to create or modify the settings.';
$string['privacy:metadata:quizaccess_seb_quizsettings:quizid'] = 'ID of the quiz the settings exist for.';
$string['privacy:metadata:quizaccess_seb_quizsettings:timecreated'] = 'Unix time that the settings were created.';
$string['privacy:metadata:quizaccess_seb_quizsettings:timemodified'] = 'Unix time that the settings were last modified.';
$string['privacy:metadata:quizaccess_seb_quizsettings:usermodified'] = 'ID of user who last created or modified the settings.';
$string['privacy:metadata:quizaccess_seb_template'] = 'Safe Exam Browser template settings. This includes the ID of the last user to create or modify the template.';
$string['privacy:metadata:quizaccess_seb_template:timecreated'] = 'Unix time that the template was created.';
$string['privacy:metadata:quizaccess_seb_template:timemodified'] = 'Unix time that the template was last modified.';
$string['privacy:metadata:quizaccess_seb_template:usermodified'] = 'ID of user who last created or modified the template.';
$string['quizsettings'] = 'Quiz settings';
$string['restoredfrom'] = '{$a->name} (restored via cmid {$a->cmid})';
$string['seb'] = 'Safe Exam Browser';
$string['seb:bypassseb'] = 'Bypass the requirement to view quiz in Safe Exam Browser.';
$string['seb:manage_filemanager_sebconfigfile'] = 'Change SEB quiz setting: Select SEB config file';
$string['seb:manage_seb_activateurlfiltering'] = 'Change SEB quiz setting: Activate URL filtering';
$string['seb:manage_seb_allowedbrowserexamkeys'] = 'Change SEB quiz setting: Allowed browser exam keys';
$string['seb:manage_seb_allowreloadinexam'] = 'Change SEB quiz setting: Allow reload';
$string['seb:manage_seb_allowspellchecking'] = 'Change SEB quiz setting: Enable spell checking';
$string['seb:manage_seb_allowuserquitseb'] = 'Change SEB quiz setting: Allow quit';
$string['seb:manage_seb_enableaudiocontrol'] = 'Change SEB quiz setting: Enable audio control';
$string['seb:manage_seb_expressionsallowed'] = 'Change SEB quiz setting: Simple expressions allowed';
$string['seb:manage_seb_expressionsblocked'] = 'Change SEB quiz setting: Simple expressions blocked';
$string['seb:manage_seb_filterembeddedcontent'] = 'Change SEB quiz setting: Filter embedded content';
$string['seb:manage_seb_linkquitseb'] = 'Change SEB quiz setting: Quit link';
$string['seb:manage_seb_muteonstartup'] = 'Change SEB quiz setting: Mute on startup';
$string['seb:manage_seb_quitpassword'] = 'Change SEB quiz setting: Quit password';
$string['seb:manage_seb_regexallowed'] = 'Change SEB quiz setting: Regex expressions allowed';
$string['seb:manage_seb_regexblocked'] = 'Change SEB quiz setting: Regex expressions blocked';
$string['seb:manage_seb_requiresafeexambrowser'] = 'Change SEB quiz setting: Require Safe Exam Browser';
$string['seb:manage_seb_showkeyboardlayout'] = 'Change SEB quiz setting: Show keyboard layout';
$string['seb:manage_seb_showreloadbutton'] = 'Change SEB quiz setting: Show reload button';
$string['seb:manage_seb_showsebtaskbar'] = 'Change SEB quiz setting: Show task bar';
$string['seb:manage_seb_showtime'] = 'Change SEB quiz setting: Show time';
$string['seb:manage_seb_showwificontrol'] = 'Change SEB quiz setting: Show Wi-Fi control';
$string['seb:manage_seb_showsebdownloadlink'] = 'Change SEB quiz setting: Show download link';
$string['seb:manage_seb_templateid'] = 'Change SEB quiz setting: Select SEB template';
$string['seb:manage_seb_userconfirmquit'] = 'Change SEB quiz setting: Confirm on quit';
$string['seb:managetemplates'] = 'Manage SEB configuration templates';
$string['seb_activateurlfiltering'] = 'Enable URL filtering';
$string['seb_activateurlfiltering_help'] = 'If enabled, URLs will be filtered when loading web pages. The filter set has to be defined below.';
$string['seb_allowedbrowserexamkeys'] = 'Allowed browser exam keys';
$string['seb_allowedbrowserexamkeys_help'] = 'In this field you can enter the allowed browser exam keys for versions of Safe Exam Browser that are permitted to access this quiz. If no keys are entered, then browser exam keys are not checked.';
$string['seb_allowreloadinexam'] = 'Enable reload in exam';
$string['seb_allowreloadinexam_help'] = 'If enabled, page reload is allowed (reload button in SEB task bar, browser tool bar, iOS side slider menu, keyboard shortcut F5/cmd+R). Note that offline caching may break if a user tries to reload a page without an internet connection.';
$string['seb_allowspellchecking'] = 'Enable spell checking';
$string['seb_allowspellchecking_help'] = 'If enabled, spell checking in the SEB browser is allowed.';
$string['seb_allowuserquitseb'] = 'Enable quitting of SEB';
$string['seb_allowuserquitseb_help'] = 'If enabled, users can quit SEB with the "Quit" button in the SEB task bar or by pressing the keys Ctrl-Q or by clicking the main browser window close button.';
$string['seb_enableaudiocontrol'] = 'Enable audio controls';
$string['seb_enableaudiocontrol_help'] = 'If enabled, the audio control icon is shown in the SEB task bar.';
$string['seb_expressionsallowed'] = 'Expressions allowed';
$string['seb_expressionsallowed_help'] = 'A text field which contains the allowed filtering expressions for the allowed URLs. Use of the wildcard char \'\*\' is possible. Examples for expressions: \'example.com\' or \'example.com/stuff/\*\'. \'example.com\' matches \'example.com\', \'www.example.com\' and \'www.mail.example.com\'. \'example.com/stuff/\*\' matches all requests to any subdomain of \'example.com\' that have \'stuff\' as the first segment of the path.';
$string['seb_expressionsblocked'] = 'Expressions blocked';
$string['seb_expressionsblocked_help'] = 'A text field which contains the filtering expressions for the blocked URLs. Use of the wildcard char \'\*\' is possible. Examples for expressions: \'example.com\' or \'example.com/stuff/\*\'. \'example.com\' matches \'example.com\', \'www.example.com\' and \'www.mail.example.com\'. \'example.com/stuff/\*\' matches all requests to any subdomain of \'example.com\' that have \'stuff\' as the first segment of the path.';
$string['seb_filterembeddedcontent'] = 'Filter also embedded content';
$string['seb_filterembeddedcontent_help'] = 'If enabled, embedded resources will also be filtered using the filter set.';
$string['seb_help'] = 'Setup quiz to use the Safe Exam Browser.';
$string['seb_linkquitseb'] = 'Show Exit Safe Exam Browser button, configured with this quit link';
$string['seb_linkquitseb_help'] = 'In this field you can enter the link to quit SEB. It is used on an "Exit Safe Exam Browser" button on the page that appears after the exam is submitted. When clicking the button or the link placed wherever you want to put it, it is possible to quit SEB without having to enter a quit password. For an HTTPS site, the quit link has to start with https://. If no link is entered, then the "Exit Safe Exam Browser" button does not appear and there is no link set to quit SEB.';
$string['seb_managetemplates'] = 'Manage Safe Exam Browser templates';
$string['seb_muteonstartup'] = 'Mute on startup';
$string['seb_muteonstartup_help'] = 'If enabled, audio is initially muted when starting SEB.';
$string['seb_quitpassword'] = 'Quit password';
$string['seb_quitpassword_help'] = 'This password is prompted when users try to quit SEB with the "Quit" button, Ctrl-Q or the close button in the main browser window. If no quit password is set, then SEB just prompts "Are you sure you want to quit SEB?".';
$string['seb_regexallowed'] = 'Regex allowed';
$string['seb_regexallowed_help'] = 'A text field which contains the filtering expressions for allowed URLs in a regular expression (Regex) format.';
$string['seb_regexblocked'] = 'Regex blocked';
$string['seb_regexblocked_help'] = 'A text field which contains the filtering expressions for blocked URLs in a regular expression (Regex) format.';
$string['seb_requiresafeexambrowser'] = 'Require the use of Safe Exam Browser';
$string['seb_requiresafeexambrowser_help'] = 'If enabled, students can only attempt the quiz using the Safe Exam Browser.
The available options are:
* No
<br/>Safe Exam Browser is not required to attempt the quiz.
* Yes Use an existing template
<br/>A template for the configuration of Safe Exam Browser can be used. Templates are managed in the site administration. Your manual settings overwrite the settings in the template.
* Yes Configure manually
<br/>No template for the configuration of Safe Exam Browser will be used. You can configure Safe Exam Browser manually.
* Yes Upload my own config
<br/>You can upload your own Safe Exam Browser configuration file. All manual settings and the use of templates will be disabled.
* Yes Use SEB client config
<br/>No configurations of Safe Exam Browser are on the Moodle side. The quiz can be attempted with any configuration of Safe Exam Browser.';
$string['seb_showkeyboardlayout'] = 'Show keyboard layout';
$string['seb_showkeyboardlayout_help'] = 'If enabled, the current keyboard layout is shown in the SEB task bar. It allows you to switch to other keyboard layouts, which have been enabled in the operating system.';
$string['seb_showreloadbutton'] = 'Show reload button';
$string['seb_showreloadbutton_help'] = 'If enabled, a reload button is displayed in the SEB task bar, allowing the current web page to be reloaded.';
$string['seb_showsebtaskbar'] = 'Show SEB task bar';
$string['seb_showsebtaskbar_help'] = 'If enabled, a task bar appears at the bottom of the SEB browser window. The task bar is required to display items such as Wi-Fi control, reload button, time and keyboard layout.';
$string['seb_showtime'] = 'Show time';
$string['seb_showtime_help'] = 'If enabled, the current time is displayed in the SEB task bar.';
$string['seb_showwificontrol'] = 'Show Wi-Fi control';
$string['seb_showwificontrol_help'] = 'If enabled, a Wi-Fi control button appears in the SEB task bar. The button allows users to reconnect to Wi-Fi networks which have previously been connected to.';
$string['seb_showsebdownloadlink'] = 'Show Safe Exam Browser download button';
$string['seb_showsebdownloadlink_help'] = 'If enabled, a button for Safe Exam Browser download will be shown on the quiz start page.';
$string['seb_templateid'] = 'Safe Exam Browser config template';
$string['seb_templateid_help'] = 'The settings in the selected config template will be used for the configuration of the Safe Exam Browser while attempting the quiz. You may overwrite the settings in the template with your manual settings.';
$string['seb_use_client'] = 'Yes Use SEB client config';
$string['seb_use_manually'] = 'Yes Configure manually';
$string['seb_use_template'] = 'Yes Use an existing template';
$string['seb_use_upload'] = 'Yes Upload my own config';
$string['seb_userconfirmquit'] = 'Ask user to confirm quitting';
$string['seb_userconfirmquit_help'] = 'If enabled, users have to confirm quitting of SEB when a quit link is detected.';
$string['sebdownloadbutton'] = 'Download Safe Exam Browser';
$string['sebkeysvalidationfailed'] = 'Error validating SEB keys';
$string['seblinkbutton'] = 'Launch Safe Exam Browser';
$string['sebrequired'] = "This quiz has been configured so that students may only attempt it using the Safe Exam Browser.";
$string['setting:autoreconfigureseb'] = 'Auto-configure SEB';
$string['setting:autoreconfigureseb_desc'] = 'If enabled, users who navigate to the quiz using the Safe Exam Browser will be automatically forced to reconfigure their Safe Exam Browser.';
$string['setting:displayblocksbeforestart'] = 'Display blocks before starting quiz';
$string['setting:displayblocksbeforestart_desc'] = 'If enabled, blocks will be displayed before a user attempts the quiz.';
$string['setting:displayblockswhenfinished'] = 'Display blocks after finishing quiz';
$string['setting:displayblockswhenfinished_desc'] = 'If enabled, blocks will be displayed after a user has finished their quiz attempt.';
$string['setting:downloadlink'] = 'Safe Exam Browser download link';
$string['setting:downloadlink_desc'] = 'URL for downloading the Safe Exam Browser application.';
$string['setting:quizpasswordrequired'] = 'Quiz password required';
$string['setting:quizpasswordrequired_desc'] = 'If enabled, all quizzes that require the Safe Exam Browser must have a quiz password set.';
$string['setting:showhttplink'] = 'Show http:// link';
$string['setting:showseblink'] = 'Show seb:// link';
$string['setting:showseblinks'] = 'Show Safe Exam Browser config links';
$string['setting:showseblinks_desc'] = 'Whether to show links for a user to access the Safe Exam Browser configuration file when access to the quiz is prevented. Note that seb:// links may not work in every browser.';
$string['setting:supportedversions'] = 'Please note that the following minimum versions of the Safe Exam Browser client are required to use the config key feature: macOS - 2.1.5pre2, Windows - 3.0, iOS - 2.1.14.';
$string['settingsfrozen'] = 'Due to there being at least one quiz attempt, the Safe Exam Browser settings can no longer be updated.';
$string['unknown_reason'] = "Unknown reason";
$string['used'] = 'In use';
+71
View File
@@ -0,0 +1,71 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Main library of plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Serve the files.
*
* @param stdClass $course the course object
* @param stdClass $cm the course module object
* @param \context $context the context
* @param string $filearea the name of the file area
* @param array $args extra arguments (itemid, path)
* @param bool $forcedownload whether or not force download
* @param array $options additional options affecting the file serving
* @return bool false if the file not found, just send the file otherwise and do not return anything
*/
function quizaccess_seb_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options= []) {
if ($context->contextlevel != CONTEXT_MODULE) {
return false;
}
if ($filearea !== 'filemanager_sebconfigfile') {
return false;
}
require_login($course, true, $cm);
if (!has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context)) {
return false;
}
$itemid = array_shift($args);
$filename = array_pop($args);
if (!$args) {
$filepath = '/';
} else {
$filepath = '/' .implode('/', $args) . '/';
}
$fs = get_file_storage();
$file = $fs->get_file($context->id, 'quizaccess_seb', $filearea, $itemid, $filepath, $filename);
if (!$file) {
return false;
}
send_stored_file($file, 0, 0, $forcedownload, $options);
}
+591
View File
@@ -0,0 +1,591 @@
<?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/>.
use mod_quiz\local\access_rule_base;
use mod_quiz\quiz_attempt;
use quizaccess_seb\seb_access_manager;
use quizaccess_seb\seb_quiz_settings;
use quizaccess_seb\settings_provider;
use quizaccess_seb\event\access_prevented;
/**
* Implementation of the quizaccess_seb plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_seb extends access_rule_base {
/** @var seb_access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */
private $accessmanager;
/**
* Create an instance of this rule for a particular quiz.
*
* @param \mod_quiz\quiz_settings $quizobj information about the quiz in question.
* @param int $timenow the time that should be considered as 'now'.
* @param seb_access_manager $accessmanager the quiz accessmanager.
*/
public function __construct(\mod_quiz\quiz_settings $quizobj, int $timenow, seb_access_manager $accessmanager) {
parent::__construct($quizobj, $timenow);
$this->accessmanager = $accessmanager;
}
/**
* Return an appropriately configured instance of this rule, if it is applicable
* to the given quiz, otherwise return null.
*
* @param \mod_quiz\quiz_settings $quizobj information about the quiz in question.
* @param int $timenow the time that should be considered as 'now'.
* @param bool $canignoretimelimits whether the current user is exempt from
* time limits by the mod/quiz:ignoretimelimits capability.
* @return access_rule_base|null the rule, if applicable, else null.
*/
public static function make(\mod_quiz\quiz_settings $quizobj, $timenow, $canignoretimelimits) {
$accessmanager = new seb_access_manager($quizobj);
// If Safe Exam Browser is not required, this access rule is not applicable.
if (!$accessmanager->seb_required()) {
return null;
}
return new self($quizobj, $timenow, $accessmanager);
}
/**
* Add any fields that this rule requires to the quiz settings form. This
* method is called from {@link mod_quiz_mod_form::definition()}, while the
* security section is being built.
*
* @param mod_quiz_mod_form $quizform the quiz settings form that is being built.
* @param MoodleQuickForm $mform the wrapped MoodleQuickForm.
*/
public static function add_settings_form_fields(mod_quiz_mod_form $quizform, MoodleQuickForm $mform) {
settings_provider::add_seb_settings_fields($quizform, $mform);
}
/**
* Validate the data from any form fields added using {@link add_settings_form_fields()}.
*
* @param array $errors the errors found so far.
* @param array $data the submitted form data.
* @param array $files information about any uploaded files.
* @param mod_quiz_mod_form $quizform the quiz form object.
* @return array $errors the updated $errors array.
*/
public static function validate_settings_form_fields(array $errors,
array $data, $files, mod_quiz_mod_form $quizform): array {
$quizid = $data['instance'];
$cmid = $data['coursemodule'];
$context = $quizform->get_context();
if (!settings_provider::can_configure_seb($context)) {
return $errors;
}
if (settings_provider::is_seb_settings_locked($quizid)) {
return $errors;
}
if (settings_provider::is_conflicting_permissions($context)) {
return $errors;
}
$settings = settings_provider::filter_plugin_settings((object) $data);
// Validate basic settings using persistent class.
$quizsettings = (new seb_quiz_settings())->from_record($settings);
// Set non-form fields.
$quizsettings->set('quizid', $quizid);
$quizsettings->set('cmid', $cmid);
$quizsettings->validate();
// Add any errors to list.
foreach ($quizsettings->get_errors() as $name => $error) {
$name = settings_provider::add_prefix($name); // Re-add prefix to match form element.
$errors[$name] = $error->out();
}
// Edge case for filemanager_sebconfig.
if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) {
$errorvalidatefile = settings_provider::validate_draftarea_configfile($data['filemanager_sebconfigfile']);
if (!empty($errorvalidatefile)) {
$errors['filemanager_sebconfigfile'] = $errorvalidatefile;
}
}
// Edge case to force user to select a template.
if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_TEMPLATE) {
if (empty($data['seb_templateid'])) {
$errors['seb_templateid'] = get_string('invalidtemplate', 'quizaccess_seb');
}
}
if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) {
// Global settings may be active which require a quiz password to be set if using SEB.
if (!empty(get_config('quizaccess_seb', 'quizpasswordrequired')) && empty($data['quizpassword'])) {
$errors['quizpassword'] = get_string('passwordnotset', 'quizaccess_seb');
}
}
return $errors;
}
/**
* Save any submitted settings when the quiz settings form is submitted. This
* is called from {@link quiz_after_add_or_update()} in lib.php.
*
* @param stdClass $quiz the data from the quiz form, including $quiz->id
* which is the id of the quiz being saved.
*/
public static function save_settings($quiz) {
$context = context_module::instance($quiz->coursemodule);
if (!settings_provider::can_configure_seb($context)) {
return;
}
if (settings_provider::is_seb_settings_locked($quiz->id)) {
return;
}
if (settings_provider::is_conflicting_permissions($context)) {
return;
}
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course, false, MUST_EXIST);
$settings = settings_provider::filter_plugin_settings($quiz);
$settings->quizid = $quiz->id;
$settings->cmid = $cm->id;
// Get existing settings or create new settings if none exist.
$quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id);
if (empty($quizsettings)) {
$quizsettings = new seb_quiz_settings(0, $settings);
} else {
$settings->id = $quizsettings->get('id');
$quizsettings->from_record($settings);
}
// Process uploaded files if required.
if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) {
$draftitemid = file_get_submitted_draft_itemid('filemanager_sebconfigfile');
settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $cm->id);
} else {
settings_provider::delete_uploaded_config_file($cm->id);
}
// Save or delete settings.
if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) {
$quizsettings->save();
} else if ($quizsettings->get('id')) {
$quizsettings->delete();
}
}
/**
* Delete any rule-specific settings when the quiz is deleted. This is called
* from {@link quiz_delete_instance()} in lib.php.
*
* @param stdClass $quiz the data from the database, including $quiz->id
* which is the id of the quiz being deleted.
*/
public static function delete_settings($quiz) {
$quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id);
// Check that there are existing settings.
if ($quizsettings !== false) {
$quizsettings->delete();
}
}
/**
* Return the bits of SQL needed to load all the settings from all the access
* plugins in one DB query. The easiest way to understand what you need to do
* here is probably to read the code of {@see \mod_quiz\access_manager::load_settings()}.
*
* If you have some settings that cannot be loaded in this way, then you can
* use the {@link get_extra_settings()} method instead, but that has
* performance implications.
*
* @param int $quizid the id of the quiz we are loading settings for. This
* can also be accessed as quiz.id in the SQL. (quiz is a table alisas for {quiz}.)
* @return array with three elements:
* 1. fields: any fields to add to the select list. These should be alised
* if neccessary so that the field name starts the name of the plugin.
* 2. joins: any joins (should probably be LEFT JOINS) with other tables that
* are needed.
* 3. params: array of placeholder values that are needed by the SQL. You must
* used named placeholders, and the placeholder names should start with the
* plugin name, to avoid collisions.
*/
public static function get_settings_sql($quizid): array {
return [
'seb.requiresafeexambrowser AS seb_requiresafeexambrowser, '
. 'seb.showsebtaskbar AS seb_showsebtaskbar, '
. 'seb.showwificontrol AS seb_showwificontrol, '
. 'seb.showreloadbutton AS seb_showreloadbutton, '
. 'seb.showtime AS seb_showtime, '
. 'seb.showkeyboardlayout AS seb_showkeyboardlayout, '
. 'seb.allowuserquitseb AS seb_allowuserquitseb, '
. 'seb.quitpassword AS seb_quitpassword, '
. 'seb.linkquitseb AS seb_linkquitseb, '
. 'seb.userconfirmquit AS seb_userconfirmquit, '
. 'seb.enableaudiocontrol AS seb_enableaudiocontrol, '
. 'seb.muteonstartup AS seb_muteonstartup, '
. 'seb.allowspellchecking AS seb_allowspellchecking, '
. 'seb.allowreloadinexam AS seb_allowreloadinexam, '
. 'seb.activateurlfiltering AS seb_activateurlfiltering, '
. 'seb.filterembeddedcontent AS seb_filterembeddedcontent, '
. 'seb.expressionsallowed AS seb_expressionsallowed, '
. 'seb.regexallowed AS seb_regexallowed, '
. 'seb.expressionsblocked AS seb_expressionsblocked, '
. 'seb.regexblocked AS seb_regexblocked, '
. 'seb.allowedbrowserexamkeys AS seb_allowedbrowserexamkeys, '
. 'seb.showsebdownloadlink AS seb_showsebdownloadlink, '
. 'sebtemplate.id AS seb_templateid '
, 'LEFT JOIN {quizaccess_seb_quizsettings} seb ON seb.quizid = quiz.id '
. 'LEFT JOIN {quizaccess_seb_template} sebtemplate ON seb.templateid = sebtemplate.id '
, []
];
}
/**
* Whether the user should be blocked from starting a new attempt or continuing
* an attempt now.
*
* @return string false if access should be allowed, a message explaining the
* reason if access should be prevented.
*/
public function prevent_access() {
global $PAGE;
if (!$this->accessmanager->seb_required()) {
return false;
}
if ($this->accessmanager->can_bypass_seb()) {
return false;
}
// If the rule is active, enforce a secure view whilst taking the quiz.
$PAGE->set_pagelayout('secure');
$this->prevent_display_blocks();
// Access has previously been validated for this session and quiz.
if ($this->accessmanager->validate_session_access()) {
return false;
}
if (!$this->accessmanager->validate_basic_header()) {
access_prevented::create_strict($this->accessmanager, $this->get_reason_text('not_seb'))->trigger();
return $this->get_require_seb_error_message();
}
if (!$this->accessmanager->validate_config_key()) {
if ($this->accessmanager->should_redirect_to_seb_config_link()) {
$this->accessmanager->redirect_to_seb_config_link();
}
access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_config_key'))->trigger();
return $this->get_invalid_key_error_message();
}
if (!$this->accessmanager->validate_browser_exam_key()) {
access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_browser_key'))->trigger();
return $this->get_invalid_key_error_message();
}
// Set the state of the access for this Moodle session.
$this->accessmanager->set_session_access(true);
return false;
}
/**
* Returns a list of finished attempts for the current user.
*
* @return array
*/
private function get_user_finished_attempts(): array {
global $USER;
return quiz_get_user_attempts(
$this->quizobj->get_quizid(),
$USER->id,
quiz_attempt::FINISHED,
false
);
}
/**
* Prevent block displaying as configured.
*/
private function prevent_display_blocks() {
global $PAGE;
if ($PAGE->has_set_url() && $PAGE->url == $this->quizobj->view_url()) {
$attempts = $this->get_user_finished_attempts();
// Don't display blocks before starting an attempt.
if (empty($attempts) && !get_config('quizaccess_seb', 'displayblocksbeforestart')) {
$PAGE->blocks->show_only_fake_blocks();
}
// Don't display blocks after finishing an attempt.
if (!empty($attempts) && !get_config('quizaccess_seb', 'displayblockswhenfinished')) {
$PAGE->blocks->show_only_fake_blocks();
}
}
}
/**
* Returns reason for access prevention as a text.
*
* @param string $identifier Reason string identifier.
* @return string
*/
private function get_reason_text(string $identifier): string {
if (in_array($identifier, ['not_seb', 'invalid_config_key', 'invalid_browser_key'])) {
return get_string($identifier, 'quizaccess_seb');
}
return get_string('unknown_reason', 'quizaccess_seb');
}
/**
* Return error message when a SEB key is not valid.
*
* @return string
*/
private function get_invalid_key_error_message(): string {
// Return error message with download link and links to get the seb config.
if ($this->accessmanager->is_using_seb()) {
return get_string('invalidkeys', 'quizaccess_seb')
. $this->display_buttons($this->get_action_buttons());
}
return $this->display_buttons($this->get_action_buttons());
}
/**
* Return error message when a SEB browser is not used.
*
* @return string
*/
private function get_require_seb_error_message(): string {
$message = get_string('clientrequiresseb', 'quizaccess_seb');
if ($this->should_display_download_seb_link()) {
$message .= $this->display_buttons($this->get_download_seb_button());
}
// Return error message with download link.
return $message;
}
/**
* Helper function to display an Exit Safe Exam Browser button if configured to do so and attempts are > 0.
*
* @return string empty or a button which has the configured seb quit link.
*/
private function get_quit_button(): string {
$quitbutton = '';
if (empty($this->get_user_finished_attempts())) {
return $quitbutton;
}
// Only display if the link has been configured and attempts are greater than 0.
if (!empty($this->quiz->seb_linkquitseb)) {
$quitbutton = html_writer::link(
$this->quiz->seb_linkquitseb,
get_string('exitsebbutton', 'quizaccess_seb'),
['class' => 'btn btn-secondary']
);
}
return $quitbutton;
}
/**
* Information, such as might be shown on the quiz view page, relating to this restriction.
* There is no obligation to return anything. If it is not appropriate to tell students
* about this rule, then just return ''.
*
* @return mixed a message, or array of messages, explaining the restriction
* (may be '' if no message is appropriate).
*/
public function description(): array {
global $PAGE;
$messages = [get_string('sebrequired', 'quizaccess_seb')];
// Display download SEB config link for those who can bypass using SEB.
if ($this->accessmanager->can_bypass_seb() && $this->accessmanager->should_validate_config_key()) {
$messages[] = $this->display_buttons($this->get_download_config_button());
}
// Those with higher level access will be able to see the button if they've made an attempt.
if (!$this->prevent_access()) {
$messages[] = $this->display_buttons($this->get_quit_button());
} else {
$PAGE->requires->js_call_amd('quizaccess_seb/validate_quiz_access', 'init',
[$this->quiz->cmid, (bool)get_config('quizaccess_seb', 'autoreconfigureseb')]);
}
return $messages;
}
/**
* Sets up the attempt (review or summary) page with any special extra
* properties required by this rule.
*
* @param moodle_page $page the page object to initialise.
*/
public function setup_attempt_page($page) {
$page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title);
$page->set_popup_notification_allowed(false); // Prevent message notifications.
$page->set_heading($page->title);
$page->set_pagelayout('secure');
}
/**
* This is called when the current attempt at the quiz is finished.
*/
public function current_attempt_finished() {
$this->accessmanager->clear_session_access();
}
/**
* Prepare buttons HTML code for being displayed on the screen.
*
* @param string $buttonshtml Html string of the buttons.
* @param string $class Optional CSS class (or classes as space-separated list)
* @param array $attributes Optional other attributes as array
*
* @return string HTML code of the provided buttons.
*/
private function display_buttons(string $buttonshtml, $class = '', array $attributes = null): string {
$html = '';
if (!empty($buttonshtml)) {
$html = html_writer::div($buttonshtml, $class, $attributes);
}
return $html;
}
/**
* Get buttons to prompt user to download SEB or config file or launch SEB.
*
* @return string Html block of all action buttons.
*/
private function get_action_buttons(): string {
$buttons = '';
if ($this->should_display_download_seb_link()) {
$buttons .= $this->get_download_seb_button();
}
// Get config for displaying links.
$linkconfig = explode(',', get_config('quizaccess_seb', 'showseblinks'));
// Display links to download config/launch SEB only if required.
if ($this->accessmanager->should_validate_config_key()) {
if (in_array('seb', $linkconfig)) {
$buttons .= $this->get_launch_seb_button();
}
if (in_array('http', $linkconfig)) {
$buttons .= $this->get_download_config_button();
}
}
return $buttons;
}
/**
* Get a button to download SEB.
*
* @return string A link to download SafeExam Browser.
*/
private function get_download_seb_button(): string {
global $OUTPUT;
$button = '';
if (!empty($this->get_seb_download_url())) {
$button = $OUTPUT->single_button($this->get_seb_download_url(), get_string('sebdownloadbutton', 'quizaccess_seb'));
}
return $button;
}
/**
* Get a button to launch Safe Exam Browser.
*
* @return string A link to launch Safe Exam Browser.
*/
private function get_launch_seb_button(): string {
// Rendering as a href and not as button in a form to circumvent browser warnings for sending to URL with unknown protocol.
$seblink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, true, is_https());
$buttonlink = html_writer::start_tag('div', ['class' => 'singlebutton']);
$buttonlink .= html_writer::link($seblink, get_string('seblinkbutton', 'quizaccess_seb'),
['class' => 'btn btn-secondary', 'title' => get_string('seblinkbutton', 'quizaccess_seb')]);
$buttonlink .= html_writer::end_tag('div');
return $buttonlink;
}
/**
* Get a button to download Safe Exam Browser config.
*
* @return string A link to launch Safe Exam Browser.
*/
private function get_download_config_button(): string {
// Rendering as a href and not as button in a form to circumvent browser warnings for sending to URL with unknown protocol.
$httplink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, false, is_https());
$buttonlink = html_writer::start_tag('div', ['class' => 'singlebutton']);
$buttonlink .= html_writer::link($httplink, get_string('httplinkbutton', 'quizaccess_seb'),
['class' => 'btn btn-secondary', 'title' => get_string('httplinkbutton', 'quizaccess_seb')]);
$buttonlink .= html_writer::end_tag('div');
return $buttonlink;
}
/**
* Returns SEB download URL.
*
* @return string
*/
private function get_seb_download_url(): string {
return get_config('quizaccess_seb', 'downloadlink');
}
/**
* Check if we should display a link to download Safe Exam Browser.
*
* @return bool
*/
private function should_display_download_seb_link(): bool {
return !empty($this->quiz->seb_showsebdownloadlink);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Global configuration settings for the quizaccess_seb plugin.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2019 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
global $ADMIN;
if ($hassiteconfig) {
$settings->add(new admin_setting_heading(
'quizaccess_seb/supportedversions',
'',
$OUTPUT->notification(get_string('setting:supportedversions', 'quizaccess_seb'), 'warning')));
$settings->add(new admin_setting_configcheckbox('quizaccess_seb/autoreconfigureseb',
get_string('setting:autoreconfigureseb', 'quizaccess_seb'),
get_string('setting:autoreconfigureseb_desc', 'quizaccess_seb'),
'1'));
$links = [
'seb' => get_string('setting:showseblink', 'quizaccess_seb'),
'http' => get_string('setting:showhttplink', 'quizaccess_seb')
];
$settings->add(new admin_setting_configmulticheckbox('quizaccess_seb/showseblinks',
get_string('setting:showseblinks', 'quizaccess_seb'),
get_string('setting:showseblinks_desc', 'quizaccess_seb'),
$links, $links));
$settings->add(new admin_setting_configtext('quizaccess_seb/downloadlink',
get_string('setting:downloadlink', 'quizaccess_seb'),
get_string('setting:downloadlink_desc', 'quizaccess_seb'),
'https://safeexambrowser.org/download_en.html',
PARAM_URL));
$settings->add(new admin_setting_configcheckbox('quizaccess_seb/quizpasswordrequired',
get_string('setting:quizpasswordrequired', 'quizaccess_seb'),
get_string('setting:quizpasswordrequired_desc', 'quizaccess_seb'),
'0'));
$settings->add(new admin_setting_configcheckbox('quizaccess_seb/displayblocksbeforestart',
get_string('setting:displayblocksbeforestart', 'quizaccess_seb'),
get_string('setting:displayblocksbeforestart_desc', 'quizaccess_seb'),
'0'));
$settings->add(new admin_setting_configcheckbox('quizaccess_seb/displayblockswhenfinished',
get_string('setting:displayblockswhenfinished', 'quizaccess_seb'),
get_string('setting:displayblockswhenfinished_desc', 'quizaccess_seb'),
'1'));
}
if (has_capability('quizaccess/seb:managetemplates', context_system::instance())) {
$ADMIN->add('modsettingsquizcat',
new admin_externalpage(
'quizaccess_seb/template',
get_string('manage_templates', 'quizaccess_seb'),
new moodle_url('/mod/quiz/accessrule/seb/template.php'),
'quizaccess/seb:managetemplates'
)
);
}
+34
View File
@@ -0,0 +1,34 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Page to manipulate templates.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
$action = optional_param('action', 'view', PARAM_ALPHANUMEXT);
$PAGE->set_context(context_system::instance());
$manager = new \quizaccess_seb\template_controller();
$manager->execute($action);
@@ -0,0 +1,34 @@
{{!
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/>.
}}
{{!
@template quizaccess_seb/loading
Loading container.
Example context (json):
{
"extraclasses": "class"
}
}}
<div class="seb-loading alert alert-info fade in {{ extraclasses }}" role="alert" data-aria-autofocus="true">
<div class="d-flex justify-content-between">
<span>
{{#str}}checkingaccess, quizaccess_seb{{/str}}
</span>
{{> core/loading }}
</div>
</div>
@@ -0,0 +1,587 @@
<?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;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/test_helper_trait.php');
/**
* PHPUnit tests for the access manager.
*
* @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
* @covers \quizaccess_seb\seb_access_manager
*/
class access_manager_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/**
* Called before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
}
/**
* Test access_manager private property quizsettings is null.
*/
public function test_access_manager_quizsettings_null(): void {
$this->quiz = $this->create_test_quiz($this->course);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->seb_required());
$reflection = new \ReflectionClass('\quizaccess_seb\seb_access_manager');
$property = $reflection->getProperty('quizsettings');
$this->assertFalse($property->getValue($accessmanager));
}
/**
* Test that SEB is not required.
*/
public function test_seb_required_false(): void {
$this->quiz = $this->create_test_quiz($this->course);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->seb_required());
}
/**
* Test that SEB is required.
*/
public function test_seb_required_true(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->seb_required());
}
/**
* Test that user has capability to bypass SEB check.
*/
public function test_user_can_bypass_seb_check(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Set the bypass SEB check capability to $USER.
$this->assign_user_capability('quizaccess/seb:bypassseb', \context_module::instance($this->quiz->cmid)->id);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->can_bypass_seb());
}
/**
* Test that user has capability to bypass SEB check.
*/
public function test_admin_user_can_bypass_seb_check(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
// Test normal user cannot bypass check.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->can_bypass_seb());
// Test with admin user.
$this->setAdminUser();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->can_bypass_seb());
}
/**
* Test user does not have capability to bypass SEB check.
*/
public function test_user_cannot_bypass_seb_check(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->can_bypass_seb());
}
/**
* Test we can detect SEB usage.
*/
public function test_is_using_seb(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->is_using_seb());
$_SERVER['HTTP_USER_AGENT'] = 'Test';
$this->assertFalse($accessmanager->is_using_seb());
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$this->assertTrue($accessmanager->is_using_seb());
}
/**
* Test that the quiz Config Key matches the incoming request header.
*/
public function test_access_keys_validate_with_config_key(): void {
global $FULLME;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$configkey = seb_quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key();
// Set up dummy request.
$FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
$expectedhash = hash('sha256', $FULLME . $configkey);
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
$this->assertTrue($accessmanager->validate_config_key());
}
/**
* Test that the quiz Config Key matches a provided config key with no incoming request header.
*/
public function test_access_keys_validate_with_provided_config_key(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$url = 'https://www.example.com/moodle';
$accessmanager = $this->get_access_manager();
$configkey = seb_quiz_settings::get_record(['quizid' => $this->quiz->id])->get_config_key();
$fullconfigkey = hash('sha256', $url . $configkey);
$this->assertTrue($accessmanager->validate_config_key($fullconfigkey, $url));
}
/**
* Test that the quiz Config Key does not match the incoming request header.
*/
public function test_access_keys_fail_to_validate_with_config_key(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->validate_config_key());
}
/**
* Test that config key is not checked when using client configuration with SEB.
*/
public function test_config_key_not_checked_if_client_requirement_is_selected(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->should_validate_config_key());
}
/**
* Test that if there are no browser exam keys for quiz, check is skipped.
*/
public function test_no_browser_exam_keys_cause_check_to_be_successful(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$settings->set('allowedbrowserexamkeys', '');
$settings->save();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->should_validate_browser_exam_key());
$this->assertTrue($accessmanager->validate_browser_exam_key());
}
/**
* Test that access fails if there is no hash in header.
*/
public function test_access_keys_fail_if_browser_exam_key_header_does_not_exist(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
$settings->save();
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->validate_browser_exam_key());
}
/**
* Test that access fails if browser exam key doesn't match hash in header.
*/
public function test_access_keys_fail_if_browser_exam_key_header_does_not_match_provided_hash(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$settings->set('allowedbrowserexamkeys', hash('sha256', 'one') . "\n" . hash('sha256', 'two'));
$settings->save();
$accessmanager = $this->get_access_manager();
$_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = hash('sha256', 'notwhatyouwereexpectinghuh');
$this->assertFalse($accessmanager->validate_browser_exam_key());
}
/**
* Test that browser exam key matches hash in header.
*/
public function test_browser_exam_keys_match_header_hash(): void {
global $FULLME;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$browserexamkey = hash('sha256', 'browserexamkey');
$settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK.
$settings->save();
$accessmanager = $this->get_access_manager();
// Set up dummy request.
$FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
$expectedhash = hash('sha256', $FULLME . $browserexamkey);
$_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash;
$this->assertTrue($accessmanager->validate_browser_exam_key());
}
/**
* Test that browser exam key matches a provided browser exam key.
*/
public function test_browser_exam_keys_match_provided_browser_exam_key(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$url = 'https://www.example.com/moodle';
$settings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$browserexamkey = hash('sha256', 'browserexamkey');
$fullbrowserexamkey = hash('sha256', $url . $browserexamkey);
$settings->set('allowedbrowserexamkeys', $browserexamkey); // Add a hashed BEK.
$settings->save();
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_browser_exam_key($fullbrowserexamkey, $url));
}
/**
* Test can get received config key.
*/
public function test_get_received_config_key(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertNull($accessmanager->get_received_config_key());
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Test key';
$this->assertEquals('Test key', $accessmanager->get_received_config_key());
}
/**
* Test can get received browser key.
*/
public function get_received_browser_exam_key() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertNull($accessmanager->get_received_browser_exam_key());
$_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Test browser key';
$this->assertEquals('Test browser key', $accessmanager->get_received_browser_exam_key());
}
/**
* Test can correctly get type of SEB usage for the quiz.
*/
public function test_get_seb_use_type(): void {
// No SEB.
$this->quiz = $this->create_test_quiz($this->course);
$accessmanager = $this->get_access_manager();
$this->assertEquals(settings_provider::USE_SEB_NO, $accessmanager->get_seb_use_type());
// Manually.
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $accessmanager->get_seb_use_type());
// Use template.
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
$quizsettings->set('templateid', $this->create_template()->get('id'));
$quizsettings->save();
$accessmanager = $this->get_access_manager();
$this->assertEquals(settings_provider::USE_SEB_TEMPLATE, $accessmanager->get_seb_use_type());
// Use uploaded config.
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header.
$xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
$this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings->save();
$accessmanager = $this->get_access_manager();
$this->assertEquals(settings_provider::USE_SEB_UPLOAD_CONFIG, $accessmanager->get_seb_use_type());
// Use client config.
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertEquals(settings_provider::USE_SEB_CLIENT_CONFIG, $accessmanager->get_seb_use_type());
}
/**
* Data provider for self::test_should_validate_basic_header.
*
* @return array
*/
public function should_validate_basic_header_data_provider() {
return [
[settings_provider::USE_SEB_NO, false],
[settings_provider::USE_SEB_CONFIG_MANUALLY, false],
[settings_provider::USE_SEB_TEMPLATE, false],
[settings_provider::USE_SEB_UPLOAD_CONFIG, false],
[settings_provider::USE_SEB_CLIENT_CONFIG, true],
];
}
/**
* Test we know when we should validate basic header.
*
* @param int $type Type of SEB usage.
* @param bool $expected Expected result.
*
* @dataProvider should_validate_basic_header_data_provider
*/
public function test_should_validate_basic_header($type, $expected): void {
$accessmanager = $this->getMockBuilder(seb_access_manager::class)
->disableOriginalConstructor()
->onlyMethods(['get_seb_use_type'])
->getMock();
$accessmanager->method('get_seb_use_type')->willReturn($type);
$this->assertEquals($expected, $accessmanager->should_validate_basic_header());
}
/**
* Data provider for self::test_should_validate_config_key.
*
* @return array
*/
public function should_validate_config_key_data_provider() {
return [
[settings_provider::USE_SEB_NO, false],
[settings_provider::USE_SEB_CONFIG_MANUALLY, true],
[settings_provider::USE_SEB_TEMPLATE, true],
[settings_provider::USE_SEB_UPLOAD_CONFIG, true],
[settings_provider::USE_SEB_CLIENT_CONFIG, false],
];
}
/**
* Test we know when we should validate config key.
*
* @param int $type Type of SEB usage.
* @param bool $expected Expected result.
*
* @dataProvider should_validate_config_key_data_provider
*/
public function test_should_validate_config_key($type, $expected): void {
$accessmanager = $this->getMockBuilder(seb_access_manager::class)
->disableOriginalConstructor()
->onlyMethods(['get_seb_use_type'])
->getMock();
$accessmanager->method('get_seb_use_type')->willReturn($type);
$this->assertEquals($expected, $accessmanager->should_validate_config_key());
}
/**
* Data provider for self::test_should_validate_browser_exam_key.
*
* @return array
*/
public function should_validate_browser_exam_key_data_provider() {
return [
[settings_provider::USE_SEB_NO, false],
[settings_provider::USE_SEB_CONFIG_MANUALLY, false],
[settings_provider::USE_SEB_TEMPLATE, false],
[settings_provider::USE_SEB_UPLOAD_CONFIG, true],
[settings_provider::USE_SEB_CLIENT_CONFIG, true],
];
}
/**
* Test we know when we should browser exam key.
*
* @param int $type Type of SEB usage.
* @param bool $expected Expected result.
*
* @dataProvider should_validate_browser_exam_key_data_provider
*/
public function test_should_validate_browser_exam_key($type, $expected): void {
$accessmanager = $this->getMockBuilder(seb_access_manager::class)
->disableOriginalConstructor()
->onlyMethods(['get_seb_use_type'])
->getMock();
$accessmanager->method('get_seb_use_type')->willReturn($type);
$this->assertEquals($expected, $accessmanager->should_validate_browser_exam_key());
}
/**
* Test that access manager uses cached Config Key.
*/
public function test_access_manager_uses_cached_config_key(): void {
global $FULLME;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$configkey = $accessmanager->get_valid_config_key();
// Set up dummy request.
$FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
$expectedhash = hash('sha256', $FULLME . $configkey);
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
$this->assertTrue($accessmanager->validate_config_key());
// Change settings (but don't save) and check that still can validate config key.
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('showsebtaskbar', 0);
$this->assertNotEquals($quizsettings->get_config_key(), $configkey);
$this->assertTrue($accessmanager->validate_config_key());
// Now save settings which should purge caches but access manager still has config key.
$quizsettings->save();
$this->assertNotEquals($quizsettings->get_config_key(), $configkey);
$this->assertTrue($accessmanager->validate_config_key());
// Initialise a new access manager. Now validation should fail.
$accessmanager = $this->get_access_manager();
$this->assertFalse($accessmanager->validate_config_key());
}
/**
* Check that valid SEB config key is null if quiz doesn't have SEB settings.
*/
public function test_valid_config_key_is_null_if_no_settings(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
$accessmanager = $this->get_access_manager();
$this->assertEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id]));
$this->assertNull($accessmanager->get_valid_config_key());
}
/**
* Test if config key should not be validated.
*/
public function test_if_config_key_should_not_be_validated(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_config_key());
}
/**
* Test if browser exam key should not be validated.
*/
public function test_if_browser_exam_key_should_not_be_validated(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
$this->assertTrue($accessmanager->validate_browser_exam_key());
}
/**
* Test that access is set correctly in Moodle session.
*/
public function test_set_session_access(): void {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid]));
$accessmanager->set_session_access(true);
$this->assertTrue($SESSION->quizaccess_seb_access[$this->quiz->cmid]);
}
/**
* Test that access is set in Moodle session for only course module associated with access manager.
*/
public function test_session_access_set_for_specific_course_module(): void {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$quiz2 = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$accessmanager->set_session_access(true);
$this->assertCount(1, $SESSION->quizaccess_seb_access);
$this->assertTrue($SESSION->quizaccess_seb_access[$this->quiz->cmid]);
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$quiz2->cmid]));
}
/**
* Test that access state can be retrieved from Moodle session.
*/
public function test_validate_session_access(): void {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$this->assertEmpty($accessmanager->validate_session_access());
$SESSION->quizaccess_seb_access[$this->quiz->cmid] = true;
$this->assertTrue($accessmanager->validate_session_access());
}
/**
* Test that access can be cleared from Moodle session.
*/
public function test_clear_session_access(): void {
global $SESSION;
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
$accessmanager = $this->get_access_manager();
$SESSION->quizaccess_seb_access[$this->quiz->cmid] = true;
$accessmanager->clear_session_access();
$this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid]));
}
/**
* Test we can decide if need to redirect to SEB config link.
*/
public function test_should_redirect_to_seb_config_link(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = $this->get_access_manager();
set_config('autoreconfigureseb', '1', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$this->assertFalse($accessmanager->should_redirect_to_seb_config_link());
set_config('autoreconfigureseb', '1', 'quizaccess_seb');
$_SERVER['HTTP_USER_AGENT'] = 'SEB';
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = hash('sha256', 'configkey');
$this->assertTrue($accessmanager->should_redirect_to_seb_config_link());
}
}
@@ -0,0 +1,285 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace quizaccess_seb;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/test_helper_trait.php');
/**
* PHPUnit tests for backup and restore functionality.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_restore_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/** @var template $template A test template. */
protected $template;
/**
* Called before every test.
*/
public function setUp(): void {
global $USER;
parent::setUp();
$this->resetAfterTest();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
$this->template = $this->create_template();
$this->user = $USER;
}
/**
* A helper method to create a quiz with template usage of SEB.
*
* @return seb_quiz_settings
*/
protected function create_quiz_with_template() {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
$quizsettings->set('templateid', $this->template->get('id'));
$quizsettings->save();
return $quizsettings;
}
/**
* A helper method to emulate backup and restore of the quiz.
*
* @return \cm_info|null
*/
protected function backup_and_restore_quiz() {
return duplicate_module($this->course, get_fast_modinfo($this->course)->get_cm($this->quiz->cmid));
}
/**
* A helper method to backup test quiz.
*
* @return mixed A backup ID ready to be restored.
*/
protected function backup_quiz() {
global $CFG;
// Get the necessary files to perform backup and restore.
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
$backupid = 'test-seb-backup-restore';
$bc = new \backup_controller(\backup::TYPE_1ACTIVITY, $this->quiz->coursemodule, \backup::FORMAT_MOODLE,
\backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $this->user->id);
$bc->execute_plan();
$results = $bc->get_results();
$file = $results['backup_destination'];
$fp = get_file_packer('application/vnd.moodle.backup');
$filepath = $CFG->dataroot . '/temp/backup/' . $backupid;
$file->extract_to_pathname($fp, $filepath);
$bc->destroy();
return $backupid;
}
/**
* A helper method to restore provided backup.
*
* @param string $backupid Backup ID to restore.
*/
protected function restore_quiz($backupid) {
$rc = new \restore_controller($backupid, $this->course->id,
\backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $this->user->id, \backup::TARGET_CURRENT_ADDING);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
}
/**
* A helper method to emulate restoring to a different site.
*/
protected function change_site() {
set_config('siteidentifier', random_string(32) . 'not the same site');
}
/**
* A helper method to validate backup and restore results.
*
* @param cm_info $newcm Restored course_module object.
*/
protected function validate_backup_restore(\cm_info $newcm) {
$this->assertEquals(2, seb_quiz_settings::count_records());
$actual = seb_quiz_settings::get_record(['quizid' => $newcm->instance]);
$expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals($expected->get('templateid'), $actual->get('templateid'));
$this->assertEquals($expected->get('requiresafeexambrowser'), $actual->get('requiresafeexambrowser'));
$this->assertEquals($expected->get('showsebdownloadlink'), $actual->get('showsebdownloadlink'));
$this->assertEquals($expected->get('allowuserquitseb'), $actual->get('allowuserquitseb'));
$this->assertEquals($expected->get('quitpassword'), $actual->get('quitpassword'));
$this->assertEquals($expected->get('allowedbrowserexamkeys'), $actual->get('allowedbrowserexamkeys'));
// Validate specific SEB config settings.
foreach (settings_provider::get_seb_config_elements() as $name => $notused) {
$name = preg_replace("/^seb_/", "", $name);
$this->assertEquals($expected->get($name), $actual->get($name));
}
}
/**
* Test backup and restore when no seb.
*/
public function test_backup_restore_no_seb(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
$this->assertEquals(0, seb_quiz_settings::count_records());
$this->backup_and_restore_quiz();
$this->assertEquals(0, seb_quiz_settings::count_records());
}
/**
* Test backup and restore when manually configured.
*/
public function test_backup_restore_manual_config(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$expected->set('showsebdownloadlink', 0);
$expected->set('quitpassword', '123');
$expected->save();
$this->assertEquals(1, seb_quiz_settings::count_records());
$newcm = $this->backup_and_restore_quiz();
$this->validate_backup_restore($newcm);
}
/**
* Test backup and restore when using template.
*/
public function test_backup_restore_template_config(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$template = $this->create_template();
$expected->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
$expected->set('templateid', $template->get('id'));
$expected->save();
$this->assertEquals(1, seb_quiz_settings::count_records());
$newcm = $this->backup_and_restore_quiz();
$this->validate_backup_restore($newcm);
}
/**
* Test backup and restore when using uploaded file.
*/
public function test_backup_restore_uploaded_config(): void {
$this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
$expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$expected->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
$this->create_module_test_file($xml, $this->quiz->cmid);
$expected->save();
$this->assertEquals(1, seb_quiz_settings::count_records());
$newcm = $this->backup_and_restore_quiz();
$this->validate_backup_restore($newcm);
$expectedfile = settings_provider::get_module_context_sebconfig_file($this->quiz->cmid);
$actualfile = settings_provider::get_module_context_sebconfig_file($newcm->id);
$this->assertEquals($expectedfile->get_content(), $actualfile->get_content());
}
/**
* No new template should be restored if restoring to a different site,
* but the template with the same name and content exists..
*/
public function test_restore_template_to_a_different_site_when_the_same_template_exists(): void {
$this->create_quiz_with_template();
$backupid = $this->backup_quiz();
$this->assertEquals(1, seb_quiz_settings::count_records());
$this->assertEquals(1, template::count_records());
$this->change_site();
$this->restore_quiz($backupid);
// Should see additional setting record, but no new template record.
$this->assertEquals(2, seb_quiz_settings::count_records());
$this->assertEquals(1, template::count_records());
}
/**
* A new template should be restored if restoring to a different site, but existing template
* has the same content, but different name.
*/
public function test_restore_template_to_a_different_site_when_the_same_content_but_different_name(): void {
$this->create_quiz_with_template();
$backupid = $this->backup_quiz();
$this->assertEquals(1, seb_quiz_settings::count_records());
$this->assertEquals(1, template::count_records());
$this->template->set('name', 'New name for template');
$this->template->save();
$this->change_site();
$this->restore_quiz($backupid);
// Should see additional setting record, and new template record.
$this->assertEquals(2, seb_quiz_settings::count_records());
$this->assertEquals(2, template::count_records());
}
/**
* A new template should be restored if restoring to a different site, but existing template
* has the same name, but different content.
*/
public function test_restore_template_to_a_different_site_when_the_same_name_but_different_content(): void {
global $CFG;
$this->create_quiz_with_template();
$backupid = $this->backup_quiz();
$this->assertEquals(1, seb_quiz_settings::count_records());
$this->assertEquals(1, template::count_records());
$newxml = file_get_contents($CFG->dirroot . '/mod/quiz/accessrule/seb/tests/fixtures/simpleunencrypted.seb');
$this->template->set('content', $newxml);
$this->template->save();
$this->change_site();
$this->restore_quiz($backupid);
// Should see additional setting record, and new template record.
$this->assertEquals(2, seb_quiz_settings::count_records());
$this->assertEquals(2, template::count_records());
}
}
@@ -0,0 +1,234 @@
@javascript @mod_quiz @quizaccess @quizaccess_seb
Feature: Safe Exam Browser settings in quiz edit form
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "activities" exist:
| activity | course | section | name |
| quiz | C1 | 1 | Quiz 1 |
Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types, except "Use an existing template".
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And the "Require the use of Safe Exam Browser" select box should contain "Yes Configure manually"
And the "Require the use of Safe Exam Browser" select box should not contain "Yes Use an existing template"
And the "Require the use of Safe Exam Browser" select box should contain "Yes Upload my own config"
And the "Require the use of Safe Exam Browser" select box should contain "Yes Use SEB client config"
And the field "Require the use of Safe Exam Browser" matches value "No"
Scenario: Quiz setting "Require the use of Safe Exam Browser" has all types if at least one template has been added.
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
When I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And the "Require the use of Safe Exam Browser" select box should contain "Yes Configure manually"
And the "Require the use of Safe Exam Browser" select box should contain "Yes Use an existing template"
And the "Require the use of Safe Exam Browser" select box should contain "Yes Upload my own config"
And the "Require the use of Safe Exam Browser" select box should contain "Yes Use SEB client config"
And the field "Require the use of Safe Exam Browser" matches value "No"
Scenario: Quiz can be edited without capability to select SEB template
Given the following "permission override" exists:
| role | editingteacher |
| capability | quizaccess/seb:manage_seb_templateid |
| permission | Prevent |
| contextlevel | System |
| reference | |
And the following "user" exists:
| username | teacher |
| firstname | Teacher |
| lastname | One |
And the following "course enrolment" exists:
| user | teacher |
| course | C1 |
| role | editingteacher |
And I log in as "teacher"
# Create the quiz.
When I add a quiz activity to course "Course 1" section "0" and I fill the form with:
| Name | My quiz |
Then I should not see "New Quiz"
# Edit the quiz.
And I am on the "My quiz" "quiz activity editing" page
And I set the field "Name" to "My quiz edited"
And I press "Save and return to course"
And I should not see "Edit settings"
Scenario: SEB settings if using No SEB
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "No"
Then I should not see "Upload Safe Exam Browser config file"
Then I should not see "Safe Exam Browser config template"
Then I should not see "Template 1"
Then I should not see "Show Safe Exam Browser download button"
Then I should not see "Enable quitting of SEB"
Then I should not see "Quit password"
Then I should not see "Allowed browser exam keys"
Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
Then I should not see "Ask user to confirm quitting"
Then I should not see "Enable reload in exam"
Then I should not see "Show SEB task bar"
Then I should not see "Show reload button"
Then I should not see "Show time"
Then I should not see "Show keyboard layout"
Then I should not see "Show Wi-Fi control"
Then I should not see "Enable audio controls"
Then I should not see "Mute on startup"
Then I should not see "Enable spell checking"
Then I should not see "Enable URL filtering"
Then I should not see "Filter also embedded content"
Then I should not see "Expressions allowed"
Then I should not see "Regex allowed"
Then I should not see "Expressions blocked"
Then I should not see "Regex blocked"
Scenario: SEB settings if using Use SEB client config
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Use SEB client config"
Then I should see "Show Safe Exam Browser download button"
Then I should see "Allowed browser exam keys"
Then I should not see "Upload Safe Exam Browser config file"
Then I should not see "Safe Exam Browser config template"
Then I should not see "Template 1"
Then I should not see "Enable quitting of SEB"
Then I should not see "Quit password"
Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
Then I should not see "Ask user to confirm quitting"
Then I should not see "Enable reload in exam"
Then I should not see "Show SEB task bar"
Then I should not see "Show reload button"
Then I should not see "Show time"
Then I should not see "Show keyboard layout"
Then I should not see "Show Wi-Fi control"
Then I should not see "Enable audio controls"
Then I should not see "Mute on startup"
Then I should not see "Enable spell checking"
Then I should not see "Enable URL filtering"
Then I should not see "Filter also embedded content"
Then I should not see "Expressions allowed"
Then I should not see "Regex allowed"
Then I should not see "Expressions blocked"
Then I should not see "Regex blocked"
Scenario: SEB settings if using Upload my own config
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Upload my own config"
Then I should see "Upload Safe Exam Browser config file"
Then I should see "Show Safe Exam Browser download button"
Then I should not see "Enable quitting of SEB"
Then I should not see "Quit password"
Then I should see "Allowed browser exam keys"
Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
Then I should not see "Ask user to confirm quitting"
Then I should not see "Enable reload in exam"
Then I should not see "Show SEB task bar"
Then I should not see "Show reload button"
Then I should not see "Show time"
Then I should not see "Show keyboard layout"
Then I should not see "Show Wi-Fi control"
Then I should not see "Enable audio controls"
Then I should not see "Mute on startup"
Then I should not see "Enable spell checking"
Then I should not see "Enable URL filtering"
Then I should not see "Filter also embedded content"
Then I should not see "Expressions allowed"
Then I should not see "Regex allowed"
Then I should not see "Expressions blocked"
Then I should not see "Regex blocked"
Then I should not see "Safe Exam Browser config template"
Then I should not see "Template 1"
Scenario: SEB settings if using Use an existing template
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Use an existing template"
Then I should see "Safe Exam Browser config template"
Then I should see "Template 1"
Then I should see "Show Safe Exam Browser download button"
Then I should see "Enable quitting of SEB"
Then I should see "Quit password"
Then I should not see "Allowed browser exam keys"
Then I should not see "Upload Safe Exam Browser config file"
Then I should not see "Show Exit Safe Exam Browser button, configured with this quit link"
Then I should not see "Ask user to confirm quitting"
Then I should not see "Enable reload in exam"
Then I should not see "Show SEB task bar"
Then I should not see "Show reload button"
Then I should not see "Show time"
Then I should not see "Show keyboard layout"
Then I should not see "Show Wi-Fi control"
Then I should not see "Enable audio controls"
Then I should not see "Mute on startup"
Then I should not see "Enable spell checking"
Then I should not see "Enable URL filtering"
Then I should not see "Filter also embedded content"
Then I should not see "Expressions allowed"
Then I should not see "Regex allowed"
Then I should not see "Expressions blocked"
Then I should not see "Regex blocked"
And I set the field "Enable quitting of SEB" to "No"
Then I should not see "Quit password"
Scenario: SEB settings if using Configure manually
Given the following "quizaccess_seb > seb templates" exist:
| name |
| Template 1 |
And I am on the "Quiz 1" "quiz activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Require the use of Safe Exam Browser" to "Yes Configure manually"
Then I should see "Show Safe Exam Browser download button"
Then I should see "Enable quitting of SEB"
Then I should see "Quit password"
Then I should see "Show Exit Safe Exam Browser button, configured with this quit link"
Then I should see "Ask user to confirm quitting"
Then I should see "Enable reload in exam"
Then I should see "Show SEB task bar"
Then I should see "Show reload button"
Then I should see "Show time"
Then I should see "Show keyboard layout"
Then I should see "Show Wi-Fi control"
Then I should see "Enable audio controls"
Then I should not see "Mute on startup"
Then I should see "Enable spell checking"
Then I should see "Enable URL filtering"
Then I should not see "Filter also embedded content"
Then I should not see "Expressions allowed"
Then I should not see "Regex allowed"
Then I should not see "Expressions blocked"
Then I should not see "Regex blocked"
And I set the field "Enable quitting of SEB" to "No"
Then I should not see "Quit password"
And I set the field "Show SEB task bar" to "No"
Then I should not see "Show reload button"
Then I should not see "Show time"
Then I should not see "Show keyboard layout"
Then I should not see "Show Wi-Fi control"
And I set the field "Enable audio controls" to "Yes"
Then I should see "Mute on startup"
And I set the field "Enable URL filtering" to "Yes"
Then I should see "Filter also embedded content"
Then I should see "Expressions allowed"
Then I should see "Regex allowed"
Then I should see "Expressions blocked"
Then I should see "Regex blocked"
Then I should not see "Upload Safe Exam Browser config file"
Then I should not see "Allowed browser exam keys"
Then I should not see "Safe Exam Browser config template"
Then I should not see "Template 1"
@@ -0,0 +1,85 @@
<?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;
/**
* PHPUnit Tests for config_key class.
*
* @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
* @covers \quizaccess_seb\config_key
*/
class config_key_test extends \advanced_testcase {
/**
* Test that trying to generate the hash key with bad xml will result in an error.
*/
public function test_config_key_not_generated_with_bad_xml(): void {
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage("Invalid a PList XML string, representing SEB config");
config_key::generate("<?xml This is some bad xml for sure.");
}
/**
* Test that a config key is generated with empty configuration. SEB would be using defaults for all settings.
*/
public function test_config_key_hash_generated_with_empty_string(): void {
$hash = config_key::generate('')->get_hash();
$this->assertEquals('4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', $hash);
}
/**
* Test config key hash is derived correctly by Moodle.
*
* @param string $config The SEB config file name.
* @param string $hash The correct config key hash for this file.
*
* @dataProvider real_ck_hash_provider
*/
public function test_config_key_hash_is_derived_correctly($config, $hash): void {
$xml = file_get_contents(__DIR__ . '/fixtures/' . $config);
$derivedhash = config_key::generate($xml)->get_hash();
$this->assertEquals($hash, $derivedhash);
}
/**
* Check that the Config Key hash is not altered if the originatorVersion is present in the XML or not.
*/
public function test_presence_of_originator_version_does_not_effect_hash(): void {
$xmlwithoriginatorversion = file_get_contents(__DIR__ . '/fixtures/simpleunencrypted.seb');
$xmlwithoutoriginatorversion = file_get_contents(__DIR__ . '/fixtures/simpleunencryptedwithoutoriginator.seb');
$hashwithorigver = config_key::generate($xmlwithoriginatorversion)->get_hash();
$hashwithoutorigver = config_key::generate($xmlwithoutoriginatorversion)->get_hash();
$this->assertEquals($hashwithorigver, $hashwithoutorigver);
}
/**
* Provide a seb file, the expected Config Key and a password if encrypted.
*
* @return array
*/
public function real_ck_hash_provider(): array {
return [
'unencrypted_mac2.1.4' => ['unencrypted_mac_001.seb',
'4fa9af8ec8759eb7c680752ef4ee5eaf1a860628608fccae2715d519849f9292', ''],
'unencrypted_win2.2.3' => ['unencrypted_win_223.seb',
'2534e4e9f3188f9f9133bf7cf7b4c5d898292bbd7e8d0230f39d1176636a1431', ''],
];
}
}
@@ -0,0 +1,179 @@
<?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\event;
use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/..//test_helper_trait.php');
/**
* PHPUnit tests for all plugin events.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class events_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/**
* Called before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->course = $this->getDataGenerator()->create_course();
}
/**
* Test creating the access_prevented event.
*
* @covers \quizaccess_seb\event\access_prevented
*/
public function test_event_access_prevented(): void {
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = new \quizaccess_seb\seb_access_manager(new quiz_settings($quiz,
get_coursemodule_from_id('quiz', $quiz->cmid), $this->course));
// Set up event with data.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'configkey';
$_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'browserexamkey';
$event = \quizaccess_seb\event\access_prevented::create_strict($accessmanager, 'Because I said so.');
// Create an event sink, trigger event and retrieve event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertEquals(1, count($events));
$event = reset($events);
$expectedconfigkey = $accessmanager->get_valid_config_key();
// Test that the event data is as expected.
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertEquals('Quiz access was prevented', $event->get_name());
$this->assertEquals(
"The user with id '$user->id' has been prevented from accessing quiz with id '$quiz->id' by the "
. "Safe Exam Browser access plugin. The reason was 'Because I said so.'. "
. "Expected config key: '$expectedconfigkey'. "
. "Received config key: 'configkey'. Received browser exam key: 'browserexamkey'.",
$event->get_description());
$this->assertEquals(\context_module::instance($quiz->cmid), $event->get_context());
$this->assertEquals($user->id, $event->userid);
$this->assertEquals($quiz->id, $event->objectid);
$this->assertEquals($this->course->id, $event->courseid);
$this->assertEquals('Because I said so.', $event->other['reason']);
$this->assertEquals($expectedconfigkey, $event->other['savedconfigkey']);
$this->assertEquals('configkey', $event->other['receivedconfigkey']);
$this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']);
}
/**
* Test creating the access_prevented event with provided SEB keys.
*
* @covers \quizaccess_seb\event\access_prevented
*/
public function test_event_access_prevented_with_keys(): void {
$this->resetAfterTest();
$this->setAdminUser();
$quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
$accessmanager = new \quizaccess_seb\seb_access_manager(new quiz_settings($quiz,
get_coursemodule_from_id('quiz', $quiz->cmid), $this->course));
// Set up event with data.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$event = \quizaccess_seb\event\access_prevented::create_strict($accessmanager, 'Because I said so.',
'configkey', 'browserexamkey');
// Create an event sink, trigger event and retrieve event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertEquals(1, count($events));
$event = reset($events);
$expectedconfigkey = $accessmanager->get_valid_config_key();
// Test that the event data is as expected.
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertEquals('Quiz access was prevented', $event->get_name());
$this->assertEquals(
"The user with id '$user->id' has been prevented from accessing quiz with id '$quiz->id' by the "
. "Safe Exam Browser access plugin. The reason was 'Because I said so.'. "
. "Expected config key: '$expectedconfigkey'. "
. "Received config key: 'configkey'. Received browser exam key: 'browserexamkey'.",
$event->get_description());
$this->assertEquals(\context_module::instance($quiz->cmid), $event->get_context());
$this->assertEquals($user->id, $event->userid);
$this->assertEquals($quiz->id, $event->objectid);
$this->assertEquals($this->course->id, $event->courseid);
$this->assertEquals('Because I said so.', $event->other['reason']);
$this->assertEquals($expectedconfigkey, $event->other['savedconfigkey']);
$this->assertEquals('configkey', $event->other['receivedconfigkey']);
$this->assertEquals('browserexamkey', $event->other['receivedbrowserexamkey']);
}
/**
* Test creating the template_created event.
*
* @covers \quizaccess_seb\event\template_created
*/
public function test_event_create_template(): void {
$this->resetAfterTest();
// Set up event with data.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$template = $this->create_template();
$event = \quizaccess_seb\event\template_created::create_strict(
$template,
\context_system::instance());
// Create an event sink, trigger event and retrieve event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertEquals(1, count($events));
$event = reset($events);
// Test that the event data is as expected.
$this->assertInstanceOf('\quizaccess_seb\event\template_created', $event);
$this->assertEquals('SEB template was created', $event->get_name());
$this->assertEquals(
"The user with id '$user->id' has created a template with id '{$template->get('id')}'.",
$event->get_description()
);
$this->assertEquals(\context_system::instance(), $event->get_context());
$this->assertEquals($user->id, $event->userid);
$this->assertEquals($template->get('id'), $event->objectid);
}
}
@@ -0,0 +1,258 @@
<?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;
use quizaccess_seb\seb_quiz_settings;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../test_helper_trait.php');
/**
* PHPUnit tests for external function.
*
* @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
* @covers \quizaccess_seb\external\validate_quiz_access
*/
class validate_quiz_access_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/**
* This method runs before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
// Generate data objects.
$this->course = $this->getDataGenerator()->create_course();
$this->quiz = $this->create_test_quiz($this->course);
$this->user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, 'student');
$this->setUser($this->user);
}
/**
* Bad parameter provider.
*
* @return array
*/
public function bad_parameters_provider(): array {
return [
'no params' => [
'cmid' => null,
'url' => null,
'configkey' => null,
'/Invalid parameter value detected \(Missing required key in single structure: cmid\)/'
],
'no course module id' => [
'cmid' => null,
'url' => 'https://www.example.com/moodle',
'configkey' => hash('sha256', 'configkey'),
'/Invalid parameter value detected \(Missing required key in single structure: cmid\)/'
],
'no url' => [
'cmid' => 123,
'url' => null,
'configkey' => hash('sha256', 'configkey'),
'/Invalid parameter value detected \(Missing required key in single structure: url\)/'
],
'cmid is not an int' => [
'cmid' => 'test',
'url' => 'https://www.example.com/moodle',
'configkey' => null,
'/Invalid external api parameter: the value is "test", the server was expecting "int" type/'
],
'url is not a url' => [
'cmid' => 123,
'url' => 123,
'configkey' => hash('sha256', 'configkey'),
'/Invalid external api parameter: the value is "123", the server was expecting "url" type/'
],
];
}
/**
* Test exception thrown for bad parameters.
*
* @param mixed $cmid Course module id.
* @param mixed $url Page URL.
* @param mixed $configkey SEB config key.
* @param mixed $messageregex Error message regex to check.
*
* @dataProvider bad_parameters_provider
*/
public function test_invalid_parameters($cmid, $url, $configkey, $messageregex): void {
$params = [];
if (!empty($cmid)) {
$params['cmid'] = $cmid;
}
if (!empty($url)) {
$params['url'] = $url;
}
if (!empty($configkey)) {
$params['configkey'] = $configkey;
}
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessageMatches($messageregex);
\core_external\external_api::validate_parameters(validate_quiz_keys::execute_parameters(), $params);
}
/**
* Test that the user has permissions to access context.
*/
public function test_context_is_not_valid_for_user(): void {
// Set user as user not enrolled in course and quiz.
$this->user = $this->getDataGenerator()->create_user();
$this->setUser($this->user);
$this->expectException(\require_login_exception::class);
$this->expectExceptionMessage('Course or activity not accessible. (Not enrolled)');
validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'configkey');
}
/**
* Test exception thrown when no key provided.
*/
public function test_no_keys_provided(): void {
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage('At least one Safe Exam Browser key must be provided.');
validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle');
}
/**
* Test exception thrown if cmid doesn't match a quiz.
*/
public function test_quiz_does_not_exist(): void {
$this->setAdminUser();
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $this->course->id]);
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage('Quiz not found matching course module ID: ' . $forum->cmid);
validate_quiz_keys::execute($forum->cmid, 'https://www.example.com/moodle', 'configkey');
}
/**
* Test config key is valid.
*/
public function test_config_key_valid(): void {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
$url = 'https://www.example.com/moodle';
// Create the quiz settings.
$quizsettings = new seb_quiz_settings(0, $settings);
$quizsettings->save();
$fullconfigkey = hash('sha256', $url . $quizsettings->get_config_key());
$result = validate_quiz_keys::execute($this->quiz->cmid, $url, $fullconfigkey);
$this->assertTrue($result['configkey']);
$this->assertTrue($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(0, $events);
}
/**
* Test config key is not valid.
*/
public function test_config_key_not_valid(): void {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
// Create the quiz settings.
$quizsettings = new seb_quiz_settings(0, $settings);
$quizsettings->save();
$result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', 'badconfigkey');
$this->assertFalse($result['configkey']);
$this->assertTrue($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertStringContainsString('Invalid SEB config key', $event->get_description());
}
/**
* Test browser exam key is valid.
*/
public function test_browser_exam_key_valid(): void {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$url = 'https://www.example.com/moodle';
$validbrowserexamkey = hash('sha256', 'validbrowserexamkey');
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
'requiresafeexambrowser' => \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG,
'allowedbrowserexamkeys' => $validbrowserexamkey,
]);
// Create the quiz settings.
$quizsettings = new seb_quiz_settings(0, $settings);
$quizsettings->save();
$fullbrowserexamkey = hash('sha256', $url . $validbrowserexamkey);
$result = validate_quiz_keys::execute($this->quiz->cmid, $url, null, $fullbrowserexamkey);
$this->assertTrue($result['configkey']);
$this->assertTrue($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(0, $events);
}
/**
* Test browser exam key is not valid.
*/
public function test_browser_exam_key_not_valid(): void {
$sink = $this->redirectEvents();
// Test settings to populate the quiz.
$validbrowserexamkey = hash('sha256', 'validbrowserexamkey');
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
'requiresafeexambrowser' => \quizaccess_seb\settings_provider::USE_SEB_CLIENT_CONFIG,
'allowedbrowserexamkeys' => $validbrowserexamkey,
]);
// Create the quiz settings.
$quizsettings = new seb_quiz_settings(0, $settings);
$quizsettings->save();
$result = validate_quiz_keys::execute($this->quiz->cmid, 'https://www.example.com/moodle', null,
hash('sha256', 'badbrowserexamkey'));
$this->assertTrue($result['configkey']);
$this->assertFalse($result['browserexamkey']);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
$this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
$this->assertStringContainsString('Invalid SEB browser key', $event->get_description());
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aaaaaa</key>
<date>1940-10-09T22:13:56Z</date>
<key>originatorVersion</key>
<string>SEB_Win_2.1.1</string>
<key>startURL</key>
<string>https://safeexambrowser.org/start</string>
<key>startResource</key>
<string />
<key>sebServerURL</key>
<string />
<key>hashedAdminPassword</key>
<string />
<key>allowQuit</key>
<true />
<key>ignoreExitKeys</key>
<true />
<key>allowUserAppFolderInstall</key>
<false />
<key>allowSiri</key>
<false />
<key>allowDictation</key>
<false />
<key>detectStoppedProcess</key>
<true />
<key>allowDisplayMirroring</key>
<false />
<key>allowedDisplaysMaxNumber</key>
<integer>1</integer>
<key>allowedDisplayBuiltin</key>
<true />
</dict>
</plist>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aaaaaa</key>
<date>1940-10-09T22:13:56Z</date>
<key>startURL</key>
<string>https://safeexambrowser.org/start</string>
<key>startResource</key>
<string />
<key>sebServerURL</key>
<string />
<key>hashedAdminPassword</key>
<string />
<key>allowQuit</key>
<true />
<key>ignoreExitKeys</key>
<true />
<key>allowUserAppFolderInstall</key>
<false />
<key>allowSiri</key>
<false />
<key>allowDictation</key>
<false />
<key>detectStoppedProcess</key>
<true />
<key>allowDisplayMirroring</key>
<false />
<key>allowedDisplaysMaxNumber</key>
<integer>1</integer>
<key>allowedDisplayBuiltin</key>
<true />
</dict>
</plist>
+797
View File
@@ -0,0 +1,797 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>originatorVersion</key>
<string>SEB_Win_2.1.1</string>
<key>startURL</key>
<string>https://safeexambrowser.org/start</string>
<key>startResource</key>
<string />
<key>sebServerURL</key>
<string />
<key>hashedAdminPassword</key>
<string />
<key>allowQuit</key>
<true />
<key>ignoreExitKeys</key>
<true />
<key>hashedQuitPassword</key>
<string />
<key>exitKey1</key>
<integer>2</integer>
<key>exitKey2</key>
<integer>10</integer>
<key>exitKey3</key>
<integer>5</integer>
<key>sebMode</key>
<integer>0</integer>
<key>browserMessagingSocket</key>
<string>ws://localhost:8706</string>
<key>browserMessagingPingTime</key>
<integer>120000</integer>
<key>sebConfigPurpose</key>
<integer>0</integer>
<key>allowPreferencesWindow</key>
<true />
<key>useAsymmetricOnlyEncryption</key>
<true />
<key>browserViewMode</key>
<integer>0</integer>
<key>mainBrowserWindowWidth</key>
<string>100%</string>
<key>mainBrowserWindowHeight</key>
<string>100%</string>
<key>mainBrowserWindowPositioning</key>
<integer>1</integer>
<key>enableBrowserWindowToolbar</key>
<false />
<key>hideBrowserWindowToolbar</key>
<false />
<key>showMenuBar</key>
<false />
<key>showTaskBar</key>
<true />
<key>taskBarHeight</key>
<integer>40</integer>
<key>touchOptimized</key>
<false />
<key>enableZoomText</key>
<true />
<key>enableZoomPage</key>
<true />
<key>zoomMode</key>
<integer>0</integer>
<key>allowSpellCheck</key>
<false />
<key>allowDictionaryLookup</key>
<false />
<key>allowSpellCheckDictionary</key>
<array></array>
<key>additionalDictionaries</key>
<array></array>
<key>showReloadButton</key>
<true />
<key>showTime</key>
<true />
<key>showInputLanguage</key>
<true />
<key>enableTouchExit</key>
<false />
<key>oskBehavior</key>
<integer>2</integer>
<key>audioControlEnabled</key>
<true />
<key>audioMute</key>
<false />
<key>audioVolumeLevel</key>
<integer>25</integer>
<key>audioSetVolumeLevel</key>
<false />
<key>browserScreenKeyboard</key>
<false />
<key>newBrowserWindowByLinkPolicy</key>
<integer>2</integer>
<key>newBrowserWindowByScriptPolicy</key>
<integer>2</integer>
<key>newBrowserWindowByLinkBlockForeign</key>
<false />
<key>newBrowserWindowByScriptBlockForeign</key>
<false />
<key>newBrowserWindowByLinkWidth</key>
<string>100%</string>
<key>newBrowserWindowByLinkHeight</key>
<string>100%</string>
<key>newBrowserWindowByLinkPositioning</key>
<integer>2</integer>
<key>enablePlugIns</key>
<true />
<key>enableJava</key>
<false />
<key>enableJavaScript</key>
<true />
<key>blockPopUpWindows</key>
<false />
<key>allowVideoCapture</key>
<false />
<key>allowAudioCapture</key>
<false />
<key>allowBrowsingBackForward</key>
<false />
<key>newBrowserWindowNavigation</key>
<true />
<key>removeBrowserProfile</key>
<false />
<key>removeLocalStorage</key>
<false />
<key>enableSebBrowser</key>
<false />
<key>browserWindowAllowReload</key>
<true />
<key>newBrowserWindowAllowReload</key>
<true />
<key>showReloadWarning</key>
<true />
<key>newBrowserWindowShowReloadWarning</key>
<false />
<key>browserUserAgentWinDesktopMode</key>
<integer>0</integer>
<key>browserUserAgentWinDesktopModeCustom</key>
<string />
<key>browserUserAgentWinTouchMode</key>
<integer>0</integer>
<key>browserUserAgentWinTouchModeIPad</key>
<string>Mozilla/5.0 (iPad; CPU OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.3 Mobile/15E216 Safari/605.1.15</string>
<key>browserUserAgentWinTouchModeCustom</key>
<string />
<key>browserUserAgent</key>
<string />
<key>browserUserAgentMac</key>
<integer>0</integer>
<key>browserUserAgentMacCustom</key>
<string />
<key>browserWindowTitleSuffix</key>
<string />
<key>allowDownUploads</key>
<true />
<key>downloadDirectoryOSX</key>
<string>~/Downloads</string>
<key>downloadDirectoryWin</key>
<string />
<key>openDownloads</key>
<false />
<key>chooseFileToUploadPolicy</key>
<integer>0</integer>
<key>downloadPDFFiles</key>
<false />
<key>allowPDFPlugIn</key>
<false />
<key>downloadAndOpenSebConfig</key>
<true />
<key>backgroundOpenSEBConfig</key>
<false />
<key>examKeySalt</key>
<data>0zVdSEqKAnK0ZNd4QFlwVKBTgw60o6xRjpfDokUYYnk=</data>
<key>browserExamKey</key>
<string />
<key>browserURLSalt</key>
<true />
<key>sendBrowserExamKey</key>
<false />
<key>quitURL</key>
<string />
<key>quitURLConfirm</key>
<true />
<key>restartExamURL</key>
<string />
<key>restartExamUseStartURL</key>
<false />
<key>restartExamText</key>
<string />
<key>restartExamPasswordProtected</key>
<true />
<key>additionalResources</key>
<array></array>
<key>monitorProcesses</key>
<true />
<key>allowSwitchToApplications</key>
<false />
<key>allowFlashFullscreen</key>
<false />
<key>permittedProcesses</key>
<array>
<dict>
<key>active</key>
<true />
<key>autostart</key>
<true />
<key>iconInTaskbar</key>
<true />
<key>runInBackground</key>
<false />
<key>allowUserToChooseApp</key>
<false />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>title</key>
<string>SEB</string>
<key>description</key>
<string />
<key>executable</key>
<string>firefox.exe</string>
<key>originalName</key>
<string>firefox.exe</string>
<key>path</key>
<string>../xulrunner/</string>
<key>identifier</key>
<string>Firefox</string>
<key>windowHandlingProcess</key>
<string />
<key>arguments</key>
<array></array>
</dict>
</array>
<key>prohibitedProcesses</key>
<array>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>join.me</string>
<key>originalName</key>
<string>join.me</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>RPCSuite</string>
<key>originalName</key>
<string>RPCSuite</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>RPCService</string>
<key>originalName</key>
<string>RPCService</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>RemotePCDesktop</string>
<key>originalName</key>
<string>RemotePCDesktop</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>beamyourscreen-host</string>
<key>originalName</key>
<string>beamyourscreen-host</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>AeroAdmin</string>
<key>originalName</key>
<string>AeroAdmin</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>Mikogo-host</string>
<key>originalName</key>
<string>Mikogo-host</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>chromoting</string>
<key>originalName</key>
<string>chromoting</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>vncserverui</string>
<key>originalName</key>
<string>vncserverui</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>vncviewer</string>
<key>originalName</key>
<string>vncviewer</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>vncserver</string>
<key>originalName</key>
<string>vncserver</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>TeamViewer</string>
<key>originalName</key>
<string>TeamViewer</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>GotoMeetingWinStore</string>
<key>originalName</key>
<string>GotoMeetingWinStore</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>g2mcomm.exe</string>
<key>originalName</key>
<string>g2mcomm.exe</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>SkypeHost</string>
<key>originalName</key>
<string>SkypeHost</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>Skype</string>
<key>originalName</key>
<string>Skype</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
</array>
<key>enableURLFilter</key>
<false />
<key>enableURLContentFilter</key>
<false />
<key>URLFilterRules</key>
<array></array>
<key>URLFilterEnable</key>
<false />
<key>URLFilterEnableContentFilter</key>
<false />
<key>blacklistURLFilter</key>
<string />
<key>whitelistURLFilter</key>
<string />
<key>urlFilterTrustedContent</key>
<true />
<key>urlFilterRegex</key>
<true />
<key>embeddedCertificates</key>
<array></array>
<key>pinEmbeddedCertificates</key>
<false />
<key>proxySettingsPolicy</key>
<integer>0</integer>
<key>proxies</key>
<dict>
<key>ExceptionsList</key>
<array></array>
<key>ExcludeSimpleHostnames</key>
<false />
<key>AutoDiscoveryEnabled</key>
<false />
<key>AutoConfigurationEnabled</key>
<false />
<key>AutoConfigurationJavaScript</key>
<string />
<key>AutoConfigurationURL</key>
<string />
<key>FTPPassive</key>
<true />
<key>HTTPEnable</key>
<false />
<key>HTTPPort</key>
<integer>80</integer>
<key>HTTPProxy</key>
<string />
<key>HTTPRequiresPassword</key>
<false />
<key>HTTPUsername</key>
<string />
<key>HTTPPassword</key>
<string />
<key>HTTPSEnable</key>
<false />
<key>HTTPSPort</key>
<integer>443</integer>
<key>HTTPSProxy</key>
<string />
<key>HTTPSRequiresPassword</key>
<false />
<key>HTTPSUsername</key>
<string />
<key>HTTPSPassword</key>
<string />
<key>FTPEnable</key>
<false />
<key>FTPPort</key>
<integer>21</integer>
<key>FTPProxy</key>
<string />
<key>FTPRequiresPassword</key>
<false />
<key>FTPUsername</key>
<string />
<key>FTPPassword</key>
<string />
<key>SOCKSEnable</key>
<false />
<key>SOCKSPort</key>
<integer>1080</integer>
<key>SOCKSProxy</key>
<string />
<key>SOCKSRequiresPassword</key>
<false />
<key>SOCKSUsername</key>
<string />
<key>SOCKSPassword</key>
<string />
<key>RTSPEnable</key>
<false />
<key>RTSPPort</key>
<integer>554</integer>
<key>RTSPProxy</key>
<string />
<key>RTSPRequiresPassword</key>
<false />
<key>RTSPUsername</key>
<string />
<key>RTSPPassword</key>
<string />
</dict>
<key>sebServicePolicy</key>
<integer>1</integer>
<key>allowVirtualMachine</key>
<true />
<key>allowScreenSharing</key>
<false />
<key>enablePrivateClipboard</key>
<true />
<key>createNewDesktop</key>
<true />
<key>killExplorerShell</key>
<false />
<key>enableLogging</key>
<true />
<key>logDirectoryOSX</key>
<string>~/Documents</string>
<key>logDirectoryWin</key>
<string />
<key>allowWlan</key>
<false />
<key>lockOnMessageSocketClose</key>
<true />
<key>minMacOSVersion</key>
<integer>4</integer>
<key>enableAppSwitcherCheck</key>
<true />
<key>forceAppFolderInstall</key>
<true />
<key>allowUserAppFolderInstall</key>
<false />
<key>allowSiri</key>
<false />
<key>allowDictation</key>
<false />
<key>detectStoppedProcess</key>
<true />
<key>allowDisplayMirroring</key>
<false />
<key>allowedDisplaysMaxNumber</key>
<integer>1</integer>
<key>allowedDisplayBuiltin</key>
<true />
<key>insideSebEnableSwitchUser</key>
<false />
<key>insideSebEnableLockThisComputer</key>
<false />
<key>insideSebEnableChangeAPassword</key>
<false />
<key>insideSebEnableStartTaskManager</key>
<false />
<key>insideSebEnableLogOff</key>
<false />
<key>insideSebEnableShutDown</key>
<false />
<key>insideSebEnableEaseOfAccess</key>
<false />
<key>insideSebEnableVmWareClientShade</key>
<false />
<key>insideSebEnableEnableNetworkConnectionSelector</key>
<false />
<key>hookKeys</key>
<true />
<key>enableEsc</key>
<true />
<key>enableCtrlEsc</key>
<false />
<key>enableAltEsc</key>
<false />
<key>enableAltTab</key>
<true />
<key>enableAltF4</key>
<false />
<key>enableStartMenu</key>
<false />
<key>enableRightMouse</key>
<true />
<key>enablePrintScreen</key>
<false />
<key>enableAltMouseWheel</key>
<false />
<key>enableF1</key>
<true />
<key>enableF2</key>
<true />
<key>enableF3</key>
<true />
<key>enableF4</key>
<true />
<key>enableF5</key>
<true />
<key>enableF6</key>
<true />
<key>enableF7</key>
<true />
<key>enableF8</key>
<true />
<key>enableF9</key>
<true />
<key>enableF10</key>
<true />
<key>enableF11</key>
<true />
<key>enableF12</key>
<true />
</dict>
</plist>
@@ -0,0 +1,488 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URLFilterEnable</key>
<false/>
<key>URLFilterEnableContentFilter</key>
<false/>
<key>URLFilterIgnoreList</key>
<array/>
<key>URLFilterMessage</key>
<integer>0</integer>
<key>URLFilterRules</key>
<array/>
<key>additionalResources</key>
<array/>
<key>allowBrowsingBackForward</key>
<false/>
<key>allowDictation</key>
<false/>
<key>allowDictionaryLookup</key>
<false/>
<key>allowDisplayMirroring</key>
<false/>
<key>allowDownUploads</key>
<false/>
<key>allowFlashFullscreen</key>
<false/>
<key>allowPDFPlugIn</key>
<false/>
<key>allowPreferencesWindow</key>
<true/>
<key>allowQuit</key>
<true/>
<key>allowScreenSharing</key>
<false/>
<key>allowSiri</key>
<false/>
<key>allowSpellCheck</key>
<false/>
<key>allowSwitchToApplications</key>
<false/>
<key>allowUserAppFolderInstall</key>
<false/>
<key>allowUserSwitching</key>
<true/>
<key>allowVideoCapture</key>
<false/>
<key>allowVirtualMachine</key>
<false/>
<key>allowWlan</key>
<false/>
<key>allowedDisplayBuiltin</key>
<true/>
<key>allowedDisplaysMaxNumber</key>
<integer>1</integer>
<key>allowiOSBetaVersionNumber</key>
<integer>0</integer>
<key>allowiOSVersionNumberMajor</key>
<integer>9</integer>
<key>allowiOSVersionNumberMinor</key>
<integer>3</integer>
<key>allowiOSVersionNumberPatch</key>
<integer>5</integer>
<key>blacklistURLFilter</key>
<string></string>
<key>blockPopUpWindows</key>
<false/>
<key>browserMediaAutoplay</key>
<false/>
<key>browserMessagingPingTime</key>
<integer>120000</integer>
<key>browserMessagingSocket</key>
<string>ws:\localhost:8706</string>
<key>browserScreenKeyboard</key>
<false/>
<key>browserURLSalt</key>
<true/>
<key>browserUserAgent</key>
<string></string>
<key>browserUserAgentMac</key>
<integer>0</integer>
<key>browserUserAgentMacCustom</key>
<string></string>
<key>browserUserAgentWinDesktopMode</key>
<integer>0</integer>
<key>browserUserAgentWinDesktopModeCustom</key>
<string></string>
<key>browserUserAgentWinTouchMode</key>
<integer>0</integer>
<key>browserUserAgentWinTouchModeCustom</key>
<string></string>
<key>browserUserAgentWinTouchModeIPad</key>
<string>Mozilla/5.0 (iPad; CPU OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1</string>
<key>browserUserAgentiOS</key>
<integer>0</integer>
<key>browserUserAgentiOSCustom</key>
<string></string>
<key>browserViewMode</key>
<integer>0</integer>
<key>browserWindowAllowReload</key>
<true/>
<key>browserWindowShowURL</key>
<integer>0</integer>
<key>chooseFileToUploadPolicy</key>
<integer>0</integer>
<key>configFileCreateIdentity</key>
<false/>
<key>configFileEncryptUsingIdentity</key>
<false/>
<key>configKeySalt</key>
<data>
</data>
<key>createNewDesktop</key>
<true/>
<key>detectStoppedProcess</key>
<true/>
<key>downloadAndOpenSebConfig</key>
<true/>
<key>downloadDirectoryOSX</key>
<string>/Users/andrewmadden/Downloads</string>
<key>downloadDirectoryWin</key>
<string>Downloads</string>
<key>downloadPDFFiles</key>
<false/>
<key>embeddedCertificates</key>
<array/>
<key>enableAltEsc</key>
<false/>
<key>enableAltF4</key>
<false/>
<key>enableAltMouseWheel</key>
<false/>
<key>enableAltTab</key>
<true/>
<key>enableAppSwitcherCheck</key>
<true/>
<key>enableBrowserWindowToolbar</key>
<false/>
<key>enableCtrlEsc</key>
<false/>
<key>enableDrawingEditor</key>
<false/>
<key>enableEsc</key>
<true/>
<key>enableF1</key>
<true/>
<key>enableF10</key>
<true/>
<key>enableF11</key>
<true/>
<key>enableF12</key>
<true/>
<key>enableF2</key>
<true/>
<key>enableF3</key>
<true/>
<key>enableF4</key>
<true/>
<key>enableF5</key>
<true/>
<key>enableF6</key>
<true/>
<key>enableF7</key>
<true/>
<key>enableF8</key>
<true/>
<key>enableF9</key>
<true/>
<key>enableJava</key>
<false/>
<key>enableJavaScript</key>
<true/>
<key>enableLogging</key>
<true/>
<key>enablePlugIns</key>
<true/>
<key>enablePrintScreen</key>
<false/>
<key>enablePrivateClipboard</key>
<true/>
<key>enableRightMouse</key>
<true/>
<key>enableSebBrowser</key>
<true/>
<key>enableStartMenu</key>
<false/>
<key>enableTouchExit</key>
<false/>
<key>enableZoomPage</key>
<true/>
<key>enableZoomText</key>
<true/>
<key>examKeySalt</key>
<data>
4iLONVAc1yKWLxbe6Vbf2vWYQO6zIRAobKgc3f25LH8=
</data>
<key>examSessionClearSessionCookies</key>
<true/>
<key>examSessionReconfigureAllow</key>
<false/>
<key>examSessionReconfigureConfigURL</key>
<string></string>
<key>exitKey1</key>
<integer>2</integer>
<key>exitKey2</key>
<integer>10</integer>
<key>exitKey3</key>
<integer>5</integer>
<key>forceAppFolderInstall</key>
<true/>
<key>hashedAdminPassword</key>
<string></string>
<key>hashedQuitPassword</key>
<string></string>
<key>hideBrowserWindowToolbar</key>
<false/>
<key>hookKeys</key>
<true/>
<key>ignoreExitKeys</key>
<true/>
<key>ignoreQuitPassword</key>
<false/>
<key>insideSebEnableChangeAPassword</key>
<false/>
<key>insideSebEnableEaseOfAccess</key>
<false/>
<key>insideSebEnableLockThisComputer</key>
<false/>
<key>insideSebEnableLogOff</key>
<false/>
<key>insideSebEnableNetworkConnectionSelector</key>
<false/>
<key>insideSebEnableShutDown</key>
<false/>
<key>insideSebEnableStartTaskManager</key>
<false/>
<key>insideSebEnableSwitchUser</key>
<false/>
<key>insideSebEnableVmWareClientShade</key>
<false/>
<key>killExplorerShell</key>
<false/>
<key>logDirectoryOSX</key>
<string></string>
<key>logDirectoryWin</key>
<string></string>
<key>logLevel</key>
<integer>4</integer>
<key>logSendingRequiresAdminPassword</key>
<false/>
<key>mainBrowserWindowHeight</key>
<string>100%</string>
<key>mainBrowserWindowPositioning</key>
<integer>1</integer>
<key>mainBrowserWindowWidth</key>
<string>100%</string>
<key>minMacOSVersion</key>
<integer>4</integer>
<key>mobileAllowInlineMediaPlayback</key>
<true/>
<key>mobileAllowPictureInPictureMediaPlayback</key>
<false/>
<key>mobileAllowQRCodeConfig</key>
<false/>
<key>mobileAllowSingleAppMode</key>
<false/>
<key>mobileCompactAllowInlineMediaPlayback</key>
<false/>
<key>mobileEnableASAM</key>
<true/>
<key>mobileEnableGuidedAccessLinkTransform</key>
<false/>
<key>mobilePreventAutoLock</key>
<true/>
<key>mobileShowSettings</key>
<false/>
<key>mobileStatusBarAppearance</key>
<integer>1</integer>
<key>mobileStatusBarAppearanceExtended</key>
<integer>1</integer>
<key>mobileSupportedFormFactorsCompact</key>
<true/>
<key>mobileSupportedFormFactorsNonTelephonyCompact</key>
<true/>
<key>mobileSupportedFormFactorsRegular</key>
<true/>
<key>mobileSupportedScreenOrientationsCompactLandscapeLeft</key>
<true/>
<key>mobileSupportedScreenOrientationsCompactLandscapeRight</key>
<true/>
<key>mobileSupportedScreenOrientationsCompactPortrait</key>
<true/>
<key>mobileSupportedScreenOrientationsCompactPortraitUpsideDown</key>
<false/>
<key>mobileSupportedScreenOrientationsRegularLandscapeLeft</key>
<true/>
<key>mobileSupportedScreenOrientationsRegularLandscapeRight</key>
<true/>
<key>mobileSupportedScreenOrientationsRegularPortrait</key>
<true/>
<key>mobileSupportedScreenOrientationsRegularPortraitUpsideDown</key>
<true/>
<key>monitorProcesses</key>
<true/>
<key>newBrowserWindowAllowReload</key>
<true/>
<key>newBrowserWindowByLinkBlockForeign</key>
<false/>
<key>newBrowserWindowByLinkHeight</key>
<string>100%</string>
<key>newBrowserWindowByLinkPolicy</key>
<integer>1</integer>
<key>newBrowserWindowByLinkPositioning</key>
<integer>2</integer>
<key>newBrowserWindowByLinkWidth</key>
<string>1000</string>
<key>newBrowserWindowByScriptBlockForeign</key>
<false/>
<key>newBrowserWindowByScriptPolicy</key>
<integer>2</integer>
<key>newBrowserWindowNavigation</key>
<true/>
<key>newBrowserWindowShowReloadWarning</key>
<false/>
<key>newBrowserWindowShowURL</key>
<integer>2</integer>
<key>openDownloads</key>
<false/>
<key>originatorVersion</key>
<string>SEB_OSX_2.1.4_2B66</string>
<key>oskBehavior</key>
<integer>2</integer>
<key>permittedProcesses</key>
<array/>
<key>pinEmbeddedCertificates</key>
<false/>
<key>prohibitedProcesses</key>
<array/>
<key>proxies</key>
<dict>
<key>org_safeexambrowser_SEB_AutoConfigurationEnabled</key>
<false/>
<key>org_safeexambrowser_SEB_AutoConfigurationJavaScript</key>
<string></string>
<key>org_safeexambrowser_SEB_AutoConfigurationURL</key>
<string></string>
<key>org_safeexambrowser_SEB_AutoDiscoveryEnabled</key>
<false/>
<key>org_safeexambrowser_SEB_ExceptionsList</key>
<array/>
<key>org_safeexambrowser_SEB_ExcludeSimpleHostnames</key>
<false/>
<key>org_safeexambrowser_SEB_FTPEnable</key>
<false/>
<key>org_safeexambrowser_SEB_FTPPassive</key>
<true/>
<key>org_safeexambrowser_SEB_FTPPassword</key>
<string></string>
<key>org_safeexambrowser_SEB_FTPPort</key>
<integer>21</integer>
<key>org_safeexambrowser_SEB_FTPProxy</key>
<string></string>
<key>org_safeexambrowser_SEB_FTPRequiresPassword</key>
<false/>
<key>org_safeexambrowser_SEB_FTPUsername</key>
<string></string>
<key>org_safeexambrowser_SEB_HTTPEnable</key>
<false/>
<key>org_safeexambrowser_SEB_HTTPPassword</key>
<string></string>
<key>org_safeexambrowser_SEB_HTTPPort</key>
<integer>80</integer>
<key>org_safeexambrowser_SEB_HTTPProxy</key>
<string></string>
<key>org_safeexambrowser_SEB_HTTPRequiresPassword</key>
<false/>
<key>org_safeexambrowser_SEB_HTTPSEnable</key>
<false/>
<key>org_safeexambrowser_SEB_HTTPSPassword</key>
<string></string>
<key>org_safeexambrowser_SEB_HTTPSPort</key>
<integer>443</integer>
<key>org_safeexambrowser_SEB_HTTPSProxy</key>
<string></string>
<key>org_safeexambrowser_SEB_HTTPSRequiresPassword</key>
<false/>
<key>org_safeexambrowser_SEB_HTTPSUsername</key>
<string></string>
<key>org_safeexambrowser_SEB_HTTPUsername</key>
<string></string>
<key>org_safeexambrowser_SEB_RTSPEnable</key>
<false/>
<key>org_safeexambrowser_SEB_RTSPPassword</key>
<string></string>
<key>org_safeexambrowser_SEB_RTSPPort</key>
<integer>554</integer>
<key>org_safeexambrowser_SEB_RTSPProxy</key>
<string></string>
<key>org_safeexambrowser_SEB_RTSPRequiresPassword</key>
<false/>
<key>org_safeexambrowser_SEB_RTSPUsername</key>
<string></string>
<key>org_safeexambrowser_SEB_SOCKSEnable</key>
<false/>
<key>org_safeexambrowser_SEB_SOCKSPassword</key>
<string></string>
<key>org_safeexambrowser_SEB_SOCKSPort</key>
<integer>1080</integer>
<key>org_safeexambrowser_SEB_SOCKSProxy</key>
<string></string>
<key>org_safeexambrowser_SEB_SOCKSRequiresPassword</key>
<false/>
<key>org_safeexambrowser_SEB_SOCKSUsername</key>
<string></string>
</dict>
<key>proxySettingsPolicy</key>
<integer>0</integer>
<key>quitURL</key>
<string></string>
<key>quitURLConfirm</key>
<true/>
<key>quitURLRestart</key>
<false/>
<key>removeBrowserProfile</key>
<false/>
<key>removeLocalStorage</key>
<false/>
<key>restartExamPasswordProtected</key>
<true/>
<key>restartExamText</key>
<string></string>
<key>restartExamURL</key>
<string></string>
<key>restartExamUseStartURL</key>
<false/>
<key>sebConfigPurpose</key>
<integer>0</integer>
<key>sebMode</key>
<integer>0</integer>
<key>sebServerFallback</key>
<false/>
<key>sebServerURL</key>
<string></string>
<key>sebServicePolicy</key>
<integer>2</integer>
<key>sendBrowserExamKey</key>
<true/>
<key>showBackToStartButton</key>
<true/>
<key>showInputLanguage</key>
<false/>
<key>showMenuBar</key>
<true/>
<key>showNavigationButtons</key>
<false/>
<key>showQuitButton</key>
<true/>
<key>showReloadButton</key>
<true/>
<key>showReloadWarning</key>
<false/>
<key>showScanQRCodeButton</key>
<false/>
<key>showTaskBar</key>
<true/>
<key>showTime</key>
<true/>
<key>startURL</key>
<string>https://www.google.com</string>
<key>startURLAllowDeepLink</key>
<false/>
<key>startURLAppendQueryParameter</key>
<false/>
<key>taskBarHeight</key>
<integer>40</integer>
<key>touchOptimized</key>
<false/>
<key>urlFilterRegex</key>
<true/>
<key>urlFilterTrustedContent</key>
<false/>
<key>whitelistURLFilter</key>
<string></string>
<key>zoomMode</key>
<integer>0</integer>
</dict>
</plist>
@@ -0,0 +1,801 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>batteryChargeThresholdCritical</key>
<real>0.1</real>
<key>batteryChargeThresholdLow</key>
<real>0.2</real>
<key>originatorVersion</key>
<string>SEB_Win_2.1.1</string>
<key>startURL</key>
<string>https://safeexambrowser.org/start</string>
<key>startResource</key>
<string />
<key>sebServerURL</key>
<string />
<key>hashedAdminPassword</key>
<string />
<key>allowQuit</key>
<true />
<key>ignoreExitKeys</key>
<true />
<key>hashedQuitPassword</key>
<string />
<key>exitKey1</key>
<integer>2</integer>
<key>exitKey2</key>
<integer>10</integer>
<key>exitKey3</key>
<integer>5</integer>
<key>sebMode</key>
<integer>0</integer>
<key>browserMessagingSocket</key>
<string>ws://localhost:8706</string>
<key>browserMessagingPingTime</key>
<integer>120000</integer>
<key>sebConfigPurpose</key>
<integer>0</integer>
<key>allowPreferencesWindow</key>
<true />
<key>useAsymmetricOnlyEncryption</key>
<false />
<key>browserViewMode</key>
<integer>0</integer>
<key>mainBrowserWindowWidth</key>
<string>100%</string>
<key>mainBrowserWindowHeight</key>
<string>100%</string>
<key>mainBrowserWindowPositioning</key>
<integer>1</integer>
<key>enableBrowserWindowToolbar</key>
<false />
<key>hideBrowserWindowToolbar</key>
<false />
<key>showMenuBar</key>
<false />
<key>showTaskBar</key>
<true />
<key>taskBarHeight</key>
<integer>40</integer>
<key>touchOptimized</key>
<false />
<key>enableZoomText</key>
<true />
<key>enableZoomPage</key>
<true />
<key>zoomMode</key>
<integer>0</integer>
<key>allowSpellCheck</key>
<false />
<key>allowDictionaryLookup</key>
<false />
<key>allowSpellCheckDictionary</key>
<array></array>
<key>additionalDictionaries</key>
<array></array>
<key>showReloadButton</key>
<true />
<key>showTime</key>
<true />
<key>showInputLanguage</key>
<true />
<key>enableTouchExit</key>
<false />
<key>oskBehavior</key>
<integer>2</integer>
<key>audioControlEnabled</key>
<true />
<key>audioMute</key>
<false />
<key>audioVolumeLevel</key>
<integer>25</integer>
<key>audioSetVolumeLevel</key>
<false />
<key>browserScreenKeyboard</key>
<false />
<key>newBrowserWindowByLinkPolicy</key>
<integer>2</integer>
<key>newBrowserWindowByScriptPolicy</key>
<integer>2</integer>
<key>newBrowserWindowByLinkBlockForeign</key>
<false />
<key>newBrowserWindowByScriptBlockForeign</key>
<false />
<key>newBrowserWindowByLinkWidth</key>
<string>1000</string>
<key>newBrowserWindowByLinkHeight</key>
<string>100%</string>
<key>newBrowserWindowByLinkPositioning</key>
<integer>2</integer>
<key>enablePlugIns</key>
<true />
<key>enableJava</key>
<false />
<key>enableJavaScript</key>
<true />
<key>blockPopUpWindows</key>
<false />
<key>allowVideoCapture</key>
<false />
<key>allowAudioCapture</key>
<false />
<key>allowBrowsingBackForward</key>
<false />
<key>newBrowserWindowNavigation</key>
<true />
<key>removeBrowserProfile</key>
<false />
<key>removeLocalStorage</key>
<false />
<key>enableSebBrowser</key>
<true />
<key>browserWindowAllowReload</key>
<true />
<key>newBrowserWindowAllowReload</key>
<true />
<key>showReloadWarning</key>
<true />
<key>newBrowserWindowShowReloadWarning</key>
<false />
<key>browserUserAgentWinDesktopMode</key>
<integer>0</integer>
<key>browserUserAgentWinDesktopModeCustom</key>
<string />
<key>browserUserAgentWinTouchMode</key>
<integer>0</integer>
<key>browserUserAgentWinTouchModeIPad</key>
<string>Mozilla/5.0 (iPad; CPU OS 11_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.3 Mobile/15E216 Safari/605.1.15</string>
<key>browserUserAgentWinTouchModeCustom</key>
<string />
<key>browserUserAgent</key>
<string />
<key>browserUserAgentMac</key>
<integer>0</integer>
<key>browserUserAgentMacCustom</key>
<string />
<key>browserWindowTitleSuffix</key>
<string />
<key>allowDownUploads</key>
<true />
<key>downloadDirectoryOSX</key>
<string>~/Downloads</string>
<key>downloadDirectoryWin</key>
<string />
<key>openDownloads</key>
<false />
<key>chooseFileToUploadPolicy</key>
<integer>0</integer>
<key>downloadPDFFiles</key>
<false />
<key>allowPDFPlugIn</key>
<false />
<key>downloadAndOpenSebConfig</key>
<true />
<key>backgroundOpenSEBConfig</key>
<false />
<key>examKeySalt</key>
<data>QJAqvg89YMP6JagAshUm6QqpqpsrVS9ZWUYjdZhfEao=</data>
<key>browserExamKey</key>
<string />
<key>browserURLSalt</key>
<true />
<key>sendBrowserExamKey</key>
<true />
<key>quitURL</key>
<string />
<key>quitURLConfirm</key>
<true />
<key>restartExamURL</key>
<string />
<key>restartExamUseStartURL</key>
<false />
<key>restartExamText</key>
<string />
<key>restartExamPasswordProtected</key>
<true />
<key>additionalResources</key>
<array></array>
<key>monitorProcesses</key>
<true />
<key>allowSwitchToApplications</key>
<false />
<key>allowFlashFullscreen</key>
<false />
<key>permittedProcesses</key>
<array>
<dict>
<key>active</key>
<true />
<key>autostart</key>
<true />
<key>iconInTaskbar</key>
<true />
<key>runInBackground</key>
<false />
<key>allowUserToChooseApp</key>
<false />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>title</key>
<string>SEB</string>
<key>description</key>
<string />
<key>executable</key>
<string>firefox.exe</string>
<key>originalName</key>
<string>firefox.exe</string>
<key>path</key>
<string>../xulrunner/</string>
<key>identifier</key>
<string>Firefox</string>
<key>windowHandlingProcess</key>
<string />
<key>arguments</key>
<array></array>
</dict>
</array>
<key>prohibitedProcesses</key>
<array>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>join.me</string>
<key>originalName</key>
<string>join.me</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>RPCSuite</string>
<key>originalName</key>
<string>RPCSuite</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>RPCService</string>
<key>originalName</key>
<string>RPCService</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>RemotePCDesktop</string>
<key>originalName</key>
<string>RemotePCDesktop</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>beamyourscreen-host</string>
<key>originalName</key>
<string>beamyourscreen-host</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>AeroAdmin</string>
<key>originalName</key>
<string>AeroAdmin</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>Mikogo-host</string>
<key>originalName</key>
<string>Mikogo-host</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>chromoting</string>
<key>originalName</key>
<string>chromoting</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>vncserverui</string>
<key>originalName</key>
<string>vncserverui</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>vncviewer</string>
<key>originalName</key>
<string>vncviewer</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>vncserver</string>
<key>originalName</key>
<string>vncserver</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>TeamViewer</string>
<key>originalName</key>
<string>TeamViewer</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>GotoMeetingWinStore</string>
<key>originalName</key>
<string>GotoMeetingWinStore</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>g2mcomm.exe</string>
<key>originalName</key>
<string>g2mcomm.exe</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>SkypeHost</string>
<key>originalName</key>
<string>SkypeHost</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
<dict>
<key>active</key>
<true />
<key>currentUser</key>
<true />
<key>strongKill</key>
<false />
<key>os</key>
<integer>1</integer>
<key>executable</key>
<string>Skype</string>
<key>originalName</key>
<string>Skype</string>
<key>description</key>
<string />
<key>identifier</key>
<string />
<key>windowHandlingProcess</key>
<string />
<key>user</key>
<string />
</dict>
</array>
<key>enableURLFilter</key>
<false />
<key>enableURLContentFilter</key>
<false />
<key>URLFilterRules</key>
<array></array>
<key>URLFilterEnable</key>
<false />
<key>URLFilterEnableContentFilter</key>
<false />
<key>blacklistURLFilter</key>
<string />
<key>whitelistURLFilter</key>
<string />
<key>urlFilterTrustedContent</key>
<true />
<key>urlFilterRegex</key>
<true />
<key>embeddedCertificates</key>
<array></array>
<key>pinEmbeddedCertificates</key>
<false />
<key>proxySettingsPolicy</key>
<integer>0</integer>
<key>proxies</key>
<dict>
<key>ExceptionsList</key>
<array></array>
<key>ExcludeSimpleHostnames</key>
<false />
<key>AutoDiscoveryEnabled</key>
<false />
<key>AutoConfigurationEnabled</key>
<false />
<key>AutoConfigurationJavaScript</key>
<string />
<key>AutoConfigurationURL</key>
<string />
<key>FTPPassive</key>
<true />
<key>HTTPEnable</key>
<false />
<key>HTTPPort</key>
<integer>80</integer>
<key>HTTPProxy</key>
<string />
<key>HTTPRequiresPassword</key>
<false />
<key>HTTPUsername</key>
<string />
<key>HTTPPassword</key>
<string />
<key>HTTPSEnable</key>
<false />
<key>HTTPSPort</key>
<integer>443</integer>
<key>HTTPSProxy</key>
<string />
<key>HTTPSRequiresPassword</key>
<false />
<key>HTTPSUsername</key>
<string />
<key>HTTPSPassword</key>
<string />
<key>FTPEnable</key>
<false />
<key>FTPPort</key>
<integer>21</integer>
<key>FTPProxy</key>
<string />
<key>FTPRequiresPassword</key>
<false />
<key>FTPUsername</key>
<string />
<key>FTPPassword</key>
<string />
<key>SOCKSEnable</key>
<false />
<key>SOCKSPort</key>
<integer>1080</integer>
<key>SOCKSProxy</key>
<string />
<key>SOCKSRequiresPassword</key>
<false />
<key>SOCKSUsername</key>
<string />
<key>SOCKSPassword</key>
<string />
<key>RTSPEnable</key>
<false />
<key>RTSPPort</key>
<integer>554</integer>
<key>RTSPProxy</key>
<string />
<key>RTSPRequiresPassword</key>
<false />
<key>RTSPUsername</key>
<string />
<key>RTSPPassword</key>
<string />
</dict>
<key>sebServicePolicy</key>
<integer>1</integer>
<key>allowVirtualMachine</key>
<true />
<key>allowScreenSharing</key>
<false />
<key>enablePrivateClipboard</key>
<true />
<key>createNewDesktop</key>
<true />
<key>killExplorerShell</key>
<false />
<key>enableLogging</key>
<true />
<key>logDirectoryOSX</key>
<string>~/Documents</string>
<key>logDirectoryWin</key>
<string />
<key>allowWlan</key>
<false />
<key>lockOnMessageSocketClose</key>
<true />
<key>minMacOSVersion</key>
<integer>4</integer>
<key>enableAppSwitcherCheck</key>
<true />
<key>forceAppFolderInstall</key>
<true />
<key>allowUserAppFolderInstall</key>
<false />
<key>allowSiri</key>
<false />
<key>allowDictation</key>
<false />
<key>detectStoppedProcess</key>
<true />
<key>allowDisplayMirroring</key>
<false />
<key>allowedDisplaysMaxNumber</key>
<integer>1</integer>
<key>allowedDisplayBuiltin</key>
<true />
<key>insideSebEnableSwitchUser</key>
<false />
<key>insideSebEnableLockThisComputer</key>
<false />
<key>insideSebEnableChangeAPassword</key>
<false />
<key>insideSebEnableStartTaskManager</key>
<false />
<key>insideSebEnableLogOff</key>
<false />
<key>insideSebEnableShutDown</key>
<false />
<key>insideSebEnableEaseOfAccess</key>
<false />
<key>insideSebEnableVmWareClientShade</key>
<false />
<key>insideSebEnableEnableNetworkConnectionSelector</key>
<false />
<key>hookKeys</key>
<true />
<key>enableEsc</key>
<true />
<key>enableCtrlEsc</key>
<false />
<key>enableAltEsc</key>
<false />
<key>enableAltTab</key>
<true />
<key>enableAltF4</key>
<false />
<key>enableStartMenu</key>
<false />
<key>enableRightMouse</key>
<true />
<key>enablePrintScreen</key>
<false />
<key>enableAltMouseWheel</key>
<false />
<key>enableF1</key>
<true />
<key>enableF2</key>
<true />
<key>enableF3</key>
<true />
<key>enableF4</key>
<true />
<key>enableF5</key>
<true />
<key>enableF6</key>
<true />
<key>enableF7</key>
<true />
<key>enableF8</key>
<true />
<key>enableF9</key>
<true />
<key>enableF10</key>
<true />
<key>enableF11</key>
<true />
<key>enableF12</key>
<true />
</dict>
</plist>
@@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Behat data generator the quizaccess_seb plugin.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Behat data generator for quizaccess_seb.
*
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_quizaccess_seb_generator extends behat_generator_base {
/**
* Get a list of the entities that can be created.
* @return array entity name => information about how to generate.
*/
protected function get_creatable_entities(): array {
return [
'seb templates' => [
'singular' => 'seb template',
'datagenerator' => 'template',
'required' => ['name'],
],
];
}
}
@@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data generator the quizaccess_seb plugin.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Data generator the quizaccess_seb plugin.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quizaccess_seb_generator extends component_generator_base {
/**
* Create SEB template.
*
* @param array $data Template data.
* @return \quizaccess_seb\template
*/
public function create_template(array $data) {
global $CFG;
if (!isset($data['name'])) {
$data['name'] = 'test';
}
if (!isset($data['content'])) {
$data['content'] = file_get_contents(
$CFG->dirroot . '/mod/quiz/accessrule/seb/tests/fixtures/unencrypted.seb'
);
}
if (!isset($data['enabled'])) {
$data['enabled'] = 1;
}
$template = new \quizaccess_seb\template();
$template->set('content', $data['content']);
$template->set('name', $data['name']);
$template->set('enabled', $data['enabled']);
$template->save();
return $template;
}
}
@@ -0,0 +1,188 @@
<?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;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/test_helper_trait.php');
/**
* PHPUnit tests for helper class.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/**
* Test that we can check valid seb string.
*/
public function test_is_valid_seb_config(): void {
$validseb = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
. "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>hashedQuitPassword</key>"
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array>"
. "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
$invalidseb = 'Invalid seb';
$emptyseb = '';
$this->assertTrue(\quizaccess_seb\helper::is_valid_seb_config($validseb));
$this->assertFalse(\quizaccess_seb\helper::is_valid_seb_config($invalidseb));
$this->assertFalse(\quizaccess_seb\helper::is_valid_seb_config($emptyseb));
}
/**
* Test that we can get seb file headers.
*/
public function test_get_seb_file_headers(): void {
$expiretime = 1582767914;
$headers = \quizaccess_seb\helper::get_seb_file_headers($expiretime);
$this->assertCount(5, $headers);
$this->assertEquals('Cache-Control: private, max-age=1, no-transform', $headers[0]);
$this->assertEquals('Expires: Thu, 27 Feb 2020 01:45:14 GMT', $headers[1]);
$this->assertEquals('Pragma: no-cache', $headers[2]);
$this->assertEquals('Content-Disposition: attachment; filename=config.seb', $headers[3]);
$this->assertEquals('Content-Type: application/seb', $headers[4]);
}
/**
* Test that the course module must exist to get a seb config file content.
*/
public function test_can_not_get_config_content_with_invalid_cmid(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$this->setUser($user); // Log user in.
$this->expectException(\dml_exception::class);
$this->expectExceptionMessage("Can't find data record in database. (SELECT cm.*, m.name, md.name AS modname \n"
. " FROM {course_modules} cm\n"
. " JOIN {modules} md ON md.id = cm.module\n"
. " JOIN {quiz} m ON m.id = cm.instance\n"
. " \n"
. " WHERE cm.id = :cmid AND md.name = :modulename\n"
. " \n"
. "[array (\n"
. " 'cmid' => '999',\n"
. " 'modulename' => 'quiz',\n"
.')])');
\quizaccess_seb\helper::get_seb_config_content('999');
}
/**
* Test that the user must be enrolled to get seb config content.
*/
public function test_can_not_get_config_content_when_user_not_enrolled_in_course(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user); // Log user in.
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage('Unsupported redirect detected, script execution terminated');
\quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
}
/**
* Test that if SEB quiz settings can't be found, a seb config content won't be provided.
*/
public function test_can_not_get_config_content_if_config_not_found_for_cmid(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->create_test_quiz($course);
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$this->setUser($user); // Log user in.
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage("No SEB config could be found for quiz with cmid: $quiz->cmid");
\quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
}
/**
* That that if config is empty for a quiz, a seb config content won't be provided.
*/
public function test_can_not_get_config_content_if_config_empty(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_NO);
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$this->setUser($user); // Log user in.
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage("No SEB config could be found for quiz with cmid: $quiz->cmid");
\quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
}
/**
* Test config content is provided successfully.
*/
public function test_config_provided(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->create_test_quiz($course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$this->setUser($user); // Log user in.
$config = \quizaccess_seb\helper::get_seb_config_content($quiz->cmid);
$url = new \moodle_url("/mod/quiz/view.php", ['id' => $quiz->cmid]);
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key>"
. "<false/><key>showReloadButton</key><true/><key>showTime</key><true/><key>showInputLanguage</key>"
. "<true/><key>allowQuit</key><true/><key>quitURLConfirm</key><true/><key>audioControlEnabled</key>"
. "<false/><key>audioMute</key><false/><key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key>"
. "<true/><key>URLFilterEnable</key><false/><key>URLFilterEnableContentFilter</key><false/>"
. "<key>URLFilterRules</key><array/><key>startURL</key><string>$url</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n", $config);
}
}
@@ -0,0 +1,40 @@
<?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;
/**
* PHPUnit tests for hideif_rule.
*
* @package quizaccess_seb
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hideif_rule_test extends \advanced_testcase {
/**
* Test that can get rule data.
*/
public function test_can_get_what_set_in_constructor(): void {
$rule = new hideif_rule('Element', 'Dependant', 'eq', 'Value');
$this->assertEquals('Element', $rule->get_element());
$this->assertEquals('Dependant', $rule->get_dependantname());
$this->assertEquals('eq', $rule->get_condition());
$this->assertEquals('Value', $rule->get_dependantvalue());
}
}
@@ -0,0 +1,93 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace quizaccess_seb;
/**
* PHPUnit tests for link_generator.
*
* @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
*/
class link_generator_test extends \advanced_testcase {
/**
* Called before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
}
/**
* Test that a http link is generated correctly.
*/
public function test_http_link_generated(): void {
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$this->assertEquals(
"http://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
link_generator::get_link($quiz->cmid, false, false));
}
/**
* Test that a http link is generated correctly.
*/
public function test_https_link_generated(): void {
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$this->assertEquals(
"https://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
link_generator::get_link($quiz->cmid, false));
}
/**
* Test that a seb link is generated correctly.
*/
public function test_seb_link_generated(): void {
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$this->assertEquals(
"seb://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
link_generator::get_link($quiz->cmid, true, false));
}
/**
* Test that a sebs link is generated correctly.
*/
public function test_sebs_link_generated(): void {
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
$this->assertEquals(
"sebs://www.example.com/moodle/mod/quiz/accessrule/seb/config.php?cmid=$quiz->cmid",
link_generator::get_link($quiz->cmid, true));
}
/**
* Test that link_generator can't not be instantiated with fake course module.
*/
public function test_course_module_does_not_exist(): void {
$this->expectException(\dml_exception::class);
$this->expectExceptionMessageMatches("/^Can't find data record in database.*/");
$generator = link_generator::get_link(123456, false);
}
}
@@ -0,0 +1,239 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* PHPUnit tests for privacy provider.
*
* @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\privacy;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use core_privacy\tests\request\approved_contextlist;
use core_privacy\tests\provider_testcase;
use quizaccess_seb\privacy\provider;
use quizaccess_seb\seb_quiz_settings;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../test_helper_trait.php');
/**
* PHPUnit tests for privacy provider.
*
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
use \quizaccess_seb_test_helper_trait;
/**
* Setup the user, the quiz and ensure that the user is the last user to modify the SEB quiz settings.
*/
public function setup_test_data() {
$this->resetAfterTest();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
$this->quiz = $this->create_test_quiz($this->course, \quizaccess_seb\settings_provider::USE_SEB_CONFIG_MANUALLY);
$this->user = $this->getDataGenerator()->create_user();
$this->setUser($this->user);
$template = $this->create_template();
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
// Modify settings so usermodified is updated. This is the user data we are testing for.
$quizsettings->set('requiresafeexambrowser', \quizaccess_seb\settings_provider::USE_SEB_TEMPLATE);
$quizsettings->set('templateid', $template->get('id'));
$quizsettings->save();
}
/**
* Test that the module context for a user who last modified the module is retrieved.
*/
public function test_get_contexts_for_userid(): void {
$this->setup_test_data();
$contexts = provider::get_contexts_for_userid($this->user->id);
$contextids = $contexts->get_contextids();
$this->assertEquals(\context_module::instance($this->quiz->cmid)->id, reset($contextids));
}
/**
* That that no module context is found for a user who has not modified any quiz settings.
*/
public function test_get_no_contexts_for_userid(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$contexts = provider::get_contexts_for_userid($user->id);
$contextids = $contexts->get_contextids();
$this->assertEmpty($contextids);
}
/**
* Test that user data is exported in format expected.
*/
public function test_export_user_data(): void {
$this->setup_test_data();
$context = \context_module::instance($this->quiz->cmid);
// Add another course_module of a differenty type - doing this lets us
// test that the data exporter is correctly limiting its selection to
// the quiz and not anything with the same instance id.
// (note this is only effective with databases not using fed (+1000) sequences
// per table, like postgres and mysql do, rendering this useless. In any
// case better to have the situation covered by some DBs,
// like sqlsrv or oracle than by none).
$this->getDataGenerator()->create_module('label', ['course' => $this->course->id]);
$contextlist = provider::get_contexts_for_userid($this->user->id);
$approvedcontextlist = new approved_contextlist(
$this->user,
'quizaccess_seb',
$contextlist->get_contextids()
);
writer::reset();
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertFalse($writer->has_any_data());
provider::export_user_data($approvedcontextlist);
$index = '1'; // Get first data returned from the quizsettings table metadata.
$data = $writer->get_data([
get_string('pluginname', 'quizaccess_seb'),
seb_quiz_settings::TABLE,
$index,
]);
$this->assertNotEmpty($data);
$index = '1'; // Get first data returned from the template table metadata.
$data = $writer->get_data([
get_string('pluginname', 'quizaccess_seb'),
\quizaccess_seb\template::TABLE,
$index,
]);
$this->assertNotEmpty($data);
$index = '2'; // There should not be more than one instance with data.
$data = $writer->get_data([
get_string('pluginname', 'quizaccess_seb'),
seb_quiz_settings::TABLE,
$index,
]);
$this->assertEmpty($data);
$index = '2'; // There should not be more than one instance with data.
$data = $writer->get_data([
get_string('pluginname', 'quizaccess_seb'),
\quizaccess_seb\template::TABLE,
$index,
]);
$this->assertEmpty($data);
}
/**
* Test that a userlist with module context is populated by usermodified user.
*/
public function test_get_users_in_context(): void {
$this->setup_test_data();
// Create empty userlist with quiz module context.
$userlist = new userlist(\context_module::instance($this->quiz->cmid), 'quizaccess_seb');
// Test that the userlist is populated with expected user/s.
provider::get_users_in_context($userlist);
$this->assertEquals($this->user->id, $userlist->get_userids()[0]);
}
/**
* Test that data is deleted for a list of users.
*/
public function test_delete_data_for_users(): void {
$this->setup_test_data();
$approveduserlist = new approved_userlist(\context_module::instance($this->quiz->cmid),
'quizaccess_seb', [$this->user->id]);
// Test data exists.
$this->assertNotEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id]));
// Test data is deleted.
provider::delete_data_for_users($approveduserlist);
$record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEmpty($record->get('usermodified'));
$template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]);
$this->assertEmpty($template->get('usermodified'));
}
/**
* Test that data is deleted for a list of contexts.
*/
public function test_delete_data_for_user(): void {
$this->setup_test_data();
$context = \context_module::instance($this->quiz->cmid);
$approvedcontextlist = new approved_contextlist($this->user,
'quizaccess_seb', [$context->id]);
// Test data exists.
$this->assertNotEmpty(seb_quiz_settings::get_record(['quizid' => $this->quiz->id]));
// Test data is deleted.
provider::delete_data_for_user($approvedcontextlist);
$record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEmpty($record->get('usermodified'));
$template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]);
$this->assertEmpty($template->get('usermodified'));
}
/**
* Test that data is deleted for a single context.
*/
public function test_delete_data_for_all_users_in_context(): void {
$this->setup_test_data();
$context = \context_module::instance($this->quiz->cmid);
// Test data exists.
$record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]);
$this->assertNotEmpty($record->get('usermodified'));
$this->assertNotEmpty($template->get('usermodified'));
// Test data is deleted.
provider::delete_data_for_all_users_in_context($context);
$record = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$template = \quizaccess_seb\template::get_record(['id' => $record->get('templateid')]);
$this->assertEmpty($record->get('usermodified'));
$this->assertEmpty($template->get('usermodified'));
}
}
@@ -0,0 +1,423 @@
<?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;
/**
* PHPUnit for property_list class.
*
* @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
*/
class property_list_test extends \advanced_testcase {
/**
* Test that an empty PList with a root dictionary is created.
*/
public function test_create_empty_plist(): void {
$emptyplist = new property_list();
$xml = trim($emptyplist->to_xml());
$this->assertEquals('<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict/></plist>', $xml);
}
/**
* Test that a Plist is constructed from an XML string.
*/
public function test_construct_plist_from_xml(): void {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$generatedxml = trim($plist->to_xml());
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
}
/**
* Test that an element can be added to the root dictionary.
*/
public function test_add_element_to_root(): void {
$plist = new property_list();
$newelement = new \CFPropertyList\CFString('testValue');
$plist->add_element_to_root('testKey', $newelement);
$generatedxml = trim($plist->to_xml());
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
}
/**
* Test that an element's value can be retrieved.
*/
public function test_get_element_value(): void {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$this->assertEquals('testValue', $plist->get_element_value('testKey'));
}
/**
* Test that an element's value can be retrieved.
*/
public function test_get_element_value_if_not_exists(): void {
$plist = new property_list();
$this->assertEmpty($plist->get_element_value('testKey'));
}
/**
* Test an element's value can be retrieved if it is an array.
*/
public function test_get_element_value_if_array(): void {
$xml = $this->get_plist_xml_header()
. "<key>testDict</key>"
. "<dict>"
. "<key>testKey</key>"
. "<string>testValue</string>"
. "</dict>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict'));
}
/**
* Test that a element's value can be updated that is not an array or dictionary.
*
* @param string $xml XML to create PList.
* @param string $key Key of element to try and update.
* @param mixed $value Value to try to update with.
*
* @dataProvider good_update_data_provider
*/
public function test_updating_element_value($xml, $key, $value): void {
$xml = $this->get_plist_xml_header()
. $xml
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->update_element_value($key, $value);
$this->assertEquals($value, $plist->get_element_value($key));
}
/**
* Test that a element's value can be updated that is not an array or dictionary.
*
* @param string $xml XML to create PList.
* @param string $key Key of element to try and update.
* @param mixed $value Bad value to try to update with.
* @param mixed $expected Expected value of element after update is called.
* @param string $exceptionmessage Message of exception expected to be thrown.
* @dataProvider bad_update_data_provider
*/
public function test_updating_element_value_with_bad_data(string $xml, string $key, $value, $expected, $exceptionmessage): void {
$xml = $this->get_plist_xml_header()
. $xml
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage($exceptionmessage);
$plist->update_element_value($key, $value);
$plistarray = json_decode($plist->to_json()); // Export elements.
$this->assertEquals($expected, $plistarray->$key);
}
/**
* Test that a dictionary can have it's value (array) updated.
*/
public function test_updating_element_array_if_dictionary(): void {
$xml = $this->get_plist_xml_header()
. "<key>testDict</key>"
. "<dict>"
. "<key>testKey</key>"
. "<string>testValue</string>"
. "</dict>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->update_element_array('testDict', ['newKey' => new \CFPropertyList\CFString('newValue')]);
$this->assertEquals(['newKey' => 'newValue'], $plist->get_element_value('testDict'));
}
/**
* Test that a dictionary can have it's value (array) updated.
*/
public function test_updating_element_array_if_dictionary_with_bad_data(): void {
$xml = $this->get_plist_xml_header()
. "<key>testDict</key>"
. "<dict>"
. "<key>testKey</key>"
. "<string>testValue</string>"
. "</dict>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage('New array must only contain CFType objects.');
$plist->update_element_array('testDict', [false]);
$this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict'));
$this->assertDebuggingCalled('property_list: If updating an array in PList, it must only contain CFType objects.',
DEBUG_DEVELOPER);
}
/**
* Test that an element can be deleted.
*/
public function test_delete_element(): void {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->delete_element('testKey');
$generatedxml = trim($plist->to_xml());
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict/></plist>", $generatedxml);
}
/**
* Test that an element can be deleted.
*/
public function test_delete_element_if_not_exists(): void {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->delete_element('nonExistentKey');
$generatedxml = trim($plist->to_xml());
// The xml should be unaltered.
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
}
/**
* Test that json is exported correctly according to SEB Config Key requirements.
*
* @param string $xml PList XML used to generate CFPropertyList.
* @param string $expectedjson Expected JSON output.
*
* @dataProvider json_data_provider
*/
public function test_export_to_json($xml, $expectedjson): void {
$xml = $this->get_plist_xml_header()
. $xml
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$generatedjson = $plist->to_json();
$this->assertEquals($expectedjson, $generatedjson);
}
/**
* Test that the xml is exported to JSON from a real SEB config file. Expected JSON extracted from SEB logs.
*/
public function test_export_to_json_full_file(): void {
$xml = file_get_contents(__DIR__ . '/fixtures/unencrypted_mac_001.seb');
$plist = new property_list($xml);
$plist->delete_element('originatorVersion'); // JSON should not contain originatorVersion key.
$generatedjson = $plist->to_json();
$json = trim(file_get_contents(__DIR__ . '/fixtures/JSON_unencrypted_mac_001.txt'));
$this->assertEquals($json, $generatedjson);
}
/**
* Test the set_or_update_value function.
*/
public function test_set_or_update_value(): void {
$plist = new property_list();
$this->assertEmpty($plist->get_element_value('string'));
$this->assertEmpty($plist->get_element_value('bool'));
$this->assertEmpty($plist->get_element_value('number'));
// Setting values.
$plist->set_or_update_value('string', new \CFPropertyList\CFString('initial string'));
$plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(true));
$plist->set_or_update_value('number', new \CFPropertyList\CFNumber('10'));
$this->assertEquals('initial string', $plist->get_element_value('string'));
$this->assertEquals(true, $plist->get_element_value('bool'));
$this->assertEquals(10, $plist->get_element_value('number'));
// Updating values.
$plist->set_or_update_value('string', new \CFPropertyList\CFString('new string'));
$plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(false));
$plist->set_or_update_value('number', new \CFPropertyList\CFNumber('42'));
$this->assertEquals('new string', $plist->get_element_value('string'));
$this->assertEquals(false, $plist->get_element_value('bool'));
$this->assertEquals(42, $plist->get_element_value('number'));
// Type exception.
$this->expectException(\TypeError::class);
$plist->set_or_update_value('someKey', 'We really need to pass in CFTypes here');
}
/**
* Get a valid PList header. Must also use footer.
*
* @return string
*/
private function get_plist_xml_header(): string {
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
. "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\">\n"
. " <dict>";
}
/**
* Get a valid PList footer. Must also use header.
*
* @return string
*/
private function get_plist_xml_footer(): string {
return " </dict>\n"
. "</plist>";
}
/**
* Data provider for good data on update.
*
* @return array Array with test data.
*/
public function good_update_data_provider(): array {
return [
'Update string' => ['<key>testKey</key><string>testValue</string>', 'testKey', 'newValue'],
'Update bool' => ['<key>testKey</key><true/>', 'testKey', false],
'Update number' => ['<key>testKey</key><real>888</real>', 'testKey', 123.4],
];
}
/**
* Data provider for bad data on update.
*
* @return array Array with test data.
*/
public function bad_update_data_provider(): array {
return [
'Update string with bool' => ['<key>testKey</key><string>testValue</string>', 'testKey', true, 'testValue',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFString'],
'Update string with number' => ['<key>testKey</key><string>testValue</string>', 'testKey', 999, 'testValue',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFString'],
'Update string with null' => ['<key>testKey</key><string>testValue</string>', 'testKey', null, 'testValue',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFString'],
'Update string with array' => ['<key>testKey</key><string>testValue</string>', 'testKey', ['arrayValue'], 'testValue',
'Use update_element_array to update a collection.'],
'Update bool with string' => ['<key>testKey</key><true/>', 'testKey', 'testValue', true,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFBool'],
'Update bool with number' => ['<key>testKey</key><true/>', 'testKey', 999, true,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFBool'],
'Update bool with null' => ['<key>testKey</key><true/>', 'testKey', null, true,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFBool'],
'Update bool with array' => ['<key>testKey</key><true/>', 'testKey', ['testValue'], true,
'Use update_element_array to update a collection.'],
'Update number with string' => ['<key>testKey</key><real>888</real>', 'testKey', 'string', 888,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFNumber'],
'Update number with bool' => ['<key>testKey</key><real>888</real>', 'testKey', true, 888,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFNumber'],
'Update number with null' => ['<key>testKey</key><real>888</real>', 'testKey', null, 888,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFNumber'],
'Update number with array' => ['<key>testKey</key><real>888</real>', 'testKey', ['testValue'], 888,
'Use update_element_array to update a collection.'],
'Update date with string' => ['<key>testKey</key><date>1940-10-09T22:13:56Z</date>', 'testKey', 'string',
'1940-10-10T06:13:56+08:00',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFDate'],
'Update data with number' => ['<key>testKey</key><data>testData</data>', 'testKey', 789, 'testData',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFData'],
];
}
/**
* Data provider for expected JSON from PList.
*
* Examples extracted from requirements listed in SEB Config Key documents.
* https://safeexambrowser.org/developer/seb-config-key.html
*
* 1. Date should be in ISO 8601 format.
* 2. Data should be base 64 encoded.
* 3. String should be UTF-8 encoded.
* 4, 5, 6, 7. No requirements for bools, arrays or dicts.
* 8. Empty dicts should not be included.
* 9. JSON key ordering should be case insensitive, and use string ordering.
* 10. URL forward slashes should not be escaped.
*
* @return array
*/
public function json_data_provider(): array {
$data = "blahblah";
$base64data = base64_encode($data);
return [
'date' => ["<key>date</key><date>1940-10-09T22:13:56Z</date>", "{\"date\":\"1940-10-09T22:13:56+00:00\"}"],
'data' => ["<key>data</key><data>$base64data</data>", "{\"data\":\"$base64data\"}"],
'string' => ["<key>string</key><string>hello wörld</string>", "{\"string\":\"hello wörld\"}"],
'string with 1 backslash' => ["<key>string</key><string>ws:\localhost</string>", "{\"string\":\"ws:\localhost\"}"],
'string with 2 backslashes' => ["<key>string</key><string>ws:\\localhost</string>",
'{"string":"ws:\\localhost"}'],
'string with 3 backslashes' => ["<key>string</key><string>ws:\\\localhost</string>",
'{"string":"ws:\\\localhost"}'],
'string with 4 backslashes' => ["<key>string</key><string>ws:\\\\localhost</string>",
'{"string":"ws:\\\\localhost"}'],
'string with 5 backslashes' => ["<key>string</key><string>ws:\\\\\localhost</string>",
'{"string":"ws:\\\\\localhost"}'],
'bool' => ["<key>bool</key><true/>", "{\"bool\":true}"],
'array' => ["<key>array</key><array><key>arraybool</key><false/><key>arraybool2</key><true/></array>"
, "{\"array\":[false,true]}"],
'empty array' => ["<key>bool</key><true/><key>array</key><array/>"
, "{\"array\":[],\"bool\":true}"],
'dict' => ["<key>dict</key><dict><key>dictbool</key><false/><key>dictbool2</key><true/></dict>"
, "{\"dict\":{\"dictbool\":false,\"dictbool2\":true}}"],
'empty dict' => ["<key>bool</key><true/><key>emptydict</key><dict/>", "{\"bool\":true}"],
'unordered elements' => ["<key>testKey</key>"
. "<string>testValue</string>"
. "<key>allowWLAN</key>"
. "<string>testValue2</string>"
. "<key>allowWlan</key>"
. "<string>testValue3</string>"
, "{\"allowWlan\":\"testValue3\",\"allowWLAN\":\"testValue2\",\"testKey\":\"testValue\"}"],
'url' => ["<key>url</key><string>http://test.com</string>", "{\"url\":\"http://test.com\"}"],
'assoc dict' => ["<key>dict</key><dict><key>banana</key><false/><key>apple</key><true/></dict>",
"{\"dict\":{\"apple\":true,\"banana\":false}}"],
'seq array' => ["<key>array</key><array><key>1</key><false/><key>2</key><true/>
<key>3</key><true/><key>4</key><true/><key>5</key><true/><key>6</key><true/>
<key>7</key><true/><key>8</key><true/><key>9</key><true/><key>10</key><true/></array>",
"{\"array\":[false,true,true,true,true,true,true,true,true,true]}"],
];
}
}
@@ -0,0 +1,853 @@
<?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;
use context_module;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/test_helper_trait.php');
/**
* PHPUnit tests for seb_quiz_settings class.
*
* @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
*/
class quiz_settings_test extends \advanced_testcase {
use \quizaccess_seb_test_helper_trait;
/** @var context_module $context Test context. */
protected $context;
/** @var moodle_url $url Test quiz URL. */
protected $url;
/**
* Called before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setAdminUser();
$this->course = $this->getDataGenerator()->create_course();
$this->quiz = $this->getDataGenerator()->create_module('quiz', [
'course' => $this->course->id,
'seb_requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
]);
$this->context = \context_module::instance($this->quiz->cmid);
$this->url = new \moodle_url("/mod/quiz/view.php", ['id' => $this->quiz->cmid]);
}
/**
* Test that config is generated immediately prior to saving quiz settings.
*/
public function test_config_is_created_from_quiz_settings(): void {
// Test settings to populate the in the object.
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
// Obtain the existing record that is created when using a generator.
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
// Update the settings with values from the test function.
$quizsettings->from_record($settings);
$quizsettings->save();
$config = $quizsettings->get_config();
$this->assertEquals(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
. "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>hashedQuitPassword</key>"
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array><key>startURL</key><string>$this->url</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/><key>allowPreferencesWindow</key><false/></dict></plist>\n",
$config);
}
/**
* Test that config string gets updated from quiz settings.
*/
public function test_config_is_updated_from_quiz_settings(): void {
// Test settings to populate the in the object.
$settings = $this->get_test_settings([
'quizid' => $this->quiz->id,
'cmid' => $this->quiz->cmid,
]);
// Obtain the existing record that is created when using a generator.
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
// Update the settings with values from the test function.
$quizsettings->from_record($settings);
$quizsettings->save();
$config = $quizsettings->get_config();
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
. "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>hashedQuitPassword</key>"
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array><key>startURL</key><string>$this->url</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n", $config);
$quizsettings->set('filterembeddedcontent', 1); // Alter the settings.
$quizsettings->save();
$config = $quizsettings->get_config();
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
. "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
. "<key>URLFilterEnableContentFilter</key><true/><key>hashedQuitPassword</key>"
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array><key>startURL</key><string>$this->url</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n", $config);
}
/**
* Test that config key is generated immediately prior to saving quiz settings.
*/
public function test_config_key_is_created_from_quiz_settings(): void {
$settings = $this->get_test_settings();
$quizsettings = new seb_quiz_settings(0, $settings);
$configkey = $quizsettings->get_config_key();
$this->assertEquals("65ff7a3b8aec80e58fbe2e7968826c33cbf0ac444a748055ebe665829cbf4201",
$configkey
);
}
/**
* Test that config key is generated immediately prior to saving quiz settings.
*/
public function test_config_key_is_updated_from_quiz_settings(): void {
$settings = $this->get_test_settings();
$quizsettings = new seb_quiz_settings(0, $settings);
$configkey = $quizsettings->get_config_key();
$this->assertEquals("65ff7a3b8aec80e58fbe2e7968826c33cbf0ac444a748055ebe665829cbf4201",
$configkey);
$quizsettings->set('filterembeddedcontent', 1); // Alter the settings.
$configkey = $quizsettings->get_config_key();
$this->assertEquals("d975b8a2ec4472495a8be7c64d7c8cc960dbb62472d5e88a8847ac0e5d77e533",
$configkey);
}
/**
* Test that different URL filter expressions are turned into config XML.
*
* @param \stdClass $settings Quiz settings
* @param string $expectedxml SEB Config XML.
*
* @dataProvider filter_rules_provider
*/
public function test_filter_rules_added_to_config(\stdClass $settings, string $expectedxml): void {
$quizsettings = new seb_quiz_settings(0, $settings);
$config = $quizsettings->get_config();
$this->assertEquals($expectedxml, $config);
}
/**
* Test that browser keys are validated and retrieved as an array instead of string.
*/
public function test_browser_exam_keys_are_retrieved_as_array(): void {
$quizsettings = new seb_quiz_settings();
$quizsettings->set('allowedbrowserexamkeys', "one two,three\nfour");
$retrievedkeys = $quizsettings->get('allowedbrowserexamkeys');
$this->assertEquals(['one', 'two', 'three', 'four'], $retrievedkeys);
}
/**
* Test validation of Browser Exam Keys.
*
* @param string $bek Browser Exam Key.
* @param string $expectederrorstring Expected error.
*
* @dataProvider bad_browser_exam_key_provider
*/
public function test_browser_exam_keys_validation_errors($bek, $expectederrorstring): void {
$quizsettings = new seb_quiz_settings();
$quizsettings->set('allowedbrowserexamkeys', $bek);
$quizsettings->validate();
$errors = $quizsettings->get_errors();
$this->assertContainsEquals($expectederrorstring, $errors);
}
/**
* Test that uploaded seb file gets converted to config string.
*/
public function test_config_file_uploaded_converted_to_config(): void {
$url = new \moodle_url("/mod/quiz/view.php", ['id' => $this->quiz->cmid]);
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>hashedQuitPassword</key><string>hashedpassword</string>"
. "<key>allowWlan</key><false/><key>startURL</key><string>$url</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer></dict></plist>\n";
$itemid = $this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$quizsettings->save();
$config = $quizsettings->get_config();
$this->assertEquals($xml, $config);
}
/**
* Test test_no_config_file_uploaded
*/
public function test_no_config_file_uploaded(): void {
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$cmid = $quizsettings->get('cmid');
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage("No uploaded SEB config file could be found for quiz with cmid: {$cmid}");
$quizsettings->get_config();
}
/**
* A helper function to build a config file.
*
* @param mixed $allowuserquitseb Required allowQuit setting.
* @param mixed $quitpassword Required hashedQuitPassword setting.
*
* @return string
*/
protected function get_config_xml($allowuserquitseb = null, $quitpassword = null) {
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>allowWlan</key><false/><key>startURL</key>"
. "<string>https://safeexambrowser.org/start</string>"
. "<key>sendBrowserExamKey</key><true/>";
if (!is_null($allowuserquitseb)) {
$allowuserquitseb = empty($allowuserquitseb) ? 'false' : 'true';
$xml .= "<key>allowQuit</key><{$allowuserquitseb}/>";
}
if (!is_null($quitpassword)) {
$xml .= "<key>hashedQuitPassword</key><string>{$quitpassword}</string>";
}
$xml .= "</dict></plist>\n";
return $xml;
}
/**
* Test using USE_SEB_TEMPLATE and have it override settings from the template when they are set.
*/
public function test_using_seb_template_override_settings_when_they_set_in_template(): void {
$xml = $this->get_config_xml(true, 'password');
$template = $this->create_template($xml);
$this->assertStringContainsString("<key>startURL</key><string>https://safeexambrowser.org/start</string>", $template->get('content'));
$this->assertStringContainsString("<key>allowQuit</key><true/>", $template->get('content'));
$this->assertStringContainsString("<key>hashedQuitPassword</key><string>password</string>", $template->get('content'));
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
$quizsettings->set('templateid', $template->get('id'));
$quizsettings->set('allowuserquitseb', 1);
$quizsettings->save();
$this->assertStringContainsString(
"<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id={$this->quiz->cmid}</string>",
$quizsettings->get_config()
);
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
$quizsettings->set('quitpassword', 'new password');
$quizsettings->save();
$hashedpassword = hash('SHA256', 'new password');
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringNotContainsString("<key>hashedQuitPassword</key><string>password</string>", $quizsettings->get_config());
$this->assertStringContainsString("<key>hashedQuitPassword</key><string>{$hashedpassword}</string>", $quizsettings->get_config());
$quizsettings->set('allowuserquitseb', 0);
$quizsettings->set('quitpassword', '');
$quizsettings->save();
$this->assertStringContainsString("<key>allowQuit</key><false/>", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
}
/**
* Test using USE_SEB_TEMPLATE and have it override settings from the template when they are not set.
*/
public function test_using_seb_template_override_settings_when_not_set_in_template(): void {
$xml = $this->get_config_xml();
$template = $this->create_template($xml);
$this->assertStringContainsString("<key>startURL</key><string>https://safeexambrowser.org/start</string>", $template->get('content'));
$this->assertStringNotContainsString("<key>allowQuit</key><true/>", $template->get('content'));
$this->assertStringNotContainsString("<key>hashedQuitPassword</key><string>password</string>", $template->get('content'));
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
$quizsettings->set('templateid', $template->get('id'));
$quizsettings->set('allowuserquitseb', 1);
$quizsettings->save();
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
$quizsettings->set('quitpassword', 'new password');
$quizsettings->save();
$hashedpassword = hash('SHA256', 'new password');
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringContainsString("<key>hashedQuitPassword</key><string>{$hashedpassword}</string>", $quizsettings->get_config());
$quizsettings->set('allowuserquitseb', 0);
$quizsettings->set('quitpassword', '');
$quizsettings->save();
$this->assertStringContainsString("<key>allowQuit</key><false/>", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
}
/**
* Test using USE_SEB_UPLOAD_CONFIG and use settings from the file if they are set.
*/
public function test_using_own_config_settings_are_not_overridden_if_set(): void {
$xml = $this->get_config_xml(true, 'password');
$this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$quizsettings->set('allowuserquitseb', 0);
$quizsettings->set('quitpassword', '');
$quizsettings->save();
$this->assertStringContainsString(
"<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id={$this->quiz->cmid}</string>",
$quizsettings->get_config()
);
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringContainsString("<key>hashedQuitPassword</key><string>password</string>", $quizsettings->get_config());
$quizsettings->set('quitpassword', 'new password');
$quizsettings->save();
$hashedpassword = hash('SHA256', 'new password');
$this->assertStringNotContainsString("<key>hashedQuitPassword</key><string>{$hashedpassword}</string>", $quizsettings->get_config());
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringContainsString("<key>hashedQuitPassword</key><string>password</string>", $quizsettings->get_config());
$quizsettings->set('allowuserquitseb', 0);
$quizsettings->set('quitpassword', '');
$quizsettings->save();
$this->assertStringContainsString("<key>allowQuit</key><true/>", $quizsettings->get_config());
$this->assertStringContainsString("<key>hashedQuitPassword</key><string>password</string>", $quizsettings->get_config());
}
/**
* Test using USE_SEB_UPLOAD_CONFIG and use settings from the file if they are not set.
*/
public function test_using_own_config_settings_are_not_overridden_if_not_set(): void {
$xml = $this->get_config_xml();
$this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$quizsettings->set('allowuserquitseb', 1);
$quizsettings->set('quitpassword', '');
$quizsettings->save();
$this->assertStringContainsString(
"<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id={$this->quiz->cmid}</string>",
$quizsettings->get_config()
);
$this->assertStringNotContainsString("allowQuit", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
$quizsettings->set('quitpassword', 'new password');
$quizsettings->save();
$this->assertStringNotContainsString("allowQuit", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
$quizsettings->set('allowuserquitseb', 0);
$quizsettings->set('quitpassword', '');
$quizsettings->save();
$this->assertStringNotContainsString("allowQuit", $quizsettings->get_config());
$this->assertStringNotContainsString("hashedQuitPassword", $quizsettings->get_config());
}
/**
* Test using USE_SEB_TEMPLATE populates the linkquitseb setting if a quitURL is found.
*/
public function test_template_has_quit_url_set(): void {
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>hashedQuitPassword</key><string>hashedpassword</string>"
. "<key>allowWlan</key><false/><key>quitURL</key><string>http://seb.quit.url</string>"
. "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
$template = $this->create_template($xml);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
$quizsettings->set('templateid', $template->get('id'));
$this->assertEmpty($quizsettings->get('linkquitseb'));
$quizsettings->save();
$this->assertNotEmpty($quizsettings->get('linkquitseb'));
$this->assertEquals('http://seb.quit.url', $quizsettings->get('linkquitseb'));
}
/**
* Test using USE_SEB_UPLOAD_CONFIG populates the linkquitseb setting if a quitURL is found.
*/
public function test_config_file_uploaded_has_quit_url_set(): void {
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>hashedQuitPassword</key><string>hashedpassword</string>"
. "<key>allowWlan</key><false/><key>quitURL</key><string>http://seb.quit.url</string>"
. "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
$itemid = $this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$this->assertEmpty($quizsettings->get('linkquitseb'));
$quizsettings->save();
$this->assertNotEmpty($quizsettings->get('linkquitseb'));
$this->assertEquals('http://seb.quit.url', $quizsettings->get('linkquitseb'));
}
/**
* Test template id set correctly.
*/
public function test_templateid_set_correctly_when_save_settings(): void {
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals(0, $quizsettings->get('templateid'));
$template = $this->create_template();
$templateid = $template->get('id');
// Initially set to USE_SEB_TEMPLATE with a template id.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals($templateid, $quizsettings->get('templateid'));
// Case for USE_SEB_NO, ensure template id reverts to 0.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_NO);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals(0, $quizsettings->get('templateid'));
// Reverting back to USE_SEB_TEMPLATE.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid);
// Case for USE_SEB_CONFIG_MANUALLY, ensure template id reverts to 0.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_CONFIG_MANUALLY);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals(0, $quizsettings->get('templateid'));
// Reverting back to USE_SEB_TEMPLATE.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid);
// Case for USE_SEB_CLIENT_CONFIG, ensure template id reverts to 0.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_CLIENT_CONFIG);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals(0, $quizsettings->get('templateid'));
// Reverting back to USE_SEB_TEMPLATE.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid);
// Case for USE_SEB_UPLOAD_CONFIG, ensure template id reverts to 0.
$xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
$this->create_module_test_file($xml, $this->quiz->cmid);
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_UPLOAD_CONFIG);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals(0, $quizsettings->get('templateid'));
// Case for USE_SEB_TEMPLATE, ensure template id is correct.
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals($templateid, $quizsettings->get('templateid'));
}
/**
* Helper function in tests to set USE_SEB_TEMPLATE and a template id on the quiz settings.
*
* @param seb_quiz_settings $quizsettings Given quiz settings instance.
* @param int $savetype Type of SEB usage.
* @param int $templateid Template ID.
*/
public function save_settings_with_optional_template($quizsettings, $savetype, $templateid = 0) {
$quizsettings->set('requiresafeexambrowser', $savetype);
if (!empty($templateid)) {
$quizsettings->set('templateid', $templateid);
}
$quizsettings->save();
}
/**
* Bad browser exam key data provider.
*
* @return array
*/
public function bad_browser_exam_key_provider(): array {
return [
'Short string' => ['fdsf434r',
'A key should be a 64-character hex string.'],
'Non hex string' => ['aadf6799aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789aadf678!',
'A key should be a 64-character hex string.'],
'Non unique' => ["aadf6799aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789"
. "\naadf6799aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789aadf6789", 'The keys must all be different.'],
];
}
/**
* Provide settings for different filter rules.
*
* @return array Test data.
*/
public function filter_rules_provider(): array {
return [
'enabled simple expessions' => [
(object) [
'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
'quizid' => 1,
'cmid' => 1,
'expressionsallowed' => "test.com\r\nsecond.hello",
'regexallowed' => '',
'expressionsblocked' => '',
'regexblocked' => '',
],
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/>"
. "<key>allowWlan</key><false/><key>showReloadButton</key>"
. "<true/><key>showTime</key><true/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><false/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><false/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>URLFilterRules</key><array>"
. "<dict><key>action</key><integer>1</integer><key>active</key><true/>"
. "<key>expression</key><string>test.com</string>"
. "<key>regex</key><false/></dict><dict><key>action</key><integer>1</integer>"
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><false/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'blocked simple expessions' => [
(object) [
'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
'quizid' => 1,
'cmid' => 1,
'expressionsallowed' => '',
'regexallowed' => '',
'expressionsblocked' => "test.com\r\nsecond.hello",
'regexblocked' => '',
],
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/>"
. "<key>allowWlan</key><false/><key>showReloadButton</key>"
. "<true/><key>showTime</key><true/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><false/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><false/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>URLFilterRules</key><array>"
. "<dict><key>action</key><integer>0</integer><key>active</key><true/>"
. "<key>expression</key><string>test.com</string>"
. "<key>regex</key><false/></dict><dict><key>action</key><integer>0</integer>"
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><false/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'enabled regex expessions' => [
(object) [
'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
'quizid' => 1,
'cmid' => 1,
'expressionsallowed' => '',
'regexallowed' => "test.com\r\nsecond.hello",
'expressionsblocked' => '',
'regexblocked' => '',
],
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/>"
. "<key>allowWlan</key><false/><key>showReloadButton</key>"
. "<true/><key>showTime</key><true/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><false/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><false/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>URLFilterRules</key><array>"
. "<dict><key>action</key><integer>1</integer><key>active</key><true/>"
. "<key>expression</key><string>test.com</string>"
. "<key>regex</key><true/></dict><dict><key>action</key><integer>1</integer>"
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><true/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'blocked regex expessions' => [
(object) [
'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
'quizid' => 1,
'cmid' => 1,
'expressionsallowed' => '',
'regexallowed' => '',
'expressionsblocked' => '',
'regexblocked' => "test.com\r\nsecond.hello",
],
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/>"
. "<key>allowWlan</key><false/><key>showReloadButton</key>"
. "<true/><key>showTime</key><true/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><false/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><false/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>URLFilterRules</key><array>"
. "<dict><key>action</key><integer>0</integer><key>active</key><true/>"
. "<key>expression</key><string>test.com</string>"
. "<key>regex</key><true/></dict><dict><key>action</key><integer>0</integer>"
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><true/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
'multiple simple expessions' => [
(object) [
'requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
'quizid' => 1,
'cmid' => 1,
'expressionsallowed' => "*",
'regexallowed' => '',
'expressionsblocked' => '',
'regexblocked' => "test.com\r\nsecond.hello",
],
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\"><dict><key>showTaskBar</key><true/>"
. "<key>allowWlan</key><false/><key>showReloadButton</key>"
. "<true/><key>showTime</key><true/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><false/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><false/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>URLFilterRules</key><array><dict><key>action</key>"
. "<integer>1</integer><key>active</key><true/><key>expression</key><string>*</string>"
. "<key>regex</key><false/></dict>"
. "<dict><key>action</key><integer>0</integer><key>active</key><true/>"
. "<key>expression</key><string>test.com</string>"
. "<key>regex</key><true/></dict><dict><key>action</key><integer>0</integer>"
. "<key>active</key><true/><key>expression</key>"
. "<string>second.hello</string><key>regex</key><true/></dict></array>"
. "<key>startURL</key><string>https://www.example.com/moodle/mod/quiz/view.php?id=1</string>"
. "<key>sendBrowserExamKey</key><true/><key>browserWindowWebView</key><integer>3</integer>"
. "<key>examSessionClearCookiesOnStart</key><false/>"
. "<key>allowPreferencesWindow</key><false/></dict></plist>\n",
],
];
}
/**
* Test that config and config key are null when expected.
*/
public function test_generates_config_values_as_null_when_expected(): void {
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertNotNull($quizsettings->get_config());
$this->assertNotNull($quizsettings->get_config_key());
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_NO);
$quizsettings->save();
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertNull($quizsettings->get_config());
$this->assertNull($quizsettings->get_config());
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
$xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
$this->create_module_test_file($xml, $this->quiz->cmid);
$quizsettings->save();
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertNotNull($quizsettings->get_config());
$this->assertNotNull($quizsettings->get_config_key());
$quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG);
$quizsettings->save();
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertNull($quizsettings->get_config());
$this->assertNull($quizsettings->get_config_key());
$template = $this->create_template();
$templateid = $template->get('id');
$this->save_settings_with_optional_template($quizsettings, settings_provider::USE_SEB_TEMPLATE, $templateid);
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertNotNull($quizsettings->get_config());
$this->assertNotNull($quizsettings->get_config_key());
}
/**
* Test that quizsettings cache exists after creation.
*/
public function test_quizsettings_cache_exists_after_creation(): void {
$expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals($expected->to_record(), \cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id));
}
/**
* Test that quizsettings cache gets deleted after deletion.
*/
public function test_quizsettings_cache_purged_after_deletion(): void {
$this->assertNotEmpty(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id));
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->delete();
$this->assertFalse(\cache::make('quizaccess_seb', 'quizsettings')->get($this->quiz->id));
}
/**
* Test that we can get seb_quiz_settings by quiz id.
*/
public function test_get_quiz_settings_by_quiz_id(): void {
$expected = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record());
// Check that data is getting from cache.
$expected->set('showsebtaskbar', 0);
$this->assertNotEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record());
// Now save and check that cached as been updated.
$expected->save();
$this->assertEquals($expected->to_record(), seb_quiz_settings::get_by_quiz_id($this->quiz->id)->to_record());
// Returns false for non existing quiz.
$this->assertFalse(seb_quiz_settings::get_by_quiz_id(7777777));
}
/**
* Test that SEB config cache exists after creation of the quiz.
*/
public function test_config_cache_exists_after_creation(): void {
$this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id));
}
/**
* Test that SEB config cache gets deleted after deletion.
*/
public function test_config_cache_purged_after_deletion(): void {
$this->assertNotEmpty(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id));
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->delete();
$this->assertFalse(\cache::make('quizaccess_seb', 'config')->get($this->quiz->id));
}
/**
* Test that we can get SEB config by quiz id.
*/
public function test_get_config_by_quiz_id(): void {
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$expected = $quizsettings->get_config();
$this->assertEquals($expected, seb_quiz_settings::get_config_by_quiz_id($this->quiz->id));
// Check that data is getting from cache.
$quizsettings->set('showsebtaskbar', 0);
$this->assertNotEquals($quizsettings->get_config(), seb_quiz_settings::get_config_by_quiz_id($this->quiz->id));
// Now save and check that cached as been updated.
$quizsettings->save();
$this->assertEquals($quizsettings->get_config(), seb_quiz_settings::get_config_by_quiz_id($this->quiz->id));
// Returns null for non existing quiz.
$this->assertNull(seb_quiz_settings::get_config_by_quiz_id(7777777));
}
/**
* Test that SEB config key cache exists after creation of the quiz.
*/
public function test_config_key_cache_exists_after_creation(): void {
$this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id));
}
/**
* Test that SEB config key cache gets deleted after deletion.
*/
public function test_config_key_cache_purged_after_deletion(): void {
$this->assertNotEmpty(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id));
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$quizsettings->delete();
$this->assertFalse(\cache::make('quizaccess_seb', 'configkey')->get($this->quiz->id));
}
/**
* Test that we can get SEB config key by quiz id.
*/
public function test_get_config_key_by_quiz_id(): void {
$quizsettings = seb_quiz_settings::get_record(['quizid' => $this->quiz->id]);
$expected = $quizsettings->get_config_key();
$this->assertEquals($expected, seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id));
// Check that data is getting from cache.
$quizsettings->set('showsebtaskbar', 0);
$this->assertNotEquals($quizsettings->get_config_key(), seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id));
// Now save and check that cached as been updated.
$quizsettings->save();
$this->assertEquals($quizsettings->get_config_key(), seb_quiz_settings::get_config_key_by_quiz_id($this->quiz->id));
// Returns null for non existing quiz.
$this->assertNull(seb_quiz_settings::get_config_key_by_quiz_id(7777777));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
<?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;
/**
* PHPUnit tests for template 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
*/
class template_test extends \advanced_testcase {
/**
* Called before every test.
*/
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
}
/**
* Test that template saved with valid content.
*/
public function test_template_is_saved(): void {
global $DB;
$data = new \stdClass();
$data->name = 'Test name';
$data->description = 'Test description';
$data->enabled = 1;
$data->content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
. "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>hashedQuitPassword</key>"
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array>"
. "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
$template = new template(0, $data);
$template->save();
$actual = $DB->get_record(template::TABLE, ['id' => $template->get('id')]);
$this->assertEquals($data->name, $actual->name);
$this->assertEquals($data->description, $actual->description);
$this->assertEquals($data->enabled, $actual->enabled);
$this->assertEquals($data->content, $actual->content);
$this->assertTrue($template->can_delete());
}
/**
* Test that template is not saved with invalid content.
*/
public function test_template_is_not_saved_with_invalid_content(): void {
$this->expectException(\core\invalid_persistent_exception::class);
$this->expectExceptionMessage('Invalid SEB config template');
$data = new \stdClass();
$data->name = 'Test name';
$data->description = 'Test description';
$data->enabled = 1;
$data->content = "Invalid content";
$template = new template(0, $data);
$template->save();
}
/**
* Test that a template cannot be deleted when assigned to a quiz.
*/
public function test_cannot_delete_template_when_assigned_to_quiz(): void {
global $DB;
$data = new \stdClass();
$data->name = 'Test name';
$data->description = 'Test description';
$data->enabled = 1;
$data->content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>showTaskBar</key><true/><key>allowWlan</key><false/><key>showReloadButton</key><true/>"
. "<key>showTime</key><false/><key>showInputLanguage</key><true/><key>allowQuit</key><true/>"
. "<key>quitURLConfirm</key><true/><key>audioControlEnabled</key><true/><key>audioMute</key><false/>"
. "<key>allowSpellCheck</key><false/><key>browserWindowAllowReload</key><true/><key>URLFilterEnable</key><true/>"
. "<key>URLFilterEnableContentFilter</key><false/><key>hashedQuitPassword</key>"
. "<string>9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</string><key>URLFilterRules</key>"
. "<array><dict><key>action</key><integer>1</integer><key>active</key><true/><key>expression</key>"
. "<string>test.com</string><key>regex</key><false/></dict></array>"
. "<key>sendBrowserExamKey</key><true/></dict></plist>\n";
$template = new template(0, $data);
$template->save();
$this->assertTrue($template->can_delete());
$DB->insert_record(seb_quiz_settings::TABLE, (object) [
'quizid' => 1,
'cmid' => 1,
'templateid' => $template->get('id'),
'requiresafeexambrowser' => '1',
'sebconfigfile' => '373552893',
'showsebtaskbar' => '1',
'showwificontrol' => '0',
'showreloadbutton' => '1',
'showtime' => '0',
'showkeyboardlayout' => '1',
'allowuserquitseb' => '1',
'quitpassword' => 'test',
'linkquitseb' => '',
'userconfirmquit' => '1',
'enableaudiocontrol' => '1',
'muteonstartup' => '0',
'allowspellchecking' => '0',
'allowreloadinexam' => '1',
'activateurlfiltering' => '1',
'filterembeddedcontent' => '0',
'expressionsallowed' => 'test.com',
'regexallowed' => '',
'expressionsblocked' => '',
'regexblocked' => '',
'showsebdownloadlink' => '1',
'config' => '',
]);
$this->assertFalse($template->can_delete());
}
}

Some files were not shown because too many files have changed in this diff Show More