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.
/lib/ -> authlib.php (source)

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Multiple plugin authentication Support library
  20   *
  21   * 2006-08-28  File created, AUTH return values defined.
  22   *
  23   * @package    core
  24   * @subpackage auth
  25   * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * Returned when the login was successful.
  33   */
  34  define('AUTH_OK',     0);
  35  
  36  /**
  37   * Returned when the login was unsuccessful.
  38   */
  39  define('AUTH_FAIL',   1);
  40  
  41  /**
  42   * Returned when the login was denied (a reason for AUTH_FAIL).
  43   */
  44  define('AUTH_DENIED', 2);
  45  
  46  /**
  47   * Returned when some error occurred (a reason for AUTH_FAIL).
  48   */
  49  define('AUTH_ERROR',  4);
  50  
  51  /**
  52   * Authentication - error codes for user confirm
  53   */
  54  define('AUTH_CONFIRM_FAIL', 0);
  55  define('AUTH_CONFIRM_OK', 1);
  56  define('AUTH_CONFIRM_ALREADY', 2);
  57  define('AUTH_CONFIRM_ERROR', 3);
  58  
  59  # MDL-14055
  60  define('AUTH_REMOVEUSER_KEEP', 0);
  61  define('AUTH_REMOVEUSER_SUSPEND', 1);
  62  define('AUTH_REMOVEUSER_FULLDELETE', 2);
  63  
  64  /** Login attempt successful. */
  65  define('AUTH_LOGIN_OK', 0);
  66  
  67  /** Can not login because user does not exist. */
  68  define('AUTH_LOGIN_NOUSER', 1);
  69  
  70  /** Can not login because user is suspended. */
  71  define('AUTH_LOGIN_SUSPENDED', 2);
  72  
  73  /** Can not login, most probably password did not match. */
  74  define('AUTH_LOGIN_FAILED', 3);
  75  
  76  /** Can not login because user is locked out. */
  77  define('AUTH_LOGIN_LOCKOUT', 4);
  78  
  79  /** Can not login becauser user is not authorised. */
  80  define('AUTH_LOGIN_UNAUTHORISED', 5);
  81  
  82  /** Can not login, failed reCaptcha challenge. */
  83  define('AUTH_LOGIN_FAILED_RECAPTCHA', 6);
  84  
  85  /**
  86   * Abstract authentication plugin.
  87   *
  88   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  89   * @package moodlecore
  90   */
  91  class auth_plugin_base {
  92  
  93      /**
  94       * The configuration details for the plugin.
  95       * @var object
  96       */
  97      var $config;
  98  
  99      /**
 100       * Authentication plugin type - the same as db field.
 101       * @var string
 102       */
 103      var $authtype;
 104      /*
 105       * The fields we can lock and update from/to external authentication backends
 106       * @var array
 107       */
 108      var $userfields = \core_user::AUTHSYNCFIELDS;
 109  
 110      /**
 111       * Moodle custom fields to sync with.
 112       * @var array()
 113       */
 114      var $customfields = null;
 115  
 116      /**
 117       * The tag we want to prepend to any error log messages.
 118       *
 119       * @var string
 120       */
 121      protected $errorlogtag = '';
 122  
 123      /** @var array Stores extra information available to the logged in event. */
 124      protected $extrauserinfo = [];
 125  
 126      /**
 127       * This is the primary method that is used by the authenticate_user_login()
 128       * function in moodlelib.php.
 129       *
 130       * This method should return a boolean indicating
 131       * whether or not the username and password authenticate successfully.
 132       *
 133       * Returns true if the username and password work and false if they are
 134       * wrong or don't exist.
 135       *
 136       * @param string $username The username (with system magic quotes)
 137       * @param string $password The password (with system magic quotes)
 138       *
 139       * @return bool Authentication success or failure.
 140       */
 141      function user_login($username, $password) {
 142          throw new \moodle_exception('mustbeoveride', 'debug', '', 'user_login()' );
 143      }
 144  
 145      /**
 146       * Returns true if this authentication plugin can change the users'
 147       * password.
 148       *
 149       * @return bool
 150       */
 151      function can_change_password() {
 152          //override if needed
 153          return false;
 154      }
 155  
 156      /**
 157       * Returns the URL for changing the users' passwords, or empty if the default
 158       * URL can be used.
 159       *
 160       * This method is used if can_change_password() returns true.
 161       * This method is called only when user is logged in, it may use global $USER.
 162       * If you are using a plugin config variable in this method, please make sure it is set before using it,
 163       * as this method can be called even if the plugin is disabled, in which case the config values won't be set.
 164       *
 165       * @return moodle_url url of the profile page or null if standard used
 166       */
 167      function change_password_url() {
 168          //override if needed
 169          return null;
 170      }
 171  
 172      /**
 173       * Returns true if this authentication plugin can edit the users'
 174       * profile.
 175       *
 176       * @return bool
 177       */
 178      function can_edit_profile() {
 179          //override if needed
 180          return true;
 181      }
 182  
 183      /**
 184       * Returns the URL for editing the users' profile, or empty if the default
 185       * URL can be used.
 186       *
 187       * This method is used if can_edit_profile() returns true.
 188       * This method is called only when user is logged in, it may use global $USER.
 189       *
 190       * @return moodle_url url of the profile page or null if standard used
 191       */
 192      function edit_profile_url() {
 193          //override if needed
 194          return null;
 195      }
 196  
 197      /**
 198       * Returns true if this authentication plugin is "internal".
 199       *
 200       * Internal plugins use password hashes from Moodle user table for authentication.
 201       *
 202       * @return bool
 203       */
 204      function is_internal() {
 205          //override if needed
 206          return true;
 207      }
 208  
 209      /**
 210       * Returns false if this plugin is enabled but not configured.
 211       *
 212       * @return bool
 213       */
 214      public function is_configured() {
 215          return false;
 216      }
 217  
 218      /**
 219       * Indicates if password hashes should be stored in local moodle database.
 220       * @return bool true means md5 password hash stored in user table, false means flag 'not_cached' stored there instead
 221       */
 222      function prevent_local_passwords() {
 223          return !$this->is_internal();
 224      }
 225  
 226      /**
 227       * Indicates if moodle should automatically update internal user
 228       * records with data from external sources using the information
 229       * from get_userinfo() method.
 230       *
 231       * @return bool true means automatically copy data from ext to user table
 232       */
 233      function is_synchronised_with_external() {
 234          return !$this->is_internal();
 235      }
 236  
 237      /**
 238       * Updates the user's password.
 239       *
 240       * In previous versions of Moodle, the function
 241       * auth_user_update_password accepted a username as the first parameter. The
 242       * revised function expects a user object.
 243       *
 244       * @param  object  $user        User table object
 245       * @param  string  $newpassword Plaintext password
 246       *
 247       * @return bool                  True on success
 248       */
 249      function user_update_password($user, $newpassword) {
 250          //override if needed
 251          return true;
 252      }
 253  
 254      /**
 255       * Called when the user record is updated.
 256       * Modifies user in external database. It takes olduser (before changes) and newuser (after changes)
 257       * compares information saved modified information to external db.
 258       *
 259       * @param mixed $olduser     Userobject before modifications    (without system magic quotes)
 260       * @param mixed $newuser     Userobject new modified userobject (without system magic quotes)
 261       * @return boolean true if updated or update ignored; false if error
 262       *
 263       */
 264      function user_update($olduser, $newuser) {
 265          //override if needed
 266          return true;
 267      }
 268  
 269      /**
 270       * User delete requested - internal user record is mared as deleted already, username not present anymore.
 271       *
 272       * Do any action in external database.
 273       *
 274       * @param object $user       Userobject before delete    (without system magic quotes)
 275       * @return void
 276       */
 277      function user_delete($olduser) {
 278          //override if needed
 279          return;
 280      }
 281  
 282      /**
 283       * Returns true if plugin allows resetting of internal password.
 284       *
 285       * @return bool
 286       */
 287      function can_reset_password() {
 288          //override if needed
 289          return false;
 290      }
 291  
 292      /**
 293       * Returns true if plugin allows resetting of internal password.
 294       *
 295       * @return bool
 296       */
 297      function can_signup() {
 298          //override if needed
 299          return false;
 300      }
 301  
 302      /**
 303       * Sign up a new user ready for confirmation.
 304       * Password is passed in plaintext.
 305       *
 306       * @param object $user new user object
 307       * @param boolean $notify print notice with link and terminate
 308       */
 309      function user_signup($user, $notify=true) {
 310          //override when can signup
 311          throw new \moodle_exception('mustbeoveride', 'debug', '', 'user_signup()' );
 312      }
 313  
 314      /**
 315       * Return a form to capture user details for account creation.
 316       * This is used in /login/signup.php.
 317       * @return moodle_form A form which edits a record from the user table.
 318       */
 319      function signup_form() {
 320          global $CFG;
 321  
 322          require_once($CFG->dirroot.'/login/signup_form.php');
 323          return new login_signup_form(null, null, 'post', '', array('autocomplete'=>'on'));
 324      }
 325  
 326      /**
 327       * Returns true if plugin allows confirming of new users.
 328       *
 329       * @return bool
 330       */
 331      function can_confirm() {
 332          //override if needed
 333          return false;
 334      }
 335  
 336      /**
 337       * Confirm the new user as registered.
 338       *
 339       * @param string $username
 340       * @param string $confirmsecret
 341       */
 342      function user_confirm($username, $confirmsecret) {
 343          //override when can confirm
 344          throw new \moodle_exception('mustbeoveride', 'debug', '', 'user_confirm()' );
 345      }
 346  
 347      /**
 348       * Checks if user exists in external db
 349       *
 350       * @param string $username (with system magic quotes)
 351       * @return bool
 352       */
 353      function user_exists($username) {
 354          //override if needed
 355          return false;
 356      }
 357  
 358      /**
 359       * return number of days to user password expires
 360       *
 361       * If userpassword does not expire it should return 0. If password is already expired
 362       * it should return negative value.
 363       *
 364       * @param mixed $username username (with system magic quotes)
 365       * @return integer
 366       */
 367      function password_expire($username) {
 368          return 0;
 369      }
 370      /**
 371       * Sync roles for this user - usually creator
 372       *
 373       * @param $user object user object (without system magic quotes)
 374       */
 375      function sync_roles($user) {
 376          //override if needed
 377      }
 378  
 379      /**
 380       * Read user information from external database and returns it as array().
 381       * Function should return all information available. If you are saving
 382       * this information to moodle user-table you should honour synchronisation flags
 383       *
 384       * @param string $username username
 385       *
 386       * @return mixed array with no magic quotes or false on error
 387       */
 388      function get_userinfo($username) {
 389          //override if needed
 390          return array();
 391      }
 392  
 393      /**
 394       * Prints a form for configuring this authentication plugin.
 395       *
 396       * This function is called from admin/auth.php, and outputs a full page with
 397       * a form for configuring this plugin.
 398       *
 399       * @param object $config
 400       * @param object $err
 401       * @param array $user_fields
 402       * @deprecated since Moodle 3.3
 403       */
 404      function config_form($config, $err, $user_fields) {
 405          debugging('Use of config.html files have been deprecated, please update your code to use the admin settings API.');
 406          //override if needed
 407      }
 408  
 409      /**
 410       * A chance to validate form data, and last chance to
 411       * do stuff before it is inserted in config_plugin
 412       * @param object object with submitted configuration settings (without system magic quotes)
 413       * @param array $err array of error messages
 414       * @deprecated since Moodle 3.3
 415       */
 416       function validate_form($form, &$err) {
 417          debugging('Use of config.html files have been deprecated, please update your code to use the admin settings API.');
 418          //override if needed
 419      }
 420  
 421      /**
 422       * Processes and stores configuration data for this authentication plugin.
 423       *
 424       * @param object object with submitted configuration settings (without system magic quotes)
 425       * @deprecated since Moodle 3.3
 426       */
 427      function process_config($config) {
 428          debugging('Use of config.html files have been deprecated, please update your code to use the admin settings API.');
 429          //override if needed
 430          return true;
 431      }
 432  
 433      /**
 434       * Hook for overriding behaviour of login page.
 435       * This method is called from login/index.php page for all enabled auth plugins.
 436       *
 437       * @global object
 438       * @global object
 439       */
 440      function loginpage_hook() {
 441          global $frm;  // can be used to override submitted login form
 442          global $user; // can be used to replace authenticate_user_login()
 443  
 444          //override if needed
 445      }
 446  
 447      /**
 448       * Hook for overriding behaviour before going to the login page.
 449       *
 450       * This method is called from require_login from potentially any page for
 451       * all enabled auth plugins and gives each plugin a chance to redirect
 452       * directly to an external login page, or to instantly login a user where
 453       * possible.
 454       *
 455       * If an auth plugin implements this hook, it must not rely on ONLY this
 456       * hook in order to work, as there are many ways a user can browse directly
 457       * to the standard login page. As a general rule in this case you should
 458       * also implement the loginpage_hook as well.
 459       *
 460       */
 461      function pre_loginpage_hook() {
 462          // override if needed, eg by redirecting to an external login page
 463          // or logging in a user:
 464          // complete_user_login($user);
 465      }
 466  
 467      /**
 468       * Pre user_login hook.
 469       * This method is called from authenticate_user_login() right after the user
 470       * object is generated. This gives the auth plugins an option to make adjustments
 471       * before the verification process starts.
 472       *
 473       * @param object $user user object, later used for $USER
 474       */
 475      public function pre_user_login_hook(&$user) {
 476          // Override if needed.
 477      }
 478  
 479      /**
 480       * Post authentication hook.
 481       * This method is called from authenticate_user_login() for all enabled auth plugins.
 482       *
 483       * @param object $user user object, later used for $USER
 484       * @param string $username (with system magic quotes)
 485       * @param string $password plain text password (with system magic quotes)
 486       */
 487      function user_authenticated_hook(&$user, $username, $password) {
 488          //override if needed
 489      }
 490  
 491      /**
 492       * Pre logout hook.
 493       * This method is called from require_logout() for all enabled auth plugins,
 494       *
 495       * @global object
 496       */
 497      function prelogout_hook() {
 498          global $USER; // use $USER->auth to find the plugin used for login
 499  
 500          //override if needed
 501      }
 502  
 503      /**
 504       * Hook for overriding behaviour of logout page.
 505       * This method is called from login/logout.php page for all enabled auth plugins.
 506       *
 507       * @global object
 508       * @global string
 509       */
 510      function logoutpage_hook() {
 511          global $USER;     // use $USER->auth to find the plugin used for login
 512          global $redirect; // can be used to override redirect after logout
 513  
 514          //override if needed
 515      }
 516  
 517      /**
 518       * Hook called before timing out of database session.
 519       * This is useful for SSO and MNET.
 520       *
 521       * @param object $user
 522       * @param string $sid session id
 523       * @param int $timecreated start of session
 524       * @param int $timemodified user last seen
 525       * @return bool true means do not timeout session yet
 526       */
 527      function ignore_timeout_hook($user, $sid, $timecreated, $timemodified) {
 528          return false;
 529      }
 530  
 531      /**
 532       * Return the properly translated human-friendly title of this auth plugin
 533       *
 534       * @todo Document this function
 535       */
 536      function get_title() {
 537          return get_string('pluginname', "auth_{$this->authtype}");
 538      }
 539  
 540      /**
 541       * Get the auth description (from core or own auth lang files)
 542       *
 543       * @return string The description
 544       */
 545      function get_description() {
 546          $authdescription = get_string("auth_{$this->authtype}description", "auth_{$this->authtype}");
 547          return $authdescription;
 548      }
 549  
 550      /**
 551       * Returns whether or not the captcha element is enabled.
 552       *
 553       * @abstract Implement in child classes
 554       * @return bool
 555       */
 556      function is_captcha_enabled() {
 557          return false;
 558      }
 559  
 560      /**
 561       * Returns whether or not this authentication plugin can be manually set
 562       * for users, for example, when bulk uploading users.
 563       *
 564       * This should be overriden by authentication plugins where setting the
 565       * authentication method manually is allowed.
 566       *
 567       * @return bool
 568       * @since Moodle 2.6
 569       */
 570      function can_be_manually_set() {
 571          // Override if needed.
 572          return false;
 573      }
 574  
 575      /**
 576       * Returns a list of potential IdPs that this authentication plugin supports.
 577       *
 578       * This is used to provide links on the login page and the login block.
 579       *
 580       * The parameter $wantsurl is typically used by the plugin to implement a
 581       * return-url feature.
 582       *
 583       * The returned value is expected to be a list of associative arrays with
 584       * string keys:
 585       *
 586       * - url => (moodle_url|string) URL of the page to send the user to for authentication
 587       * - name => (string) Human readable name of the IdP
 588       * - iconurl => (moodle_url|string) URL of the icon representing the IdP (since Moodle 3.3)
 589       *
 590       * For legacy reasons, pre-3.3 plugins can provide the icon via the key:
 591       *
 592       * - icon => (pix_icon) Icon representing the IdP
 593       *
 594       * @param string $wantsurl The relative url fragment the user wants to get to.
 595       * @return array List of associative arrays with keys url, name, iconurl|icon
 596       */
 597      function loginpage_idp_list($wantsurl) {
 598          return array();
 599      }
 600  
 601      /**
 602       * Return custom user profile fields.
 603       *
 604       * @return array list of custom fields.
 605       */
 606      public function get_custom_user_profile_fields() {
 607          global $CFG;
 608          require_once($CFG->dirroot . '/user/profile/lib.php');
 609  
 610          // If already retrieved then return.
 611          if (!is_null($this->customfields)) {
 612              return $this->customfields;
 613          }
 614  
 615          $this->customfields = array();
 616          if ($proffields = profile_get_custom_fields()) {
 617              foreach ($proffields as $proffield) {
 618                  $this->customfields[] = 'profile_field_'.$proffield->shortname;
 619              }
 620          }
 621          unset($proffields);
 622  
 623          return $this->customfields;
 624      }
 625  
 626      /**
 627       * Post logout hook.
 628       *
 629       * This method is used after moodle logout by auth classes to execute server logout.
 630       *
 631       * @param stdClass $user clone of USER object before the user session was terminated
 632       */
 633      public function postlogout_hook($user) {
 634      }
 635  
 636      /**
 637       * Update a local user record from an external source.
 638       * This is a lighter version of the one in moodlelib -- won't do
 639       * expensive ops such as enrolment.
 640       *
 641       * @param string $username username
 642       * @param array $updatekeys fields to update, false updates all fields.
 643       * @param bool $triggerevent set false if user_updated event should not be triggered.
 644       *             This will not affect user_password_updated event triggering.
 645       * @param bool $suspenduser Should the user be suspended?
 646       * @return stdClass|bool updated user record or false if there is no new info to update.
 647       */
 648      protected function update_user_record($username, $updatekeys = false, $triggerevent = false, $suspenduser = false) {
 649          global $CFG, $DB;
 650  
 651          require_once($CFG->dirroot.'/user/profile/lib.php');
 652  
 653          // Just in case check text case.
 654          $username = trim(core_text::strtolower($username));
 655  
 656          // Get the current user record.
 657          $user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id));
 658          if (empty($user)) { // Trouble.
 659              error_log($this->errorlogtag . get_string('auth_usernotexist', 'auth', $username));
 660              throw new \moodle_exception('auth_usernotexist', 'auth', '', $username);
 661              die;
 662          }
 663  
 664          // Protect the userid from being overwritten.
 665          $userid = $user->id;
 666  
 667          $needsupdate = false;
 668  
 669          if ($newinfo = $this->get_userinfo($username)) {
 670              $newinfo = truncate_userinfo($newinfo);
 671  
 672              if (empty($updatekeys)) { // All keys? this does not support removing values.
 673                  $updatekeys = array_keys($newinfo);
 674              }
 675  
 676              if (!empty($updatekeys)) {
 677                  $newuser = new stdClass();
 678                  $newuser->id = $userid;
 679                  // The cast to int is a workaround for MDL-53959.
 680                  $newuser->suspended = (int) $suspenduser;
 681                  // Load all custom fields.
 682                  $profilefields = (array) profile_user_record($user->id, false);
 683                  $newprofilefields = [];
 684  
 685                  foreach ($updatekeys as $key) {
 686                      if (isset($newinfo[$key])) {
 687                          $value = $newinfo[$key];
 688                      } else {
 689                          $value = '';
 690                      }
 691  
 692                      if (!empty($this->config->{'field_updatelocal_' . $key})) {
 693                          if (preg_match('/^profile_field_(.*)$/', $key, $match)) {
 694                              // Custom field.
 695                              $field = $match[1];
 696                              $currentvalue = isset($profilefields[$field]) ? $profilefields[$field] : null;
 697                              $newprofilefields[$field] = $value;
 698                          } else {
 699                              // Standard field.
 700                              $currentvalue = isset($user->$key) ? $user->$key : null;
 701                              $newuser->$key = $value;
 702                          }
 703  
 704                          // Only update if it's changed.
 705                          if ($currentvalue !== $value) {
 706                              $needsupdate = true;
 707                          }
 708                      }
 709                  }
 710              }
 711  
 712              if ($needsupdate) {
 713                  user_update_user($newuser, false, $triggerevent);
 714                  profile_save_custom_fields($newuser->id, $newprofilefields);
 715                  return $DB->get_record('user', array('id' => $userid, 'deleted' => 0));
 716              }
 717          }
 718  
 719          return false;
 720      }
 721  
 722      /**
 723       * Return the list of enabled identity providers.
 724       *
 725       * Each identity provider data contains the keys url, name and iconurl (or
 726       * icon). See the documentation of {@link auth_plugin_base::loginpage_idp_list()}
 727       * for detailed description of the returned structure.
 728       *
 729       * @param array $authsequence site's auth sequence (list of auth plugins ordered)
 730       * @return array List of arrays describing the identity providers
 731       */
 732      public static function get_identity_providers($authsequence) {
 733          global $SESSION;
 734  
 735          $identityproviders = [];
 736          foreach ($authsequence as $authname) {
 737              $authplugin = get_auth_plugin($authname);
 738              $wantsurl = (isset($SESSION->wantsurl)) ? $SESSION->wantsurl : '';
 739              $identityproviders = array_merge($identityproviders, $authplugin->loginpage_idp_list($wantsurl));
 740          }
 741          return $identityproviders;
 742      }
 743  
 744      /**
 745       * Prepare a list of identity providers for output.
 746       *
 747       * @param array $identityproviders as returned by {@link self::get_identity_providers()}
 748       * @param renderer_base $output
 749       * @return array the identity providers ready for output
 750       */
 751      public static function prepare_identity_providers_for_output($identityproviders, renderer_base $output) {
 752          $data = [];
 753          foreach ($identityproviders as $idp) {
 754              if (!empty($idp['icon'])) {
 755                  // Pre-3.3 auth plugins provide icon as a pix_icon instance. New auth plugins (since 3.3) provide iconurl.
 756                  $idp['iconurl'] = $output->image_url($idp['icon']->pix, $idp['icon']->component);
 757              }
 758              if ($idp['iconurl'] instanceof moodle_url) {
 759                  $idp['iconurl'] = $idp['iconurl']->out(false);
 760              }
 761              unset($idp['icon']);
 762              if ($idp['url'] instanceof moodle_url) {
 763                  $idp['url'] = $idp['url']->out(false);
 764              }
 765              $data[] = $idp;
 766          }
 767          return $data;
 768      }
 769  
 770      /**
 771       * Returns information on how the specified user can change their password.
 772       *
 773       * @param stdClass $user A user object
 774       * @return string[] An array of strings with keys subject and message
 775       */
 776      public function get_password_change_info(stdClass $user) : array {
 777  
 778          global $USER;
 779  
 780          $site = get_site();
 781          $systemcontext = context_system::instance();
 782  
 783          $data = new stdClass();
 784          $data->firstname = $user->firstname;
 785          $data->lastname  = $user->lastname;
 786          $data->username  = $user->username;
 787          $data->sitename  = format_string($site->fullname);
 788          $data->admin     = generate_email_signoff();
 789  
 790          // This is a workaround as change_password_url() is designed to allow
 791          // use of the $USER global. See MDL-66984.
 792          $olduser = $USER;
 793          $USER = $user;
 794          if ($this->can_change_password() and $this->change_password_url()) {
 795              // We have some external url for password changing.
 796              $data->link = $this->change_password_url()->out();
 797          } else {
 798              // No way to change password, sorry.
 799              $data->link = '';
 800          }
 801          $USER = $olduser;
 802  
 803          if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
 804              $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
 805              $message = get_string('emailpasswordchangeinfo', '', $data);
 806          } else {
 807              $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
 808              $message = get_string('emailpasswordchangeinfofail', '', $data);
 809          }
 810  
 811          return [
 812              'subject' => $subject,
 813              'message' => $message
 814          ];
 815      }
 816  
 817      /**
 818       * Set extra user information.
 819       *
 820       * @param array $values Any Key value pair.
 821       * @return void
 822       */
 823      public function set_extrauserinfo(array $values): void {
 824          $this->extrauserinfo = $values;
 825      }
 826  
 827      /**
 828       * Returns extra user information.
 829       *
 830       * @return array An array of keys and values
 831       */
 832      public function get_extrauserinfo(): array {
 833          return $this->extrauserinfo;
 834      }
 835  }
 836  
 837  /**
 838   * Verify if user is locked out.
 839   *
 840   * @param stdClass $user
 841   * @return bool true if user locked out
 842   */
 843  function login_is_lockedout($user) {
 844      global $CFG;
 845  
 846      if ($user->mnethostid != $CFG->mnet_localhost_id) {
 847          return false;
 848      }
 849      if (isguestuser($user)) {
 850          return false;
 851      }
 852  
 853      if (empty($CFG->lockoutthreshold)) {
 854          // Lockout not enabled.
 855          return false;
 856      }
 857  
 858      if (get_user_preferences('login_lockout_ignored', 0, $user)) {
 859          // This preference may be used for accounts that must not be locked out.
 860          return false;
 861      }
 862  
 863      $locked = get_user_preferences('login_lockout', 0, $user);
 864      if (!$locked) {
 865          return false;
 866      }
 867  
 868      if (empty($CFG->lockoutduration)) {
 869          // Locked out forever.
 870          return true;
 871      }
 872  
 873      if (time() - $locked < $CFG->lockoutduration) {
 874          return true;
 875      }
 876  
 877      login_unlock_account($user);
 878  
 879      return false;
 880  }
 881  
 882  /**
 883   * To be called after valid user login.
 884   * @param stdClass $user
 885   */
 886  function login_attempt_valid($user) {
 887      global $CFG;
 888  
 889      // Note: user_loggedin event is triggered in complete_user_login().
 890  
 891      if ($user->mnethostid != $CFG->mnet_localhost_id) {
 892          return;
 893      }
 894      if (isguestuser($user)) {
 895          return;
 896      }
 897  
 898      // Always unlock here, there might be some race conditions or leftovers when switching threshold.
 899      login_unlock_account($user);
 900  }
 901  
 902  /**
 903   * To be called after failed user login.
 904   * @param stdClass $user
 905   * @throws moodle_exception
 906   */
 907  function login_attempt_failed($user) {
 908      global $CFG;
 909  
 910      if ($user->mnethostid != $CFG->mnet_localhost_id) {
 911          return;
 912      }
 913      if (isguestuser($user)) {
 914          return;
 915      }
 916  
 917      // Force user preferences cache reload to ensure the most up-to-date login_failed_count is fetched.
 918      // This is perhaps overzealous but is the documented way of reloading the cache, as per the test method
 919      // 'test_check_user_preferences_loaded'.
 920      unset($user->preference);
 921  
 922      $resource = 'user:' . $user->id;
 923      $lockfactory = \core\lock\lock_config::get_lock_factory('core_failed_login_count_lock');
 924  
 925      // Get a new lock for the resource, waiting for it for a maximum of 10 seconds.
 926      if ($lock = $lockfactory->get_lock($resource, 10)) {
 927          try {
 928              $count = get_user_preferences('login_failed_count', 0, $user);
 929              $last = get_user_preferences('login_failed_last', 0, $user);
 930              $sincescuccess = get_user_preferences('login_failed_count_since_success', $count, $user);
 931              $sincescuccess = $sincescuccess + 1;
 932              set_user_preference('login_failed_count_since_success', $sincescuccess, $user);
 933  
 934              if (empty($CFG->lockoutthreshold)) {
 935                  // No threshold means no lockout.
 936                  // Always unlock here, there might be some race conditions or leftovers when switching threshold.
 937                  login_unlock_account($user);
 938                  $lock->release();
 939                  return;
 940              }
 941  
 942              if (!empty($CFG->lockoutwindow) and time() - $last > $CFG->lockoutwindow) {
 943                  $count = 0;
 944              }
 945  
 946              $count = $count + 1;
 947  
 948              set_user_preference('login_failed_count', $count, $user);
 949              set_user_preference('login_failed_last', time(), $user);
 950  
 951              if ($count >= $CFG->lockoutthreshold) {
 952                  login_lock_account($user);
 953              }
 954  
 955              // Release locks when we're done.
 956              $lock->release();
 957          } catch (Exception $e) {
 958              // Always release the lock on a failure.
 959              $lock->release();
 960          }
 961      } else {
 962          // We did not get access to the resource in time, give up.
 963          throw new moodle_exception('locktimeout');
 964      }
 965  }
 966  
 967  /**
 968   * Lockout user and send notification email.
 969   *
 970   * @param stdClass $user
 971   */
 972  function login_lock_account($user) {
 973      global $CFG;
 974  
 975      if ($user->mnethostid != $CFG->mnet_localhost_id) {
 976          return;
 977      }
 978      if (isguestuser($user)) {
 979          return;
 980      }
 981  
 982      if (get_user_preferences('login_lockout_ignored', 0, $user)) {
 983          // This user can not be locked out.
 984          return;
 985      }
 986  
 987      $alreadylockedout = get_user_preferences('login_lockout', 0, $user);
 988  
 989      set_user_preference('login_lockout', time(), $user);
 990  
 991      if ($alreadylockedout == 0) {
 992          $secret = random_string(15);
 993          set_user_preference('login_lockout_secret', $secret, $user);
 994  
 995          $oldforcelang = force_current_language($user->lang);
 996  
 997          $site = get_site();
 998          $supportuser = core_user::get_support_user();
 999  
1000          $data = new stdClass();
1001          $data->firstname = $user->firstname;
1002          $data->lastname  = $user->lastname;
1003          $data->username  = $user->username;
1004          $data->sitename  = format_string($site->fullname);
1005          $data->link      = $CFG->wwwroot.'/login/unlock_account.php?u='.$user->id.'&s='.$secret;
1006          $data->admin     = generate_email_signoff();
1007  
1008          $message = get_string('lockoutemailbody', 'admin', $data);
1009          $subject = get_string('lockoutemailsubject', 'admin', format_string($site->fullname));
1010  
1011          if ($message) {
1012              // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
1013              email_to_user($user, $supportuser, $subject, $message);
1014          }
1015  
1016          force_current_language($oldforcelang);
1017      }
1018  }
1019  
1020  /**
1021   * Unlock user account and reset timers.
1022   *
1023   * @param stdClass $user
1024   * @param bool $notify Notify the user their account has been unlocked.
1025   */
1026  function login_unlock_account($user, bool $notify = false) {
1027      global $SESSION;
1028  
1029      unset_user_preference('login_lockout', $user);
1030      unset_user_preference('login_failed_count', $user);
1031      unset_user_preference('login_failed_last', $user);
1032  
1033      if ($notify) {
1034          $SESSION->logininfomsg = get_string('accountunlocked', 'admin');
1035      }
1036      // Note: do not clear the lockout secret because user might click on the link repeatedly.
1037  }
1038  
1039  /**
1040   * Returns whether or not the captcha element is enabled, and the admin settings fulfil its requirements.
1041   * @return bool
1042   */
1043  function signup_captcha_enabled() {
1044      global $CFG;
1045      $authplugin = get_auth_plugin($CFG->registerauth);
1046      return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && $authplugin->is_captcha_enabled();
1047  }
1048  
1049  /**
1050   * Returns whether the captcha element is enabled for the login form, and the admin settings fulfil its requirements.
1051   * @return bool
1052   */
1053  function login_captcha_enabled(): bool {
1054      global $CFG;
1055      return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && $CFG->enableloginrecaptcha == true;
1056  }
1057  
1058  /**
1059   * Check the submitted captcha is valid or not.
1060   *
1061   * @param string|bool $captcha The value submitted in the login form that we are validating.
1062   *                             If false is passed for the captcha, this function will always return true.
1063   * @return boolean If the submitted captcha is valid.
1064   */
1065  function validate_login_captcha(string|bool $captcha): bool {
1066      global $CFG;
1067      if (!empty($CFG->alternateloginurl)) {
1068          // An external login page cannot use the reCaptcha.
1069          return true;
1070      }
1071      if ($captcha === false) {
1072          // The authenticate_user_login() is a core function was extended to validate captcha.
1073          // For existing uses other than the login form it does not need to validate the captcha.
1074          // Example: login/change_password_form.php or login/token.php.
1075          return true;
1076      }
1077  
1078      require_once($CFG->libdir . '/recaptchalib_v2.php');
1079      $response = recaptcha_check_response(RECAPTCHA_VERIFY_URL, $CFG->recaptchaprivatekey, getremoteaddr(), $captcha);
1080      return $response['isvalid'];
1081  }
1082  
1083  /**
1084   * Validates the standard sign-up data (except recaptcha that is validated by the form element).
1085   *
1086   * @param  array $data  the sign-up data
1087   * @param  array $files files among the data
1088   * @return array list of errors, being the key the data element name and the value the error itself
1089   * @since Moodle 3.2
1090   */
1091  function signup_validate_data($data, $files) {
1092      global $CFG, $DB;
1093  
1094      $errors = array();
1095      $authplugin = get_auth_plugin($CFG->registerauth);
1096  
1097      if ($DB->record_exists('user', array('username' => $data['username'], 'mnethostid' => $CFG->mnet_localhost_id))) {
1098          $errors['username'] = get_string('usernameexists');
1099      } else {
1100          // Check allowed characters.
1101          if ($data['username'] !== core_text::strtolower($data['username'])) {
1102              $errors['username'] = get_string('usernamelowercase');
1103          } else {
1104              if ($data['username'] !== core_user::clean_field($data['username'], 'username')) {
1105                  $errors['username'] = get_string('invalidusername');
1106              }
1107  
1108          }
1109      }
1110  
1111      // Check if user exists in external db.
1112      // TODO: maybe we should check all enabled plugins instead.
1113      if ($authplugin->user_exists($data['username'])) {
1114          $errors['username'] = get_string('usernameexists');
1115      }
1116  
1117      if (! validate_email($data['email'])) {
1118          $errors['email'] = get_string('invalidemail');
1119  
1120      } else if (empty($CFG->allowaccountssameemail)) {
1121          // Emails in Moodle as case-insensitive and accents-sensitive. Such a combination can lead to very slow queries
1122          // on some DBs such as MySQL. So we first get the list of candidate users in a subselect via more effective
1123          // accent-insensitive query that can make use of the index and only then we search within that limited subset.
1124          $sql = "SELECT 'x'
1125                    FROM {user}
1126                   WHERE " . $DB->sql_equal('email', ':email1', false, true) . "
1127                     AND id IN (SELECT id
1128                                  FROM {user}
1129                                 WHERE " . $DB->sql_equal('email', ':email2', false, false) . "
1130                                   AND mnethostid = :mnethostid)";
1131  
1132          $params = array(
1133              'email1' => $data['email'],
1134              'email2' => $data['email'],
1135              'mnethostid' => $CFG->mnet_localhost_id,
1136          );
1137  
1138          // If there are other user(s) that already have the same email, show an error.
1139          if ($DB->record_exists_sql($sql, $params)) {
1140              $forgotpasswordurl = new moodle_url('/login/forgot_password.php');
1141              $forgotpasswordlink = html_writer::link($forgotpasswordurl, get_string('emailexistshintlink'));
1142              $errors['email'] = get_string('emailexists') . ' ' . get_string('emailexistssignuphint', 'moodle', $forgotpasswordlink);
1143          }
1144      }
1145      if (empty($data['email2'])) {
1146          $errors['email2'] = get_string('missingemail');
1147  
1148      } else if (core_text::strtolower($data['email2']) != core_text::strtolower($data['email'])) {
1149          $errors['email2'] = get_string('invalidemail');
1150      }
1151      if (!isset($errors['email'])) {
1152          if ($err = email_is_not_allowed($data['email'])) {
1153              $errors['email'] = $err;
1154          }
1155      }
1156  
1157      // Construct fake user object to check password policy against required information.
1158      $tempuser = new stdClass();
1159      // To prevent errors with check_password_policy(),
1160      // the temporary user and the guest must not share the same ID.
1161      $tempuser->id = (int)$CFG->siteguest + 1;
1162      $tempuser->username = $data['username'];
1163      $tempuser->firstname = $data['firstname'];
1164      $tempuser->lastname = $data['lastname'];
1165      $tempuser->email = $data['email'];
1166  
1167      $errmsg = '';
1168      if (!check_password_policy($data['password'], $errmsg, $tempuser)) {
1169          $errors['password'] = $errmsg;
1170      }
1171  
1172      // Validate customisable profile fields. (profile_validation expects an object as the parameter with userid set).
1173      $dataobject = (object)$data;
1174      $dataobject->id = 0;
1175      $errors += profile_validation($dataobject, $files);
1176  
1177      return $errors;
1178  }
1179  
1180  /**
1181   * Add the missing fields to a user that is going to be created
1182   *
1183   * @param  stdClass $user the new user object
1184   * @return stdClass the user filled
1185   * @since Moodle 3.2
1186   */
1187  function signup_setup_new_user($user) {
1188      global $CFG;
1189  
1190      $user->confirmed   = 0;
1191      $user->lang        = current_language();
1192      $user->firstaccess = 0;
1193      $user->timecreated = time();
1194      $user->mnethostid  = $CFG->mnet_localhost_id;
1195      $user->secret      = random_string(15);
1196      $user->auth        = $CFG->registerauth;
1197      // Initialize alternate name fields to empty strings.
1198      $namefields = array_diff(\core_user\fields::get_name_fields(), useredit_get_required_name_fields());
1199      foreach ($namefields as $namefield) {
1200          $user->$namefield = '';
1201      }
1202      return $user;
1203  }
1204  
1205  /**
1206   * Check if user confirmation is enabled on this site and return the auth plugin handling registration if enabled.
1207   *
1208   * @return stdClass the current auth plugin handling user registration or false if registration not enabled
1209   * @since Moodle 3.2
1210   */
1211  function signup_get_user_confirmation_authplugin() {
1212      global $CFG;
1213  
1214      if (empty($CFG->registerauth)) {
1215          return false;
1216      }
1217      $authplugin = get_auth_plugin($CFG->registerauth);
1218  
1219      if (!$authplugin->can_confirm()) {
1220          return false;
1221      }
1222      return $authplugin;
1223  }
1224  
1225  /**
1226   * Check if sign-up is enabled in the site. If is enabled, the function will return the authplugin instance.
1227   *
1228   * @return mixed false if sign-up is not enabled, the authplugin instance otherwise.
1229   * @since  Moodle 3.2
1230   */
1231  function signup_is_enabled() {
1232      global $CFG;
1233  
1234      if (!empty($CFG->registerauth)) {
1235          $authplugin = get_auth_plugin($CFG->registerauth);
1236          if ($authplugin->can_signup()) {
1237              return $authplugin;
1238          }
1239      }
1240      return false;
1241  }
1242  
1243  /**
1244   * Helper function used to print locking for auth plugins on admin pages.
1245   * @param admin_settingpage $settings Moodle admin settings instance
1246   * @param string $auth authentication plugin shortname
1247   * @param array $userfields user profile fields
1248   * @param string $helptext help text to be displayed at top of form
1249   * @param boolean $mapremotefields Map fields or lock only.
1250   * @param boolean $updateremotefields Allow remote updates
1251   * @param array $customfields list of custom profile fields
1252   * @since Moodle 3.3
1253   */
1254  function display_auth_lock_options($settings, $auth, $userfields, $helptext, $mapremotefields, $updateremotefields, $customfields = array()) {
1255      global $CFG;
1256      require_once($CFG->dirroot . '/user/profile/lib.php');
1257  
1258      // Introductory explanation and help text.
1259      if ($mapremotefields) {
1260          $settings->add(new admin_setting_heading($auth.'/data_mapping', new lang_string('auth_data_mapping', 'auth'), $helptext));
1261      } else {
1262          $settings->add(new admin_setting_heading($auth.'/auth_fieldlocks', new lang_string('auth_fieldlocks', 'auth'), $helptext));
1263      }
1264  
1265      // Generate the list of options.
1266      $lockoptions = array ('unlocked'        => get_string('unlocked', 'auth'),
1267                            'unlockedifempty' => get_string('unlockedifempty', 'auth'),
1268                            'locked'          => get_string('locked', 'auth'));
1269      $updatelocaloptions = array('oncreate'  => get_string('update_oncreate', 'auth'),
1270                                  'onlogin'   => get_string('update_onlogin', 'auth'));
1271      $updateextoptions = array('0'  => get_string('update_never', 'auth'),
1272                                '1'  => get_string('update_onupdate', 'auth'));
1273  
1274      // Generate the list of profile fields to allow updates / lock.
1275      if (!empty($customfields)) {
1276          $userfields = array_merge($userfields, $customfields);
1277          $allcustomfields = profile_get_custom_fields();
1278          $customfieldname = array_combine(array_column($allcustomfields, 'shortname'), $allcustomfields);
1279      }
1280  
1281      foreach ($userfields as $field) {
1282          // Define the fieldname we display to the  user.
1283          // this includes special handling for some profile fields.
1284          $fieldname = $field;
1285          $fieldnametoolong = false;
1286          if ($fieldname === 'lang') {
1287              $fieldname = get_string('language');
1288          } else if (!empty($customfields) && in_array($field, $customfields)) {
1289              // If custom field then pick name from database.
1290              $fieldshortname = str_replace('profile_field_', '', $fieldname);
1291              $fieldname = $customfieldname[$fieldshortname]->name;
1292              if (core_text::strlen($fieldshortname) > 67) {
1293                  // If custom profile field name is longer than 67 characters we will not be able to store the setting
1294                  // such as 'field_updateremote_profile_field_NOTSOSHORTSHORTNAME' in the database because the character
1295                  // limit for the setting name is 100.
1296                  $fieldnametoolong = true;
1297              }
1298          } else {
1299              $fieldname = get_string($fieldname);
1300          }
1301  
1302          // Generate the list of fields / mappings.
1303          if ($fieldnametoolong) {
1304              // Display a message that the field can not be mapped because it's too long.
1305              $url = new moodle_url('/user/profile/index.php');
1306              $a = (object)['fieldname' => s($fieldname), 'shortname' => s($field), 'charlimit' => 67, 'link' => $url->out()];
1307              $settings->add(new admin_setting_heading($auth.'/field_not_mapped_'.sha1($field), '',
1308                  get_string('cannotmapfield', 'auth', $a)));
1309          } else if ($mapremotefields) {
1310              // We are mapping to a remote field here.
1311              // Mapping.
1312              $settings->add(new admin_setting_configtext("auth_{$auth}/field_map_{$field}",
1313                      get_string('auth_fieldmapping', 'auth', $fieldname), '', '', PARAM_RAW, 30));
1314  
1315              // Update local.
1316              $settings->add(new admin_setting_configselect("auth_{$auth}/field_updatelocal_{$field}",
1317                      get_string('auth_updatelocalfield', 'auth', $fieldname), '', 'oncreate', $updatelocaloptions));
1318  
1319              // Update remote.
1320              if ($updateremotefields) {
1321                      $settings->add(new admin_setting_configselect("auth_{$auth}/field_updateremote_{$field}",
1322                          get_string('auth_updateremotefield', 'auth', $fieldname), '', 0, $updateextoptions));
1323              }
1324  
1325              // Lock fields.
1326              $settings->add(new admin_setting_configselect("auth_{$auth}/field_lock_{$field}",
1327                      get_string('auth_fieldlockfield', 'auth', $fieldname), '', 'unlocked', $lockoptions));
1328  
1329          } else {
1330              // Lock fields Only.
1331              $settings->add(new admin_setting_configselect("auth_{$auth}/field_lock_{$field}",
1332                      get_string('auth_fieldlockfield', 'auth', $fieldname), '', 'unlocked', $lockoptions));
1333          }
1334      }
1335  }