Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 400] [Versions 310 and 401]

   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   * Manage the access to the quiz.
  19   *
  20   * @package    quizaccess_seb
  21   * @author     Tim Hunt
  22   * @author     Luca Bösch <luca.boesch@bfh.ch>
  23   * @author     Andrew Madden <andrewmadden@catalyst-au.net>
  24   * @author     Dmitrii Metelkin <dmitriim@catalyst-au.net>
  25   * @copyright  2019 Catalyst IT
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  namespace quizaccess_seb;
  30  
  31  use context_module;
  32  use quiz;
  33  
  34  defined('MOODLE_INTERNAL') || die();
  35  
  36  /**
  37   * Manage the access to the quiz.
  38   *
  39   * @copyright  2020 Catalyst IT
  40   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class access_manager {
  43  
  44      /** Header sent by Safe Exam Browser containing the Config Key hash. */
  45      private const CONFIG_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH';
  46  
  47      /** Header sent by Safe Exam Browser containing the Browser Exam Key hash. */
  48      private const BROWSER_EXAM_KEY_HEADER = 'HTTP_X_SAFEEXAMBROWSER_REQUESTHASH';
  49  
  50      /** @var quiz $quiz A quiz object containing all information pertaining to current quiz. */
  51      private $quiz;
  52  
  53      /** @var quiz_settings $quizsettings A quiz settings persistent object containing plugin settings */
  54      private $quizsettings;
  55  
  56      /** @var context_module $context Context of this quiz activity. */
  57      private $context;
  58  
  59      /** @var string|null $validconfigkey Expected valid SEB config key.
  60       */
  61      private $validconfigkey = null;
  62  
  63      /**
  64       * The access_manager constructor.
  65       *
  66       * @param quiz $quiz The details of the quiz.
  67       */
  68      public function __construct(quiz $quiz) {
  69          $this->quiz = $quiz;
  70          $this->context = context_module::instance($quiz->get_cmid());
  71          $this->quizsettings = quiz_settings::get_by_quiz_id($quiz->get_quizid());
  72          $this->validconfigkey = quiz_settings::get_config_key_by_quiz_id($quiz->get_quizid());
  73      }
  74  
  75      /**
  76       * Check if the browser exam key hash in header matches one of the listed browser exam keys from quiz settings.
  77       *
  78       * @return bool True if header key matches one of the saved keys.
  79       */
  80      public function validate_browser_exam_keys() : bool {
  81          // If browser exam keys are entered in settings, check they match the header.
  82          $browserexamkeys = $this->quizsettings->get('allowedbrowserexamkeys');
  83          if (empty($browserexamkeys)) {
  84              return true; // If no browser exam keys, no check required.
  85          }
  86  
  87          // If the Browser Exam Key header isn't present, prevent access.
  88          if (is_null($this->get_received_browser_exam_key())) {
  89              return false;
  90          }
  91  
  92          return $this->check_browser_exam_keys($browserexamkeys, $this->get_received_browser_exam_key());
  93      }
  94  
  95      /**
  96       * Check if the config key hash in header matches quiz settings.
  97       *
  98       * @return bool True if header key matches saved key.
  99       */
 100      public function validate_config_key() : bool {
 101          // If using client config, or with no requirement, then no check required.
 102          $requiredtype = $this->get_seb_use_type();
 103          if ($requiredtype == settings_provider::USE_SEB_NO
 104                  || $requiredtype == settings_provider::USE_SEB_CLIENT_CONFIG) {
 105              return true;
 106          }
 107  
 108          if (empty($this->validconfigkey)) {
 109              return false; // No config key has been saved.
 110          }
 111  
 112          // If the Config Key header isn't present, prevent access.
 113          if (is_null($this->get_received_config_key())) {
 114              return false;
 115          }
 116  
 117          return $this->check_key($this->validconfigkey, $this->get_received_config_key());
 118      }
 119  
 120      /**
 121       * Check if Safe Exam Browser is required to access quiz.
 122       * If quizsettings do not exist, then there is no requirement for using SEB.
 123       *
 124       * @return bool If required.
 125       */
 126      public function seb_required() : bool {
 127          if (!$this->quizsettings) {
 128              return false;
 129          } else {
 130              return $this->get_seb_use_type() != settings_provider::USE_SEB_NO;
 131          }
 132      }
 133  
 134      /**
 135       * This is the basic check for the Safe Exam Browser previously used in the quizaccess_safebrowser plugin that
 136       * managed basic Moodle interactions with SEB.
 137       *
 138       * @return bool
 139       */
 140      public function validate_basic_header() : bool {
 141          if ($this->get_seb_use_type() == settings_provider::USE_SEB_CLIENT_CONFIG) {
 142              return $this->is_using_seb();
 143          }
 144          return true;
 145      }
 146  
 147      /**
 148       * Check if using Safe Exam Browser.
 149       *
 150       * @return bool
 151       */
 152      public function is_using_seb() : bool {
 153          if (isset($_SERVER['HTTP_USER_AGENT'])) {
 154              return strpos($_SERVER['HTTP_USER_AGENT'], 'SEB') !== false;
 155          }
 156  
 157          return false;
 158      }
 159  
 160      /**
 161       * Check if user has any capability to bypass the Safe Exam Browser requirement.
 162       *
 163       * @return bool True if user can bypass check.
 164       */
 165      public function can_bypass_seb() : bool {
 166          return has_capability('quizaccess/seb:bypassseb', $this->context);
 167      }
 168  
 169      /**
 170       * Return the full URL that was used to request the current page, which is
 171       * what we need for verifying the X-SafeExamBrowser-RequestHash header.
 172       */
 173      private function get_this_page_url() : string {
 174          global $CFG, $FULLME;
 175          // If $FULLME not set fall back to wwwroot.
 176          if ($FULLME == null) {
 177              return $CFG->wwwroot;
 178          }
 179          return $FULLME;
 180      }
 181  
 182      /**
 183       * Return expected SEB config key.
 184       *
 185       * @return string|null
 186       */
 187      public function get_valid_config_key() : ?string {
 188          return $this->validconfigkey;
 189      }
 190  
 191      /**
 192       * Getter for the quiz object.
 193       *
 194       * @return quiz
 195       */
 196      public function get_quiz() : quiz {
 197          return $this->quiz;
 198      }
 199  
 200      /**
 201       * Check the hash from the request header against the permitted browser exam keys.
 202       *
 203       * @param array $keys Allowed browser exam keys.
 204       * @param string $header The value of the X-SafeExamBrowser-RequestHash to check.
 205       * @return bool True if the hash matches.
 206       */
 207      private function check_browser_exam_keys(array $keys, string $header) : bool {
 208          foreach ($keys as $key) {
 209              if ($this->check_key($key, $header)) {
 210                  return true;
 211              }
 212          }
 213          return false;
 214      }
 215  
 216      /**
 217       * Check the hash from the request header against a single permitted key.
 218       *
 219       * @param string $key an allowed key.
 220       * @param string $header the value of the X-SafeExamBrowser-RequestHash or X-SafeExamBrowser-ConfigKeyHash to check.
 221       * @return bool true if the hash matches.
 222       */
 223      private function check_key($key, $header) : bool {
 224          return hash('sha256', $this->get_this_page_url() . $key) === $header;
 225      }
 226  
 227      /**
 228       * Returns Safe Exam Browser Config Key hash.
 229       *
 230       * @return string|null
 231       */
 232      public function get_received_config_key() {
 233          if (isset($_SERVER[self::CONFIG_KEY_HEADER])) {
 234              return trim($_SERVER[self::CONFIG_KEY_HEADER]);
 235          }
 236  
 237          return null;
 238      }
 239  
 240      /**
 241       * Returns the Browser Exam Key hash.
 242       *
 243       * @return string|null
 244       */
 245      public function get_received_browser_exam_key() {
 246          if (isset($_SERVER[self::BROWSER_EXAM_KEY_HEADER])) {
 247              return trim($_SERVER[self::BROWSER_EXAM_KEY_HEADER]);
 248          }
 249  
 250          return null;
 251      }
 252  
 253      /**
 254       * Get type of SEB usage for the quiz.
 255       *
 256       * @return int
 257       */
 258      public function get_seb_use_type() : int {
 259          if (empty($this->quizsettings)) {
 260              return settings_provider::USE_SEB_NO;
 261          } else {
 262              return $this->quizsettings->get('requiresafeexambrowser');
 263          }
 264      }
 265  
 266      /**
 267       * Should validate basic header?
 268       *
 269       * @return bool
 270       */
 271      public function should_validate_basic_header() : bool {
 272          return in_array($this->get_seb_use_type(), [
 273              settings_provider::USE_SEB_CLIENT_CONFIG,
 274          ]);
 275      }
 276  
 277      /**
 278       * Should validate SEB config key?
 279       * @return bool
 280       */
 281      public function should_validate_config_key() : bool {
 282          return in_array($this->get_seb_use_type(), [
 283              settings_provider::USE_SEB_CONFIG_MANUALLY,
 284              settings_provider::USE_SEB_TEMPLATE,
 285              settings_provider::USE_SEB_UPLOAD_CONFIG,
 286          ]);
 287      }
 288  
 289      /**
 290       * Should validate browser exam key?
 291       *
 292       * @return bool
 293       */
 294      public function should_validate_browser_exam_key() : bool {
 295          return in_array($this->get_seb_use_type(), [
 296              settings_provider::USE_SEB_UPLOAD_CONFIG,
 297              settings_provider::USE_SEB_CLIENT_CONFIG,
 298          ]);
 299      }
 300  }