Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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  /**
  18   * Class for providing quiz settings, to make setting up quiz form manageable.
  19   *
  20   * To make sure there are no inconsistencies between data sets, run tests in tests/phpunit/settings_provider_test.php.
  21   *
  22   * @package    quizaccess_seb
  23   * @author     Luca Bösch <luca.boesch@bfh.ch>
  24   * @author     Andrew Madden <andrewmadden@catalyst-au.net>
  25   * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
  26   * @copyright  2019 Catalyst IT
  27   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  
  30  namespace quizaccess_seb;
  31  
  32  use context_module;
  33  use context_user;
  34  use lang_string;
  35  use stdClass;
  36  use stored_file;
  37  
  38  defined('MOODLE_INTERNAL') || die();
  39  
  40  /**
  41   * Helper class for providing quiz settings, to make setting up quiz form manageable.
  42   *
  43   * @copyright  2020 Catalyst IT
  44   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class settings_provider {
  47  
  48      /**
  49       * No SEB should be used.
  50       */
  51      const USE_SEB_NO = 0;
  52  
  53      /**
  54       * Use SEB and configure it manually.
  55       */
  56      const USE_SEB_CONFIG_MANUALLY = 1;
  57  
  58      /**
  59       * Use SEB config from pre configured template.
  60       */
  61      const USE_SEB_TEMPLATE = 2;
  62  
  63      /**
  64       * Use SEB config from uploaded config file.
  65       */
  66      const USE_SEB_UPLOAD_CONFIG = 3;
  67  
  68      /**
  69       * Use client config. Not SEB config is required.
  70       */
  71      const USE_SEB_CLIENT_CONFIG = 4;
  72  
  73      /**
  74       * Insert form element.
  75       *
  76       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
  77       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
  78       * @param \HTML_QuickForm_element $element Element to insert.
  79       * @param string $before Insert element before.
  80       */
  81      protected static function insert_element(\mod_quiz_mod_form $quizform,
  82                                               \MoodleQuickForm $mform, \HTML_QuickForm_element $element, $before = 'security') {
  83          $mform->insertElementBefore($element, $before);
  84      }
  85  
  86      /**
  87       * Remove element from the form.
  88       *
  89       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
  90       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
  91       * @param string $elementname Element name.
  92       */
  93      protected static function remove_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string  $elementname) {
  94          if ($mform->elementExists($elementname)) {
  95              $mform->removeElement($elementname);
  96              $mform->setDefault($elementname, null);
  97          }
  98      }
  99  
 100      /**
 101       * Add help button to the element.
 102       *
 103       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 104       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 105       * @param string $elementname Element name.
 106       */
 107      protected static function add_help_button(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) {
 108          if ($mform->elementExists($elementname)) {
 109              $mform->addHelpButton($elementname, $elementname, 'quizaccess_seb');
 110          }
 111      }
 112  
 113      /**
 114       * Set default value for the element.
 115       *
 116       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 117       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 118       * @param string $elementname Element name.
 119       * @param mixed $value Default value.
 120       */
 121      protected static function set_default(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string  $elementname, $value) {
 122          $mform->setDefault($elementname, $value);
 123      }
 124  
 125      /**
 126       * Set element type.
 127       *
 128       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 129       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 130       * @param string $elementname Element name.
 131       * @param string $type Type of the form element.
 132       */
 133      protected static function set_type(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname, string $type) {
 134          $mform->setType($elementname, $type);
 135      }
 136  
 137      /**
 138       * Freeze form element.
 139       *
 140       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 141       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 142       * @param string $elementname Element name.
 143       */
 144      protected static function freeze_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform, string $elementname) {
 145          if ($mform->elementExists($elementname)) {
 146              $mform->freeze($elementname);
 147          }
 148      }
 149  
 150      /**
 151       * Add SEB header element to  the form.
 152       *
 153       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 154       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 155       */
 156      protected static function add_seb_header_element(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 157          global  $OUTPUT;
 158  
 159          $element = $mform->createElement('header', 'seb', get_string('seb', 'quizaccess_seb'));
 160          self::insert_element($quizform, $mform, $element);
 161  
 162          // Display notification about locked settings.
 163          if (self::is_seb_settings_locked($quizform->get_instance())) {
 164              $notify = new \core\output\notification(
 165                  get_string('settingsfrozen', 'quizaccess_seb'),
 166                  \core\output\notification::NOTIFY_WARNING
 167              );
 168  
 169              $notifyelement = $mform->createElement('html', $OUTPUT->render($notify));
 170              self::insert_element($quizform, $mform, $notifyelement);
 171          }
 172  
 173          if (self::is_conflicting_permissions($quizform->get_context())) {
 174              $notify = new \core\output\notification(
 175                  get_string('conflictingsettings', 'quizaccess_seb'),
 176                  \core\output\notification::NOTIFY_WARNING
 177              );
 178  
 179              $notifyelement = $mform->createElement('html', $OUTPUT->render($notify));
 180              self::insert_element($quizform, $mform, $notifyelement);
 181          }
 182      }
 183  
 184      /**
 185       * Add SEB usage element with all available options.
 186       *
 187       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 188       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 189       */
 190      protected static function add_seb_usage_options(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 191          $element = $mform->createElement(
 192              'select',
 193              'seb_requiresafeexambrowser',
 194              get_string('seb_requiresafeexambrowser', 'quizaccess_seb'),
 195              self::get_requiresafeexambrowser_options($quizform->get_context())
 196          );
 197  
 198          self::insert_element($quizform, $mform, $element);
 199          self::set_type($quizform, $mform, 'seb_requiresafeexambrowser', PARAM_INT);
 200          self::set_default($quizform, $mform, 'seb_requiresafeexambrowser', self::USE_SEB_NO);
 201          self::add_help_button($quizform, $mform, 'seb_requiresafeexambrowser');
 202  
 203          if (self::is_conflicting_permissions($quizform->get_context())) {
 204              self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser');
 205          }
 206      }
 207  
 208      /**
 209       * Add Templates element.
 210       *
 211       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 212       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 213       */
 214      protected static function add_seb_templates(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 215          if (self::can_use_seb_template($quizform->get_context()) || self::is_conflicting_permissions($quizform->get_context())) {
 216              $element = $mform->createElement(
 217                  'select',
 218                  'seb_templateid',
 219                  get_string('seb_templateid', 'quizaccess_seb'),
 220                  self::get_template_options()
 221              );
 222          } else {
 223              $element = $mform->createElement('hidden', 'seb_templateid');
 224          }
 225  
 226          self::insert_element($quizform, $mform, $element);
 227          self::set_type($quizform, $mform, 'seb_templateid', PARAM_INT);
 228          self::set_default($quizform, $mform, 'seb_templateid', 0);
 229          self::add_help_button($quizform, $mform, 'seb_templateid');
 230  
 231          // In case if the user can't use templates, but the quiz is configured to use them,
 232          // we'd like to display template, but freeze it.
 233          if (self::is_conflicting_permissions($quizform->get_context())) {
 234              self::freeze_element($quizform, $mform, 'seb_templateid');
 235          }
 236      }
 237  
 238      /**
 239       * Add upload config file element.
 240       *
 241       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 242       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 243       */
 244      protected static function add_seb_config_file(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 245          $itemid = 0;
 246  
 247          $draftitemid = 0;
 248          file_prepare_draft_area(
 249              $draftitemid,
 250              $quizform->get_context()->id,
 251              'quizaccess_seb',
 252              'filemanager_sebconfigfile',
 253              $itemid
 254          );
 255  
 256          if (self::can_upload_seb_file($quizform->get_context())) {
 257              $element = $mform->createElement(
 258                  'filemanager',
 259                  'filemanager_sebconfigfile',
 260                  get_string('filemanager_sebconfigfile', 'quizaccess_seb'),
 261                  null,
 262                  self::get_filemanager_options()
 263              );
 264          } else {
 265              $element = $mform->createElement('hidden', 'filemanager_sebconfigfile');
 266          }
 267  
 268          self::insert_element($quizform, $mform, $element);
 269          self::set_type($quizform, $mform, 'filemanager_sebconfigfile', PARAM_RAW);
 270          self::set_default($quizform, $mform, 'filemanager_sebconfigfile', $draftitemid);
 271          self::add_help_button($quizform, $mform, 'filemanager_sebconfigfile');
 272      }
 273  
 274      /**
 275       * Add Show Safe Exam Browser download button.
 276       *
 277       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 278       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 279       */
 280      protected static function add_seb_show_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 281          if (self::can_change_seb_showsebdownloadlink($quizform->get_context())) {
 282              $element = $mform->createElement('selectyesno',
 283                  'seb_showsebdownloadlink',
 284                  get_string('seb_showsebdownloadlink', 'quizaccess_seb')
 285              );
 286              self::insert_element($quizform, $mform, $element);
 287              self::set_type($quizform, $mform, 'seb_showsebdownloadlink', PARAM_BOOL);
 288              self::set_default($quizform, $mform, 'seb_showsebdownloadlink', 1);
 289              self::add_help_button($quizform, $mform, 'seb_showsebdownloadlink');
 290          }
 291      }
 292  
 293      /**
 294       * Add Allowed Browser Exam Keys setting.
 295       *
 296       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 297       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 298       */
 299      protected static function add_seb_allowedbrowserexamkeys(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 300          if (self::can_change_seb_allowedbrowserexamkeys($quizform->get_context())) {
 301              $element = $mform->createElement('textarea',
 302                  'seb_allowedbrowserexamkeys',
 303                  get_string('seb_allowedbrowserexamkeys', 'quizaccess_seb')
 304              );
 305              self::insert_element($quizform, $mform, $element);
 306              self::set_type($quizform, $mform, 'seb_allowedbrowserexamkeys', PARAM_RAW);
 307              self::set_default($quizform, $mform, 'seb_allowedbrowserexamkeys', '');
 308              self::add_help_button($quizform, $mform, 'seb_allowedbrowserexamkeys');
 309          }
 310      }
 311  
 312      /**
 313       * Add SEB config elements.
 314       *
 315       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 316       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 317       */
 318      protected static function add_seb_config_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 319          $defaults = self::get_seb_config_element_defaults();
 320          $types = self::get_seb_config_element_types();
 321  
 322          foreach (self::get_seb_config_elements() as $name => $type) {
 323              if (!self::can_manage_seb_config_setting($name, $quizform->get_context())) {
 324                  $type = 'hidden';
 325              }
 326  
 327              $element = $mform->createElement($type, $name, get_string($name, 'quizaccess_seb'));
 328              self::insert_element($quizform, $mform, $element);
 329              unset($element); // We need to make sure each &element only references the current element in loop.
 330  
 331              self::add_help_button($quizform, $mform, $name);
 332  
 333              if (isset($defaults[$name])) {
 334                  self::set_default($quizform, $mform, $name, $defaults[$name]);
 335              }
 336  
 337              if (isset($types[$name])) {
 338                  self::set_type($quizform, $mform, $name, $types[$name]);
 339              }
 340          }
 341      }
 342  
 343      /**
 344       * Add setting fields.
 345       *
 346       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 347       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 348       */
 349      public static function add_seb_settings_fields(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 350          if (self::can_configure_seb($quizform->get_context())) {
 351              self::add_seb_header_element($quizform, $mform);
 352              self::add_seb_usage_options($quizform, $mform);
 353              self::add_seb_templates($quizform, $mform);
 354              self::add_seb_config_file($quizform, $mform);
 355              self::add_seb_show_download_link($quizform, $mform);
 356              self::add_seb_config_elements($quizform, $mform);
 357              self::add_seb_allowedbrowserexamkeys($quizform, $mform);
 358              self::hide_seb_elements($quizform, $mform);
 359              self::lock_seb_elements($quizform, $mform);
 360          }
 361      }
 362  
 363      /**
 364       * Hide SEB elements if required.
 365       *
 366       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 367       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 368       */
 369      protected static function hide_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 370          foreach (self::get_quiz_hideifs() as $elname => $rules) {
 371              if ($mform->elementExists($elname)) {
 372                  foreach ($rules as $hideif) {
 373                      $mform->hideIf(
 374                          $hideif->get_element(),
 375                          $hideif->get_dependantname(),
 376                          $hideif->get_condition(),
 377                          $hideif->get_dependantvalue()
 378                      );
 379                  }
 380              }
 381          }
 382      }
 383  
 384      /**
 385       * Lock SEB elements if required.
 386       *
 387       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 388       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 389       */
 390      protected static function lock_seb_elements(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) {
 391          if (self::is_seb_settings_locked($quizform->get_instance()) || self::is_conflicting_permissions($quizform->get_context())) {
 392              // Freeze common quiz settings.
 393              self::freeze_element($quizform, $mform, 'seb_requiresafeexambrowser');
 394              self::freeze_element($quizform, $mform, 'seb_templateid');
 395              self::freeze_element($quizform, $mform, 'seb_showsebdownloadlink');
 396              self::freeze_element($quizform, $mform, 'seb_allowedbrowserexamkeys');
 397  
 398              $quizsettings = quiz_settings::get_by_quiz_id((int) $quizform->get_instance());
 399  
 400              // If the file has been uploaded, then replace it with the link to download the file.
 401              if (!empty($quizsettings) && $quizsettings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) {
 402                  self::remove_element($quizform, $mform, 'filemanager_sebconfigfile');
 403                  if ($link = self::get_uploaded_seb_file_download_link($quizform, $mform)) {
 404                      $element = $mform->createElement(
 405                          'static',
 406                          'filemanager_sebconfigfile',
 407                          get_string('filemanager_sebconfigfile', 'quizaccess_seb'),
 408                          $link
 409                      );
 410                      self::insert_element($quizform, $mform, $element, 'seb_showsebdownloadlink');
 411                  }
 412              }
 413  
 414              // Remove template ID if not using template for this quiz.
 415              if (empty($quizsettings) || $quizsettings->get('requiresafeexambrowser') != self::USE_SEB_TEMPLATE) {
 416                  $mform->removeElement('seb_templateid');
 417              }
 418  
 419              // Freeze all SEB specific settings.
 420              foreach (self::get_seb_config_elements() as $element => $type) {
 421                  self::freeze_element($quizform, $mform, $element);
 422              }
 423          }
 424      }
 425  
 426      /**
 427       * Return uploaded SEB config file link.
 428       *
 429       * @param \mod_quiz_mod_form $quizform the quiz settings form that is being built.
 430       * @param \MoodleQuickForm $mform the wrapped MoodleQuickForm.
 431       * @return string
 432       */
 433      protected static function get_uploaded_seb_file_download_link(\mod_quiz_mod_form $quizform, \MoodleQuickForm $mform) : string {
 434          $link = '';
 435          $file = self::get_module_context_sebconfig_file($quizform->get_coursemodule()->id);
 436  
 437          if ($file) {
 438              $url = \moodle_url::make_pluginfile_url(
 439                  $file->get_contextid(),
 440                  $file->get_component(),
 441                  $file->get_filearea(),
 442                  $file->get_itemid(),
 443                  $file->get_filepath(),
 444                  $file->get_filename(),
 445                  true
 446              );
 447              $link = \html_writer::link($url, get_string('downloadsebconfig', 'quizaccess_seb'));
 448          }
 449  
 450          return $link;
 451      }
 452  
 453      /**
 454       * Get the type of element for each of the form elements in quiz settings.
 455       *
 456       * Contains all setting elements. Array key is name of 'form element'/'database column (excluding prefix)'.
 457       *
 458       * @return array All quiz form elements to be added and their types.
 459       */
 460      public static function get_seb_config_elements() : array {
 461          return [
 462              'seb_linkquitseb' => 'text',
 463              'seb_userconfirmquit' => 'selectyesno',
 464              'seb_allowuserquitseb' => 'selectyesno',
 465              'seb_quitpassword' => 'passwordunmask',
 466              'seb_allowreloadinexam' => 'selectyesno',
 467              'seb_showsebtaskbar' => 'selectyesno',
 468              'seb_showreloadbutton' => 'selectyesno',
 469              'seb_showtime' => 'selectyesno',
 470              'seb_showkeyboardlayout' => 'selectyesno',
 471              'seb_showwificontrol' => 'selectyesno',
 472              'seb_enableaudiocontrol' => 'selectyesno',
 473              'seb_muteonstartup' => 'selectyesno',
 474              'seb_allowspellchecking' => 'selectyesno',
 475              'seb_activateurlfiltering' => 'selectyesno',
 476              'seb_filterembeddedcontent' => 'selectyesno',
 477              'seb_expressionsallowed' => 'textarea',
 478              'seb_regexallowed' => 'textarea',
 479              'seb_expressionsblocked' => 'textarea',
 480              'seb_regexblocked' => 'textarea',
 481          ];
 482      }
 483  
 484  
 485      /**
 486       * Get the types of the quiz settings elements.
 487       * @return array List of types for the setting elements.
 488       */
 489      public static function get_seb_config_element_types() : array {
 490          return [
 491              'seb_linkquitseb' => PARAM_RAW,
 492              'seb_userconfirmquit' => PARAM_BOOL,
 493              'seb_allowuserquitseb' => PARAM_BOOL,
 494              'seb_quitpassword' => PARAM_RAW,
 495              'seb_allowreloadinexam' => PARAM_BOOL,
 496              'seb_showsebtaskbar' => PARAM_BOOL,
 497              'seb_showreloadbutton' => PARAM_BOOL,
 498              'seb_showtime' => PARAM_BOOL,
 499              'seb_showkeyboardlayout' => PARAM_BOOL,
 500              'seb_showwificontrol' => PARAM_BOOL,
 501              'seb_enableaudiocontrol' => PARAM_BOOL,
 502              'seb_muteonstartup' => PARAM_BOOL,
 503              'seb_allowspellchecking' => PARAM_BOOL,
 504              'seb_activateurlfiltering' => PARAM_BOOL,
 505              'seb_filterembeddedcontent' => PARAM_BOOL,
 506              'seb_expressionsallowed' => PARAM_RAW,
 507              'seb_regexallowed' => PARAM_RAW,
 508              'seb_expressionsblocked' => PARAM_RAW,
 509              'seb_regexblocked' => PARAM_RAW,
 510          ];
 511      }
 512  
 513      /**
 514       * Check that we have conflicting permissions.
 515       *
 516       * In Some point we can have settings save by the person who use specific
 517       * type of SEB usage (e.g. use templates). But then another person who can't
 518       * use template (but still can update other settings) edit the same quiz. This is
 519       * conflict of permissions and we'd like to build the settings form having this in
 520       * mind.
 521       *
 522       * @param \context $context Context used with capability checking.
 523       *
 524       * @return bool
 525       */
 526      public static function is_conflicting_permissions(\context $context) {
 527          if ($context instanceof \context_course) {
 528              return false;
 529          }
 530  
 531          $settings = quiz_settings::get_record(['cmid' => (int) $context->instanceid]);
 532  
 533          if (empty($settings)) {
 534              return false;
 535          }
 536  
 537          if (!self::can_use_seb_template($context) &&
 538              $settings->get('requiresafeexambrowser') == self::USE_SEB_TEMPLATE) {
 539              return true;
 540          }
 541  
 542          if (!self::can_upload_seb_file($context) &&
 543              $settings->get('requiresafeexambrowser') == self::USE_SEB_UPLOAD_CONFIG) {
 544              return true;
 545          }
 546  
 547          if (!self::can_configure_manually($context) &&
 548              $settings->get('requiresafeexambrowser') == self::USE_SEB_CONFIG_MANUALLY) {
 549              return true;
 550          }
 551  
 552          return false;
 553      }
 554  
 555      /**
 556       * Returns a list of all options of SEB usage.
 557       *
 558       * @param \context $context Context used with capability checking selection options.
 559       * @return array
 560       */
 561      public static function get_requiresafeexambrowser_options(\context $context) : array {
 562          $options[self::USE_SEB_NO] = get_string('no');
 563  
 564          if (self::can_configure_manually($context) || self::is_conflicting_permissions($context)) {
 565              $options[self::USE_SEB_CONFIG_MANUALLY] = get_string('seb_use_manually', 'quizaccess_seb');
 566          }
 567  
 568          if (self::can_use_seb_template($context) || self::is_conflicting_permissions($context)) {
 569              if (!empty(self::get_template_options())) {
 570                  $options[self::USE_SEB_TEMPLATE] = get_string('seb_use_template', 'quizaccess_seb');
 571              }
 572          }
 573  
 574          if (self::can_upload_seb_file($context) || self::is_conflicting_permissions($context)) {
 575              $options[self::USE_SEB_UPLOAD_CONFIG] = get_string('seb_use_upload', 'quizaccess_seb');
 576          }
 577  
 578          $options[self::USE_SEB_CLIENT_CONFIG] = get_string('seb_use_client', 'quizaccess_seb');
 579  
 580          return $options;
 581      }
 582  
 583      /**
 584       * Returns a list of templates.
 585       * @return array
 586       */
 587      protected static function get_template_options() : array {
 588          $templates = [];
 589          $records = template::get_records(['enabled' => 1], 'name');
 590          if ($records) {
 591              foreach ($records as $record) {
 592                  $templates[$record->get('id')] = $record->get('name');
 593              }
 594          }
 595  
 596          return $templates;
 597      }
 598  
 599      /**
 600       * Returns a list of options for the file manager element.
 601       * @return array
 602       */
 603      public static function get_filemanager_options() : array {
 604          return [
 605              'subdirs' => 0,
 606              'maxfiles' => 1,
 607              'accepted_types' => ['.seb']
 608          ];
 609      }
 610  
 611      /**
 612       * Get the default values of the quiz settings.
 613       *
 614       * Array key is name of 'form element'/'database column (excluding prefix)'.
 615       *
 616       * @return array List of settings and their defaults.
 617       */
 618      public static function get_seb_config_element_defaults() : array {
 619          return [
 620              'seb_linkquitseb' => '',
 621              'seb_userconfirmquit' => 1,
 622              'seb_allowuserquitseb' => 1,
 623              'seb_quitpassword' => '',
 624              'seb_allowreloadinexam' => 1,
 625              'seb_showsebtaskbar' => 1,
 626              'seb_showreloadbutton' => 1,
 627              'seb_showtime' => 1,
 628              'seb_showkeyboardlayout' => 1,
 629              'seb_showwificontrol' => 0,
 630              'seb_enableaudiocontrol' => 0,
 631              'seb_muteonstartup' => 0,
 632              'seb_allowspellchecking' => 0,
 633              'seb_activateurlfiltering' => 0,
 634              'seb_filterembeddedcontent' => 0,
 635              'seb_expressionsallowed' => '',
 636              'seb_regexallowed' => '',
 637              'seb_expressionsblocked' => '',
 638              'seb_regexblocked' => '',
 639          ];
 640      }
 641  
 642      /**
 643       * Validate that if a file has been uploaded by current user, that it is a valid PLIST XML file.
 644       * This function is only called if requiresafeexambrowser == settings_provider::USE_SEB_UPLOAD_CONFIG.
 645       *
 646       * @param string $itemid Item ID of file in user draft file area.
 647       * @return void|lang_string
 648       */
 649      public static function validate_draftarea_configfile($itemid) {
 650          // When saving the settings, this value will be null.
 651          if (is_null($itemid)) {
 652              return;
 653          }
 654          // If there is a config file uploaded, make sure it is a PList XML file.
 655          $file = self::get_current_user_draft_file($itemid);
 656  
 657          // If we require an SEB config uploaded, and the file exists, parse it.
 658          if ($file) {
 659              if (!helper::is_valid_seb_config($file->get_content())) {
 660                  return new lang_string('fileparsefailed', 'quizaccess_seb');
 661              }
 662          }
 663  
 664          // If we require an SEB config uploaded, and the file does not exist, error.
 665          if (!$file) {
 666              return new lang_string('filenotpresent', 'quizaccess_seb');
 667          }
 668      }
 669  
 670      /**
 671       * Try and get a file in the user draft filearea by itemid.
 672       *
 673       * @param string $itemid Item ID of the file.
 674       * @return stored_file|null Returns null if no file is found.
 675       */
 676      public static function get_current_user_draft_file(string $itemid) : ?stored_file {
 677          global $USER;
 678          $context = context_user::instance($USER->id);
 679          $fs = get_file_storage();
 680          if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $itemid, 'id DESC', false)) {
 681              return null;
 682          }
 683          return reset($files);
 684      }
 685  
 686      /**
 687       * Get the file that is stored in the course module file area.
 688       *
 689       * @param string $cmid The course module id which is used as an itemid reference.
 690       * @return stored_file|null Returns null if no file is found.
 691       */
 692      public static function get_module_context_sebconfig_file(string $cmid) : ?stored_file {
 693          $fs = new \file_storage();
 694          $context = context_module::instance($cmid);
 695  
 696          if (!$files = $fs->get_area_files($context->id, 'quizaccess_seb', 'filemanager_sebconfigfile', 0,
 697              'id DESC', false)) {
 698              return null;
 699          }
 700  
 701          return reset($files);
 702      }
 703  
 704      /**
 705       * Saves filemanager_sebconfigfile files to the moodle storage backend.
 706       *
 707       * @param string $draftitemid The id of the draft area to use.
 708       * @param string $cmid The cmid of for the quiz.
 709       * @return bool Always true
 710       */
 711      public static function save_filemanager_sebconfigfile_draftarea(string $draftitemid, string $cmid) : bool {
 712          if ($draftitemid) {
 713              $context = context_module::instance($cmid);
 714              file_save_draft_area_files($draftitemid, $context->id, 'quizaccess_seb', 'filemanager_sebconfigfile',
 715                  0, []);
 716          }
 717  
 718          return true;
 719      }
 720  
 721      /**
 722       * Cleanup function to delete the saved config when it has not been specified.
 723       * This will be called when settings_provider::USE_SEB_UPLOAD_CONFIG is not true.
 724       *
 725       * @param string $cmid The cmid of for the quiz.
 726       * @return bool Always true or exception if error occurred
 727       */
 728      public static function delete_uploaded_config_file(string $cmid) : bool {
 729          $file = self::get_module_context_sebconfig_file($cmid);
 730  
 731          if (!empty($file)) {
 732              return $file->delete();
 733          }
 734  
 735          return false;
 736      }
 737  
 738      /**
 739       * Check if the current user can configure SEB.
 740       *
 741       * @param \context $context Context to check access in.
 742       * @return bool
 743       */
 744      public static function can_configure_seb(\context $context) : bool {
 745          return has_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', $context);
 746      }
 747  
 748      /**
 749       * Check if the current user can use preconfigured templates.
 750       *
 751       * @param \context $context Context to check access in.
 752       * @return bool
 753       */
 754      public static function can_use_seb_template(\context $context) : bool {
 755          return has_capability('quizaccess/seb:manage_seb_templateid', $context);
 756      }
 757  
 758      /**
 759       * Check if the current user can upload own SEB config file.
 760       *
 761       * @param \context $context Context to check access in.
 762       * @return bool
 763       */
 764      public static function can_upload_seb_file(\context $context) : bool {
 765          return has_capability('quizaccess/seb:manage_filemanager_sebconfigfile', $context);
 766      }
 767  
 768      /**
 769       * Check if the current user can change Show Safe Exam Browser download button setting.
 770       *
 771       * @param \context $context Context to check access in.
 772       * @return bool
 773       */
 774      public static function can_change_seb_showsebdownloadlink(\context $context) : bool {
 775          return has_capability('quizaccess/seb:manage_seb_showsebdownloadlink', $context);
 776      }
 777  
 778      /**
 779       * Check if the current user can change Allowed Browser Exam Keys setting.
 780       *
 781       * @param \context $context Context to check access in.
 782       * @return bool
 783       */
 784      public static function can_change_seb_allowedbrowserexamkeys(\context $context) : bool {
 785          return has_capability('quizaccess/seb:manage_seb_allowedbrowserexamkeys', $context);
 786      }
 787  
 788      /**
 789       * Check if the current user can config SEB manually.
 790       *
 791       * @param \context $context Context to check access in.
 792       * @return bool
 793       */
 794      public static function can_configure_manually(\context $context) : bool {
 795          foreach (self::get_seb_config_elements() as $name => $type) {
 796              if (self::can_manage_seb_config_setting($name, $context)) {
 797                  return true;
 798              }
 799          }
 800  
 801          return false;
 802      }
 803  
 804      /**
 805       * Check if the current user can manage provided SEB setting.
 806       *
 807       * @param string $settingname Name of the setting.
 808       * @param \context $context Context to check access in.
 809       * @return bool
 810       */
 811      public static function can_manage_seb_config_setting(string $settingname, \context $context) : bool {
 812          $capsttocheck = [];
 813  
 814          foreach (self::get_seb_settings_map() as $type => $settings) {
 815              $capsttocheck = self::build_config_capabilities_to_check($settingname, $settings);
 816              if (!empty($capsttocheck)) {
 817                  break;
 818              }
 819          }
 820  
 821          foreach ($capsttocheck as $capability) {
 822              // Capability must exist.
 823              if (!$capinfo = get_capability_info($capability)) {
 824                  throw new \coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
 825              }
 826          }
 827  
 828          return has_all_capabilities($capsttocheck, $context);
 829      }
 830  
 831      /**
 832       * Helper method to build a list of capabilities to check.
 833       *
 834       * @param string $settingname Given setting name to build caps for.
 835       * @param array $settings A list of settings to go through.
 836       * @return array
 837       */
 838      protected static function build_config_capabilities_to_check(string $settingname, array $settings) : array {
 839          $capsttocheck = [];
 840  
 841          foreach ($settings as $setting => $children) {
 842              if ($setting == $settingname) {
 843                  $capsttocheck[$setting] = self::build_setting_capability_name($setting);
 844                  break; // Found what we need exit the loop.
 845              }
 846  
 847              // Recursively check all children.
 848              $capsttocheck = self::build_config_capabilities_to_check($settingname, $children);
 849              if (!empty($capsttocheck)) {
 850                  // Matching child found, add the parent capability to the list of caps to check.
 851                  $capsttocheck[$setting] = self::build_setting_capability_name($setting);
 852                  break; // Found what we need exit the loop.
 853              }
 854          }
 855  
 856          return $capsttocheck;
 857      }
 858  
 859      /**
 860       * Helper method to return a map of all settings.
 861       *
 862       * @return array
 863       */
 864      public static function get_seb_settings_map() : array {
 865          return [
 866              self::USE_SEB_NO => [
 867  
 868              ],
 869              self::USE_SEB_CONFIG_MANUALLY => [
 870                  'seb_showsebdownloadlink' => [],
 871                  'seb_linkquitseb' => [],
 872                  'seb_userconfirmquit' => [],
 873                  'seb_allowuserquitseb' => [
 874                      'seb_quitpassword' => []
 875                  ],
 876                  'seb_allowreloadinexam' => [],
 877                  'seb_showsebtaskbar' => [
 878                      'seb_showreloadbutton' => [],
 879                      'seb_showtime' => [],
 880                      'seb_showkeyboardlayout' => [],
 881                      'seb_showwificontrol' => [],
 882                  ],
 883                  'seb_enableaudiocontrol' => [
 884                      'seb_muteonstartup' => [],
 885                  ],
 886                  'seb_allowspellchecking' => [],
 887                  'seb_activateurlfiltering' => [
 888                      'seb_filterembeddedcontent' => [],
 889                      'seb_expressionsallowed' => [],
 890                      'seb_regexallowed' => [],
 891                      'seb_expressionsblocked' => [],
 892                      'seb_regexblocked' => [],
 893                  ],
 894              ],
 895              self::USE_SEB_TEMPLATE => [
 896                  'seb_templateid' => [],
 897                  'seb_showsebdownloadlink' => [],
 898                  'seb_allowuserquitseb' => [
 899                      'seb_quitpassword' => [],
 900                  ],
 901              ],
 902              self::USE_SEB_UPLOAD_CONFIG => [
 903                  'filemanager_sebconfigfile' => [],
 904                  'seb_showsebdownloadlink' => [],
 905                  'seb_allowedbrowserexamkeys' => [],
 906              ],
 907              self::USE_SEB_CLIENT_CONFIG => [
 908                  'seb_showsebdownloadlink' => [],
 909                  'seb_allowedbrowserexamkeys' => [],
 910              ],
 911          ];
 912      }
 913  
 914      /**
 915       * Get allowed settings for provided SEB usage type.
 916       *
 917       * @param int $requiresafeexambrowser SEB usage type.
 918       * @return array
 919       */
 920      private static function get_allowed_settings(int $requiresafeexambrowser) : array {
 921          $result = [];
 922          $map = self::get_seb_settings_map();
 923  
 924          if (!key_exists($requiresafeexambrowser, $map)) {
 925              return $result;
 926          }
 927  
 928          return self::build_allowed_settings($map[$requiresafeexambrowser]);
 929      }
 930  
 931      /**
 932       * Recursive method to build a list of allowed settings.
 933       *
 934       * @param array $settings A list of settings from settings map.
 935       * @return array
 936       */
 937      private static function build_allowed_settings(array $settings) : array {
 938          $result = [];
 939  
 940          foreach ($settings as $name => $children) {
 941              $result[] = $name;
 942              foreach ($children as $childname => $child) {
 943                  $result[] = $childname;
 944                  $result = array_merge($result, self::build_allowed_settings($child));
 945              }
 946          }
 947  
 948          return $result;
 949      }
 950  
 951      /**
 952       * Get the conditions that an element should be hid in the form. Expects matching using 'eq'.
 953       *
 954       * Array key is name of 'form element'/'database column (excluding prefix)'.
 955       * Values are instances of hideif_rule class.
 956       *
 957       * @return array List of rules per element.
 958       */
 959      public static function get_quiz_hideifs() : array {
 960          $hideifs = [];
 961  
 962          // We are building rules based on the settings map, that means children will be dependant on parent.
 963          // In most cases it's all pretty standard.
 964          // However it could be some specific cases for some fields, which will be overridden later.
 965          foreach (self::get_seb_settings_map() as $type => $settings) {
 966              foreach ($settings as $setting => $children) {
 967                  $hideifs[$setting][] = new hideif_rule($setting, 'seb_requiresafeexambrowser', 'noteq', $type);
 968  
 969                  foreach ($children as $childname => $child) {
 970                      $hideifs[$childname][] = new hideif_rule($childname, 'seb_requiresafeexambrowser', 'noteq', $type);
 971                      $hideifs[$childname][] = new hideif_rule($childname, $setting, 'eq', 0);
 972                  }
 973              }
 974          }
 975  
 976          // Specific case for "Enable quitting of SEB". It should available for Manual and Template.
 977          $hideifs['seb_allowuserquitseb'] = [
 978              new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
 979              new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG),
 980              new hideif_rule('seb_allowuserquitseb', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_UPLOAD_CONFIG),
 981          ];
 982  
 983          // Specific case for "Quit password". It should be available for Manual and Template. As it's parent.
 984          $hideifs['seb_quitpassword'] = [
 985              new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
 986              new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CLIENT_CONFIG),
 987              new hideif_rule('seb_quitpassword', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_UPLOAD_CONFIG),
 988              new hideif_rule('seb_quitpassword', 'seb_allowuserquitseb', 'eq', 0),
 989          ];
 990  
 991          // Specific case for "Show Safe Exam Browser download button". It should be available for all cases, except No Seb.
 992          $hideifs['seb_showsebdownloadlink'] = [
 993              new hideif_rule('seb_showsebdownloadlink', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO)
 994          ];
 995  
 996          // Specific case for "Allowed Browser Exam Keys". It should be available for Template and Browser config.
 997          $hideifs['seb_allowedbrowserexamkeys'] = [
 998              new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_NO),
 999              new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_CONFIG_MANUALLY),
