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; 18 19 use dml_exception; 20 use tool_mfa\plugininfo\factor; 21 22 /** 23 * MFA management class. 24 * 25 * @package tool_mfa 26 * @author Peter Burnett <peterburnett@catalyst-au.net> 27 * @copyright Catalyst IT 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 class manager { 31 32 /** @var int */ 33 const REDIRECT = 1; 34 35 /** @var int */ 36 const NO_REDIRECT = 0; 37 38 /** @var int */ 39 const REDIRECT_EXCEPTION = -1; 40 41 /** @var int */ 42 const REDIR_LOOP_THRESHOLD = 5; 43 44 /** 45 * Displays a debug table with current factor information. 46 * 47 * @return void 48 */ 49 public static function display_debug_notification(): void { 50 global $OUTPUT, $PAGE; 51 52 if (!get_config('tool_mfa', 'debugmode')) { 53 return; 54 } 55 $html = $OUTPUT->heading(get_string('debugmode:heading', 'tool_mfa'), 3); 56 57 $table = new \html_table(); 58 $table->head = [ 59 get_string('weight', 'tool_mfa'), 60 get_string('factor', 'tool_mfa'), 61 get_string('setup', 'tool_mfa'), 62 get_string('achievedweight', 'tool_mfa'), 63 get_string('status'), 64 ]; 65 $table->attributes['class'] = 'admintable generaltable table table-bordered'; 66 $table->colclasses = [ 67 'text-right', 68 '', 69 '', 70 'text-right', 71 'text-center', 72 ]; 73 $factors = factor::get_enabled_factors(); 74 $userfactors = factor::get_active_user_factor_types(); 75 $runningtotal = 0; 76 $weighttoggle = false; 77 78 foreach ($factors as $factor) { 79 $namespace = 'factor_'.$factor->name; 80 $name = get_string('pluginname', $namespace); 81 82 // If factor is unknown, pending from here. 83 if ($factor->get_state() == factor::STATE_UNKNOWN) { 84 $weighttoggle = true; 85 } 86 87 // Stop adding weight if 100 achieved. 88 if (!$weighttoggle) { 89 $achieved = $factor->get_state() == factor::STATE_PASS ? $factor->get_weight() : 0; 90 $achieved = '+'.$achieved; 91 $runningtotal += $achieved; 92 } else { 93 $achieved = ''; 94 } 95 96 // Setup. 97 if ($factor->has_setup()) { 98 $found = false; 99 foreach ($userfactors as $userfactor) { 100 if ($userfactor->name == $factor->name) { 101 $found = true; 102 } 103 } 104 $setup = $found ? get_string('yes') : get_string('no'); 105 } else { 106 $setup = get_string('na', 'tool_mfa'); 107 } 108 109 // Status. 110 $OUTPUT = $PAGE->get_renderer('tool_mfa'); 111 // If toggle has been flipped, fall to default pending badge. 112 if ($weighttoggle) { 113 $state = $OUTPUT->get_state_badge(''); 114 } else { 115 $state = $OUTPUT->get_state_badge($factor->get_state()); 116 } 117 118 $table->data[] = [ 119 $factor->get_weight(), 120 $name, 121 $setup, 122 $achieved, 123 $state, 124 ]; 125 126 // If we just hit 100, flip toggle. 127 if ($runningtotal >= 100) { 128 $weighttoggle = true; 129 } 130 } 131 132 $finalstate = self::get_status(); 133 $table->data[] = [ 134 '', 135 '', 136 '<b>' . get_string('overall', 'tool_mfa') . '</b>', 137 self::get_cumulative_weight(), 138 $OUTPUT->get_state_badge($finalstate), 139 ]; 140 141 $html .= \html_writer::table($table); 142 echo $html; 143 } 144 145 /** 146 * Returns the total weight from all factors currently enabled for user. 147 * 148 * @return int 149 */ 150 public static function get_total_weight(): int { 151 $totalweight = 0; 152 $factors = factor::get_active_user_factor_types(); 153 154 foreach ($factors as $factor) { 155 if ($factor->get_state() == factor::STATE_PASS) { 156 $totalweight += $factor->get_weight(); 157 } 158 } 159 return $totalweight; 160 } 161 162 /** 163 * Checks that provided factorid exists and belongs to current user. 164 * 165 * @param int $factorid 166 * @param object $user 167 * @return bool 168 * @throws \dml_exception 169 */ 170 public static function is_factorid_valid(int $factorid, object $user): bool { 171 global $DB; 172 return $DB->record_exists('tool_mfa', ['userid' => $user->id, 'id' => $factorid]); 173 } 174 175 /** 176 * Function to display to the user that they cannot login, then log them out. 177 * 178 * @return void 179 */ 180 public static function cannot_login(): void { 181 global $ME, $PAGE, $SESSION, $USER; 182 183 // Determine page URL without triggering warnings from $PAGE. 184 if (!preg_match("~(\/admin\/tool\/mfa\/auth.php)~", $ME)) { 185 // If URL isn't set, we need to redir to auth.php. 186 // This ensures URL and required info is correctly set. 187 // Then we arrive back here. 188 redirect(new \moodle_url('/admin/tool/mfa/auth.php')); 189 } 190 191 $renderer = $PAGE->get_renderer('tool_mfa'); 192 193 echo $renderer->header(); 194 if (get_config('tool_mfa', 'debugmode')) { 195 self::display_debug_notification(); 196 } 197 echo $renderer->not_enough_factors(); 198 echo $renderer->footer(); 199 // Emit an event for failure, then logout. 200 $event = \tool_mfa\event\user_failed_mfa::user_failed_mfa_event($USER); 201 $event->trigger(); 202 203 // We should set the redir flag, as this page is generated through auth.php. 204 $SESSION->tool_mfa_has_been_redirected = true; 205 die; 206 } 207 208 /** 209 * Logout user. 210 * 211 * @return void 212 */ 213 public static function mfa_logout(): void { 214 $authsequence = get_enabled_auth_plugins(); 215 foreach ($authsequence as $authname) { 216 $authplugin = get_auth_plugin($authname); 217 $authplugin->logoutpage_hook(); 218 } 219 require_logout(); 220 } 221 222 /** 223 * Function to get the overall status of a user's authentication. 224 * 225 * @return string a STATE variable from plugininfo 226 */ 227 public static function get_status(): string { 228 global $SESSION; 229 230 // Check for any instant fail states. 231 $factors = factor::get_active_user_factor_types(); 232 foreach ($factors as $factor) { 233 $factor->load_locked_state(); 234 235 if ($factor->get_state() == factor::STATE_FAIL) { 236 return factor::STATE_FAIL; 237 } 238 } 239 240 $passcondition = ((isset($SESSION->tool_mfa_authenticated) && $SESSION->tool_mfa_authenticated) || 241 self::passed_enough_factors()); 242 243 // Check next factor for instant fail (fallback). 244 if (factor::get_next_user_login_factor()->get_state() == factor::STATE_FAIL) { 245 // We need to handle a special case here, where someone reached the fallback, 246 // If they were able to modify their state on the error page, such as passing iprange, 247 // We must return pass. 248 if ($passcondition) { 249 return factor::STATE_PASS; 250 } 251 252 return factor::STATE_FAIL; 253 } 254 255 // Now check for general passing state. If found, ensure that session var is set. 256 if ($passcondition) { 257 return factor::STATE_PASS; 258 } 259 260 // Else return neutral state. 261 return factor::STATE_NEUTRAL; 262 } 263 264 /** 265 * Function to check the overall status of a users authentication, 266 * and perform any required actions. 267 * 268 * @param bool $shouldreload whether the function should reload (used for auth.php). 269 * @return void 270 */ 271 public static function resolve_mfa_status(bool $shouldreload = false): void { 272 global $SESSION; 273 274 $state = self::get_status(); 275 if ($state == factor::STATE_PASS) { 276 self::set_pass_state(); 277 // Check if user even had to reach auth page. 278 if (isset($SESSION->tool_mfa_has_been_redirected)) { 279 if (empty($SESSION->wantsurl)) { 280 $wantsurl = '/'; 281 } else { 282 $wantsurl = $SESSION->wantsurl; 283 } 284 unset($SESSION->wantsurl); 285 redirect(new \moodle_url($wantsurl)); 286 } else { 287 // Don't touch anything, let user be on their way. 288 return; 289 } 290 } else if ($state == factor::STATE_FAIL) { 291 self::cannot_login(); 292 } else if ($shouldreload) { 293 // Set a session variable to track whether user is where they want to be. 294 $SESSION->tool_mfa_has_been_redirected = true; 295 $authurl = new \moodle_url('/admin/tool/mfa/auth.php'); 296 redirect($authurl); 297 } 298 } 299 300 /** 301 * Checks whether user has passed enough factors to be allowed in. 302 * 303 * @return bool true if user has passed enough factors. 304 */ 305 public static function passed_enough_factors(): bool { 306 307 // Check for any instant fail states. 308 $factors = factor::get_active_user_factor_types(); 309 foreach ($factors as $factor) { 310 if ($factor->get_state() == factor::STATE_FAIL) { 311 self::mfa_logout(); 312 } 313 } 314 315 $totalweight = self::get_cumulative_weight(); 316 if ($totalweight >= 100) { 317 return true; 318 } 319 320 return false; 321 } 322 323 /** 324 * Sets the session variable for pass_state, if not already set. 325 * 326 * @return void 327 */ 328 public static function set_pass_state(): void { 329 global $DB, $SESSION, $USER; 330 if (!isset($SESSION->tool_mfa_authenticated)) { 331 $SESSION->tool_mfa_authenticated = true; 332 $event = \tool_mfa\event\user_passed_mfa::user_passed_mfa_event($USER); 333 $event->trigger(); 334 335 // Add/update record in DB for users last mfa auth. 336 self::update_pass_time(); 337 338 // Unset session vars during mfa auth. 339 unset($SESSION->mfa_redir_referer); 340 unset($SESSION->mfa_redir_count); 341 342 // Unset user preferences during mfa auth. 343 unset_user_preference('mfa_sleep_duration', $USER); 344 345 try { 346 // Clear locked user factors, they may now reauth with anything. 347 @$DB->set_field('tool_mfa', 'lockcounter', 0, ['userid' => $USER->id]); 348 // @codingStandardsIgnoreStart 349 } catch (\Exception $e) { 350 // This occurs when upgrade.php hasn't been run. Nothing to do here. 351 // Coding standards ignored, they break on empty catches. 352 } 353 // @codingStandardsIgnoreEnd 354 355 // Fire post pass state factor actions. 356 $factors = factor::get_active_user_factor_types(); 357 foreach ($factors as $factor) { 358 $factor->post_pass_state(); 359 // Also set the states for this session to neutral if they were locked. 360 if ($factor->get_state() == factor::STATE_LOCKED) { 361 $factor->set_state(factor::STATE_NEUTRAL); 362 } 363 } 364 365 // Output notifications if any factors were reset for this user. 366 $enabledfactors = factor::get_enabled_factors(); 367 foreach ($enabledfactors as $factor) { 368 $pref = 'tool_mfa_reset_' . $factor->name; 369 $factorpref = get_user_preferences($pref, false); 370 if ($factorpref) { 371 $url = new \moodle_url('/admin/tool/mfa/user_preferences.php'); 372 $link = \html_writer::link($url, get_string('preferenceslink', 'tool_mfa')); 373 $data = ['factor' => $factor->get_display_name(), 'url' => $link]; 374 \core\notification::warning(get_string('factorreset', 'tool_mfa', $data)); 375 unset_user_preference($pref); 376 } 377 } 378 379 // Also check for a global reset. 380 // TODO: Delete this in a few months, the reset all preference is no longer set. 381 $allfactor = get_user_preferences('tool_mfa_reset_all', false); 382 if ($allfactor) { 383 $url = new \moodle_url('/admin/tool/mfa/user_preferences.php'); 384 $link = \html_writer::link($url, get_string('preferenceslink', 'tool_mfa')); 385 \core\notification::warning(get_string('factorresetall', 'tool_mfa', $link)); 386 unset_user_preference('tool_mfa_reset_all'); 387 } 388 } 389 } 390 391 /** 392 * Inserts or updates user's last MFA pass time in DB. 393 * This should only be called from set_pass_state. 394 * 395 * @return void 396 */ 397 private static function update_pass_time(): void { 398 global $DB, $USER; 399 400 $exists = $DB->record_exists('tool_mfa_auth', ['userid' => $USER->id]); 401 402 if ($exists) { 403 $DB->set_field('tool_mfa_auth', 'lastverified', time(), ['userid' => $USER->id]); 404 } else { 405 $DB->insert_record('tool_mfa_auth', ['userid' => $USER->id, 'lastverified' => time()]); 406 } 407 } 408 409 /** 410 * Checks whether the user should be redirected from the provided url. 411 * 412 * @param string|\moodle_url $url 413 * @param bool|null $preventredirect 414 * @return int 415 */ 416 public static function should_require_mfa(string|\moodle_url $url, bool|null $preventredirect): int { 417 global $CFG, $USER, $SESSION; 418 419 // If no cookies then no session so cannot do MFA. 420 // Unit testing based on defines is not viable. 421 if (NO_MOODLE_COOKIES && !PHPUNIT_TEST) { 422 return self::NO_REDIRECT; 423 } 424 425 // Remove all params before comparison. 426 $url->remove_all_params(); 427 428 // Checks for upgrades pending. 429 if (is_siteadmin()) { 430 // We should only allow an upgrade from the frontend to complete. 431 // After that is completed, only the settings shouldn't redirect. 432 // Everything else should be safe to enforce MFA. 433 if (moodle_needs_upgrading()) { 434 return self::NO_REDIRECT; 435 } 436 // An upgrade isn't complete if there are settings that must be saved. 437 $upgradesettings = new \moodle_url('/admin/upgradesettings.php'); 438 if ($url->compare($upgradesettings, URL_MATCH_BASE)) { 439 return self::NO_REDIRECT; 440 } 441 } 442 443 // Dont redirect logo images from pluginfile.php (for example: logo in header). 444 $logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/'); 445 if ($url->compare($logourl)) { 446 return self::NO_REDIRECT; 447 } 448 449 // Admin not setup. 450 if (!empty($CFG->adminsetuppending)) { 451 return self::NO_REDIRECT; 452 } 453 454 // Initial installation. 455 // We get this for free from get_plugins_with_function. 456 457 // Upgrade check. 458 // We get this for free from get_plugins_with_function. 459 460 // Honor prevent_redirect. 461 if ($preventredirect) { 462 return self::NO_REDIRECT; 463 } 464 465 // User not properly setup. 466 if (user_not_fully_set_up($USER)) { 467 return self::NO_REDIRECT; 468 } 469 470 // Enrolment. 471 $enrol = new \moodle_url('/enrol/index.php'); 472 if ($enrol->compare($url, URL_MATCH_BASE)) { 473 return self::NO_REDIRECT; 474 } 475 476 // Guest access. 477 if (isguestuser()) { 478 return self::NO_REDIRECT; 479 } 480 481 // Forced password changes. 482 if (get_user_preferences('auth_forcepasswordchange')) { 483 return self::NO_REDIRECT; 484 } 485 486 // Login as. 487 if (\core\session\manager::is_loggedinas()) { 488 return self::NO_REDIRECT; 489 } 490 491 // Site policy. 492 if (isset($USER->policyagreed) && !$USER->policyagreed) { 493 $manager = new \core_privacy\local\sitepolicy\manager(); 494 $policyurl = $manager->get_redirect_url(false); 495 if (!empty($policyurl) && $url->compare($policyurl, URL_MATCH_BASE)) { 496 return self::NO_REDIRECT; 497 } 498 } 499 500 // WS/AJAX check. 501 if (WS_SERVER || AJAX_SCRIPT) { 502 if (isset($SESSION->mfa_pending) && !empty($SESSION->mfa_pending)) { 503 // Allow AJAX and WS, but never from auth.php. 504 return self::NO_REDIRECT; 505 } 506 return self::REDIRECT_EXCEPTION; 507 } 508 509 // Check factor defined safe urls. 510 $factorurls = self::get_no_redirect_urls(); 511 foreach ($factorurls as $factorurl) { 512 if ($factorurl->compare($url)) { 513 return self::NO_REDIRECT; 514 } 515 } 516 517 // Circular checks. 518 $authurl = new \moodle_url('/admin/tool/mfa/auth.php'); 519 $authlocal = $authurl->out_as_local_url(); 520 if (isset($SESSION->mfa_redir_referer) 521 && $SESSION->mfa_redir_referer != $authlocal) { 522 if ($SESSION->mfa_redir_referer == get_local_referer(true)) { 523 // Possible redirect loop. 524 if (!isset($SESSION->mfa_redir_count)) { 525 $SESSION->mfa_redir_count = 1; 526 } else { 527 $SESSION->mfa_redir_count++; 528 } 529 if ($SESSION->mfa_redir_count > self::REDIR_LOOP_THRESHOLD) { 530 return self::REDIRECT_EXCEPTION; 531 } 532 } else { 533 // If not a match, reset counter. 534 $SESSION->mfa_redir_count = 0; 535 } 536 } 537 // Set referer after checks. 538 $SESSION->mfa_redir_referer = get_local_referer(true); 539 540 // Don't redirect if already on auth.php. 541 if ($url->compare($authurl, URL_MATCH_BASE)) { 542 return self::NO_REDIRECT; 543 } 544 545 return self::REDIRECT; 546 } 547 548 /** 549 * Clears the redirect counter for infinite redirect loops. Called from auth.php when a valid load is resolved. 550 * 551 * @return void 552 */ 553 public static function clear_redirect_counter(): void { 554 global $SESSION; 555 556 unset($SESSION->mfa_redir_referer); 557 unset($SESSION->mfa_redir_count); 558 } 559 560 /** 561 * Gets all defined factor urls that should not redirect. 562 * 563 * @return array 564 */ 565 public static function get_no_redirect_urls(): array { 566 $factors = factor::get_factors(); 567 $urls = [ 568 new \moodle_url('/login/logout.php'), 569 new \moodle_url('/admin/tool/mfa/guide.php'), 570 ]; 571 foreach ($factors as $factor) { 572 $urls = array_merge($urls, $factor->get_no_redirect_urls()); 573 } 574 575 // Allow forced redirection exclusions. 576 if ($exclusions = get_config('tool_mfa', 'redir_exclusions')) { 577 foreach (explode("\n", $exclusions) as $exclusion) { 578 $urls[] = new \moodle_url($exclusion); 579 } 580 } 581 582 return $urls; 583 } 584 585 /** 586 * Sleeps for an increasing period of time. 587 * 588 * @return void 589 */ 590 public static function sleep_timer(): void { 591 global $USER; 592 593 $duration = get_user_preferences('mfa_sleep_duration', null, $USER); 594 if (!empty($duration)) { 595 // Double current time. 596 $duration *= 2; 597 $duration = min(2, $duration); 598 } else { 599 // No duration set. 600 $duration = 0.05; 601 } 602 set_user_preference('mfa_sleep_duration', $duration, $USER); 603 sleep((int)$duration); 604 } 605 606 /** 607 * If MFA Plugin is ready check tool_mfa_authenticated USER property and 608 * start MFA authentication if it's not set or false. 609 * 610 * @param mixed $courseorid 611 * @param mixed $autologinguest 612 * @param mixed $cm 613 * @param mixed $setwantsurltome 614 * @param mixed $preventredirect 615 * @return void 616 */ 617 public static function require_auth($courseorid = null, $autologinguest = null, $cm = null, 618 $setwantsurltome = null, $preventredirect = null): void { 619 global $PAGE, $SESSION, $FULLME; 620 621 // Guest user should never interact with MFA, 622 // And $SESSION->tool_mfa_authenticated should never be set in a guest session. 623 if (isguestuser()) { 624 return; 625 } 626 627 if (!self::is_ready()) { 628 // Set session var so if MFA becomes ready, you dont get locked from session. 629 $SESSION->tool_mfa_authenticated = true; 630 return; 631 } 632 633 if (empty($SESSION->tool_mfa_authenticated) || !$SESSION->tool_mfa_authenticated) { 634 if ($PAGE->has_set_url()) { 635 $cleanurl = $PAGE->url; 636 } else { 637 // Use $FULLME instead. 638 $cleanurl = new \moodle_url($FULLME); 639 } 640 $authurl = new \moodle_url('/admin/tool/mfa/auth.php'); 641 642 $redir = self::should_require_mfa($cleanurl, $preventredirect); 643 644 if ($redir == self::NO_REDIRECT && !$cleanurl->compare($authurl, URL_MATCH_BASE)) { 645 // A non-MFA page that should take precedence. 646 // This check is for any pages, such as site policy, that must occur before MFA. 647 // This check allows AJAX and WS requests to fire on these pages without throwing an exception. 648 $SESSION->mfa_pending = true; 649 } 650 651 if ($redir == self::REDIRECT) { 652 if (empty($SESSION->wantsurl)) { 653 !empty($setwantsurltome) 654 ? $SESSION->wantsurl = qualified_me() 655 : $SESSION->wantsurl = new \moodle_url('/'); 656 657 $SESSION->tool_mfa_setwantsurl = true; 658 } 659 // Remove pending status. 660 // We must now auth with MFA, now that pending statuses are resolved. 661 unset($SESSION->mfa_pending); 662 663 // Call resolve_status to instantly pass if no redirect is required. 664 self::resolve_mfa_status(true); 665 } else if ($redir == self::REDIRECT_EXCEPTION) { 666 if (!empty($SESSION->mfa_redir_referer)) { 667 throw new \moodle_exception('redirecterrordetected', 'tool_mfa', 668 $SESSION->mfa_redir_referer, $SESSION->mfa_redir_referer); 669 } else { 670 throw new \moodle_exception('redirecterrordetected', 'error'); 671 } 672 } 673 } 674 } 675 676 /** 677 * Sets config variable for given factor. 678 * 679 * @param array $data 680 * @param string $factor 681 * 682 * @return bool true or exception 683 * @throws dml_exception 684 */ 685 public static function set_factor_config(array $data, string $factor): bool|dml_exception { 686 $factorconf = get_config($factor); 687 foreach ($data as $key => $newvalue) { 688 if (empty($factorconf->$key)) { 689 add_to_config_log($key, null, $newvalue, $factor); 690 set_config($key, $newvalue, $factor); 691 } else if ($factorconf->$key != $newvalue) { 692 add_to_config_log($key, $factorconf->$key, $newvalue, $factor); 693 set_config($key, $newvalue, $factor); 694 } 695 } 696 return true; 697 } 698 699 /** 700 * Checks if MFA Plugin is enabled and has enabled factor. 701 * If plugin is disabled or there is no enabled factors, 702 * it means there is nothing to do from user side. 703 * Thus, login flow shouldn't be extended with MFA. 704 * 705 * @return bool 706 * @throws \dml_exception 707 */ 708 public static function is_ready(): bool { 709 global $CFG, $USER; 710 711 if (!empty($CFG->upgraderunning)) { 712 return false; 713 } 714 715 $pluginenabled = get_config('tool_mfa', 'enabled'); 716 if (empty($pluginenabled)) { 717 return false; 718 } 719 720 // Check if user can interact with MFA. 721 $usercontext = \context_user::instance($USER->id); 722 if (!has_capability('tool/mfa:mfaaccess', $usercontext)) { 723 return false; 724 } 725 726 $enabledfactors = factor::get_enabled_factors(); 727 if (count($enabledfactors) == 0) { 728 return false; 729 } 730 731 return true; 732 } 733 734 /** 735 * Performs factor actions for given factor. 736 * Change factor order and enable/disable. 737 * 738 * @param string $factorname 739 * @param string $action 740 * 741 * @return void 742 * @throws dml_exception 743 */ 744 public static function do_factor_action(string $factorname, string $action): void { 745 $order = explode(',', get_config('tool_mfa', 'factor_order')); 746 $key = array_search($factorname, $order); 747 748 switch ($action) { 749 case 'up': 750 if ($key >= 1) { 751 $fsave = $order[$key]; 752 $order[$key] = $order[$key - 1]; 753 $order[$key - 1] = $fsave; 754 } 755 break; 756 757 case 'down': 758 if ($key < (count($order) - 1)) { 759 $fsave = $order[$key]; 760 $order[$key] = $order[$key + 1]; 761 $order[$key + 1] = $fsave; 762 } 763 break; 764 765 case 'enable': 766 if (!$key) { 767 $order[] = $factorname; 768 } 769 break; 770 771 case 'disable': 772 if ($key) { 773 unset($order[$key]); 774 } 775 break; 776 777 default: 778 break; 779 } 780 self::set_factor_config(['factor_order' => implode(',', $order)], 'tool_mfa'); 781 } 782 783 /** 784 * Checks if a factor that can make a user pass can be setup. 785 * It checks if a user will always pass regardless, 786 * then checks if there are factors that can be setup to let a user pass. 787 * 788 * @return bool 789 */ 790 public static function possible_factor_setup(): bool { 791 global $USER; 792 793 // Get all active factors. 794 $factors = factor::get_enabled_factors(); 795 796 // Check if there are enough factors that a user can ONLY pass, if so, don't display the menu. 797 $weight = 0; 798 foreach ($factors as $factor) { 799 $states = $factor->possible_states($USER); 800 if (count($states) == 1 && reset($states) == factor::STATE_PASS) { 801 $weight += $factor->get_weight(); 802 if ($weight >= 100) { 803 return false; 804 } 805 } 806 } 807 808 // Now if there is a factor that can be setup, that may return a pass state for the user, display menu. 809 foreach ($factors as $factor) { 810 if ($factor->has_setup()) { 811 if (in_array(factor::STATE_PASS, $factor->possible_states($USER))) { 812 return true; 813 } 814 } 815 } 816 817 return false; 818 } 819 820 /** 821 * Gets current user weight, up until first unknown factor. 822 * 823 * @return int $totalweight Total weight of all factors. 824 */ 825 public static function get_cumulative_weight(): int { 826 $factors = factor::get_active_user_factor_types(); 827 // Factor order is important here, so sort the factors by state. 828 $sortedfactors = factor::sort_factors_by_state($factors, factor::STATE_PASS); 829 $totalweight = 0; 830 foreach ($sortedfactors as $factor) { 831 if ($factor->get_state() == factor::STATE_PASS) { 832 $totalweight += $factor->get_weight(); 833 // If over 100, break. Don't care about >100. 834 if ($totalweight >= 100) { 835 break; 836 } 837 } else if ($factor->get_state() == factor::STATE_UNKNOWN) { 838 break; 839 } 840 } 841 return $totalweight; 842 } 843 844 /** 845 * Checks whether the factor was actually used in the login process. 846 * 847 * @param string $factorname the name of the factor. 848 * @return bool true if factor is pending. 849 */ 850 public static function check_factor_pending(string $factorname): bool { 851 $factors = factor::get_active_user_factor_types(); 852 // Setup vars. 853 $pending = []; 854 $totalweight = 0; 855 $weighttoggle = false; 856 857 foreach ($factors as $factor) { 858 // If toggle is reached, put in pending and continue. 859 if ($weighttoggle) { 860 $pending[] = $factor->name; 861 continue; 862 } 863 864 if ($factor->get_state() == factor::STATE_PASS) { 865 $totalweight += $factor->get_weight(); 866 if ($totalweight >= 100) { 867 $weighttoggle = true; 868 } 869 } 870 } 871 872 // Check whether factor falls into pending category. 873 return in_array($factorname, $pending); 874 } 875 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body