Search moodle.org's
Developer Documentation

See Release Notes

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

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