1000              new hideif_rule('seb_allowedbrowserexamkeys', 'seb_requiresafeexambrowser', 'eq', self::USE_SEB_TEMPLATE),
1001          ];
1002  
1003          return $hideifs;
1004      }
1005  
1006      /**
1007       * Build a capability name for the provided SEB setting.
1008       *
1009       * @param string $settingname Name of the setting.
1010       * @return string
1011       */
1012      public static function build_setting_capability_name(string $settingname) : string {
1013          if (!key_exists($settingname, self::get_seb_config_elements())) {
1014              throw new \coding_exception('Incorrect SEB quiz setting ' . $settingname);
1015          }
1016  
1017          return 'quizaccess/seb:manage_' . $settingname;
1018      }
1019  
1020      /**
1021       * Check if settings is locked.
1022       *
1023       * @param int $quizid Quiz ID.
1024       * @return bool
1025       */
1026      public static function is_seb_settings_locked($quizid) : bool {
1027          if (empty($quizid)) {
1028              return false;
1029          }
1030  
1031          return quiz_has_attempts($quizid);
1032      }
1033  
1034      /**
1035       * Filter a standard class by prefix.
1036       *
1037       * @param stdClass $settings Quiz settings object.
1038       * @return stdClass Filtered object.
1039       */
1040      private static function filter_by_prefix(\stdClass $settings): stdClass {
1041          $newsettings = new \stdClass();
1042          foreach ($settings as $name => $setting) {
1043              // Only add it, if not there.
1044              if (strpos($name, "seb_") === 0) {
1045                  $newsettings->$name = $setting; // Add new key.
1046              }
1047          }
1048          return $newsettings;
1049      }
1050  
1051      /**
1052       * Filter settings based on the setting map. Set value of not allowed settings to null.
1053       *
1054       * @param stdClass $settings Quiz settings.
1055       * @return \stdClass
1056       */
1057      private static function filter_by_settings_map(stdClass $settings) : stdClass {
1058          if (!isset($settings->seb_requiresafeexambrowser)) {
1059              return $settings;
1060          }
1061  
1062          $newsettings = new \stdClass();
1063          $newsettings->seb_requiresafeexambrowser = $settings->seb_requiresafeexambrowser;
1064          $allowedsettings = self::get_allowed_settings((int)$newsettings->seb_requiresafeexambrowser);
1065          unset($settings->seb_requiresafeexambrowser);
1066  
1067          foreach ($settings as $name => $value) {
1068              if (!in_array($name, $allowedsettings)) {
1069                  $newsettings->$name = null;
1070              } else {
1071                  $newsettings->$name = $value;
1072              }
1073          }
1074  
1075          return $newsettings;
1076      }
1077  
1078      /**
1079       * Filter quiz settings for this plugin only.
1080       *
1081       * @param stdClass $settings Quiz settings.
1082       * @return stdClass Filtered settings.
1083       */
1084      public static function filter_plugin_settings(stdClass $settings) : stdClass {
1085          $settings = self::filter_by_prefix($settings);
1086          $settings = self::filter_by_settings_map($settings);
1087  
1088          return self::strip_all_prefixes($settings);
1089      }
1090  
1091      /**
1092       * Strip the seb_ prefix from each setting key.
1093       *
1094       * @param \stdClass $settings Object containing settings.
1095       * @return \stdClass The modified settings object.
1096       */
1097      private static function strip_all_prefixes(\stdClass $settings): stdClass {
1098          $newsettings = new \stdClass();
1099          foreach ($settings as $name => $setting) {
1100              $newname = preg_replace("/^seb_/", "", $name);
1101              $newsettings->$newname = $setting; // Add new key.
1102          }
1103          return $newsettings;
1104      }
1105  
1106      /**
1107       * Add prefix to string.
1108       *
1109       * @param string $name String to add prefix to.
1110       * @return string String with prefix.
1111       */
1112      public static function add_prefix(string $name): string {
1113          if (strpos($name, 'seb_') !== 0) {
1114              $name = 'seb_' . $name;
1115          }
1116          return $name;
1117      }
1118  }