Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 use mod_quiz\local\access_rule_base; 18 use mod_quiz\quiz_attempt; 19 use quizaccess_seb\seb_access_manager; 20 use quizaccess_seb\seb_quiz_settings; 21 use quizaccess_seb\settings_provider; 22 use quizaccess_seb\event\access_prevented; 23 24 /** 25 * Implementation of the quizaccess_seb plugin. 26 * 27 * @package quizaccess_seb 28 * @author Andrew Madden <andrewmadden@catalyst-au.net> 29 * @author Dmitrii Metelkin <dmitriim@catalyst-au.net> 30 * @copyright 2019 Catalyst IT 31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 32 */ 33 class quizaccess_seb extends access_rule_base { 34 35 /** @var seb_access_manager $accessmanager Instance to manage the access to the quiz for this plugin. */ 36 private $accessmanager; 37 38 /** 39 * Create an instance of this rule for a particular quiz. 40 * 41 * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. 42 * @param int $timenow the time that should be considered as 'now'. 43 * @param seb_access_manager $accessmanager the quiz accessmanager. 44 */ 45 public function __construct(\mod_quiz\quiz_settings $quizobj, int $timenow, seb_access_manager $accessmanager) { 46 parent::__construct($quizobj, $timenow); 47 $this->accessmanager = $accessmanager; 48 } 49 50 /** 51 * Return an appropriately configured instance of this rule, if it is applicable 52 * to the given quiz, otherwise return null. 53 * 54 * @param \mod_quiz\quiz_settings $quizobj information about the quiz in question. 55 * @param int $timenow the time that should be considered as 'now'. 56 * @param bool $canignoretimelimits whether the current user is exempt from 57 * time limits by the mod/quiz:ignoretimelimits capability. 58 * @return access_rule_base|null the rule, if applicable, else null. 59 */ 60 public static function make(\mod_quiz\quiz_settings $quizobj, $timenow, $canignoretimelimits) { 61 $accessmanager = new seb_access_manager($quizobj); 62 // If Safe Exam Browser is not required, this access rule is not applicable. 63 if (!$accessmanager->seb_required()) { 64 return null; 65 } 66 67 return new self($quizobj, $timenow, $accessmanager); 68 } 69 70 /** 71 * Add any fields that this rule requires to the quiz settings form. This 72 * method is called from {@link mod_quiz_mod_form::definition()}, while the 73 * security section is being built. 74 * 75 * @param mod_quiz_mod_form $quizform the quiz settings form that is being built. 76 * @param MoodleQuickForm $mform the wrapped MoodleQuickForm. 77 */ 78 public static function add_settings_form_fields(mod_quiz_mod_form $quizform, MoodleQuickForm $mform) { 79 settings_provider::add_seb_settings_fields($quizform, $mform); 80 } 81 82 /** 83 * Validate the data from any form fields added using {@link add_settings_form_fields()}. 84 * 85 * @param array $errors the errors found so far. 86 * @param array $data the submitted form data. 87 * @param array $files information about any uploaded files. 88 * @param mod_quiz_mod_form $quizform the quiz form object. 89 * @return array $errors the updated $errors array. 90 */ 91 public static function validate_settings_form_fields(array $errors, 92 array $data, $files, mod_quiz_mod_form $quizform) : array { 93 94 $quizid = $data['instance']; 95 $cmid = $data['coursemodule']; 96 $context = $quizform->get_context(); 97 98 if (!settings_provider::can_configure_seb($context)) { 99 return $errors; 100 } 101 102 if (settings_provider::is_seb_settings_locked($quizid)) { 103 return $errors; 104 } 105 106 if (settings_provider::is_conflicting_permissions($context)) { 107 return $errors; 108 } 109 110 $settings = settings_provider::filter_plugin_settings((object) $data); 111 112 // Validate basic settings using persistent class. 113 $quizsettings = (new seb_quiz_settings())->from_record($settings); 114 // Set non-form fields. 115 $quizsettings->set('quizid', $quizid); 116 $quizsettings->set('cmid', $cmid); 117 $quizsettings->validate(); 118 119 // Add any errors to list. 120 foreach ($quizsettings->get_errors() as $name => $error) { 121 $name = settings_provider::add_prefix($name); // Re-add prefix to match form element. 122 $errors[$name] = $error->out(); 123 } 124 125 // Edge case for filemanager_sebconfig. 126 if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) { 127 $errorvalidatefile = settings_provider::validate_draftarea_configfile($data['filemanager_sebconfigfile']); 128 if (!empty($errorvalidatefile)) { 129 $errors['filemanager_sebconfigfile'] = $errorvalidatefile; 130 } 131 } 132 133 // Edge case to force user to select a template. 134 if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_TEMPLATE) { 135 if (empty($data['seb_templateid'])) { 136 $errors['seb_templateid'] = get_string('invalidtemplate', 'quizaccess_seb'); 137 } 138 } 139 140 if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) { 141 // Global settings may be active which require a quiz password to be set if using SEB. 142 if (!empty(get_config('quizaccess_seb', 'quizpasswordrequired')) && empty($data['quizpassword'])) { 143 $errors['quizpassword'] = get_string('passwordnotset', 'quizaccess_seb'); 144 } 145 } 146 147 return $errors; 148 } 149 150 /** 151 * Save any submitted settings when the quiz settings form is submitted. This 152 * is called from {@link quiz_after_add_or_update()} in lib.php. 153 * 154 * @param stdClass $quiz the data from the quiz form, including $quiz->id 155 * which is the id of the quiz being saved. 156 */ 157 public static function save_settings($quiz) { 158 $context = context_module::instance($quiz->coursemodule); 159 160 if (!settings_provider::can_configure_seb($context)) { 161 return; 162 } 163 164 if (settings_provider::is_seb_settings_locked($quiz->id)) { 165 return; 166 } 167 168 if (settings_provider::is_conflicting_permissions($context)) { 169 return; 170 } 171 172 $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course, false, MUST_EXIST); 173 174 $settings = settings_provider::filter_plugin_settings($quiz); 175 $settings->quizid = $quiz->id; 176 $settings->cmid = $cm->id; 177 178 // Get existing settings or create new settings if none exist. 179 $quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id); 180 if (empty($quizsettings)) { 181 $quizsettings = new seb_quiz_settings(0, $settings); 182 } else { 183 $settings->id = $quizsettings->get('id'); 184 $quizsettings->from_record($settings); 185 } 186 187 // Process uploaded files if required. 188 if ($quizsettings->get('requiresafeexambrowser') == settings_provider::USE_SEB_UPLOAD_CONFIG) { 189 $draftitemid = file_get_submitted_draft_itemid('filemanager_sebconfigfile'); 190 settings_provider::save_filemanager_sebconfigfile_draftarea($draftitemid, $cm->id); 191 } else { 192 settings_provider::delete_uploaded_config_file($cm->id); 193 } 194 195 // Save or delete settings. 196 if ($quizsettings->get('requiresafeexambrowser') != settings_provider::USE_SEB_NO) { 197 $quizsettings->save(); 198 } else if ($quizsettings->get('id')) { 199 $quizsettings->delete(); 200 } 201 } 202 203 /** 204 * Delete any rule-specific settings when the quiz is deleted. This is called 205 * from {@link quiz_delete_instance()} in lib.php. 206 * 207 * @param stdClass $quiz the data from the database, including $quiz->id 208 * which is the id of the quiz being deleted. 209 */ 210 public static function delete_settings($quiz) { 211 $quizsettings = seb_quiz_settings::get_by_quiz_id($quiz->id); 212 // Check that there are existing settings. 213 if ($quizsettings !== false) { 214 $quizsettings->delete(); 215 } 216 } 217 218 /** 219 * Return the bits of SQL needed to load all the settings from all the access 220 * plugins in one DB query. The easiest way to understand what you need to do 221 * here is probably to read the code of {@see \mod_quiz\access_manager::load_settings()}. 222 * 223 * If you have some settings that cannot be loaded in this way, then you can 224 * use the {@link get_extra_settings()} method instead, but that has 225 * performance implications. 226 * 227 * @param int $quizid the id of the quiz we are loading settings for. This 228 * can also be accessed as quiz.id in the SQL. (quiz is a table alisas for {quiz}.) 229 * @return array with three elements: 230 * 1. fields: any fields to add to the select list. These should be alised 231 * if neccessary so that the field name starts the name of the plugin. 232 * 2. joins: any joins (should probably be LEFT JOINS) with other tables that 233 * are needed. 234 * 3. params: array of placeholder values that are needed by the SQL. You must 235 * used named placeholders, and the placeholder names should start with the 236 * plugin name, to avoid collisions. 237 */ 238 public static function get_settings_sql($quizid) : array { 239 return [ 240 'seb.requiresafeexambrowser AS seb_requiresafeexambrowser, ' 241 . 'seb.showsebtaskbar AS seb_showsebtaskbar, ' 242 . 'seb.showwificontrol AS seb_showwificontrol, ' 243 . 'seb.showreloadbutton AS seb_showreloadbutton, ' 244 . 'seb.showtime AS seb_showtime, ' 245 . 'seb.showkeyboardlayout AS seb_showkeyboardlayout, ' 246 . 'seb.allowuserquitseb AS seb_allowuserquitseb, ' 247 . 'seb.quitpassword AS seb_quitpassword, ' 248 . 'seb.linkquitseb AS seb_linkquitseb, ' 249 . 'seb.userconfirmquit AS seb_userconfirmquit, ' 250 . 'seb.enableaudiocontrol AS seb_enableaudiocontrol, ' 251 . 'seb.muteonstartup AS seb_muteonstartup, ' 252 . 'seb.allowspellchecking AS seb_allowspellchecking, ' 253 . 'seb.allowreloadinexam AS seb_allowreloadinexam, ' 254 . 'seb.activateurlfiltering AS seb_activateurlfiltering, ' 255 . 'seb.filterembeddedcontent AS seb_filterembeddedcontent, ' 256 . 'seb.expressionsallowed AS seb_expressionsallowed, ' 257 . 'seb.regexallowed AS seb_regexallowed, ' 258 . 'seb.expressionsblocked AS seb_expressionsblocked, ' 259 . 'seb.regexblocked AS seb_regexblocked, ' 260 . 'seb.allowedbrowserexamkeys AS seb_allowedbrowserexamkeys, ' 261 . 'seb.showsebdownloadlink AS seb_showsebdownloadlink, ' 262 . 'sebtemplate.id AS seb_templateid ' 263 , 'LEFT JOIN {quizaccess_seb_quizsettings} seb ON seb.quizid = quiz.id ' 264 . 'LEFT JOIN {quizaccess_seb_template} sebtemplate ON seb.templateid = sebtemplate.id ' 265 , [] 266 ]; 267 } 268 269 /** 270 * Whether the user should be blocked from starting a new attempt or continuing 271 * an attempt now. 272 * 273 * @return string false if access should be allowed, a message explaining the 274 * reason if access should be prevented. 275 */ 276 public function prevent_access() { 277 global $PAGE; 278 279 if (!$this->accessmanager->seb_required()) { 280 return false; 281 } 282 283 if ($this->accessmanager->can_bypass_seb()) { 284 return false; 285 } 286 287 // If the rule is active, enforce a secure view whilst taking the quiz. 288 $PAGE->set_pagelayout('secure'); 289 $this->prevent_display_blocks(); 290 291 // Access has previously been validated for this session and quiz. 292 if ($this->accessmanager->validate_session_access()) { 293 return false; 294 } 295 296 if (!$this->accessmanager->validate_basic_header()) { 297 access_prevented::create_strict($this->accessmanager, $this->get_reason_text('not_seb'))->trigger(); 298 return $this->get_require_seb_error_message(); 299 } 300 301 if (!$this->accessmanager->validate_config_key()) { 302 if ($this->accessmanager->should_redirect_to_seb_config_link()) { 303 $this->accessmanager->redirect_to_seb_config_link(); 304 } 305 306 access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_config_key'))->trigger(); 307 return $this->get_invalid_key_error_message(); 308 } 309 310 if (!$this->accessmanager->validate_browser_exam_key()) { 311 access_prevented::create_strict($this->accessmanager, $this->get_reason_text('invalid_browser_key'))->trigger(); 312 return $this->get_invalid_key_error_message(); 313 } 314 315 // Set the state of the access for this Moodle session. 316 $this->accessmanager->set_session_access(true); 317 318 return false; 319 } 320 321 /** 322 * Returns a list of finished attempts for the current user. 323 * 324 * @return array 325 */ 326 private function get_user_finished_attempts() : array { 327 global $USER; 328 329 return quiz_get_user_attempts( 330 $this->quizobj->get_quizid(), 331 $USER->id, 332 quiz_attempt::FINISHED, 333 false 334 ); 335 } 336 337 /** 338 * Prevent block displaying as configured. 339 */ 340 private function prevent_display_blocks() { 341 global $PAGE; 342 343 if ($PAGE->has_set_url() && $PAGE->url == $this->quizobj->view_url()) { 344 $attempts = $this->get_user_finished_attempts(); 345 346 // Don't display blocks before starting an attempt. 347 if (empty($attempts) && !get_config('quizaccess_seb', 'displayblocksbeforestart')) { 348 $PAGE->blocks->show_only_fake_blocks(); 349 } 350 351 // Don't display blocks after finishing an attempt. 352 if (!empty($attempts) && !get_config('quizaccess_seb', 'displayblockswhenfinished')) { 353 $PAGE->blocks->show_only_fake_blocks(); 354 } 355 } 356 } 357 358 /** 359 * Returns reason for access prevention as a text. 360 * 361 * @param string $identifier Reason string identifier. 362 * @return string 363 */ 364 private function get_reason_text(string $identifier) : string { 365 if (in_array($identifier, ['not_seb', 'invalid_config_key', 'invalid_browser_key'])) { 366 return get_string($identifier, 'quizaccess_seb'); 367 } 368 369 return get_string('unknown_reason', 'quizaccess_seb'); 370 } 371 372 /** 373 * Return error message when a SEB key is not valid. 374 * 375 * @return string 376 */ 377 private function get_invalid_key_error_message() : string { 378 // Return error message with download link and links to get the seb config. 379 return get_string('invalidkeys', 'quizaccess_seb') 380 . $this->display_buttons($this->get_action_buttons()); 381 } 382 383 /** 384 * Return error message when a SEB browser is not used. 385 * 386 * @return string 387 */ 388 private function get_require_seb_error_message() : string { 389 $message = get_string('clientrequiresseb', 'quizaccess_seb'); 390 391 if ($this->should_display_download_seb_link()) { 392 $message .= $this->display_buttons($this->get_download_seb_button()); 393 } 394 395 // Return error message with download link. 396 return $message; 397 } 398 399 /** 400 * Helper function to display an Exit Safe Exam Browser button if configured to do so and attempts are > 0. 401 * 402 * @return string empty or a button which has the configured seb quit link. 403 */ 404 private function get_quit_button() : string { 405 $quitbutton = ''; 406 407 if (empty($this->get_user_finished_attempts())) { 408 return $quitbutton; 409 } 410 411 // Only display if the link has been configured and attempts are greater than 0. 412 if (!empty($this->quiz->seb_linkquitseb)) { 413 $quitbutton = html_writer::link( 414 $this->quiz->seb_linkquitseb, 415 get_string('exitsebbutton', 'quizaccess_seb'), 416 ['class' => 'btn btn-secondary'] 417 ); 418 } 419 420 return $quitbutton; 421 } 422 423 /** 424 * Information, such as might be shown on the quiz view page, relating to this restriction. 425 * There is no obligation to return anything. If it is not appropriate to tell students 426 * about this rule, then just return ''. 427 * 428 * @return mixed a message, or array of messages, explaining the restriction 429 * (may be '' if no message is appropriate). 430 */ 431 public function description() : array { 432 global $PAGE; 433 434 $messages = [get_string('sebrequired', 'quizaccess_seb')]; 435 436 // Display download SEB config link for those who can bypass using SEB. 437 if ($this->accessmanager->can_bypass_seb() && $this->accessmanager->should_validate_config_key()) { 438 $messages[] = $this->display_buttons($this->get_download_config_button()); 439 } 440 441 // Those with higher level access will be able to see the button if they've made an attempt. 442 if (!$this->prevent_access()) { 443 $messages[] = $this->display_buttons($this->get_quit_button()); 444 } else { 445 $PAGE->requires->js_call_amd('quizaccess_seb/validate_quiz_access', 'init', 446 [$this->quiz->cmid, (bool)get_config('quizaccess_seb', 'autoreconfigureseb')]); 447 } 448 449 return $messages; 450 } 451 452 /** 453 * Sets up the attempt (review or summary) page with any special extra 454 * properties required by this rule. 455 * 456 * @param moodle_page $page the page object to initialise. 457 */ 458 public function setup_attempt_page($page) { 459 $page->set_title($this->quizobj->get_course()->shortname . ': ' . $page->title); 460 $page->set_popup_notification_allowed(false); // Prevent message notifications. 461 $page->set_heading($page->title); 462 $page->set_pagelayout('secure'); 463 } 464 465 /** 466 * This is called when the current attempt at the quiz is finished. 467 */ 468 public function current_attempt_finished() { 469 $this->accessmanager->clear_session_access(); 470 } 471 472 /** 473 * Prepare buttons HTML code for being displayed on the screen. 474 * 475 * @param string $buttonshtml Html string of the buttons. 476 * @param string $class Optional CSS class (or classes as space-separated list) 477 * @param array $attributes Optional other attributes as array 478 * 479 * @return string HTML code of the provided buttons. 480 */ 481 private function display_buttons(string $buttonshtml, $class = '', array $attributes = null) : string { 482 $html = ''; 483 484 if (!empty($buttonshtml)) { 485 $html = html_writer::div($buttonshtml, $class, $attributes); 486 } 487 488 return $html; 489 } 490 491 /** 492 * Get buttons to prompt user to download SEB or config file or launch SEB. 493 * 494 * @return string Html block of all action buttons. 495 */ 496 private function get_action_buttons() : string { 497 $buttons = ''; 498 499 if ($this->should_display_download_seb_link()) { 500 $buttons .= $this->get_download_seb_button(); 501 } 502 503 // Get config for displaying links. 504 $linkconfig = explode(',', get_config('quizaccess_seb', 'showseblinks')); 505 506 // Display links to download config/launch SEB only if required. 507 if ($this->accessmanager->should_validate_config_key()) { 508 if (in_array('seb', $linkconfig)) { 509 $buttons .= $this->get_launch_seb_button(); 510 } 511 512 if (in_array('http', $linkconfig)) { 513 $buttons .= $this->get_download_config_button(); 514 } 515 } 516 517 return $buttons; 518 } 519 520 /** 521 * Get a button to download SEB. 522 * 523 * @return string A link to download SafeExam Browser. 524 */ 525 private function get_download_seb_button() : string { 526 global $OUTPUT; 527 528 $button = ''; 529 530 if (!empty($this->get_seb_download_url())) { 531 $button = $OUTPUT->single_button($this->get_seb_download_url(), get_string('sebdownloadbutton', 'quizaccess_seb')); 532 } 533 534 return $button; 535 } 536 537 /** 538 * Get a button to launch Safe Exam Browser. 539 * 540 * @return string A link to launch Safe Exam Browser. 541 */ 542 private function get_launch_seb_button() : string { 543 // Rendering as a href and not as button in a form to circumvent browser warnings for sending to URL with unknown protocol. 544 $seblink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, true, is_https()); 545 546 $buttonlink = html_writer::start_tag('div', ['class' => 'singlebutton']); 547 $buttonlink .= html_writer::link($seblink, get_string('seblinkbutton', 'quizaccess_seb'), 548 ['class' => 'btn btn-secondary', 'title' => get_string('seblinkbutton', 'quizaccess_seb')]); 549 $buttonlink .= html_writer::end_tag('div'); 550 551 return $buttonlink; 552 } 553 554 /** 555 * Get a button to download Safe Exam Browser config. 556 * 557 * @return string A link to launch Safe Exam Browser. 558 */ 559 private function get_download_config_button() : string { 560 // Rendering as a href and not as button in a form to circumvent browser warnings for sending to URL with unknown protocol. 561 $httplink = \quizaccess_seb\link_generator::get_link($this->quiz->cmid, false, is_https()); 562 563 $buttonlink = html_writer::start_tag('div', ['class' => 'singlebutton']); 564 $buttonlink .= html_writer::link($httplink, get_string('httplinkbutton', 'quizaccess_seb'), 565 ['class' => 'btn btn-secondary', 'title' => get_string('httplinkbutton', 'quizaccess_seb')]); 566 $buttonlink .= html_writer::end_tag('div'); 567 568 return $buttonlink; 569 } 570 571 /** 572 * Returns SEB download URL. 573 * 574 * @return string 575 */ 576 private function get_seb_download_url() : string { 577 return get_config('quizaccess_seb', 'downloadlink'); 578 } 579 580 /** 581 * Check if we should display a link to download Safe Exam Browser. 582 * 583 * @return bool 584 */ 585 private function should_display_download_seb_link() : bool { 586 return !empty($this->quiz->seb_showsebdownloadlink); 587 } 588 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body