Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.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  namespace tool_mfa\plugininfo;
  18  
  19  use moodle_url;
  20  use stdClass;
  21  
  22  /**
  23   * Subplugin info class.
  24   *
  25   * @package     tool_mfa
  26   * @author      Mikhail Golenkov <golenkovm@gmail.com>
  27   * @copyright   Catalyst IT
  28   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  class factor extends \core\plugininfo\base {
  31  
  32      /** @var string */
  33      const STATE_UNKNOWN = 'unknown';
  34  
  35      /** @var string */
  36      const STATE_PASS = 'pass';
  37  
  38      /** @var string */
  39      const STATE_FAIL = 'fail';
  40  
  41      /** @var string */
  42      const STATE_NEUTRAL = 'neutral';
  43  
  44      /** @var string Locked state is identical to neutral, but can't be overridden */
  45      const STATE_LOCKED = 'locked';
  46  
  47      /**
  48       * Finds all MFA factors.
  49       *
  50       * @return array of factor objects.
  51       */
  52      public static function get_factors(): array {
  53          $return = [];
  54          $factors = \core_plugin_manager::instance()->get_plugins_of_type('factor');
  55  
  56          foreach ($factors as $factor) {
  57              $classname = '\\factor_'.$factor->name.'\\factor';
  58              if (class_exists($classname)) {
  59                  $return[] = new $classname($factor->name);
  60              }
  61          }
  62          return self::sort_factors_by_order($return);
  63      }
  64  
  65      /**
  66       * Sorts factors by configured order.
  67       *
  68       * @param array $unsorted of factor objects
  69       * @return array of factor objects
  70       * @throws \dml_exception
  71       */
  72      public static function sort_factors_by_order(array $unsorted): array {
  73          $sorted = [];
  74          $orderarray = explode(',', get_config('tool_mfa', 'factor_order'));
  75  
  76          foreach ($orderarray as $order => $factorname) {
  77              foreach ($unsorted as $key => $factor) {
  78                  if ($factor->name == $factorname) {
  79                      $sorted[] = $factor;
  80                      unset($unsorted[$key]);
  81                  }
  82              }
  83          }
  84  
  85          $sorted = array_merge($sorted, $unsorted);
  86          return $sorted;
  87      }
  88  
  89      /**
  90       * Finds factor by its name.
  91       *
  92       * @param string $name
  93       *
  94       * @return mixed factor object or false if factor not found.
  95       */
  96      public static function get_factor(string $name): object|bool {
  97          $factors = \core_plugin_manager::instance()->get_plugins_of_type('factor');
  98  
  99          foreach ($factors as $factor) {
 100              if ($name == $factor->name) {
 101                  $classname = '\\factor_'.$factor->name.'\\factor';
 102                  if (class_exists($classname)) {
 103                      return new $classname($factor->name);
 104                  }
 105              }
 106          }
 107  
 108          return false;
 109      }
 110  
 111      /**
 112       * Finds all enabled factors.
 113       *
 114       * @return array of factor objects
 115       */
 116      public static function get_enabled_factors(): array {
 117          $return = [];
 118          $factors = self::get_factors();
 119  
 120          foreach ($factors as $factor) {
 121              if ($factor->is_enabled()) {
 122                  $return[] = $factor;
 123              }
 124          }
 125  
 126          return $return;
 127      }
 128  
 129      /**
 130       * Finds active factors for a user.
 131       * If user is not specified, current user is used.
 132       *
 133       * @param mixed $user user object or null.
 134       * @return array of factor objects.
 135       */
 136      public static function get_active_user_factor_types(mixed $user = null): array {
 137          global $USER;
 138          if (is_null($user)) {
 139              $user = $USER;
 140          }
 141  
 142          $return = [];
 143          $factors = self::get_enabled_factors();
 144  
 145          foreach ($factors as $factor) {
 146              $userfactors = $factor->get_active_user_factors($user);
 147              if (count($userfactors) > 0) {
 148                  $return[] = $factor;
 149              }
 150          }
 151  
 152          return $return;
 153      }
 154  
 155      /**
 156       * Returns next factor to authenticate user.
 157       * Only returns factors that require user input.
 158       *
 159       * @return mixed factor object the next factor to be authenticated or false.
 160       */
 161      public static function get_next_user_login_factor(): mixed {
 162          $factors = self::get_active_user_factor_types();
 163  
 164          foreach ($factors as $factor) {
 165              if (!$factor->has_input()) {
 166                  continue;
 167              }
 168  
 169              if ($factor->get_state() == self::STATE_UNKNOWN) {
 170                  return $factor;
 171              }
 172          }
 173  
 174          return new \tool_mfa\local\factor\fallback();
 175      }
 176  
 177      /**
 178       * Returns all factors that require user input.
 179       *
 180       * @return array of factor objects.
 181       */
 182      public static function get_all_user_login_factors(): array {
 183          $factors = self::get_active_user_factor_types();
 184          $loginfactors = [];
 185          foreach ($factors as $factor) {
 186              if ($factor->has_input()) {
 187                  $loginfactors[] = $factor;
 188              }
 189  
 190          }
 191          return $loginfactors;
 192      }
 193  
 194      /**
 195       * Returns the list of available actions with factor.
 196       *
 197       * @return array
 198       */
 199      public static function get_factor_actions(): array {
 200          $actions = [];
 201          $actions[] = 'setup';
 202          $actions[] = 'revoke';
 203          $actions[] = 'enable';
 204          $actions[] = 'revoke';
 205          $actions[] = 'disable';
 206          $actions[] = 'up';
 207          $actions[] = 'down';
 208  
 209          return $actions;
 210      }
 211  
 212      /**
 213       * Returns the information about plugin availability
 214       *
 215       * True means that the plugin is enabled. False means that the plugin is
 216       * disabled. Null means that the information is not available, or the
 217       * plugin does not support configurable availability or the availability
 218       * can not be changed.
 219       *
 220       * @return null|bool
 221       */
 222      public function is_enabled(): null|bool {
 223          if (!$this->rootdir) {
 224              // Plugin missing.
 225              return false;
 226          }
 227  
 228          $factor = $this->get_factor($this->name);
 229  
 230          if ($factor) {
 231              return $factor->is_enabled();
 232          }
 233  
 234          return false;
 235      }
 236  
 237      /**
 238       * Returns section name for settings.
 239       *
 240       * @return string
 241       */
 242      public function get_settings_section_name(): string {
 243          return $this->type . '_' . $this->name;
 244      }
 245  
 246      /**
 247       * Loads factor settings to the settings tree
 248       *
 249       * This function usually includes settings.php file in plugins folder.
 250       * Alternatively it can create a link to some settings page (instance of admin_externalpage)
 251       *
 252       * @param \part_of_admin_tree $adminroot
 253       * @param string $parentnodename
 254       * @param bool $hassiteconfig whether the current user has moodle/site:config capability
 255       */
 256      public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig): void {
 257  
 258          if (!$this->is_installed_and_upgraded()) {
 259              return;
 260          }
 261  
 262          if (!$hassiteconfig || !file_exists($this->full_path('settings.php'))) {
 263              return;
 264          }
 265  
 266          $section = $this->get_settings_section_name();
 267  
 268          $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
 269  
 270          if ($adminroot->fulltree) {
 271              include($this->full_path('settings.php'));
 272          }
 273  
 274          $adminroot->add($parentnodename, $settings);
 275      }
 276  
 277      /**
 278       * Checks that given factor exists.
 279       *
 280       * @param string $factorname
 281       *
 282       * @return bool
 283       */
 284      public static function factor_exists(string $factorname): bool {
 285          $factor = self::get_factor($factorname);
 286          return !$factor ? false : true;
 287      }
 288  
 289      /**
 290       * Returns instance of any factor from the factorid.
 291       *
 292       * @param int $factorid
 293       *
 294       * @return stdClass|null Factor instance or nothing if not found.
 295       */
 296      public static function get_instance_from_id(int $factorid): stdClass|null {
 297          global $DB;
 298          return $DB->get_record('tool_mfa', ['id' => $factorid]);
 299      }
 300  
 301      /**
 302       * Return URL used for management of plugins of this type.
 303       *
 304       * @return moodle_url
 305       */
 306      public static function get_manage_url(): moodle_url {
 307          return new moodle_url('/admin/settings.php', [
 308              'section' => 'managemfa',
 309          ]);
 310      }
 311  
 312      /**
 313       * These subplugins can be uninstalled.
 314       *
 315       * @return bool
 316       */
 317      public function is_uninstall_allowed(): bool {
 318          return $this->name !== 'nosetup';
 319      }
 320  
 321      /**
 322       * Pre-uninstall hook.
 323       *
 324       * This is intended for disabling of plugin, some DB table purging, etc.
 325       *
 326       * NOTE: to be called from uninstall_plugin() only.
 327       * @private
 328       */
 329      public function uninstall_cleanup() {
 330          global $DB, $CFG;
 331  
 332          $DB->delete_records('tool_mfa', ['factor' => $this->name]);
 333          $DB->delete_records('tool_mfa_secrets', ['factor' => $this->name]);
 334  
 335          $order = explode(',', get_config('tool_mfa', 'factor_order'));
 336          if (in_array($this->name, $order)) {
 337              $order = array_diff($order, [$this->name]);
 338              \tool_mfa\manager::set_factor_config(['factor_order' => implode(',', $order)], 'tool_mfa');
 339          }
 340  
 341          parent::uninstall_cleanup();
 342      }
 343  
 344      /**
 345       * Sorts factors by state.
 346       *
 347       * @param array $factors The factors to sort.
 348       * @param string $state The state to sort by.
 349       * @return array $factors The sorted factors.
 350       */
 351      public static function sort_factors_by_state(array $factors, string $state): array {
 352          usort($factors, function ($a, $b) use ($state) {
 353              $statea = $a->get_state();
 354              $stateb = $b->get_state();
 355  
 356              if ($statea === $state && $stateb !== $state) {
 357                  return -1;  // A comes before B.
 358              }
 359  
 360              if ($stateb === $state && $statea !== $state) {
 361                  return 1;  // B comes before A.
 362              }
 363  
 364              return 0;  // They are the same, keep current order.
 365          });
 366  
 367          return $factors;
 368      }
 369  }